Skip to main content

Native DEX

✅ Ready to Dev

This contract specification has passed business logic audit, technical review, cross-program integration check (OT + RWT + YD), fee architecture verification, and crank flow analysis. A developer can implement from this document.
Purpose-built AMM for trading OT and RWT tokens. Two pool types: constant-product (StandardCurve) for OT/RWT and third-party pairs, and single-sided bin-based concentrated liquidity (Monotonic Ladder) for RWT/USDC and RWT/USDY master pools. Pool creation is whitelisted. Swap fees split between LP holders (tracked per-side via Q64.64 accumulators on PoolState, claimable via claim_lp_fees) and Areal Finance (protocol fee in RWT), charged on top of the swap to preserve pool capitalization. OT pairs charge an additional 0.5% to the OT project treasury. Master pools route USDC→RWT trades to rwt_engine::mint_rwt when DEX ask is not competitive — no DEX fee on mint-path.
Upgradeable contract. Program upgrade authority = Team Multisig (Squads). Separate from config authority and pause authority.

Key Concepts

23 Instructions

Config, create pools, add/remove/zap liquidity, swap (with mint-routing branch), grow, compress, compound, nexus (init, deposit, withdraw profits, swap, add/remove LP, update manager), pause, authority

6 State Accounts

DexConfig, PoolState, PoolCreators, LpPosition, BinArray, LiquidityNexus

6 PDA Seeds

dex_config, pool_creators, pool, lp, bins, liquidity_nexus

Two Pool Types

StandardCurve

Constant product: x × y = k
  • Simple, proven AMM math
  • Continuous pricing: price = reserve_b / reserve_a
  • LP shares: sqrt(a × b) on first add, proportional after
  • Used for: OT/RWT pairs, third-party pairs

Monotonic Ladder

Single-sided concentrated liquidity (bid-only)
  • 1000 bins per pool, log-scale, bin_step_bps = 10 (0.1%) default → covers NAV growth of ~2.7× from initial anchor
  • Three zones: permanent tail (immovable USDC at initial NAV − 1%), active bid wall (geometric density r = 0.85 around NAV), organic ask (RWT accumulated from user sells; not pre-funded)
  • Nexus LP deposits USDC only — no RWT pre-deposit
  • Swap(USDC→RWT) routes to rwt_engine::mint_rwt when organic ask exhausted or DEX price > NAV × 1.005 (mint becomes cheaper)
  • grow_liquidity (Rebalancer) extends bid wall rightward as NAV rises — never moves permanent tail, never touches organic ask
  • compress_liquidity (Rebalancer) recenters density on writedown — frozen ask RWT above new NAV retained for recovery
  • Used exclusively for: RWT/USDY and RWT/USDC master pools
Why single-sided? RWT has monotonically non-decreasing NAV and a permissionless mint_rwt instruction that produces RWT at a deterministic ceiling price of NAV × 1.01. This makes any pre-funded RWT ask-side above NAV × 1.005 economically dead capital — buyers would always prefer mint. The Monotonic Ladder puts 100% of Nexus LP into the scarce resource (USDC bid depth) and delegates ask-side to the vault itself via mint routing.

Fee Architecture

1

Base Fee

Applied to every swap. Default: 50 bps (0.5%). Max: 1,000 bps (10%). Set per pool at creation, copied from DexConfig.
2

Fee Split

  • LP Fee (default 50% of base) → accrued per-side via Q64.64 cumulative-per-share accumulators on PoolState (cumulative_fees_per_share_a / _b). Fees stay inside the pool’s reserve vaults (vault_a / vault_b); pool_state.reserve_a / _b are not diluted. LP holders claim at any time via claim_lp_fees and receive both token sides — no vesting, no merkle proof, no off-chain server.
  • Protocol Fee (default 50% of base) → always in RWT, transferred to areal_fee_destination (Areal Finance RWT ATA).
3

Fees On Top (No Pool Dilution)

All fees are charged on top of the swap amount in RWT. Pool reserves are never reduced by fees — total pool capitalization stays intact. If user sells RWT: user pays amount_in + fees (extra RWT on top). If user buys RWT: user receives amount_out in full, fees are taken separately from the RWT output gross before delivery.
4

Always RWT

Every pool pairs with RWT (e.g., OT/RWT, RWT/USDC). All fees (LP, protocol, OT treasury) are always taken in RWT. No fee token designation needed, no conversion logic.
5

LP Fee → Per-Side Accumulator (Instant Claim)

LP fees stay inside the pool’s reserve vaults (vault_a and vault_b). The contract maintains two per-side Q64.64 cumulative-per-share accumulators on PoolState (cumulative_fees_per_share_a / _b); each LpPosition snapshots them on every interaction (fees_claimed_per_share_a / _b). LP holders claim at any time via claim_lp_fees and receive both sides of the pool — no vesting, no merkle proof, no off-chain server. Per-side claimable: (cumulative_fees_per_share_<side> − fees_claimed_per_share_<side>) × shares >> 64. The current swap implementation always accrues the LP fee on the RWT side, so for an RWT-paired pool only the RWT-side accumulator advances; the dual-side layout is forward-compat for non-RWT pairs.
6

OT Treasury Fee (OT pairs only)

Pools paired with an OT token charge an additional 50 bps (0.5%) on every swap — sent entirely in RWT to the OT project’s Treasury RWT ATA (controlled by Futarchy). Total effective fee for OT pairs: 100 bps (1%). Non-OT pools (e.g., RWT/USDC) are unaffected — standard 50 bps only. The OT treasury fee is calculated on the same gross amount as the base fee (no compounding). Stored as OT_TREASURY_FEE_BPS = 50 program constant — changeable only via program upgrade.
7

Mint-Path Fee Skip (master pools only)

When a Monotonic Ladder master pool routes USDC→RWT to rwt_engine::mint_rwt (organic ask exhausted or best ask price > NAV × 1.005), no DEX fee (LP or protocol) is charged. The 1% mint fee — 0.5% to RWT vault (NAV accrual) + 0.5% to Areal DAO — already fulfills the fee role, and LPs did not provide the RWT that the user receives. Charging both would double-tax the same trade. The swap instruction detects the routing path and branches fee logic accordingly.

Six Roles

🏛️ Authority

Team Multisig (after bootstrap)
  • update_dex_config — fees, destinations, rebalancer
  • update_pool_creators — whitelist management
  • propose_authority_transfer

🛑 Pause Authority

Team Multisig (Squads)
  • pause_pool / unpause_pool
Immutable — set at init, changed only via program upgrade.

⚖️ Rebalancer

Pool Rebalancer wallet (dedicated bot keypair)
  • grow_liquidity — extend bid wall rightward as NAV rises
  • compress_liquidity — recenter density on writedown
Set at init, changeable by authority via update_dex_config.

💧 Nexus Manager

Bot wallet (Areal Finance LP bot)
  • nexus_swap / nexus_add_liquidity / nexus_remove_liquidity
Manages Areal Finance LP positions. Changeable by authority via update_nexus_manager.

🏗️ Pool Creators

Whitelisted wallets (max 10)
  • create_pool / create_concentrated_pool
Closed platform — only approved creators. Managed by authority.

🌐 Permissionless

Any wallet
  • swap — trade tokens
  • add_liquidity / zap_liquidity / remove_liquidity
  • compound_yield — auto-compound RWT

Instructions

Initialization

Create global DEX configuration and pool creators whitelist. Called once.Parameters:
ParameterTypeDescription
areal_fee_destinationPubkeyAreal Finance RWT ATA — receives protocol fees in RWT. Set at init; updatable by authority via update_areal_fee_destination (validated as RWT ATA on update).
pause_authorityPubkeyEmergency pause signer (Team Multisig, immutable)
rebalancerPubkeyPool Rebalancer wallet — can call grow_liquidity and compress_liquidity
Caller: Deployer (one-time)Accounts:
  • authority (signer, mut) — deployer, pays for creation
  • dex_config (init) — PDA seed: ["dex_config"]
  • pool_creators (init) — PDA seed: ["pool_creators"]
  • system_program
Creates:
  • DexConfig PDA — global config singleton
  • PoolCreators PDA — whitelist (deployer auto-added as first creator)
Initial state:
  • base_fee_bps = 50 (0.5%)
  • lp_fee_share_bps = 5,000 (50% of fee to LP)
  • is_active = true

Pool Creation

Create a StandardCurve (constant product) pool for a token pair. No parameters — pool configuration (fee, type) is derived from DexConfig and accounts.Caller: Whitelisted pool creatorAccounts:
  • creator (signer, mut) — must be in pool_creators whitelist, pays for account creation
  • dex_config — validates is_active, provides base_fee_bps and lp_fee_share_bps
  • pool_creators — validates creator is in whitelist
  • pool_state (init) — PDA seed: ["pool", token_a_mint, token_b_mint]
  • token_a_mint — constraint: key < token_b_mint.key (canonical order enforced)
  • token_b_mint — constraint: key > token_a_mint.key
  • vault_a (init) — SPL token account for A, authority = pool_state PDA (keypair, not ATA)
  • vault_b (init) — SPL token account for B, authority = pool_state PDA (keypair, not ATA)
  • ot_treasury (optional) — OT Treasury PDA: ["ot_treasury", ot_mint]. Required when creating an OT pair. Must be owned by OT_PROGRAM_ID.
  • ot_treasury_rwt_ata (optional) — RWT ATA owned by ot_treasury PDA. Required when ot_treasury is provided.
  • token_program, system_program
