Skip to main content
The CLP program is Continuum’s treasury and market maker. It holds protocol LP capital, runs the v2 trading venues - native concentrated-liquidity pools for instant swaps and a commit-then-settle path for size - and mints/redeems its own L+S inventory via CPI into mint-redeem. Per-market Clp PDAs hold inventory budgets, the OI cap, and swap-guard state. A single GlobalClp PDA tracks total deposits and routes capital to per-market vaults.
User-facing instructions: pool_swap, clp_swap, commit_swap, settle_swap (permissionless crank), and cancel_swap. Everything else is gated by admin or keeper authority. Liquidity lives inline in each market’s native Pool accounts (no per-bin rent), and repositioning a book is a single transaction.

The two swap venues

The pool book is deliberately lean - posted depth is a written option against latency arbitrage, and idle bid cUSDC forgoes carry, so the keeper sizes it to organic demand, not to TVL. Committed orders carry the size: because the fill price is set strictly after the commitment, latency arbitrage is structurally impossible and no depth needs to be posted in advance. clp_swap (the oracle-pegged swap at NAV ± spread from CLP inventory) remains live as a fallback to the pool book.

Accounts

GlobalClp PDA

One per cluster.

Clp PDA

One per market.

SwapGuardsState

Persisted on Clp; configured via configure_swap. The all-zero default keeps swaps off until configured.

Pool PDA

One per (market, side) - a native concentrated-liquidity book for one synth side vs cUSDC. The entire book lives inline in the account (max 16 bins per side, POOL_MAX_BINS), so there is no per-bin rent. Two pool-owned token accounts hold the actual reserves: a synth vault (asks) and a cUSDC vault (bids). Bin geometry. Ask bin i sits at base · r^(i+1), bid bin i at base · r^-(i+1). The active price itself is the empty mid, so the minimum round trip costs ~2 steps. With 2-zone geometry the first core_bins steps use step_bps (fine - precise pricing near NAV) and the rest use wing_step_bps (wide - the same 16 bins span the full float range).

Order PDA (committed orders)

A commit-then-settle order: the user escrowed amount_in at created_at, and the fill executes at the first oracle push strictly after that. The byte offsets are load-bearing: the keeper discovers pending orders with getProgramAccounts memcmp filters at user@8, market@40, status@74. Every terminal path (settle, refund, cancel) closes both the order and the escrow, returning all rent to the user - which also makes concurrent settle attempts idempotent (the loser fails at account load).

AdminWithdrawWindow PDA

Rolling 24h cap state for admin_withdraw: window_start_at, cumulative_in_window.

Instructions

Swaps (permissionless)

pool_swap(direction: u8, amount_in: u64, min_out: u64)

Swap against a pool’s bin book. direction: 0 = buy synth (pay cUSDC), 1 = sell synth (receive cUSDC). The handler walks the bins - buys consume ask bins upward from base·r, sells consume bid bins downward from base/r - decrementing reserves in place so the stored book always matches the vault balances. Partial fills round in the pool’s favor. Rejects with SwapDisabled if the pool is disabled or frozen, SlippageExceeded if output < min_out, and InsufficientFunds on an empty book. Accounts: user (signer), user_usdc, user_synth, pool, synth_vault, usdc_vault, token program. The pool PDA signs the outbound transfer.

clp_swap(side: u8, direction: u8, amount_in: u64, min_out: u64)

The oracle-pegged instant swap: trade cUSDC ↔ one synth side at NAV ± spread_bps, filled from CLP inventory. Delta-neutral by construction - the tokens being moved are already-outstanding, fully-backed supply, so total supply and collateral are unchanged; only ownership moves. This path never mints; the keeper replenishes inventory separately via vault_mint_pairs. Every call runs the full guard battery from SwapGuardsState: enabled/frozen, NAV staleness, oracle confidence, deviation vs the last accepted price, the rolling-window notional cap, and the market-level collateral floor.

commit_swap(side: u8, direction: u8, amount_in: u64, min_out: u64, nonce: u64)

