Skip to main content
The mint-redeem program holds one Market account per synthetic asset, mints and burns its L and S tokens, and keeps the market’s collateral ledger. Users call mint_paired and redeem_paired. The keeper writes the price, the confidence band, γ and the buffer target. The market authority configures and maintains the market.

Accounts

Market account

The account is at most 899 bytes: an 8-byte discriminator plus Market::INIT_SPACE, which reserves room for a 10-byte symbol and five collateral_vaults entries. collateral_vaults is a variable-length Vec (empty on every live market), so the fields after it have no fixed byte offsets. Decode the account with the IDL rather than reading at offsets. Devnet markets are allocated 891 bytes (10 Oct 2026); the data fits because the list is empty. Fields in IDL order. Prices and amounts are 1e6 fixed point (6 decimals). Anchor TS decodes gamma_day_decay_1e9 and short_payoff_cap_c_1e9 as gammaDayDecay1E9 and shortPayoffCapC1E9; every other field is plain camelCase (userTwapPrice).

RedemptionRequest

One open request per owner and market. request_redemption creates it and moves the owner’s L and S into two escrow token accounts owned by the request PDA. Fields: owner, market, long_mint, short_mint, l_requested, s_requested, l_remaining, s_remaining, the running totals gross_paid, net_paid, fee_paid and dev_tax_paid, fills, created_at, last_fill_at and the three bumps. The request closes, and its rent goes back to the owner, when it is paid in full or cancelled.

MarketAdvance

The ledger of the treasury’s loan to one market, created by configure_market_advance. Fields: market, lender (the treasury vault, the only account an advance can come from), max_advance_bps (0 turns advances off), outstanding, total_advanced, total_repaid, total_written_off, last_advance_at, last_repay_at and bump. Advance cash sits in the market’s collateral vault but is not counted in total_collateral, so the collateral ratio and the redeem haircut never see it.

The collateral_authority slot

Every market keeps its collateral in its own vault, owned by its Market PDA, and the Market PDA signs every payout. The upgrade of 11 Oct 2026 removed the unified vault from the program, along with the CollateralAuthority account type. Six instructions still have a collateral_authority account in its old position:
  • redeem_paired
  • keeper_mint_single and keeper_redeem_single
  • redeem_single_for_vault
  • withdraw_excess_to_clp and deploy_collateral_to_yield
It is now an ignored optional slot. The program never reads it and accepts any account. Pass mint-redeem’s program ID there: in Anchor that is collateralAuthority: null in TypeScript or collateral_authority: None in a Rust CPI. The four unified-vault instructions are gone: init_collateral_authority, migrate_to_unified_vault, exit_unified_vault and sweep_unified_surplus. Calling one now fails with Anchor error 101 (InstructionFallbackNotFound).

Pricing and fees

Price source

In ProxyMode, Stress and Recovery, mint and redeem price from user_twap_price and revert with TwapNotAvailable when it is 0 or twap_updated_at is more than 300 s old. The keeper doesn’t push while the underlying market is closed, so mint and redeem revert until it reopens. In Normal the program reads the oracle account itself. A Pyth feed must then be at most 60 s old, with a confidence interval within 2% of the price; a Continuum OracleConfig must be unpaused, Active and fresh. No market runs in Normal today. With P the price above, L = P and S = K / P × γ, where K = initial_l_price × initial_s_price and γ = s_gamma_index / 1e9. Mint and redeem quote each leg against the user. Let c be oracle_confidence times the state’s confidence multiplier: 1.5× in ProxyMode, 3× in Stress, 1× in Normal, and in Recovery 3× falling to 1× over the hour after the market enters Recovery (risk_state_updated_at). A mint prices L at P + c and S at K / (P − c) × γ. A redeem prices L at P − c and S at K / (P + c) × γ. Worst-case quoting is on by default, and configure_worst_case_quoting can switch it off per market; as of 10 Oct 2026 it is off on QQQ.

Fees

The charged fee is mint_fee_bps or redeem_fee_bps times the state’s fee multiplier: 1× in Normal, 1.5× in ProxyMode, 3× in Stress, and in Recovery 3× falling to 1× over the hour after the market enters Recovery (risk_state_updated_at). No waiver or discount applies, and donating earns none. The keeper posts both fees through update_market as clamp(fee_mult × half-spread, 10, 100) bps, using its own half-spread estimate for the market (fee_mult defaults to 1.0). As of 10 Oct 2026 the posted fees are QQQ 18, SPY 16, NVDA 31 and EUR, GBP and JPY 10 bps, so QQQ is charged 27 bps in ProxyMode. The mint fee comes out of the deposit; the redeem fee comes out of the gross payout after any haircut. dev_tax_pct (10%) of each fee goes to the fee recipient’s cUSDC account, the treasury on every live market since 12 Oct 2026. The rest stays in the vault and counts in total_collateral.