Validation:
  • token_a_mint ≠ token_b_mint
  • One of token_a_mint or token_b_mint must be RWT_MINT (all pools pair with RWT — required for protocol fee in RWT)
  • token_a_mint < token_b_mint (lexicographic order — canonical PDA derivation, prevents duplicate pools with reversed order)
  • Creator must be whitelisted
  • If ot_treasury provided: verify PDA derivation ["ot_treasury", ot_mint] where ot_mint is the non-RWT mint. Verify account owner is OT_PROGRAM_ID. Verify ot_treasury_rwt_ata is the associated token address for (ot_treasury, RWT_MINT). Fails with InvalidOtTreasuryDestination if any check fails.
Initial state: Reserves = 0, total_lp_shares = 0, fee_bps copied from DexConfig. ot_treasury_fee_destination = ot_treasury_rwt_ata.key() if OT treasury accounts provided, otherwise None.
Per-pool fee_bps is copied from DexConfig at creation and immutable after that. Changing base_fee_bps in DexConfig only affects future pools. To change an existing pool’s fee, a program upgrade is required. Similarly, ot_treasury_fee_destination is set at creation and immutable.
Create a Monotonic Ladder master pool with a 1000-slot BinArray. Used exclusively for RWT/USDC and RWT/USDY.
ParameterTypeDescription
bin_step_bpsu16Log price step between bins (default: 10 = 0.1%)
initial_active_bini32Starting active bin ID — anchors initial NAV in log-scale
permanent_tail_offset_bpsi32Permanent-tail position below initial_active_bin. Default: 100 (= NAV − 1%). Immutable after init.
Caller: Whitelisted pool creatorAccounts: Same as create_pool plus:
  • bin_array (init) — PDA seed: ["bins", pool_state]. Size = 1000 bins × 24 bytes + overhead ≈ 32 KB, rent ~0.22 SOL.
Validation:
  • bin_step_bps > 0
  • One of token_a_mint or token_b_mint must be RWT_MINT
  • The non-RWT mint must be USDC_MINT or USDY_MINT (the Monotonic Ladder pattern is defined only for these two pairs)
  • token_a_mint < token_b_mint (canonical order)
  • Creator must be whitelisted
  • permanent_tail_offset_bps ≥ 30 (must be at least 0.3% below initial NAV so the active zone and the tail don’t overlap)
  • OT treasury validation: must be absent (master pools are not OT pairs)
Initial state:
  • 1000 empty bins. active_bin_id = initial_active_bin.
  • left_anchor_bin = initial_active_bin − permanent_tail_offset_bps / bin_step_bps — immutable, marks the top of the permanent tail.
  • permanent_tail_floor_bin = left_anchor_bin − 70 — i.e., the tail spans 70 bins (≈ 0.7% at 0.1% step) immediately below the left anchor. Also immutable.
  • No liquidity deployed at init — Nexus seeds via nexus_add_liquidity after pool creation.
The Monotonic Ladder is not a general-purpose concentrated pool. Its geometry, fee branching, and rebalance semantics are specialized for the RWT/USDx pair type. OT/RWT pools use StandardCurve.

Liquidity

Unified LP interface for StandardCurve pools. Users interact with add_liquidity, zap_liquidity, and remove_liquidity on OT/RWT and third-party pairs. LPs receive proportional shares and earn per-pool fee-vault rewards via claim_lp_fees.Master pools (Monotonic Ladder) are seeded exclusively by Nexus. User-level add_liquidity and zap_liquidity on RWT/USDC and RWT/USDY master pools fail with MasterPoolUserLpDisabled. Rationale: the Ladder is a single-sided USDC structure with protocol-managed geometry (permanent tail, active zone weights, mint routing). Accepting arbitrary user LP would break density invariants and create ask-side positions that undercut mint routing. Users who want yield exposure to master-pool flow can hold RWT directly (which appreciates via NAV growth) and arbitrage when market price lags NAV.Ask-side RWT entering master pools from user sells is the only way RWT ever accumulates in master-pool bins. It does not mint LP shares — it is organic inventory, owned by the pool for consumption by future buyers via bin walk.
Add liquidity to any pool (StandardCurve or Concentrated). Receive LP shares proportional to deposit. For Concentrated pools, tokens go to vaults and the Rebalancer distributes across bins separately.
ParameterTypeDescription
amount_au64Token A amount
amount_bu64Token B amount
Caller: PermissionlessAccounts:
  • provider (signer) — owns token accounts
  • payer (signer, mut) — pays rent for LpPosition init
  • pool_state (mut) — updates reserves, lp_shares
  • lp_position (init_if_needed) — PDA seed: ["lp", pool_state, provider]
  • provider_token_a (mut) — constraint: owner == provider, mint == pool.token_a_mint
  • provider_token_b (mut) — constraint: owner == provider, mint == pool.token_b_mint
  • vault_a (mut), vault_b (mut) — pool vaults
  • bin_array (mut, optional) — required for Concentrated pools (to distribute across bins)
  • token_program, system_program
Validation:
  • amount_a > 0 && amount_b > 0 (both required; use zap_liquidity for single-token)
  • Pool must be active
Math (same for both pool types):
  • First add: shares = sqrt(amount_a × amount_b), must be ≥ MIN_LIQUIDITY (1,000). Overflow guard: amount_a * amount_b must fit u128.
  • Subsequent: shares = min(amount_a × total_shares / reserve_a, amount_b × total_shares / reserve_b). The min() means excess tokens of one side are not deposited — only the proportional amount is used. Unused tokens remain in the user’s wallet.
Imbalanced deposits lose value. If user deposits at a ratio that differs from pool ratio, they effectively receive shares based on the LESSER side. Use zap_liquidity for imbalanced amounts — it auto-swaps to match the pool ratio first. Off-chain UIs should calculate optimal amounts before calling add_liquidity.
Effect: Transfers tokens to vaults, mints LP shares, updates reserves. Emits LiquidityAdded.
add_liquidity is available only on StandardCurve pools. On Monotonic Ladder master pools (RWT/USDC, RWT/USDY) user-initiated add_liquidity and zap_liquidity fail with MasterPoolUserLpDisabled. Bid-side growth on master pools happens exclusively through grow_liquidity funded from the Nexus accumulator. See the Monotonic Ladder section in Two Pool Types for rationale.
Add liquidity with any token ratio — including a single token. Contract auto-swaps to match pool ratio, then adds both sides. Works for both StandardCurve and Concentrated pools. Atomic — no risk of price change between swap and add.
ParameterTypeDescription
amount_au64Token A amount (can be 0)
amount_bu64Token B amount (can be 0)
min_sharesu128Minimum LP shares to receive (slippage protection)
Caller: PermissionlessAccounts:
  • provider (signer) — owns token accounts
  • payer (signer, mut) — pays rent for LpPosition init
  • pool_state (mut) — must be active
  • dex_config — for fee calculation
  • lp_position (init_if_needed) — PDA seed: ["lp", pool_state, provider]
  • provider_token_a (mut) — constraint: owner == provider, mint == pool.token_a_mint
  • provider_token_b (mut) — constraint: owner == provider, mint == pool.token_b_mint
  • vault_a (mut), vault_b (mut) — pool vaults
  • areal_fee_account (mut) — receives protocol fee from internal swap
  • ot_treasury_fee_account (mut, optional) — receives OT treasury fee from internal swap (required if OT pair)
  • bin_array (mut, optional) — required for Concentrated pools
  • token_program, system_program
Validation:
  • amount_a > 0 || amount_b > 0 (at least one token)
  • Pool must be active
Logic:
  1. Read current pool ratio. If pool is empty (reserves = 0): skip swap, treat as regular add_liquidity with both amounts as-is. Otherwise: target_ratio = reserve_a / reserve_b
  2. Calculate how much to swap to match ratio:
    If amount_a / amount_b > target_ratio:
      // Too much A — swap excess A → B
      excess_a = amount_a - (amount_b * reserve_a / reserve_b)
      swap_amount = excess_a / 2  (swap half of excess)
      Internal swap: A → B (same fee math as regular swap)
    Else:
      // Too much B — swap excess B → A
      excess_b = amount_b - (amount_a * reserve_b / reserve_a)
      swap_amount = excess_b / 2
      Internal swap: B → A
    
  3. After internal swap: pool ratio changed, recalculate optimal add amounts
  4. Add liquidity with balanced amounts (same math as add_liquidity)
  5. shares ≥ min_shares (slippage check)
  6. Emit ZapLiquidityExecuted