Open a committed order: escrow amount_in (cUSDC for buys, synth for sells) into the order-owned escrow account. The payout ATA must exist at commit time so settlement can never strand on a missing account. Fails fast if the market is inactive or swaps are disabled/frozen (settle re-checks authoritatively). Emits OrderCommitted. Constants: COMMIT_TTL_SECS = 180, COMMIT_SPREAD_BPS = 10. The committed spread is thinner than the instant path’s because a fill priced strictly after the commitment carries no adverse selection - there is no stale quote to select against.

settle_swap() (permissionless crank)

Fill a pending order at NAV(now) ± 10 bps. Anyone may crank it; the keeper does in practice. Valid only when:
  • now ≤ expires_at (else OrderExpired),
  • market.twap_updated_at > order.created_at - the freshness rule that makes latency arbitrage impossible (else NoFreshOraclePush),
  • the oracle health guards pass, and the market collateral floor holds.
Outcome semantics: Committed fills advance the deviation anchor (swap_prev_price) but do not consume the rolling window cap - the cap bounds stale-price extraction, which a committed fill carries none of.
Mainnet prerequisite (flagged in the code): settlement currently reads the keeper-relayed user_twap_price. Before any mainnet deploy it must read Pyth directly and enforce publish_time > order.created_at.

cancel_swap() (order owner)

Refund an expired order. Allowed strictly after expires_at (settle owns ≤, cancel owns > - no gap, no overlap; else OrderNotExpired). Deliberately checks nothing else - no frozen/enabled/oracle/active gates: escrowed funds are always recoverable after the TTL regardless of protocol state. Returns the escrow and all rent to the user, emits OrderCancelled.

Native pool management (admin / keeper)

init_pool(side: u8, step_bps: u32, num_bins: u8) (admin)

Create the per-(market, side) Pool PDA. step_bps ∈ (0, 1000], num_bins ∈ [1, 16]. The pool starts disabled with an empty book; the first set_pool_shape enables it.

set_pool_shape(base_price_1e6: u64, asks: Vec<u64>, bids: Vec<u64>) (keeper)

The reposition: set the active price (≈ NAV) and the per-bin reserves in one call. Each vector must be exactly num_bins long and sum to no more than the corresponding vault balance. This is also how the book recaptures LVR - repricing to NAV instead of being walked there. The keeper shapes the book with a geometric decay per side, typically skewed (protective skew: thin/steep asks, deeper bids for an up-drifting asset).

configure_pool_geom(wing_step_bps: u32, core_bins: u8) (keeper)

Configure the 2-zone bin geometry: fine core_bins near NAV at the pool’s step_bps, then wing_step_bps for the outer wings. Validated: wing ≥ core step and core_bins ≤ num_bins. wing_step_bps = 0 resets to the uniform ladder.

realloc_pool() (pool authority)

Grow a pre-2-zone Pool account to the current layout. Raw realloc with zero-fill - new fields land as 0, which bin_geom() reads as the uniform ladder, so legacy pools keep behaving until configured.

pool_sweep_usdc(amount: u64) / pool_sweep_synth(amount: u64) (keeper)

Sweep accumulated cUSDC out of a pool’s bid vault back to the per-market CLP vault, or trim excess synth from the ask vault back to CLP custody. One-directional buy flow converts ask inventory into cUSDC that piles up in the pool; the sweep recycles it through vault_mint_pairs into fresh L+S (the elastic refill). The synth sweep exists because ask depth far beyond the bid side’s value is pure arb bait - trimmed synth serves committed-order provisioning instead.

clp_fund_pool_synth(amount: u64) (keeper)

Move synth from the CLP’s L/S ATA into a pool’s ask vault - the refill leg after vault_mint_pairs. The inventory was already counted at mint time, so no counters change; this just stages tokens for the next set_pool_shape.

Mint/redeem inventory (keeper)

The CLP mints and redeems on its own behalf via CPI into mint-redeem, with the CLP PDA acting as the “user”.