Limits

Mint and redeem each need at least 10 cUSDC of value (BelowMinimum). Stress blocks both by default, and every live market keeps that default. stress_mint_cap applies only in Stress and is 0 (no cap).

Instructions

Grouped by the signer the program checks. Anchor TS account names are camelCase versions of the IDL names (user_long_account becomes userLongAccount).

Anyone

mint_paired(collateral_amount: u64)

Deposits collateral_amount cUSDC (at least 10 cUSDC) and mints L and S to the user, half of the net value to each leg at the mint NAVs. Accounts: market, user, userCollateral, userLongAccount, userShortAccount, longMint, shortMint, collateralVault, devTokenAccount, oracle, tokenProgram. The three user token accounts must already exist. See the mint flow.

redeem_paired(l_amount: u64, s_amount: u64)

Burns the largest value-balanced pair inside l_amount and s_amount and pays l × L + s × S for the pair it burns, at the redeem NAVs, less the fee. Both amounts must be above 0, and they are maxima. The balanced pair is all of the leg worth less at the worst-case redeem NAVs, and the same value of the other leg, rounded down. The rest of the larger leg stays in the caller’s wallet. The accounts are those of mint_paired plus the ignored collateralAuthority slot; pass null. If the market’s collateral ratio at those NAVs is under 100%, every redeemer is paid nominal × collateral ratio, so early redeemers can’t drain the vault ahead of others. Above 100% the haircut does nothing. If the payout is larger than the market’s liquid collateral (total_collateral minus deployed_to_yield) or the vault’s balance, it fails with InsufficientLiquidCollateral (6022). Queue the redemption with request_redemption instead. See the redeem flow. The balanced burn closes audit finding MR-2. A lopsided paired redeem, for example one atom of L plus all S right after a mint, used to be an instant one-leg sale at the stored price, so a trader who saw the next price push coming could sell the falling leg before it landed. Details:
  • An offer already balanced to within one base unit of the dearer leg’s NAV is burned exactly as sent, so the rule is idempotent.
  • If the balanced pair has a zero leg, it fails with InvalidAmount. One atom of L worth less than one base unit of S is an example.
  • The pair it burns must be worth at least 10 cUSDC, or it fails with BelowMinimum.
  • When it trims, it logs Balanced pair: burning X L + Y S of the A L + B S offered; the rest stays with the redeemer. The PairedActionExecuted event and the Redeemed paired tokens log report the amounts actually burned.
  • The CLP redeeming its own inventory through vault_redeem_pairs (the CLP PDA signs) is exempt. It burns exactly what it asks.
  • A balanced burn moves traders’ net position by less than $1, so redemptions take no exposure remaining accounts and can no longer fail with ExposureCapExceeded (6060) or ExposureAccountsMissing (6062). These codes are raised only by the redemption check, so they can no longer occur in practice. They stay in the enum and the check stays in the code as a guard. The cap errors on the swap paths (pool_swap, settle_swap) belong to the CLP program, as 6045 and 6046. See the exposure cap.
The instruction’s arguments and accounts did not change. Only the doc comments in the IDL did. The change shipped on devnet on 14 Oct 2026 (slot 511012606). Adds cUSDC to a market’s collateral without minting anything. The market must be active and amount above 0. The amount goes into the collateral vault and total_collateral, and the donor gets nothing back: no tokens, no fee waiver and no other reward. It reads no price, so it has no staleness check. Accounts: market, collateralVault, donorCollateral, donor (signer), tokenProgram. The app stopped offering donations on 9 Oct 2026.

request_redemption(params: RedemptionAmounts)