Single-token zap: If amount_a = 0, the entire amount_b is split — half swapped to token A, half kept as token B. Same in reverse. This is the simplest UX: “deposit USDC, get LP shares.”
Internal swap incurs the same fees as a regular swap and follows the same per-side Q64.64 accumulator model: the LP fee bumps pool_state.cumulative_fees_per_share_<rwt_side> (currently always the RWT side, per the today’s-swap-rule documented in claim_lp_fees) so LPs can claim their share via claim_lp_fees. Protocol fee → Areal Finance RWT ATA. OT treasury fee → OT Treasury RWT ATA (OT pairs only). This is by design — zap should not be a fee-free backdoor. LpPosition pinning: a fresh-init LP entering via zap snapshots fees_claimed_per_share_<side> to the post-bump cumulative, so the new shares get zero claimable from the bump they themselves triggered. An existing LP doing zap auto-claims their fair share of the zap-internal bump (bump_per_share × shares_pre) atomically before incrementing shares, then advances the snapshot to post-bump — preventing the newly minted shares from retroactively claiming against the same bump. Net invariant: total outstanding claim across all LPs from one zap-internal bump equals fee_lp exactly.
Remove liquidity from any pool. Burn LP shares, receive proportional tokens from total reserves.
ParameterTypeDescription
shares_to_burnu128LP shares to redeem
Caller: LP provider (must own the position)Accounts:
  • provider (signer) — must match lp_position.owner
  • pool_state (mut)
  • lp_position (mut) — constraint: owner == provider, pool == pool_state
  • provider_token_a (mut), provider_token_b (mut)
  • vault_a (mut), vault_b (mut)
  • token_program
Math (same for both pool types):
amount_a = shares_to_burn * reserve_a / total_lp_shares
amount_b = shares_to_burn * reserve_b / total_lp_shares
Validation:
  • shares_to_burn ≤ lp_position.shares
  • Works even when pool is paused (LP can always exit)
Effect: Pool PDA signs vault transfers. If lp_position.shares == 0 after burn, the LpPosition account is closed and rent returned to provider. Emits LiquidityRemoved.
Extend the active bid wall rightward as NAV rises. Rebalancer only. Operates on Monotonic Ladder master pools. Draws fresh USDC from the Nexus accumulator and redistributes existing active-zone USDC so the geometric density peak sits at the new NAV. The permanent tail is never touched. The organic ask (RWT above active_bin) is never touched.
ParameterTypeDescription
new_nav_bini32Target NAV bin ID calculated off-chain by Rebalancer: new_nav_bin = log(nav_book_value) / log(1 + bin_step_bps/10000)
active_zone_widthu16Total number of bins in the active bid wall (default: 40; ~4% price range at 0.1% step). Centered-right of new_nav_bin, extending down through the extended bid below
Caller: Rebalancer only (must match dex_config.rebalancer)Accounts:
  • rebalancer (signer)
  • dex_config — validates rebalancer
  • pool_state (mut) — must be Monotonic Ladder type
  • bin_array (mut)
  • liquidity_nexus (mut) — source of fresh USDC
  • nexus_usdc_ata (mut) — Nexus’s USDC account (debited)
  • pool_vault_b (mut) — pool’s USDC vault (credited)
  • token_program
Validation:
  • Pool must be Monotonic Ladder type
  • new_nav_bin > pool_state.last_rebalance_nav_bin (growth rebalance — strictly rightward). For leftward moves use compress_liquidity.
  • new_nav_bin − pool_state.left_anchor_bin ≤ MAX_BINS − 10 (leave buffer at the right edge of the BinArray — prevents overflow)
  • Computed active-zone lower bound new_nav_bin − active_zone_width/2 ≥ left_anchor_bin + 1 (cannot overlap permanent tail)
  • pool_state.is_active
  • new_nav_bin must round-trip the live NAV: |price_at_bin(new_nav_bin) − rwt_vault.nav_book_value| ≤ rwt_vault.nav_book_value × pool_state.bin_step_bps × 2 / 10_000. Reverts NavBinMismatch otherwise. Defends against a compromised Rebalancer passing an arbitrary bin; the × 2 factor covers floor-rounding ambiguity at bin boundaries plus a small intra-tx NAV drift.
Logic (asymmetric: grow right, redistribute existing):
  1. Define ranges:
    tail_lower    = pool_state.permanent_tail_floor_bin
    tail_upper    = pool_state.left_anchor_bin
    old_active_lo = pool_state.active_zone_lower
    new_active_lo = new_nav_bin − active_zone_width / 2
    new_active_up = new_nav_bin                         // active zone ends at NAV; above NAV is organic ask
    
  2. Compute target density weights (geometric, r = GEOMETRIC_R_BPS / 10000, default 8500 = 0.85):
    for bin in [new_active_lo .. new_active_up]:
      d = new_nav_bin − bin                  // distance to peak (0 at NAV, larger below)
      weight(bin) = r^d
    total_weight = sum(weight(bin))
    
  3. Determine required USDC:
    current_active_usdc = sum(bin.liquidity_b for bin in [old_active_lo .. new_nav_bin])
    nexus_available     = nexus_usdc_ata.balance
    // Target: geometric-weighted capital in the new active zone, scaled up by the growth factor
    target_total_usdc   = current_active_usdc * GROWTH_SCALING + nexus_available
    
  4. Pull from Nexus: transfer nexus_available USDC from nexus_usdc_atapool_vault_b. Nexus accumulator is drained toward zero (residual stays for next cycle).
  5. Redistribute across new active zone:
    for bin in [new_active_lo .. new_active_up]:
      target_bin_usdc = target_total_usdc * weight(bin) / total_weight
    
    // Diff rebalance: move USDC from over-funded to under-funded bins within the active zone.
    // Bins in [old_active_lo .. new_active_lo − 1] are NOT drained — they become extended bid.
    
  6. Permanent tail: [tail_lower .. tail_upper] untouched. Never rebalanced in any operation.
  7. Organic ask: [new_nav_bin + 1 .. MAX_BINS] untouched. Contains RWT from prior user sells, if any.
  8. Update state: pool_state.last_rebalance_nav_bin = new_nav_bin, pool_state.active_zone_lower = new_active_lo, active_bin_id NOT changed by grow_liquidity (only swaps move it).
Ladder shape after multiple growths:
permanent tail       extended bid         active bid wall    organic ask
▁▁▁▁                 ▂▂▃▃▃▄▄▄▄▄          ▅▆▇█              ▁░░                    (time t0)
▁▁▁▁                 ▂▂▃▃▃▄▄▄▄▄▄▄▄▄▄    ▅▆▇█              ░░                     (after grow at t1)
▁▁▁▁                 ▂▂▃▃▃▄▄▄▄▄▄▄▄▄▄▄▄▄ ▅▆▇█              ░                      (after grow at t2)
└ NAV-1% floor       └ old active zones  └ current NAV     └ RWT (user sells)
Effect: USDC enters pool via Nexus. Pool capitalization grows strictly. Emits LiquidityGrown.
Recenter the active bid wall on a lower NAV after governance writedown (rwt_engine::adjust_capital). Rebalancer only. Rare. No token inflow — purely a weight redistribution within existing capital.
ParameterTypeDescription
new_nav_bini32Lower target NAV bin ID
active_zone_widthu16Same semantics as grow_liquidity
Caller: Rebalancer onlyAccounts:
  • rebalancer (signer), dex_config, pool_state (mut), bin_array (mut)
Validation:
  • Pool must be Monotonic Ladder type
  • new_nav_bin < pool_state.last_rebalance_nav_bin (leftward only; for rightward use grow_liquidity)
  • new_nav_bin > pool_state.left_anchor_bin + 10 (new NAV must stay above the permanent tail)
  • new_nav_bin must round-trip the live NAV: |price_at_bin(new_nav_bin) − rwt_vault.nav_book_value| ≤ rwt_vault.nav_book_value × pool_state.bin_step_bps × 2 / 10_000. Reverts NavBinMismatch otherwise. Defends against a compromised Rebalancer passing an arbitrary bin; the × 2 factor covers floor-rounding ambiguity at bin boundaries plus a small intra-tx NAV drift.
Logic:
  1. Define new active zone: [new_nav_bin − active_zone_width/2 .. new_nav_bin].
  2. Absorb former active zone into extended bid: bins that were in the active zone but now sit above new_nav_bin (they held USDC at peak density) merge into the extended bid — they become natural stress-buffer depth immediately.
  3. Recompute geometric density weights around new_nav_bin using only the existing USDC capital in the new active zone (no Nexus pull on compress).
  4. Redistribute USDC within the new active zone per weights. Former extended bid bins far below new NAV stay as-is.
  5. Organic ask is NOT touched. RWT that was in ask bins above the old NAV but is now above the even-lower new NAV remains there as a frozen ask wall — if NAV recovers via future yield, those positions become productive again without any rebalance.
  6. Permanent tail is NOT touched.
  7. Update state: pool_state.last_rebalance_nav_bin = new_nav_bin, pool_state.active_zone_lower = new_active_lo.