vault_mint_pairs(usdc_amount: u64)

CPI into mint_redeem::mint_paired using per-market vault cUSDC. Minted L+S land in the CLP PDA’s ATAs; vault_l_deployed / vault_s_deployed advance by the actual deltas.

vault_redeem_pairs(l_amount: u64, s_amount: u64)

Reverse: burn L+S held by the CLP PDA, cUSDC returns to the per-market vault.

vault_redeem_single(is_long: bool, token_amount: u64)

Redeem one side at NAV via mint_redeem::redeem_single_for_vault. Used by the pool depth balancer to convert one side’s excess into cUSDC for re-pairing. The inner instruction enforces a post-redeem collateralization floor of 100%, so an underwater vault refuses the operation.

clp_burn_excess(is_long: bool, amount: u64) (admin)

Burn asymmetric L or S residue held by the CLP without any collateral payout (CPI into permissionless_burn). Recovery/cleanup path.

Capital flow

admin_fund(amount: u64) (admin)

Deposit cUSDC from the admin wallet into global_clp_vault. Advances total_deposits and stamps last_admin_fund_at.

admin_withdraw(amount: u64) (admin)

Withdraw idle cUSDC from the global vault. Two protections against a compromised admin key: a rolling 24h cap of 10% of cumulative funded principal, and a 1h settlement cooldown after any admin_fund (so the cap denominator can’t be flash-pumped). Capital already allocated to markets must first come back via return_to_global.

allocate_to_market(amount: u64) / return_to_global(amount: u64) (keeper)

Move cUSDC between global_clp_vault and a per-market clp_vault, updating allocated_from_global and the global per-market allocation counters.

deposit_profit(amount: u64) (keeper)

Route arb/spread profit from the keeper wallet into the global vault, minus a TVL-tiered dev tax to admin_wallet: 15% below $5M TVL, 10% to $25M, 5% above.

collect_fees(amount: u64) (keeper)

Transfer trading fees into the per-market vault; advances total_fees_collected.

Configuration (admin)

  • configure_swap(enabled, frozen, spread_bps, max_staleness_secs, max_confidence_bps, max_deviation_bps, window_cap_usd, window_secs, collateral_floor_bps) - all args optional; None leaves a field unchanged. frozen = true is the hard kill switch.
  • update_oi_cap(new_cap) - per-market OI ceiling.
  • configure_inventory_budget(...) - soft and hard q-imbalance + drawdown bounds.
  • configure_yield_strategy(...) - per-state deployment percentages (yield is off by default).
  • configure_liquidity_seeder(threshold) - below threshold the seeder skips. Default $1k.
  • realloc_clp() - grow a pre-upgrade Clp account to the current layout.

Inventory ops

  • update_inventory(volatile_value, stable_value) (keeper) - recompute current_q_bps, drawdown, and breach flags.
  • update_oi(new_oi) (keeper) - mirror market.total_l_supply into clp.current_oi; rejects past oi_cap.
  • check_inventory_restrictions() / get_yield_status() - read-only helpers, CPI-callable.

Yield (scaffolded, off)

record_deployment / record_withdrawal / record_harvest track deployments to lending venues; add_yield_to_buffer / absorb_loss maintain the global protocol buffer, which absorbs losses before they hit user collateral. All off by default.

Engineering note: Boxed accounts and the 4KB stack

The SVM caps each stack frame at 4096 bytes. Instructions with many deserialized TokenAccount/Mint payloads (commit_swap, settle_swap, vault_mint_pairs, …) heap-Box their accounts, and the mint-redeem CPIs are hand-rolled invoke_signed calls instead of generated CpiContexts - a large CPI context on the outer frame pushes it past the limit, which surfaces as a runtime access violation rather than a compile error. If you fork or extend the program, keep new account structs boxed.

Errors

Full error catalog

TypeScript

Committed-order and pool_swap examples

See also

CLP concept

Why CLP exists, the capital flow, the inventory bounds.

Reading state

OI utilization, vault balances, pool books.