Queues a paired redemption the vault cannot pay now. RedemptionAmounts holds l_amount and s_amount, both above 0. It runs the same checks as redeem_paired (market active, risk state, price freshness, the 10 cUSDC minimum) and applies the same balanced-pair rule, then moves only the balanced pair into the request’s escrow token accounts and creates the RedemptionRequest. Nothing is burned or paid yet. Signer: the owner. Accounts: market, owner, request, escrowL, escrowS, ownerLongAccount, ownerShortAccount, longMint, shortMint, oracle, tokenProgram, systemProgram. Emits RedemptionRequested. The rest of the larger leg stays with the owner. l_requested, s_requested and the event’s amounts are the escrowed, balanced amounts, and settle_redemption burns the escrow pro rata, so a request stays balanced. A request takes no exposure remaining accounts, and settle_redemption does not check the cap. See the redeem flow.

cancel_redemption()

Returns the unpaid part of a request, with all rent, and closes it. Signer: the owner. It reads no Market, so it works while the market is paused, the price is stale or the keeper is down. It returns each escrow’s actual balance, so atoms sent to an escrow by someone else can’t block the close. Emits RedemptionCancelled.

permissionless_burn(is_long: bool, amount: u64)

Burns L or S from the signer’s own token account with no payout, which lowers the supply ledger. A delegate can’t call it on someone else’s account. The CLP’s clp_burn_excess uses it to retire inventory.

Setup

initialize_market(params: InitMarketParams)

InitMarketParams { asset_symbol, oracle_type, initial_l_price, initial_s_price, mint_fee_bps, redeem_fee_bps, keeper_authority }. Only the mint-redeem program’s upgrade authority can call it: the instruction takes the program and its program_data as its last two accounts and fails with UnauthorizedInitializer (6046) for any other signer. The signer becomes market.authority and fee_recipient. The symbol must be at most 10 bytes, both fees at most 1,000 bps, and both prices above 0. oracle_type must be Pyth or Continuum, and for Continuum the oracle account must be the market’s OracleConfig PDA. The instruction doesn’t create the L and S mints or the vault: they must already exist, with the Market PDA as mint authority of both mints. A new market starts in Normal with user_twap_price = initial_l_price, dev_tax_pct 10, the default risk-fee and worst-case settings, and a 1/1 price adjustment. It also sets yield_enabled = true and leaves yield_ceiling_bps at 0, so harvests can run at once but nothing can be deployed to a lending venue until configure_yield_ceiling raises the ceiling. The full sequence is in listing a market.

Keeper (signer = market.keeper_authority)

update_risk_state_v2(new_risk_state: RiskState, user_twap_price: u64, oracle_confidence: u64, source_publish_time: i64)

The keeper’s price write. It maps the price and the confidence through the corporate-action ratio, then stores them with the risk state and sets twap_updated_at to source_publish_time. It rejects a source time in the future, more than 300 s old or before the latest corporate-action boundary. It also rejects a price of 0, a move of more than 25% from the stored price, and any move out of Stress except to Recovery. update_risk_state(new_risk_state, user_twap_price, oracle_confidence) does the same without a source time and stamps twap_updated_at with the chain time.

set_s_gamma_index(params: SetGammaParams)

SetGammaParams { s_gamma_index: u64, expected_epoch: u32 } posts γ at 1e9 scale. Bad writes are rejected, never clamped: a value outside 0.5 to 1.0 (GammaOutOfRange), a value above the current γ (GammaMustRatchetDown), or an expected_epoch other than gamma_epoch (GammaEpochMismatch). There is no per-write or per-day cap. γ is always on; the keeper refuses to boot if CLMM_GAMMA_DECAY_ENABLED is set.

set_buffer_target(buffer_target_bps: u16)

Posts the market’s buffer over parity, clamped to 100 to 5,000 bps (a target ratio of 101% to 150%). The keeper posts each market’s daily Monte Carlo target, and only when it differs from the stored one; see Solvency. The target gates how much excess a market can shed (withdraw_excess_to_clp, rebalance_collateral). Mints and redeems don’t read it.

settle_redemption(params: SettleRedemptionParams)

Pays the largest part of a request that the vault’s cash covers now, up to max_l_amount of L with the matching share of S. Prices, haircut, fee and dev tax work exactly as in redeem_paired. It burns that part from escrow, pays the owner and updates the request. Unlike redeem_paired, it may spend the cash of a treasury advance, but never more than the vault holds. It fails with NothingToSettle (6053) when the request is empty or no part of it fits. When the request is paid in full and the owner is passed, the escrows and the request close and the rent goes back to the owner. Emits RedemptionSettled and PairedActionExecuted.

keeper_mint_single, keeper_redeem_single, keeper_burn_single