Capital conservation: total USDC in pool vault is unchanged. Total RWT in pool vault is unchanged. Only the weight distribution within the active zone changes.Effect: Emits LiquidityCompressed.

Swap

Swap tokens through any pool. Contract automatically uses the correct math based on pool type: constant product for StandardCurve, bin-walk for Concentrated.
ParameterTypeDescription
amount_inu64Input token amount
min_amount_outu64Minimum output (slippage protection)
a_to_bboolSwap direction
Caller: PermissionlessAccounts:
  • user (signer)
  • pool_state (mut) — must be active
  • dex_config — must be active
  • bin_array (mut, optional) — required only for Concentrated pools
  • user_token_in (mut), user_token_out (mut)
  • vault_in (mut), vault_out (mut)
  • areal_fee_account (mut) — receives protocol fee
  • ot_treasury_fee_account (mut, optional) — receives OT treasury fee. Required if pool_state.ot_treasury_fee_destination is Some. Must match stored address.
  • token_program
Fee math (both pool types). All fees in RWT, charged on top:If input = RWT (user sells RWT):
// Fees calculated on swap amount
fee_total = amount_in * fee_bps / 10,000
fee_lp = fee_total * lp_fee_share_bps / 10,000
fee_protocol = fee_total - fee_lp

// OT Treasury fee (only if ot_treasury_fee_destination is Some)
ot_treasury_fee = amount_in * OT_TREASURY_FEE_BPS / 10,000  // 50 bps on gross amount

// User pays fees ON TOP — total debit from user wallet:
user_total_debit = amount_in + fee_total + ot_treasury_fee
// Full amount_in goes into pool (no fee deduction from reserves)
amount_out = constant_product(amount_in)
If input = OT (user buys RWT):
amount_out_gross = constant_product(amount_in)

// Fees calculated on gross RWT output
fee_total = amount_out_gross * fee_bps / 10,000
fee_lp = fee_total * lp_fee_share_bps / 10,000
fee_protocol = fee_total - fee_lp

// OT Treasury fee (only if ot_treasury_fee_destination is Some)
ot_treasury_fee = amount_out_gross * OT_TREASURY_FEE_BPS / 10,000  // 50 bps on gross amount

// Fees deducted from gross output — user receives net:
amount_out = amount_out_gross - fee_total - ot_treasury_fee
Fee destinations (both directions):
  • fee_lp → stays inside the pool’s reserve vault on the fee-bearing side (currently always the RWT side); the contract bumps pool_state.cumulative_fees_per_share_<side> so LP holders can claim their share later via claim_lp_fees
  • fee_protocol → RWT transferred to areal_fee_account (Areal Finance RWT ATA)
  • ot_treasury_fee → RWT transferred to ot_treasury_fee_account (OT pairs only)
  • For non-OT pools, ot_treasury_fee = 0 (no additional fee)
Reserve updates after swap (for programmer):
// Reserves stay intact — fees never touch pool liquidity
reserve_in  += amount_in   // full swap amount enters pool
reserve_out -= amount_out_gross  // gross amount leaves pool (before fee split)
// All fees are external to pool reserves
total_fees_accumulated += fee_total + ot_treasury_fee
StandardCurve output:
amount_out = reserve_out * net_input / (reserve_in + net_input)
Concentrated output (bin walk):
remaining = net_input  (or amount_in if fee taken from output)
total_out = 0
current_bin = active_bin_id

while remaining > 0:
  bin = bin_array[current_bin]
  
  if a_to_b (selling RWT for USDC):
    available = bin.liquidity_b  (USDC in this bin)
    price = (1 + bin_step_bps/10000) ^ current_bin
    consumable = min(remaining * price, available)  // cast u128
    total_out += consumable
    remaining -= consumable / price
    bin.liquidity_b -= consumable
    bin.liquidity_a += consumable / price
    if bin.liquidity_b == 0:
      current_bin -= 1  // move to cheaper bin
      if current_bin < lower_bin_id: break  // no more liquidity
  
  else (buying RWT with USDC):
    available = bin.liquidity_a  (RWT in this bin)
    price = (1 + bin_step_bps/10000) ^ current_bin
    consumable = min(remaining / price, available)  // cast u128
    total_out += consumable
    remaining -= consumable * price
    bin.liquidity_a -= consumable
    bin.liquidity_b += consumable * price
    if bin.liquidity_a == 0:
      current_bin += 1  // move to more expensive bin
      if current_bin > upper_bin_id: break

active_bin_id = current_bin
amount_out = total_out
Validation:
  • amount_in > 0
  • Pool reserves non-zero: reserve_in > 0 && reserve_out > 0 (StandardCurve) or at least one bin has liquidity (Concentrated). Fails with EmptyReserves if pool has no liquidity.
  • amount_out ≥ min_amount_out (slippage check)
  • amount_out > 0
  • Pool and DEX must be active
Effect: User sends input → vault. Pool sends output → user. LP fee stays inside the pool’s reserve vault on the fee-bearing side; pool_state.cumulative_fees_per_share_<side> is bumped so LP holders can claim later. Protocol fee → areal_fee_account. OT treasury fee → ot_treasury_fee_account (OT pairs only). All fees in RWT, charged on top of swap — pool reserves are not diluted. Emits SwapExecuted.
Monotonic Ladder mint-routing branch (USDC→RWT on master pools only):Before executing the standard bin walk for a_to_b = false on a Monotonic Ladder pool, the swap instruction evaluates whether mint would give the user a better price:
nav = rwt_vault.nav_book_value                                 // read via CPI or account
best_ask_price = price_at_bin(active_bin_id + 1)              // cheapest RWT in the pool
has_organic_ask = exists bin > active_bin_id with liquidity_a > 0

mint_cheaper_threshold = nav * (10_000 + 50) / 10_000          // NAV × 1.005 (DEX becomes non-competitive above this)

if !has_organic_ask OR best_ask_price > mint_cheaper_threshold:
    // Route to mint — no DEX fee charged
    CPI rwt_engine::mint_rwt(amount_in_usdc)
      accounts = {
        user, rwt_vault, rwt_mint, user_deposit (USDC),
        user_rwt, capital_accumulator_ata, dao_fee_account
      }
    // User pays 1% mint fee inside rwt_engine — split 0.5% vault / 0.5% DAO
    // No LP fee, no DEX protocol fee. Pool state untouched.
    emit SwapRoutedToMint { user, usdc_in, rwt_out, nav }
    return

// Otherwise: standard bin walk from active_bin_id upward, consuming organic RWT
// Standard DEX fees apply (LP + protocol) as in base logic above
Required additional accounts for mint-route path (must be passed by caller; unused on bin-walk path):
  • rwt_vault (mut) — passed when pool is master pool
  • rwt_mint (mut)
  • capital_accumulator_ata (mut) — vault’s USDC ATA
  • dao_fee_account (mut) — matches rwt_vault.areal_fee_destination
  • user_rwt (mut)
Why this works:
  • Mint always succeeds if RWT Engine is not paused. Vault accepts unlimited USDC at current NAV + 1%.
  • Organic ask depletes naturally via this routing (when it exists) until its price rises above mint_cheaper_threshold.
  • Ask-side above NAV × 1.005 is never filled by LPs — routing makes it unnecessary. LP capital is not “waiting for nothing” in those bins.
Master-pool swap instruction must check pool_state.pool_type and !a_to_b && non_rwt_mint_is_USDC_or_USDY before invoking the mint-route branch. StandardCurve pools and OT/RWT pools never route to mint.

Yield Compounding

Claim RWT yield from Yield Distribution on behalf of the pool PDA, auto-compounding OT yield into pool reserves. Benefits all LP holders proportionally. Note: this is for OT yield only — LP swap fees are tracked separately via the per-side accumulator on PoolState and claimed individually via claim_lp_fees.
ParameterTypeDescription
cumulative_amountu64Pool’s cumulative share (from merkle leaf)
proofVec<[u8; 32]>Merkle proof path
Caller: Permissionless (crank)Accounts:
  • crank (signer, mut) — pays for ClaimStatus init
  • pool_state (mut) — updates reserves
  • target_vault (mut) — constraint: must be pool.vault_a or pool.vault_b (whichever is RWT)
  • YD CPI accounts: yd_distributor, yd_claim_status, yd_reward_vault
  • yd_program — constraint: key == YD_PROGRAM_ID
  • token_program, system_program
Logic:
  1. Snapshot target_vault balance before
  2. CPI → yield_distribution::claim with pool PDA as claimant
  3. Measure RWT received (after - before)
  4. Add received amount to pool reserves (reserve_a or reserve_b)
  5. Emit CompoundYieldExecuted