(is_long: bool, collateral_amount: u64) for the mint and (is_long: bool, token_amount: u64) for the other two. They are a fee-free single-leg mint and redeem at the base NAV, with no worst-case band and no minimum, and a burn with no payout. The mint must leave the collateral ratio at or above the lower of its pre-trade value and 100% (SingleSideMintStressFloor). The redeem must leave it at or above 100%. Users have no single-leg redeem.

rebalance_collateral(amount: u64)

Moves up to amount of collateral from one market’s vault to another’s. The signer must be the keeper of both markets, and the two markets must differ (InvalidAmount). The move is capped at the smallest of the request, the source’s collateral above its own target ratio at the stored price, and its liquid collateral (total_collateral − deployed_to_yield). A request above the cap moves the capped amount, and the log line shows both figures. It fails only when the source has nothing above target (InsufficientCollateral) or no liquid collateral (InsufficientLiquidCollateral). The source’s price must be at most 300 s old. The move is a plain token transfer from the source market’s vault to the other market’s vault, signed by the source Market PDA. If the two markets share a vault, the transfer would move nothing while both ledgers changed, so the program refuses it (InvalidAmount).

deploy_collateral_to_yield, recall_collateral_from_yield, harvest_collateral_yield

(amount: u64, _protocol: u8) for deploy and recall, (yield_amount: u64) for harvest. Deploy and recall move collateral to and from a lending venue through the keeper’s token account. A deployment must stay within yield_ceiling_bps of total_collateral. Harvest moves yield_amount from the keeper’s token account into market.collateral_vault and adds it to total_collateral and total_yield_harvested, so harvested yield raises the market’s collateral ratio. Collateral above the target can still reach the CLP through withdraw_excess_to_clp. Harvest’s fourth account was named clp_vault and received the yield until 11 Oct 2026. It is now collateral_vault, in the same position, and must be the market’s own vault. Every live market has yield_enabled = true. Nothing is deployed on devnet: the keeper’s emulator calls harvest every hour with cUSDC minted at the rate its mainnet lending plan would earn.

CLP PDA (CPI only)

redeem_single_for_vault(is_long: bool, token_amount: u64)

Like keeper_redeem_single, but it burns from CLP-owned token accounts and pays the market’s CLP vault. The signer must be the CLP PDA [b"clp", market], so it runs only through the CLP program’s vault_redeem_single.

Market authority (signer = market.authority)

update_market(mint_fee_bps?, redeem_fee_bps?, is_active?, dev_tax_pct?)

Fees at most 1,000 bps each; dev_tax_pct at most 25. The keeper posts fees through this instruction.

update_keeper_authority(new_keeper_authority), update_market_oracle(oracle_type), update_fee_recipient(new_recipient)

Rotate the keeper signer, repoint the oracle account (validated like initialize_market), or change the fee recipient.

configure_risk_fees(params: ConfigureRiskFeesParams)

ConfigureRiskFeesParams { proxy_fee_multiplier, stress_fee_multiplier, stress_mint_cap, allow_mint_in_stress, allow_redeem_in_stress }, every field optional. Sets the fields of risk_fee_config; a field left None is unchanged. Multipliers use 100 = 1×. The struct holds the separate arguments the instruction took until 11 Oct 2026, with the same fields in the same order, so the bytes on the wire are unchanged.

configure_worst_case_quoting(enabled?, normal_multiplier?, proxy_multiplier?, stress_multiplier?)

Sets worst_case_config. Multipliers use 100 = 1×.

configure_yield_ceiling(ceiling_bps?, enabled?)

Sets yield_ceiling_bps (at most 10,000) and yield_enabled.

reanchor_gamma()

Folds γ into the short anchor: initial_s_price becomes floor(initial_s_price × γ), γ resets to 1.0 and gamma_epoch goes up by one. Every holder’s claim stays the same at every price; the fold only resets how far γ can still fall. It does not reset the anchor to the spot price. Calls must be at least 14 days apart. Emits GammaReanchored. As of 10 Oct 2026 no market has been re-anchored.

apply_corporate_action(params: CorporateActionParams)

CorporateActionParams { ratio_num, ratio_den, effective_ts }, as new shares to old shares (a 2-for-1 split is 2:1). It folds the ratio into price_adjust_num and price_adjust_den and touches no price, supply or collateral. From then on each incoming keeper price is multiplied by the ratio before it is stored, which keeps P continuous across the split. An effective_ts at or before now applies immediately. A later time, up to 400 days ahead, arms the ratio for the ex-date; only one action can be armed at a time. Ratios beyond 1,000:1 are rejected. Emits CorporateActionApplied. As of 10 Oct 2026 no action is armed.

withdraw_excess_to_clp(amount: u64)

Moves collateral above the market’s target ratio into the market’s CLP vault. It needs a price at most 300 s old and moves only liquid collateral.

mark_yield_impairment(amount: u64)

Writes down a lost yield deployment by reducing both deployed_to_yield and total_collateral, so the redeem haircut sees the loss.

configure_market_advance(params: ConfigureAdvanceParams)

Sets the market’s advance cap, max_advance_bps of total_collateral (at most 10,000; 0 turns advances off), and its lender, creating the MarketAdvance on first use. The lender must be the CLP treasury vault, the cUSDC account of the GlobalClp PDA (InvalidAdvanceLender). It can change only while nothing is outstanding (AdvanceLenderLocked). Emits MarketAdvanceConfigured.

receive_advance(amount: u64)

Takes a treasury advance into the collateral vault. Both market.authority and the funder who owns the lender account sign; the CLP’s advance_to_market calls it with the GlobalClp PDA as funder. The cash is not added to total_collateral. The total outstanding can’t pass the cap (AdvanceCapExceeded). Emits AdvanceReceived.

repay_advance(amount: u64)

Pays advance cash back from the vault to the lender. Signer: market.authority or the keeper. It can’t repay more than is outstanding (AdvanceRepayExceedsOutstanding), and the vault must keep the market’s own liquid collateral (AdvanceWouldTakeOwnLiquidity). Emits AdvanceRepaid.

write_off_advance(amount: u64)

Charges a venue loss to the outstanding advance first, because the treasury is first-loss capital. It lowers deployed_to_yield and outstanding by amount and moves no cash. A loss larger than the advance goes through mark_yield_impairment, which refuses while any advance is outstanding (AdvanceOutstanding). Emits AdvanceWrittenOff.

create_token_metadata(name, symbol, uri)

Creates Metaplex metadata for the market’s L or S mint. The Market PDA signs as mint authority; the signer pays and becomes update authority. Limits: name 32 bytes, symbol 10, URI 200.

realloc_market()

Grows a Market account to 899 bytes after an upgrade that adds fields, with the signer paying the extra rent. It checks the owner, the discriminator and the authority stored at bytes 8 to 40. An account that is already large enough is left alone.

close_market_admin()

Zero-fills a market account and returns its lamports to the signer. For an account in the current Market layout it fails with MarketNotEmpty unless L supply, S supply, total_collateral and deployed_to_yield are all 0.

fund_collateral(amount: u64)

Tops up a market’s collateral from the authority’s own cUSDC account. It transfers amount into market.collateral_vault and adds it to total_collateral. Like donate_to_vault, it grants no fee waiver and has no price check. Emits CollateralFunded.

fund_collateral_from(amount: u64)

fund_collateral with a separate funder, so a program PDA such as the CLP treasury can fund a market through CPI. Added on 11 Oct 2026. It moves amount cUSDC from the funder’s account into the market’s vault, adds it to total_collateral and emits CollateralFunded. Accounts, in order: The CLP’s backstop_market calls it with the GlobalClp PDA as funder. The keeper sends that backstop when a market falls below 100% or stays short of its target.

enable_multi_collateral(), add_collateral_vault(collateral_type, weight_bps), update_collateral_vault(collateral_mint, is_active?, weight_bps?)

Manage the collateral_vaults list. Mint and redeem don’t read it, and it is empty on every market.

Events

is_keeper is true when the user is the market authority, the keeper or the market’s CLP PDA.

Errors

mint-redeem errors run from 6000 to 6062. Codes 6029 and 6030 are reserved placeholders that are never raised. Codes 6048 to 6052 belonged to the unified vault and are no longer returned; they stay in the enum so later codes don’t shift. Codes 6053 to 6059 come from the redemption requests and the treasury advance, and 6060 to 6062 from the exposure cap. Every code, with its message and cause, is in Errors.

TypeScript

Read a market’s price, NAV and freshness. The IDL and its generated type come from IDL files.
These are base NAVs. Mint and redeem quote each leg against the user by the confidence band described in pricing and fees. For a full mint and redeem script, see the example.

See also

Mint flow

Step-by-step mint_paired walkthrough.

Redeem flow

Step-by-step redeem_paired walkthrough.