Pool PDA acts as an OT holder (if pool contains OT tokens in its reserves). Claimed RWT goes directly into reserves, increasing the value of all LP positions equally. No individual LP claim needed. target_vault mint is validated by YD program during CPI (claimant_token.mint == reward_vault.mint) — crank cannot redirect RWT to wrong vault. Only applicable to pools that hold OT (e.g., OT/RWT). Calling compound_yield on RWT/USDC pool will fail with InvalidProof (pool has no OT balance → not in merkle tree).
Claim accumulated LP swap fee rewards from the pool’s reserve vaults. Each LP holder claims proportionally to their share of total LP supply. No vesting — rewards are available instantly. Pays out both token sides of the pool (A and B); the side that the swap fee accrued on is the side that pays in this claim.Caller: LP holder (permissionless — anyone with an LP position)Accounts:
  • lp_holder (signer) — must own the LP position
  • pool_state — reads total_lp_shares and both cumulative accumulators
  • lp_position (mut) — reads shares, updates fees_claimed_per_share_a and fees_claimed_per_share_b
  • pool_vault_a (mut) — pool’s token-A reserve vault, owned by pool PDA
  • pool_vault_b (mut) — pool’s token-B reserve vault, owned by pool PDA
  • recipient_token_a_ata (mut) — holder’s token-A ATA, receives claimed A-side fees
  • recipient_token_b_ata (mut) — holder’s token-B ATA, receives claimed B-side fees
  • token_program
Logic:
delta_a = pool_state.cumulative_fees_per_share_a − lp_position.fees_claimed_per_share_a
delta_b = pool_state.cumulative_fees_per_share_b − lp_position.fees_claimed_per_share_b

claimable_a = (delta_a × lp_position.shares) >> 64    // Q64.64 → integer token amount
claimable_b = (delta_b × lp_position.shares) >> 64

if claimable_a == 0 && claimable_b == 0: return

if claimable_a > 0:
    transfer claimable_a token_A: pool_vault_a → recipient_token_a_ata (pool PDA signs)
if claimable_b > 0:
    transfer claimable_b token_B: pool_vault_b → recipient_token_b_ata (pool PDA signs)

lp_position.fees_claimed_per_share_a = pool_state.cumulative_fees_per_share_a
lp_position.fees_claimed_per_share_b = pool_state.cumulative_fees_per_share_b
Validation:
  • lp_position.owner == lp_holder.key()
  • pool_vault_a.amount ≥ claimable_a and pool_vault_b.amount ≥ claimable_b
No-op when both claimable amounts are zero: if claimable_a == 0 && claimable_b == 0, the instruction returns successfully without performing any SPL transfer.Effect: Claimed amounts transferred from each side’s reserve vault to the LP holder’s corresponding ATA. Emits LpFeesClaimed { claimable_a, claimable_b, recipient, ... } — the dual-side claim event.
How the per-side accumulator works. On each swap the contract bumps the appropriate side’s cumulative-per-share accumulator: cumulative_fees_per_share_<side> += (fee_lp << 64) / total_lp_shares. Each LpPosition snapshots this value (fees_claimed_per_share_<side>) on its last interaction. Pending fees are computed lazily as (cumulative − snapshot) × shares >> 64, which gives O(1) per-holder accounting without iterating over LP holders. The accumulator stores fees per share in Q64.64 fixed-point, not absolute fees — it never overflows even at high fee throughput.Today’s swap rule. The current swap implementation always accrues the LP fee on the RWT side of the pool. For an RWT-paired pool (RWT/USDC, OT/RWT, etc.) only one of the two accumulators ever advances per swap, so claimable_<non-RWT-side> is 0 for any RWT-paired LP position. The dual-side accumulator is forward-compat for non-RWT pairs (e.g. OT/USDC) and avoids special-cased single-side logic in the contract.

Liquidity Nexus

Areal Finance LP management. Nexus PDA owns LP positions and token accounts. Manager bot executes operations; funds are protected by the program (manager cannot extract tokens directly). LP fee rewards accrue per-side on the pool’s accumulators and are claimed to the Areal Treasury via nexus_claim_rewards.
Direct SPL Transfer policy. Tokens sent into a Nexus token account via a raw SPL Transfer (i.e., bypassing nexus_deposit or nexus_record_deposit) WILL increase the on-chain Nexus token balance but will NOT advance total_deposited_* — they are intentionally non-tracked. Such balances are still spendable by the manager via nexus_swap / nexus_add_liquidity, but nexus_withdraw_profits releases only the delta above the tracked principal floor, so untracked deposits effectively raise the withdrawable-profit ceiling. Do not rely on this for accounting; always route capital via nexus_deposit (USDC lane) or withdraw_liquidity_holding in Yield Distribution (RWT lane).
Create Liquidity Nexus PDA and set initial manager. One Nexus per DEX deployment (singleton).
ParameterTypeDescription
managerPubkeyInitial Nexus manager wallet (bot)
Caller: Authority (Team Multisig)Accounts:
  • authority (signer, mut) — must match dex_config.authority
  • dex_config — validates authority
  • nexus (init) — PDA seed: ["liquidity_nexus"]
  • system_program
Creates:
  • LiquidityNexus PDA — singleton, owns LP positions
Initial state: manager = manager param, is_active = true
Deposit tokens into Nexus PDA. This is the standard way to add capital to Nexus. Permissionless — cranks call this after OT revenue distribution or RWT yield claim.
ParameterTypeDescription
amountu64Token amount to deposit
Caller: Permissionless (crank)Accounts:
  • depositor (signer) — source wallet
  • nexus (mut) — validates is_active
  • depositor_token_account (mut) — constraint: owner == depositor
  • nexus_token_account (mut) — constraint: owner == nexus.key()
  • token_mint — validates accepted token type
  • token_program
Validation:
  • amount > 0
  • nexus.is_active == true
  • token_mint == USDC_MINT || token_mint == RWT_MINT (only accepted tokens)
Logic:
  1. Transfer amount from depositor → nexus ATA
  2. Transfer amount from depositor_token_account to nexus_token_account
  3. Emit NexusDeposited
USDC lane. OT distribute_revenue sends 10% USDC to an intermediate crank wallet, which then calls nexus_deposit directly. A future iteration may stage the USDC side through a holding PDA for parity with the RWT side.RWT lane. RWT enters the Nexus atomically via yield_distribution::withdraw_liquidity_holding: the YD program transfers RWT from the LiquidityHolding PDA’s RWT ATA into the Nexus RWT ATA and CPIs into nexus_record_deposit to update the principal floor — all in a single transaction. There is no intermediate crank wallet on the RWT side.
State-only principal-floor update. Invoked exclusively as a CPI target by an upstream program (currently yield_distribution::withdraw_liquidity_holding) that has already moved tokens into a Nexus token account in the same transaction. No SPL transfer happens in this instruction — only the bookkeeping leg.
ParameterTypeDescription
amountu64Amount that was just transferred into the Nexus by the calling program in the same TX
token_kindu80 = USDC, 1 = RWT
Caller: an upstream program-owned PDA (currently LiquidityHolding from Yield Distribution), via CPIAccounts:
  • liquidity_holding (signer) — caller PDA; the signature proves the bookkeeping leg is atomic with the SPL transfer the caller just performed
  • liquidity_nexus (mut) — increments total_deposited_usdc or total_deposited_rwt by amount
Validation:
  • liquidity_nexus.is_active == true
  • token_kind ∈ {0, 1} — only the USDC and RWT principal lanes
  • Caller is a program-owned signing PDA — direct signers are rejected
Logic:
  1. Validate active + token_kind.
  2. Add amount to total_deposited_usdc or total_deposited_rwt (saturating).
  3. Emit NexusDeposited with the via_record = true flag.
Why two instructions? nexus_deposit is for SPL transfer + state update from a regular signer (USDC lane). nexus_record_deposit is the state-only path used when the calling program has already moved the tokens via its own CPI and just needs the principal floor refreshed (RWT lane). Splitting avoids re-locking SPL transfer signer authority across program boundaries.
Claim accumulated LP fee rewards from a pool’s reserve vaults into the Nexus’s own token ATAs. The Nexus PDA acts as an LP holder and claims its proportional share on both token sides of the pool. Funds land in the Nexus ATAs, not in the Areal Treasury directly — Treasury sweep is a separate Authority step via nexus_withdraw_profits (delta above the principal floor only). Authority only.
ParameterTypeDescription
poolPubkeyPool to claim fees from
Caller: Authority (Team Multisig)Accounts:
  • authority (signer) — must match dex_config.authority
  • dex_config — for authority validation
  • nexus — PDA, signs as LP holder
  • pool_state — reads both cumulative accumulators
  • nexus_lp_position (mut) — Nexus’s LP position in this pool, updates fees_claimed_per_share_a and _b
  • pool_vault_a (mut) — pool’s token-A reserve vault
  • pool_vault_b (mut) — pool’s token-B reserve vault
  • nexus_token_a_ata (mut) — Nexus-owned ATA on token-A side, receives claimed A-side fees
  • nexus_token_b_ata (mut) — Nexus-owned ATA on token-B side, receives claimed B-side fees
  • token_program
Logic:
  1. Compute claimable_a and claimable_b using the same Q64.64 formula as claim_lp_fees.
  2. Transfer claimable_a token-A from pool_vault_anexus_token_a_ata if > 0; same for B.
  3. Update nexus_lp_position.fees_claimed_per_share_a and _b to the current cumulative values.
  4. Emit LpFeesClaimed { claimable_a, claimable_b, recipient, ... } — the same dual-side event used by user-side claim_lp_fees; distinguishable by recipient == nexus.address().
Two-step Treasury settlement. This instruction moves LP fee rewards from the pool reserve vaults into the Nexus’s own ATAs. Funds remain inside the Nexus subsystem at this point and contribute to the withdrawable profit ceiling — they are not counted toward the principal floor (total_deposited_* is only written by nexus_deposit / nexus_record_deposit). To settle into the Areal Treasury, Authority then calls nexus_withdraw_profits, which releases up to nexus_balance(t) − total_deposited(t) and reverts on overflow.
Uses the same per-side accumulator as claim_lp_fees. For RWT-paired pools, only the RWT-side amount is non-zero in practice today (swap accrual writes one side per swap), but the instruction issues both transfers unconditionally for forward-compat with non-RWT pairs. Both-zero is a clean no-op (both sides skipped, snapshot still refreshed).
Swap tokens from Nexus PDA through any DEX pool. Same swap logic as regular swap but Nexus PDA signs as user.
ParameterTypeDescription
amount_inu64Input token amount
min_amount_outu64Minimum output (slippage protection)
a_to_bboolSwap direction
Caller: Nexus manager onlyAccounts:
  • manager (signer) — must match nexus.manager
  • nexus — PDA, signs swap as user
  • nexus_token_in (mut) — constraint: owner == nexus.key()
  • nexus_token_out (mut) — constraint: owner == nexus.key()
  • pool_state (mut), dex_config, bin_array (optional)
  • vault_in (mut), vault_out (mut)
  • areal_fee_account (mut)
  • ot_treasury_fee_account (mut, optional) — required if OT pair
  • token_program
Validation:
  • amount_in > 0
  • min_amount_out > 0 (slippage protection required — prevents manager from executing swaps at arbitrary prices)
  • manager == nexus.manager
Effect: Internal CPI to swap logic with Nexus PDA as user. OT treasury fee charged if OT pair. Emits SwapExecuted.
Manager trust assumption. While min_amount_out > 0 is enforced, the manager sets the value. A compromised manager can set min_amount_out = 1 and execute at terrible prices. Mitigation: authority (Team Multisig) monitors swap events and replaces manager via update_nexus_manager if anomalous trades detected. All swaps are auditable via SwapExecuted events.
Add liquidity from Nexus PDA to any pool. Works as zap — accepts any token ratio including single token. Nexus PDA signs as provider.
ParameterTypeDescription
amount_au64Token A amount (can be 0)
amount_bu64Token B amount (can be 0)
min_sharesu128Minimum LP shares (slippage protection)
Caller: Nexus manager onlyAccounts:
  • manager (signer) — must match nexus.manager
  • nexus — PDA, signs as provider
  • pool_state (mut), dex_config
  • lp_position (init_if_needed) — PDA seed: ["lp", pool_state, nexus]
  • nexus_token_a (mut) — constraint: owner == nexus.key()
  • nexus_token_b (mut) — constraint: owner == nexus.key()
  • vault_a (mut), vault_b (mut)
  • areal_fee_account (mut) — for internal swap fee if zap
  • bin_array (mut, optional)
  • token_program, system_program
Effect: Zap logic (auto-swap to match ratio) + add liquidity. Nexus PDA as provider. Emits LiquidityAdded.
Remove liquidity from Nexus PDA’s LP position.
ParameterTypeDescription
shares_to_burnu128LP shares to redeem
Caller: Nexus manager onlyAccounts:
  • manager (signer) — must match nexus.manager
  • nexus — PDA, signs as provider
  • pool_state (mut)
  • lp_position (mut) — constraint: owner == nexus.key()
  • nexus_token_a (mut), nexus_token_b (mut)
  • vault_a (mut), vault_b (mut)
  • token_program
Effect: Pool PDA signs vault→nexus transfers. Emits LiquidityRemoved.
Withdraw realised profit (the delta above the tracked principal floor) from a Nexus token ATA into a recipient ATA. Authority-gated. Reverts if amount > nexus_balance(t) − total_deposited(t).
ParameterTypeDescription
amountu64Profit amount to withdraw
token_kindu80 = USDC, 1 = RWT
Caller: Authority (Team Multisig)Accounts:
  • authority (signer) — must match dex_config.authority
  • dex_config
  • liquidity_nexus (mut)
  • nexus_token_account (mut) — Nexus ATA on the requested side
  • recipient_token_account (mut) — Areal Treasury ATA (USDC or RWT)
  • mint
  • token_program
Validation:
  • amount > 0
  • liquidity_nexus.is_active == true
  • token_kind ∈ {0, 1}
  • Principal-lock invariant: amount ≤ nexus_token_account.balance − total_deposited_{usdc,rwt} (saturating; reverts on violation with InsufficientNexusProfit)
Logic:
  1. Validate principal-lock.
  2. Nexus PDA signs SPL Transfer of amount to recipient_token_account.
  3. Emit NexusProfitsWithdrawn { token_mint, amount, remaining_profit, treasury_destination }.
The principal floor is intentionally one-way (write-only via nexus_deposit / nexus_record_deposit). To return capital from the Nexus to a holding PDA or the Treasury, profit must be withdrawn separately, and principal must remain on-chain — there is no nexus_withdraw_principal instruction.
Change the Nexus manager wallet.
ParameterTypeDescription
new_managerPubkeyNew manager wallet
Caller: Authority (Team Multisig)Accounts:
  • authority (signer) — must match dex_config.authority
  • dex_config
  • nexus (mut)
Effect: Sets nexus.manager = new_manager. Emits NexusManagerUpdated.

Configuration & Authority

Update global DEX configuration. Full overwrite.
ParameterTypeDescription
base_fee_bpsu16New base fee (max 1,000 = 10%)
lp_fee_share_bpsu16New LP fee share (max 10,000 = 100%)
rebalancerPubkeyNew Rebalancer wallet
is_activeboolGlobal DEX active flag
Caller: Authority (Team Multisig)Accounts:
  • authority (signer) — must match dex_config.authority
  • dex_config (mut)
Validation:
  • base_fee_bps ≤ 1,000 (max 10%)
  • lp_fee_share_bps ≤ 10,000
Effect: Overwrites config. Emits DexConfigUpdated.
pause_authority is immutable — set at init, cannot be changed. Only a program upgrade can modify it. areal_fee_destination is now updatable via the dedicated update_areal_fee_destination instruction.
Kill-switch: Setting rebalancer = [0u8;32] (zero pubkey) is permitted by design and freezes grow_liquidity and compress_liquidity — no signer can produce a valid signature for the zero pubkey. Symmetric with LiquidityNexus.manager kill-switch. Restore by calling update_dex_config again with a real Rebalancer pubkey.
Rotate the protocol fee destination. Authority-only. The new destination is validated on-chain as an SPL Token Account whose mint equals RWT_MINT — preventing the same misconfiguration that motivated the runtime mint-check in swap/zap.Caller: Authority (Team Multisig after bootstrap)Accounts:
  • authority (signer) — must match dex_config.authority
  • dex_config (mut) — PDA seed: ["dex_config"]
  • new_areal_fee_account — SPL Token Account; constraint: owner == SPL_TOKEN_PROGRAM, mint == RWT_MINT (validated on-chain)
Validation:
  • Signer == dex_config.authority
  • read_token_account_mint(new_areal_fee_account) == RWT_MINT (else InvalidProtocolFeeDestination)
Logic:
  1. Read current dex_config.areal_fee_destination into old_destination
  2. Compute new_destination = new_areal_fee_account.address()
  3. If old_destination == new_destination → return Ok (idempotent, no event)
  4. Write dex_config.areal_fee_destination = new_destination
  5. Emit ArealFeeDestinationUpdated { old_destination, new_destination, timestamp }
Effect: Future swap and zap_liquidity protocol-fee transfers go to the new destination. Existing pending claims are unaffected (LP-fee accumulator is independent of areal_fee_destination).
Mint constraint is the trust anchor. The on-chain mint check means the stored areal_fee_destination is always an RWT ATA — once this instruction has been called for the first time, the runtime mint-check in swap/zap becomes a defense-in-depth tier rather than a guard against bootstrap misconfiguration.
Audit trail. Every rotation emits ArealFeeDestinationUpdated with both old and new destinations and a unix timestamp. Observers can reconstruct the full rotation history off-chain by indexing this event.
Add or remove a wallet from the pool creators whitelist.
ParameterTypeDescription
walletPubkeyCreator wallet to add/remove
actionCreatorActionAdd or Remove
Caller: Authority (Team Multisig)Accounts:
  • authority (signer) — must match pool_creators.authority
  • pool_creators (mut)
Validation:
  • Add: not already whitelisted, count < MAX_POOL_CREATORS (10)
  • Remove: must exist in whitelist
Effect: Updates whitelist. Emits PoolCreatorsUpdated.
Step 1: Current authority proposes a new authority.
ParameterTypeDescription
new_authorityPubkeyProposed new authority
Caller: Current authorityAccounts:
  • authority (signer) — must match dex_config.authority
  • dex_config (mut)
Validation: new_authority ≠ current authorityEffect: Sets dex_config.pending_authority = Some(new_authority). Emits AuthorityTransferProposed.
Calling again overwrites any existing pending_authority. The previous proposed authority loses their ability to accept.
Step 2: Proposed authority accepts.Caller: New authority (must sign)Accounts:
  • new_authority (signer) — must match dex_config.pending_authority
  • dex_config (mut)
  • pool_creators (mut) — authority also updated here
Validation: signer == dex_config.pending_authorityEffect: Sets authority = new_authority on both dex_config and pool_creators. Clears pending_authority. Emits AuthorityTransferAccepted.

Emergency

Emergency pause a single pool. Stops swaps and new liquidity adds. Does NOT affect remove_liquidity (LPs can always exit).Caller: Pause Authority (Team Multisig)Accounts:
  • pause_authority (signer) — must match dex_config.pause_authority
  • dex_config — for pause_authority validation
  • pool_state (mut)
Effect: Sets pool_state.is_active = false. Emits PoolPaused.
Resume a paused pool.Caller: Pause Authority (Team Multisig)Accounts:
  • pause_authority (signer) — must match dex_config.pause_authority
  • dex_config
  • pool_state (mut)
Effect: Sets pool_state.is_active = true. Emits PoolUnpaused.

State Accounts

DexConfig

Global singleton. One per protocol deployment.
FieldTypeDescription
authorityPubkeyConfig authority (Team Multisig after bootstrap)
pending_authorityOption<Pubkey>Pending authority transfer target
pause_authorityPubkeyEmergency pause signer (Team Multisig, immutable)
base_fee_bpsu16Default swap fee (default: 50 = 0.5%)
lp_fee_share_bpsu16LP’s share of fee (default: 5,000 = 50%)
areal_fee_destinationPubkeyAreal Finance RWT ATA — receives protocol fees in RWT. Set at init; updatable by authority via update_areal_fee_destination (validated as RWT ATA on update).
rebalancerPubkeyPool Rebalancer wallet — only signer allowed to call grow_liquidity and compress_liquidity. Set to [0u8;32] to disable grow/compress (kill-switch).
is_activeboolGlobal DEX kill switch
bumpu8PDA bump seed
PDA Seed: ["dex_config"]

PoolState

One per token pair. Stores reserves, shares, fees.
FieldTypeDescription
pool_typePoolTypeStandardCurve or MonotonicLadder
token_a_mintPubkeyToken A mint
token_b_mintPubkeyToken B mint
vault_aPubkeyToken A vault (authority = pool PDA)
vault_bPubkeyToken B vault (authority = pool PDA)
reserve_au64Current A balance
reserve_bu64Current B balance
total_lp_sharesu128Outstanding LP shares
fee_bpsu16Swap fee (copied from DexConfig at creation)
is_activeboolPool active flag (pauseable by Team Multisig)
total_fees_accumulatedu64Lifetime total fees (LP + protocol)
cumulative_fees_per_share_au128Q64.64 running sum on the token-A side: cumulative_fees_per_share_a += (fee_lp_a << 64) / total_lp_shares per swap. O(1) per-holder fee accounting
cumulative_fees_per_share_bu128Q64.64 running sum on the token-B side: cumulative_fees_per_share_b += (fee_lp_b << 64) / total_lp_shares per swap
bin_step_bpsu16Log bin step (0 for StandardCurve; typically 10 = 0.1% for MonotonicLadder)
active_bin_idi32Current active bin (MonotonicLadder only)
left_anchor_bini32Top of permanent tail (MonotonicLadder only, immutable)
permanent_tail_floor_bini32Bottom of permanent tail (MonotonicLadder only, immutable)
last_rebalance_nav_bini32NAV bin of the most recent grow_liquidity or compress_liquidity call (MonotonicLadder only)
active_zone_loweri32Lower bound of current active bid wall (MonotonicLadder only)
ot_treasury_fee_destinationOption<Pubkey>RWT ATA of OT Treasury PDA. When Some, additional 50 bps fee charged on swaps and sent here. None for non-OT pools (e.g., RWT/USDC). Set at creation, immutable.
bumpu8PDA bump seed
PDA Seed: ["pool", token_a_mint, token_b_mint]

PoolCreators

Whitelist of wallets allowed to create pools. Max 10.
FieldTypeDescription
authorityPubkeyWho can add/remove creators
creators[Pubkey; 10]Whitelisted wallets
active_countu8Number of active creators
bumpu8PDA bump seed
PDA Seed: ["pool_creators"]

LpPosition

Per (pool, provider) LP tracking. Created on first add via init_if_needed.
FieldTypeDescription
poolPubkeyAssociated pool
ownerPubkeyLP provider wallet
sharesu128LP shares held
fees_claimed_per_share_au128Q64.64 snapshot of PoolState.cumulative_fees_per_share_a taken at position open / last claim. Pending token-A fees = (cumulative_fees_per_share_a − fees_claimed_per_share_a) × shares >> 64
fees_claimed_per_share_bu128Q64.64 snapshot of PoolState.cumulative_fees_per_share_b. Pending token-B fees = (cumulative_fees_per_share_b − fees_claimed_per_share_b) × shares >> 64
last_update_tsi64Last interaction timestamp
bumpu8PDA bump seed
PDA Seed: ["lp", pool_state, provider]

BinArray

Monotonic Ladder liquidity bins. One per MonotonicLadder pool.
FieldTypeDescription
poolPubkeyAssociated pool
bins[Bin; 1000]Log-scale bins. Each bin: liquidity_a: u64 (RWT), liquidity_b: u64 (USDC). Below active: USDC only (bid — permanent tail + extended bid + active bid wall). Above active: RWT only (organic ask, not pre-funded). Active bin: both.
lower_bin_idi32ID of bins[0]
bin_step_bpsu16Log price step between bins (e.g., 10 = 0.1%)
active_bin_idi32Current active bin (moved only by swaps)
bumpu8PDA bump seed
PDA Seed: ["bins", pool_state]

LiquidityNexus

Areal Finance LP management PDA. Singleton — one per DEX. Owns token ATAs and LP positions. Manager bot executes operations; funds protected by program.
FieldTypeDescription
managerPubkeyBot wallet that can execute nexus operations
total_deposited_usdcu64Cumulative USDC deposited via nexus_deposit
total_deposited_rwtu64Cumulative RWT deposited via nexus_deposit
is_activeboolActive flag
bumpu8PDA bump seed
PDA Seed: ["liquidity_nexus"]
Nexus PDA owns LP positions (LpPosition where owner == nexus.key()) and token ATAs. Manager can swap/add/remove LP but CANNOT transfer tokens out of Nexus ATAs directly — only through DEX instructions. LP fee rewards are claimed from YD directly to Areal Treasury via nexus_claim_rewards. If manager compromised, authority replaces via update_nexus_manager.

PDA Seeds

AccountSeedsDescription
DexConfig"dex_config"Global config singleton
PoolCreators"pool_creators"Creator whitelist
PoolState"pool", token_a_mint, token_b_mintPer-pair pool
LpPosition"lp", pool_state, providerPer-LP tracking
BinArray"bins", pool_stateConcentrated bins
LiquidityNexus"liquidity_nexus"Areal Finance LP manager
Vault accounts (vault_a, vault_b) are NOT PDAs — they are regular SPL token accounts with authority = pool_state PDA. Created as keypair accounts at pool creation.

Constants

ConstantValueDescription
BPS_DENOMINATOR10,000100% in basis points
DEFAULT_BASE_FEE_BPS500.5% swap fee
DEFAULT_LP_FEE_SHARE_BPS5,00050% of fee to LP
MAX_FEE_BPS1,00010% max fee cap
MAX_BINS1000Bins per MonotonicLadder pool
GEOMETRIC_R_BPS8500Density ratio for active bid wall (r = 0.85)
ACTIVE_ZONE_WIDTH40Bins in the active bid wall
DEFAULT_PERMANENT_TAIL_OFFSET_BPS100Permanent tail sits at initial_NAV − 1%
DEFAULT_BIN_STEP_BPS100.1% between bins
MAX_POOL_CREATORS10Max whitelisted creators
MIN_LIQUIDITY1,000Min shares on first add (dust prevention)
OT_TREASURY_FEE_BPS500.5% additional fee for OT pairs — sent to OT Treasury RWT ATA
RWT_MINThardcodedRWT token mint — all pools must include RWT (validated in create_pool)
USDC_MINThardcodedUSDC mint — for nexus_deposit token validation
OT_PROGRAM_IDhardcodedOT contract program ID — validated in create_pool to verify OT Treasury PDA ownership
YD_PROGRAM_IDhardcodedValidated in compound_yield

Events

EventFieldsWhen
DexInitializedauthority, base_fee_bps, timestampDEX created
PoolCreatedpool, token_a_mint, token_b_mint, pool_type, creator, ot_treasury_fee_destination, timestampPool created
LiquidityAddedpool, provider, amount_a, amount_b, shares_minted, timestampLP added
ZapLiquidityExecutedpool, provider, input_a, input_b, swapped_amount, shares_minted, timestampZap: auto-swap + add LP
LiquidityRemovedpool, provider, amount_a, amount_b, shares_burned, timestampLP removed
LiquidityShiftedpool, rebalancer, old_lower, old_upper, new_lower, new_upper, timestampBins rebalanced
SwapExecutedpool, user, a_to_b, amount_in, amount_out, fee_lp, fee_protocol, fee_ot_treasury, timestampSwap completed
LpFeesClaimedpool, lp_holder, amount, timestampLP holder claimed swap fee rewards
CompoundYieldExecutedpool, rwt_claimed, timestampOT yield auto-compounded into pool
PoolCreatorsUpdatedwallet, action, active_count, timestampWhitelist changed
DexConfigUpdatedbase_fee_bps, lp_fee_share_bps, rebalancer, is_active, timestampConfig changed
ArealFeeDestinationUpdatedold_destination, new_destination, timestampProtocol fee destination rotated by authority
AuthorityTransferProposedcurrent_authority, pending_authority, timestampTransfer proposed
AuthorityTransferAcceptedold_authority, new_authority, timestampTransfer accepted
PoolPausedpool, timestampPool emergency paused
PoolUnpausedpool, timestampPool resumed
NexusInitializedmanager, timestampNexus created
NexusDepositedtoken_mint, amount, timestampCapital deposited into Nexus
NexusRewardsClaimedamount, treasury_destination, timestampLP fee rewards claimed from YD to Areal Treasury
NexusProfitsWithdrawntoken_mint, amount, remaining_profit, treasury_destination, timestampProfits withdrawn to Areal Treasury
NexusManagerUpdatedold_manager, new_manager, timestampNexus manager changed

Error Codes

ErrorDescription
UnauthorizedNot DEX authority
CreatorNotWhitelistedNot in pool creators whitelist
DexPausedGlobal DEX is_active = false
PoolNotActivePool is_active = false
WhitelistFullMax 10 creators
IdenticalMintstoken_a == token_b
CreatorNotFoundRemove: creator not in whitelist
ZeroAmountAmount = 0
InsufficientLiquidityPool reserves empty
InsufficientSharesLP has fewer shares than burn
InitialLiquidityTooSmallFirst add < MIN_LIQUIDITY
SlippageExceededOutput < min_amount_out
ZeroOutputSwap would produce 0
EmptyReservesCannot swap with 0 reserves
MathOverflowArithmetic overflow
InvalidFeebase_fee_bps > MAX_FEE_BPS
InvalidFeeSharelp_fee_share_bps > 10,000
InvalidBinRangelower ≥ upper or out of bounds
BinOutOfRangeBin ID outside BinArray
InsufficientBinLiquidityNo liquidity in bins for swap
InvalidBinStepbin_step_bps = 0 for concentrated
MissingRwtMintNeither token_a nor token_b is RWT_MINT
InvalidMintOrdertoken_a_mint >= token_b_mint (must be canonical order)
InvalidVaulttarget_vault not vault_a or vault_b
NothingToCompoundNo RWT received from YD
SelfTransferCannot transfer authority to yourself
NoPendingAuthorityNo pending transfer
InvalidPendingAuthoritySigner ≠ pending_authority
InvalidOtTreasuryDestinationot_treasury_fee_destination does not match derived RWT ATA for OT Treasury PDA, or OT Treasury PDA not owned by OT_PROGRAM_ID
InvalidProtocolFeeDestinationareal_fee_account mint does not match RWT_MINT — protocol fee destination must be an RWT ATA
MissingOtTreasuryAccountPool has ot_treasury_fee_destination set but ot_treasury_fee_account not provided in swap
InvalidNexusTokennexus_deposit token_mint is not USDC_MINT or RWT_MINT
NexusNotActiveNexus is_active = false
InvalidNexusManagerSigner ≠ nexus.manager
NexusClaimFailedNexus PDA not found in merkle tree or proof invalid

Architecture & Integration Guide

Cross-Program Integration

  • RWT Engine vault_swap CPIs to native_dex::swap
  • RWT Vault PDA signs as user
  • Manager controls direction and slippage
  • YD convert_to_rwt CPIs to native_dex::swap (buy RWT below NAV)
  • YD Accumulator PDA signs as user
  • Nexus PDA lives inside DEX contract — owns LP positions and token ATAs
  • Capital enters via nexus_deposit: 10% OT revenue (USDC) + 15% RWT Engine yield (RWT)
  • Manager bot calls nexus_swap, nexus_add_liquidity, nexus_remove_liquidity
  • LP fee rewards claimed from YD to Areal Treasury via nexus_claim_rewards (Authority)
  • Manager changeable by DEX authority (Team Multisig) via update_nexus_manager
  • Pool PDA claims OT yield RWT from YD via CPI → auto-compounds into pool reserves
  • All LP holders benefit proportionally (deeper pool = better trades)
  • LP swap fees are handled separately via the per-side accumulator on PoolState (claim_lp_fees)
  • Rebalancer bot calls grow_liquidity on Monotonic Ladder pools when NAV rises past the 1% deviation threshold — extends bid wall rightward using Nexus USDC
  • Rebalancer bot calls compress_liquidity after governance writedown — recenters density on the new (lower) NAV; organic ask RWT above new NAV remains as frozen ask wall
  • Rebalancer wallet signs as signer (dedicated keypair, not PDA) — cannot extract funds

Trust Assumptions

Pool creators trust: Whitelisted creators can set initial pool parameters (bin_step). Malicious creator could set unfavorable initial price via first liquidity add. Mitigation: authority (Team Multisig) controls whitelist.
compound_yield YD dependency: Uses hardcoded YD program ID for CPI. If YD program is upgraded to new address, compound_yield must be updated via program upgrade.

Deployment Checklist

Prerequisites: Yield Distribution must be deployed (needed for compound_yield CPI).
  1. Call initialize_dex with areal_fee_destination (Areal Finance RWT ATA), pause_authority (Team Multisig), rebalancer (bot wallet)
  2. Call initialize_nexus with manager (Nexus bot wallet)
  3. Create Nexus ATAs — create USDC ATA and RWT ATA owned by Nexus PDA (needed before OT destinations and RWT Engine config point to them)
  4. Add pool creators via update_pool_creators
  5. Create pools — all pools pair with RWT (e.g., OT/RWT, RWT/USDC). Mints must be in canonical order (token_a < token_b). For OT pairs: pass ot_treasury PDA and its RWT ATA — OT Treasury must already be initialized via the OT contract before pool creation
  6. Transfer authority to Team Multisig via propose_authority_transfer + accept_authority_transfer

Token Flow Summary

FromToMechanismWho triggers
User token_inPool vault_inswapUser
Pool vault_outUser token_outswapUser
Swap fee (LP share)Stays in pool reserve vault on the fee-bearing side; tracked via cumulative_fees_per_share_<side> on PoolStateswapAutomatic
Pool reserve vaults (A and B)LP holder ATAs (A and B)claim_lp_fees (per-side Q64.64 payout)LP holder
Swap fee (protocol share)Areal Finance RWT ATA (always in RWT)swapAutomatic
Swap fee (OT treasury)OT Treasury RWT ATA (OT pairs only, always in RWT)swapAutomatic
LP tokens A+BPool vaultsadd_liquidityLP provider
LP any ratio / single tokenPool vaults (via internal swap)zap_liquidityLP provider
Pool vaultsLP tokens A+Bremove_liquidityLP provider
YD reward vaultPool reserves (RWT)compound_yield (CPI to YD)Crank
OT Revenue (10% USDC)Crank USDC ATA → Nexus USDC ATAOT distribute_revenuenexus_deposit (two-step: crank receives, then deposits)Crank
RWT Engine yield (15% RWT)LiquidityHolding RWT ATA → Nexus RWT ATAYD withdraw_liquidity_holding (single-TX atomic drain + nexus_record_deposit CPI)Authority
Nexus LP fee rewards (token A and/or B)Nexus ATAs (A and B)nexus_claim_rewards (per-side Q64.64 payout; Treasury settlement is a separate nexus_withdraw_profits call)Authority
Nexus tokensPool vaultsnexus_add_liquidityNexus manager
Pool vaultsNexus tokensnexus_remove_liquidityNexus manager
Nexus token (USDC or RWT)Areal Treasury ATAnexus_withdraw_profits (delta above principal floor only)Authority

See also

  • Liquidity Nexus — subsystem-level overview, principal-lock invariant, three trust tiers, and the two deposit lanes that feed the Nexus
  • Yield Distribution contractLiquidityHolding PDA and withdraw_liquidity_holding (the RWT-lane atomic drain that CPIs into nexus_record_deposit)
  • RWT Engine contractclaim_yield 70 / 15 / 15 split that produces the 15% RWT slice routed through the RWT lane