Native DEX
✅ Ready to Dev
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.
Key Concepts
23 Instructions
6 State Accounts
6 PDA Seeds
Two Pool Types
StandardCurve
- 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
- 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.85around 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_rwtwhen 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 askcompress_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
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
Base Fee
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/_bare not diluted. LP holders claim at any time viaclaim_lp_feesand 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).
Fees On Top (No Pool Dilution)
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.Always RWT
LP Fee → Per-Side Accumulator (Instant Claim)
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.OT Treasury Fee (OT pairs only)
OT_TREASURY_FEE_BPS = 50 program constant — changeable only via program upgrade.Mint-Path Fee Skip (master pools only)
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
update_dex_config— fees, destinations, rebalancerupdate_pool_creators— whitelist managementpropose_authority_transfer
🛑 Pause Authority
pause_pool/unpause_pool
⚖️ Rebalancer
grow_liquidity— extend bid wall rightward as NAV risescompress_liquidity— recenter density on writedown
update_dex_config.💧 Nexus Manager
nexus_swap/nexus_add_liquidity/nexus_remove_liquidity
update_nexus_manager.🏗️ Pool Creators
create_pool/create_concentrated_pool
🌐 Permissionless
swap— trade tokensadd_liquidity/zap_liquidity/remove_liquiditycompound_yield— auto-compound RWT
Instructions
Initialization
initialize_dex
initialize_dex
| Parameter | Type | Description |
|---|---|---|
areal_fee_destination | Pubkey | Areal 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_authority | Pubkey | Emergency pause signer (Team Multisig, immutable) |
rebalancer | Pubkey | Pool Rebalancer wallet — can call grow_liquidity and compress_liquidity |
authority(signer, mut) — deployer, pays for creationdex_config(init) — PDA seed:["dex_config"]pool_creators(init) — PDA seed:["pool_creators"]system_program
DexConfigPDA — global config singletonPoolCreatorsPDA — whitelist (deployer auto-added as first creator)
base_fee_bps = 50(0.5%)lp_fee_share_bps = 5,000(50% of fee to LP)is_active = true
Pool Creation
create_pool
create_pool
creator(signer, mut) — must be in pool_creators whitelist, pays for account creationdex_config— validates is_active, provides base_fee_bps and lp_fee_share_bpspool_creators— validatescreatoris in whitelistpool_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.keyvault_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 byOT_PROGRAM_ID.ot_treasury_rwt_ata(optional) — RWT ATA owned byot_treasuryPDA. Required whenot_treasuryis provided.token_program,system_program
token_a_mint ≠ token_b_mint- One of
token_a_mintortoken_b_mintmust beRWT_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_treasuryprovided: verify PDA derivation["ot_treasury", ot_mint]whereot_mintis the non-RWT mint. Verify account owner isOT_PROGRAM_ID. Verifyot_treasury_rwt_atais the associated token address for (ot_treasury,RWT_MINT). Fails withInvalidOtTreasuryDestinationif any check fails.
ot_treasury_fee_destination = ot_treasury_rwt_ata.key() if OT treasury accounts provided, otherwise None.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_concentrated_pool
create_concentrated_pool
| Parameter | Type | Description |
|---|---|---|
bin_step_bps | u16 | Log price step between bins (default: 10 = 0.1%) |
initial_active_bin | i32 | Starting active bin ID — anchors initial NAV in log-scale |
permanent_tail_offset_bps | i32 | Permanent-tail position below initial_active_bin. Default: 100 (= NAV − 1%). Immutable after init. |
create_pool plus:bin_array(init) — PDA seed:["bins", pool_state]. Size = 1000 bins × 24 bytes + overhead ≈ 32 KB, rent ~0.22 SOL.
bin_step_bps > 0- One of
token_a_mintortoken_b_mintmust beRWT_MINT - The non-RWT mint must be
USDC_MINTorUSDY_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)
- 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_liquidityafter pool creation.
Liquidity
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
add_liquidity
| Parameter | Type | Description |
|---|---|---|
amount_a | u64 | Token A amount |
amount_b | u64 | Token B amount |
provider(signer) — owns token accountspayer(signer, mut) — pays rent for LpPosition initpool_state(mut) — updates reserves, lp_shareslp_position(init_if_needed) — PDA seed:["lp", pool_state, provider]provider_token_a(mut) — constraint:owner == provider,mint == pool.token_a_mintprovider_token_b(mut) — constraint:owner == provider,mint == pool.token_b_mintvault_a(mut),vault_b(mut) — pool vaultsbin_array(mut, optional) — required for Concentrated pools (to distribute across bins)token_program,system_program
amount_a > 0 && amount_b > 0(both required; usezap_liquidityfor single-token)- Pool must be active
- First add:
shares = sqrt(amount_a × amount_b), must be ≥ MIN_LIQUIDITY (1,000). Overflow guard:amount_a * amount_bmust fit u128. - Subsequent:
shares = min(amount_a × total_shares / reserve_a, amount_b × total_shares / reserve_b). Themin()means excess tokens of one side are not deposited — only the proportional amount is used. Unused tokens remain in the user’s wallet.
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.zap_liquidity
zap_liquidity
| Parameter | Type | Description |
|---|---|---|
amount_a | u64 | Token A amount (can be 0) |
amount_b | u64 | Token B amount (can be 0) |
min_shares | u128 | Minimum LP shares to receive (slippage protection) |
provider(signer) — owns token accountspayer(signer, mut) — pays rent for LpPosition initpool_state(mut) — must be activedex_config— for fee calculationlp_position(init_if_needed) — PDA seed:["lp", pool_state, provider]provider_token_a(mut) — constraint:owner == provider,mint == pool.token_a_mintprovider_token_b(mut) — constraint:owner == provider,mint == pool.token_b_mintvault_a(mut),vault_b(mut) — pool vaultsareal_fee_account(mut) — receives protocol fee from internal swapot_treasury_fee_account(mut, optional) — receives OT treasury fee from internal swap (required if OT pair)bin_array(mut, optional) — required for Concentrated poolstoken_program,system_program
amount_a > 0 || amount_b > 0(at least one token)- Pool must be active
- Read current pool ratio. If pool is empty (reserves = 0): skip swap, treat as regular
add_liquiditywith both amounts as-is. Otherwise:target_ratio = reserve_a / reserve_b - Calculate how much to swap to match ratio:
- After internal swap: pool ratio changed, recalculate optimal add amounts
- Add liquidity with balanced amounts (same math as
add_liquidity) shares ≥ min_shares(slippage check)- Emit
ZapLiquidityExecuted
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.”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
remove_liquidity
| Parameter | Type | Description |
|---|---|---|
shares_to_burn | u128 | LP shares to redeem |
provider(signer) — must matchlp_position.ownerpool_state(mut)lp_position(mut) — constraint:owner == provider,pool == pool_stateprovider_token_a(mut),provider_token_b(mut)vault_a(mut),vault_b(mut)token_program
shares_to_burn ≤ lp_position.shares- Works even when pool is paused (LP can always exit)
lp_position.shares == 0 after burn, the LpPosition account is closed and rent returned to provider. Emits LiquidityRemoved.grow_liquidity
grow_liquidity
| Parameter | Type | Description |
|---|---|---|
new_nav_bin | i32 | Target NAV bin ID calculated off-chain by Rebalancer: new_nav_bin = log(nav_book_value) / log(1 + bin_step_bps/10000) |
active_zone_width | u16 | Total 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 |
dex_config.rebalancer)Accounts:rebalancer(signer)dex_config— validates rebalancerpool_state(mut) — must be Monotonic Ladder typebin_array(mut)liquidity_nexus(mut) — source of fresh USDCnexus_usdc_ata(mut) — Nexus’s USDC account (debited)pool_vault_b(mut) — pool’s USDC vault (credited)token_program
- Pool must be Monotonic Ladder type
new_nav_bin > pool_state.last_rebalance_nav_bin(growth rebalance — strictly rightward). For leftward moves usecompress_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_activenew_nav_binmust 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. RevertsNavBinMismatchotherwise. Defends against a compromised Rebalancer passing an arbitrary bin; the× 2factor covers floor-rounding ambiguity at bin boundaries plus a small intra-tx NAV drift.
-
Define ranges:
-
Compute target density weights (geometric,
r = GEOMETRIC_R_BPS / 10000, default 8500 = 0.85): -
Determine required USDC:
-
Pull from Nexus: transfer
nexus_availableUSDC fromnexus_usdc_ata→pool_vault_b. Nexus accumulator is drained toward zero (residual stays for next cycle). -
Redistribute across new active zone:
-
Permanent tail:
[tail_lower .. tail_upper]untouched. Never rebalanced in any operation. -
Organic ask:
[new_nav_bin + 1 .. MAX_BINS]untouched. Contains RWT from prior user sells, if any. -
Update state:
pool_state.last_rebalance_nav_bin = new_nav_bin,pool_state.active_zone_lower = new_active_lo,active_bin_idNOT changed by grow_liquidity (only swaps move it).
LiquidityGrown.compress_liquidity
compress_liquidity
rwt_engine::adjust_capital). Rebalancer only. Rare. No token inflow — purely a weight redistribution within existing capital.| Parameter | Type | Description |
|---|---|---|
new_nav_bin | i32 | Lower target NAV bin ID |
active_zone_width | u16 | Same semantics as grow_liquidity |
rebalancer(signer),dex_config,pool_state(mut),bin_array(mut)
- Pool must be Monotonic Ladder type
new_nav_bin < pool_state.last_rebalance_nav_bin(leftward only; for rightward usegrow_liquidity)new_nav_bin > pool_state.left_anchor_bin + 10(new NAV must stay above the permanent tail)new_nav_binmust 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. RevertsNavBinMismatchotherwise. Defends against a compromised Rebalancer passing an arbitrary bin; the× 2factor covers floor-rounding ambiguity at bin boundaries plus a small intra-tx NAV drift.
- Define new active zone:
[new_nav_bin − active_zone_width/2 .. new_nav_bin]. - 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. - Recompute geometric density weights around
new_nav_binusing only the existing USDC capital in the new active zone (no Nexus pull on compress). - Redistribute USDC within the new active zone per weights. Former extended bid bins far below new NAV stay as-is.
- 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.
- Permanent tail is NOT touched.
- Update state:
pool_state.last_rebalance_nav_bin = new_nav_bin,pool_state.active_zone_lower = new_active_lo.
LiquidityCompressed.Swap
swap
swap
| Parameter | Type | Description |
|---|---|---|
amount_in | u64 | Input token amount |
min_amount_out | u64 | Minimum output (slippage protection) |
a_to_b | bool | Swap direction |
user(signer)pool_state(mut) — must be activedex_config— must be activebin_array(mut, optional) — required only for Concentrated poolsuser_token_in(mut),user_token_out(mut)vault_in(mut),vault_out(mut)areal_fee_account(mut) — receives protocol feeot_treasury_fee_account(mut, optional) — receives OT treasury fee. Required ifpool_state.ot_treasury_fee_destinationisSome. Must match stored address.token_program
fee_lp→ stays inside the pool’s reserve vault on the fee-bearing side (currently always the RWT side); the contract bumpspool_state.cumulative_fees_per_share_<side>so LP holders can claim their share later viaclaim_lp_feesfee_protocol→ RWT transferred toareal_fee_account(Areal Finance RWT ATA)ot_treasury_fee→ RWT transferred toot_treasury_fee_account(OT pairs only)- For non-OT pools,
ot_treasury_fee = 0(no additional fee)
amount_in > 0- Pool reserves non-zero:
reserve_in > 0 && reserve_out > 0(StandardCurve) or at least one bin has liquidity (Concentrated). Fails withEmptyReservesif pool has no liquidity. amount_out ≥ min_amount_out(slippage check)amount_out > 0- Pool and DEX must be active
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:rwt_vault(mut) — passed when pool is master poolrwt_mint(mut)capital_accumulator_ata(mut) — vault’s USDC ATAdao_fee_account(mut) — matchesrwt_vault.areal_fee_destinationuser_rwt(mut)
- 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.005is never filled by LPs — routing makes it unnecessary. LP capital is not “waiting for nothing” in those bins.
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
compound_yield
compound_yield
PoolState and claimed individually via claim_lp_fees.| Parameter | Type | Description |
|---|---|---|
cumulative_amount | u64 | Pool’s cumulative share (from merkle leaf) |
proof | Vec<[u8; 32]> | Merkle proof path |
crank(signer, mut) — pays for ClaimStatus initpool_state(mut) — updates reservestarget_vault(mut) — constraint: must bepool.vault_aorpool.vault_b(whichever is RWT)- YD CPI accounts:
yd_distributor,yd_claim_status,yd_reward_vault yd_program— constraint:key == YD_PROGRAM_IDtoken_program,system_program
- Snapshot target_vault balance before
- CPI →
yield_distribution::claimwith pool PDA as claimant - Measure RWT received (after - before)
- Add received amount to pool reserves (reserve_a or reserve_b)
- Emit
CompoundYieldExecuted
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_lp_fees
claim_lp_fees
lp_holder(signer) — must own the LP positionpool_state— readstotal_lp_sharesand both cumulative accumulatorslp_position(mut) — readsshares, updatesfees_claimed_per_share_aandfees_claimed_per_share_bpool_vault_a(mut) — pool’s token-A reserve vault, owned by pool PDApool_vault_b(mut) — pool’s token-B reserve vault, owned by pool PDArecipient_token_a_ata(mut) — holder’s token-A ATA, receives claimed A-side feesrecipient_token_b_ata(mut) — holder’s token-B ATA, receives claimed B-side feestoken_program
lp_position.owner == lp_holder.key()pool_vault_a.amount ≥ claimable_aandpool_vault_b.amount ≥ claimable_b
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.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 vianexus_claim_rewards.
initialize_nexus
initialize_nexus
| Parameter | Type | Description |
|---|---|---|
manager | Pubkey | Initial Nexus manager wallet (bot) |
authority(signer, mut) — must matchdex_config.authoritydex_config— validates authoritynexus(init) — PDA seed:["liquidity_nexus"]system_program
LiquidityNexusPDA — singleton, owns LP positions
manager = manager param, is_active = truenexus_deposit
nexus_deposit
| Parameter | Type | Description |
|---|---|---|
amount | u64 | Token amount to deposit |
depositor(signer) — source walletnexus(mut) — validates is_activedepositor_token_account(mut) — constraint:owner == depositornexus_token_account(mut) — constraint:owner == nexus.key()token_mint— validates accepted token typetoken_program
amount > 0nexus.is_active == truetoken_mint == USDC_MINT || token_mint == RWT_MINT(only accepted tokens)
- Transfer
amountfrom depositor → nexus ATA - Transfer
amountfromdepositor_token_accounttonexus_token_account - Emit
NexusDeposited
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.nexus_record_deposit
nexus_record_deposit
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.| Parameter | Type | Description |
|---|---|---|
amount | u64 | Amount that was just transferred into the Nexus by the calling program in the same TX |
token_kind | u8 | 0 = USDC, 1 = RWT |
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 performedliquidity_nexus(mut) — incrementstotal_deposited_usdcortotal_deposited_rwtbyamount
liquidity_nexus.is_active == truetoken_kind ∈ {0, 1}— only the USDC and RWT principal lanes- Caller is a program-owned signing PDA — direct signers are rejected
- Validate active +
token_kind. - Add
amounttototal_deposited_usdcortotal_deposited_rwt(saturating). - Emit
NexusDepositedwith thevia_record = trueflag.
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.nexus_claim_rewards
nexus_claim_rewards
nexus_withdraw_profits (delta above the principal floor only). Authority only.| Parameter | Type | Description |
|---|---|---|
pool | Pubkey | Pool to claim fees from |
authority(signer) — must matchdex_config.authoritydex_config— for authority validationnexus— PDA, signs as LP holderpool_state— reads both cumulative accumulatorsnexus_lp_position(mut) — Nexus’s LP position in this pool, updatesfees_claimed_per_share_aand_bpool_vault_a(mut) — pool’s token-A reserve vaultpool_vault_b(mut) — pool’s token-B reserve vaultnexus_token_a_ata(mut) — Nexus-owned ATA on token-A side, receives claimed A-side feesnexus_token_b_ata(mut) — Nexus-owned ATA on token-B side, receives claimed B-side feestoken_program
- Compute
claimable_aandclaimable_busing the same Q64.64 formula asclaim_lp_fees. - Transfer
claimable_atoken-A frompool_vault_a→nexus_token_a_ataif> 0; same for B. - Update
nexus_lp_position.fees_claimed_per_share_aand_bto the current cumulative values. - Emit
LpFeesClaimed { claimable_a, claimable_b, recipient, ... }— the same dual-side event used by user-sideclaim_lp_fees; distinguishable byrecipient == nexus.address().
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.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).nexus_swap
nexus_swap
swap but Nexus PDA signs as user.| Parameter | Type | Description |
|---|---|---|
amount_in | u64 | Input token amount |
min_amount_out | u64 | Minimum output (slippage protection) |
a_to_b | bool | Swap direction |
manager(signer) — must matchnexus.managernexus— PDA, signs swap as usernexus_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 pairtoken_program
amount_in > 0min_amount_out > 0(slippage protection required — prevents manager from executing swaps at arbitrary prices)manager == nexus.manager
swap logic with Nexus PDA as user. OT treasury fee charged if OT pair. Emits SwapExecuted.nexus_add_liquidity
nexus_add_liquidity
| Parameter | Type | Description |
|---|---|---|
amount_a | u64 | Token A amount (can be 0) |
amount_b | u64 | Token B amount (can be 0) |
min_shares | u128 | Minimum LP shares (slippage protection) |
manager(signer) — must matchnexus.managernexus— PDA, signs as providerpool_state(mut),dex_configlp_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 zapbin_array(mut, optional)token_program,system_program
LiquidityAdded.nexus_remove_liquidity
nexus_remove_liquidity
| Parameter | Type | Description |
|---|---|---|
shares_to_burn | u128 | LP shares to redeem |
manager(signer) — must matchnexus.managernexus— PDA, signs as providerpool_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
LiquidityRemoved.nexus_withdraw_profits
nexus_withdraw_profits
amount > nexus_balance(t) − total_deposited(t).| Parameter | Type | Description |
|---|---|---|
amount | u64 | Profit amount to withdraw |
token_kind | u8 | 0 = USDC, 1 = RWT |
authority(signer) — must matchdex_config.authoritydex_configliquidity_nexus(mut)nexus_token_account(mut) — Nexus ATA on the requested siderecipient_token_account(mut) — Areal Treasury ATA (USDC or RWT)minttoken_program
amount > 0liquidity_nexus.is_active == truetoken_kind ∈ {0, 1}- Principal-lock invariant:
amount ≤ nexus_token_account.balance − total_deposited_{usdc,rwt}(saturating; reverts on violation withInsufficientNexusProfit)
- Validate principal-lock.
- Nexus PDA signs SPL Transfer of
amounttorecipient_token_account. - Emit
NexusProfitsWithdrawn { token_mint, amount, remaining_profit, treasury_destination }.
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.update_nexus_manager
update_nexus_manager
| Parameter | Type | Description |
|---|---|---|
new_manager | Pubkey | New manager wallet |
authority(signer) — must matchdex_config.authoritydex_confignexus(mut)
nexus.manager = new_manager. Emits NexusManagerUpdated.Configuration & Authority
update_dex_config
update_dex_config
| Parameter | Type | Description |
|---|---|---|
base_fee_bps | u16 | New base fee (max 1,000 = 10%) |
lp_fee_share_bps | u16 | New LP fee share (max 10,000 = 100%) |
rebalancer | Pubkey | New Rebalancer wallet |
is_active | bool | Global DEX active flag |
authority(signer) — must matchdex_config.authoritydex_config(mut)
base_fee_bps ≤ 1,000(max 10%)lp_fee_share_bps ≤ 10,000
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.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.update_areal_fee_destination
update_areal_fee_destination
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 matchdex_config.authoritydex_config(mut) — PDA seed:["dex_config"]new_areal_fee_account— SPL Token Account; constraint:owner == SPL_TOKEN_PROGRAM,mint == RWT_MINT(validated on-chain)
- Signer ==
dex_config.authority read_token_account_mint(new_areal_fee_account) == RWT_MINT(elseInvalidProtocolFeeDestination)
- Read current
dex_config.areal_fee_destinationintoold_destination - Compute
new_destination = new_areal_fee_account.address() - If
old_destination == new_destination→ return Ok (idempotent, no event) - Write
dex_config.areal_fee_destination = new_destination - Emit
ArealFeeDestinationUpdated { old_destination, new_destination, timestamp }
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).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.ArealFeeDestinationUpdated with both old and new destinations and a unix timestamp. Observers can reconstruct the full rotation history off-chain by indexing this event.update_pool_creators
update_pool_creators
| Parameter | Type | Description |
|---|---|---|
wallet | Pubkey | Creator wallet to add/remove |
action | CreatorAction | Add or Remove |
authority(signer) — must matchpool_creators.authoritypool_creators(mut)
- Add: not already whitelisted, count < MAX_POOL_CREATORS (10)
- Remove: must exist in whitelist
PoolCreatorsUpdated.propose_authority_transfer
propose_authority_transfer
accept_authority_transfer
accept_authority_transfer
Emergency
pause_pool
pause_pool
pause_authority(signer) — must matchdex_config.pause_authoritydex_config— for pause_authority validationpool_state(mut)
pool_state.is_active = false. Emits PoolPaused.unpause_pool
unpause_pool
pause_authority(signer) — must matchdex_config.pause_authoritydex_configpool_state(mut)
pool_state.is_active = true. Emits PoolUnpaused.State Accounts
DexConfig
Global singleton. One per protocol deployment.| Field | Type | Description |
|---|---|---|
authority | Pubkey | Config authority (Team Multisig after bootstrap) |
pending_authority | Option<Pubkey> | Pending authority transfer target |
pause_authority | Pubkey | Emergency pause signer (Team Multisig, immutable) |
base_fee_bps | u16 | Default swap fee (default: 50 = 0.5%) |
lp_fee_share_bps | u16 | LP’s share of fee (default: 5,000 = 50%) |
areal_fee_destination | Pubkey | Areal 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). |
rebalancer | Pubkey | Pool Rebalancer wallet — only signer allowed to call grow_liquidity and compress_liquidity. Set to [0u8;32] to disable grow/compress (kill-switch). |
is_active | bool | Global DEX kill switch |
bump | u8 | PDA bump seed |
["dex_config"]
PoolState
One per token pair. Stores reserves, shares, fees.| Field | Type | Description |
|---|---|---|
pool_type | PoolType | StandardCurve or MonotonicLadder |
token_a_mint | Pubkey | Token A mint |
token_b_mint | Pubkey | Token B mint |
vault_a | Pubkey | Token A vault (authority = pool PDA) |
vault_b | Pubkey | Token B vault (authority = pool PDA) |
reserve_a | u64 | Current A balance |
reserve_b | u64 | Current B balance |
total_lp_shares | u128 | Outstanding LP shares |
fee_bps | u16 | Swap fee (copied from DexConfig at creation) |
is_active | bool | Pool active flag (pauseable by Team Multisig) |
total_fees_accumulated | u64 | Lifetime total fees (LP + protocol) |
cumulative_fees_per_share_a | u128 | Q64.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_b | u128 | Q64.64 running sum on the token-B side: cumulative_fees_per_share_b += (fee_lp_b << 64) / total_lp_shares per swap |
bin_step_bps | u16 | Log bin step (0 for StandardCurve; typically 10 = 0.1% for MonotonicLadder) |
active_bin_id | i32 | Current active bin (MonotonicLadder only) |
left_anchor_bin | i32 | Top of permanent tail (MonotonicLadder only, immutable) |
permanent_tail_floor_bin | i32 | Bottom of permanent tail (MonotonicLadder only, immutable) |
last_rebalance_nav_bin | i32 | NAV bin of the most recent grow_liquidity or compress_liquidity call (MonotonicLadder only) |
active_zone_lower | i32 | Lower bound of current active bid wall (MonotonicLadder only) |
ot_treasury_fee_destination | Option<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. |
bump | u8 | PDA bump seed |
["pool", token_a_mint, token_b_mint]
PoolCreators
Whitelist of wallets allowed to create pools. Max 10.| Field | Type | Description |
|---|---|---|
authority | Pubkey | Who can add/remove creators |
creators | [Pubkey; 10] | Whitelisted wallets |
active_count | u8 | Number of active creators |
bump | u8 | PDA bump seed |
["pool_creators"]
LpPosition
Per (pool, provider) LP tracking. Created on first add viainit_if_needed.
| Field | Type | Description |
|---|---|---|
pool | Pubkey | Associated pool |
owner | Pubkey | LP provider wallet |
shares | u128 | LP shares held |
fees_claimed_per_share_a | u128 | Q64.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_b | u128 | Q64.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_ts | i64 | Last interaction timestamp |
bump | u8 | PDA bump seed |
["lp", pool_state, provider]
BinArray
Monotonic Ladder liquidity bins. One per MonotonicLadder pool.| Field | Type | Description |
|---|---|---|
pool | Pubkey | Associated 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_id | i32 | ID of bins[0] |
bin_step_bps | u16 | Log price step between bins (e.g., 10 = 0.1%) |
active_bin_id | i32 | Current active bin (moved only by swaps) |
bump | u8 | PDA bump 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.| Field | Type | Description |
|---|---|---|
manager | Pubkey | Bot wallet that can execute nexus operations |
total_deposited_usdc | u64 | Cumulative USDC deposited via nexus_deposit |
total_deposited_rwt | u64 | Cumulative RWT deposited via nexus_deposit |
is_active | bool | Active flag |
bump | u8 | PDA bump seed |
["liquidity_nexus"]
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
| Account | Seeds | Description |
|---|---|---|
| DexConfig | "dex_config" | Global config singleton |
| PoolCreators | "pool_creators" | Creator whitelist |
| PoolState | "pool", token_a_mint, token_b_mint | Per-pair pool |
| LpPosition | "lp", pool_state, provider | Per-LP tracking |
| BinArray | "bins", pool_state | Concentrated bins |
| LiquidityNexus | "liquidity_nexus" | Areal Finance LP manager |
authority = pool_state PDA. Created as keypair accounts at pool creation.Constants
| Constant | Value | Description |
|---|---|---|
BPS_DENOMINATOR | 10,000 | 100% in basis points |
DEFAULT_BASE_FEE_BPS | 50 | 0.5% swap fee |
DEFAULT_LP_FEE_SHARE_BPS | 5,000 | 50% of fee to LP |
MAX_FEE_BPS | 1,000 | 10% max fee cap |
MAX_BINS | 1000 | Bins per MonotonicLadder pool |
GEOMETRIC_R_BPS | 8500 | Density ratio for active bid wall (r = 0.85) |
ACTIVE_ZONE_WIDTH | 40 | Bins in the active bid wall |
DEFAULT_PERMANENT_TAIL_OFFSET_BPS | 100 | Permanent tail sits at initial_NAV − 1% |
DEFAULT_BIN_STEP_BPS | 10 | 0.1% between bins |
MAX_POOL_CREATORS | 10 | Max whitelisted creators |
MIN_LIQUIDITY | 1,000 | Min shares on first add (dust prevention) |
OT_TREASURY_FEE_BPS | 50 | 0.5% additional fee for OT pairs — sent to OT Treasury RWT ATA |
RWT_MINT | hardcoded | RWT token mint — all pools must include RWT (validated in create_pool) |
USDC_MINT | hardcoded | USDC mint — for nexus_deposit token validation |
OT_PROGRAM_ID | hardcoded | OT contract program ID — validated in create_pool to verify OT Treasury PDA ownership |
YD_PROGRAM_ID | hardcoded | Validated in compound_yield |
Events
| Event | Fields | When |
|---|---|---|
DexInitialized | authority, base_fee_bps, timestamp | DEX created |
PoolCreated | pool, token_a_mint, token_b_mint, pool_type, creator, ot_treasury_fee_destination, timestamp | Pool created |
LiquidityAdded | pool, provider, amount_a, amount_b, shares_minted, timestamp | LP added |
ZapLiquidityExecuted | pool, provider, input_a, input_b, swapped_amount, shares_minted, timestamp | Zap: auto-swap + add LP |
LiquidityRemoved | pool, provider, amount_a, amount_b, shares_burned, timestamp | LP removed |
LiquidityShifted | pool, rebalancer, old_lower, old_upper, new_lower, new_upper, timestamp | Bins rebalanced |
SwapExecuted | pool, user, a_to_b, amount_in, amount_out, fee_lp, fee_protocol, fee_ot_treasury, timestamp | Swap completed |
LpFeesClaimed | pool, lp_holder, amount, timestamp | LP holder claimed swap fee rewards |
CompoundYieldExecuted | pool, rwt_claimed, timestamp | OT yield auto-compounded into pool |
PoolCreatorsUpdated | wallet, action, active_count, timestamp | Whitelist changed |
DexConfigUpdated | base_fee_bps, lp_fee_share_bps, rebalancer, is_active, timestamp | Config changed |
ArealFeeDestinationUpdated | old_destination, new_destination, timestamp | Protocol fee destination rotated by authority |
AuthorityTransferProposed | current_authority, pending_authority, timestamp | Transfer proposed |
AuthorityTransferAccepted | old_authority, new_authority, timestamp | Transfer accepted |
PoolPaused | pool, timestamp | Pool emergency paused |
PoolUnpaused | pool, timestamp | Pool resumed |
NexusInitialized | manager, timestamp | Nexus created |
NexusDeposited | token_mint, amount, timestamp | Capital deposited into Nexus |
NexusRewardsClaimed | amount, treasury_destination, timestamp | LP fee rewards claimed from YD to Areal Treasury |
NexusProfitsWithdrawn | token_mint, amount, remaining_profit, treasury_destination, timestamp | Profits withdrawn to Areal Treasury |
NexusManagerUpdated | old_manager, new_manager, timestamp | Nexus manager changed |
Error Codes
| Error | Description |
|---|---|
Unauthorized | Not DEX authority |
CreatorNotWhitelisted | Not in pool creators whitelist |
DexPaused | Global DEX is_active = false |
PoolNotActive | Pool is_active = false |
WhitelistFull | Max 10 creators |
IdenticalMints | token_a == token_b |
CreatorNotFound | Remove: creator not in whitelist |
ZeroAmount | Amount = 0 |
InsufficientLiquidity | Pool reserves empty |
InsufficientShares | LP has fewer shares than burn |
InitialLiquidityTooSmall | First add < MIN_LIQUIDITY |
SlippageExceeded | Output < min_amount_out |
ZeroOutput | Swap would produce 0 |
EmptyReserves | Cannot swap with 0 reserves |
MathOverflow | Arithmetic overflow |
InvalidFee | base_fee_bps > MAX_FEE_BPS |
InvalidFeeShare | lp_fee_share_bps > 10,000 |
InvalidBinRange | lower ≥ upper or out of bounds |
BinOutOfRange | Bin ID outside BinArray |
InsufficientBinLiquidity | No liquidity in bins for swap |
InvalidBinStep | bin_step_bps = 0 for concentrated |
MissingRwtMint | Neither token_a nor token_b is RWT_MINT |
InvalidMintOrder | token_a_mint >= token_b_mint (must be canonical order) |
InvalidVault | target_vault not vault_a or vault_b |
NothingToCompound | No RWT received from YD |
SelfTransfer | Cannot transfer authority to yourself |
NoPendingAuthority | No pending transfer |
InvalidPendingAuthority | Signer ≠ pending_authority |
InvalidOtTreasuryDestination | ot_treasury_fee_destination does not match derived RWT ATA for OT Treasury PDA, or OT Treasury PDA not owned by OT_PROGRAM_ID |
InvalidProtocolFeeDestination | areal_fee_account mint does not match RWT_MINT — protocol fee destination must be an RWT ATA |
MissingOtTreasuryAccount | Pool has ot_treasury_fee_destination set but ot_treasury_fee_account not provided in swap |
InvalidNexusToken | nexus_deposit token_mint is not USDC_MINT or RWT_MINT |
NexusNotActive | Nexus is_active = false |
InvalidNexusManager | Signer ≠ nexus.manager |
NexusClaimFailed | Nexus PDA not found in merkle tree or proof invalid |
Architecture & Integration Guide
Cross-Program Integration
← RWT Engine (vault swaps)
← RWT Engine (vault swaps)
- RWT Engine
vault_swapCPIs tonative_dex::swap - RWT Vault PDA signs as user
- Manager controls direction and slippage
← Yield Distribution (USDC→RWT conversion)
← Yield Distribution (USDC→RWT conversion)
- YD
convert_to_rwtCPIs tonative_dex::swap(buy RWT below NAV) - YD Accumulator PDA signs as user
Liquidity Nexus (internal DEX PDA)
Liquidity Nexus (internal DEX PDA)
- 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
→ Yield Distribution (compound_yield)
→ Yield Distribution (compound_yield)
- 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)
← Pool Rebalancer (grow_liquidity / compress_liquidity)
← Pool Rebalancer (grow_liquidity / compress_liquidity)
- Rebalancer bot calls
grow_liquidityon Monotonic Ladder pools when NAV rises past the 1% deviation threshold — extends bid wall rightward using Nexus USDC - Rebalancer bot calls
compress_liquidityafter 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
Deployment Checklist
Prerequisites: Yield Distribution must be deployed (needed for compound_yield CPI).- Call
initialize_dexwith areal_fee_destination (Areal Finance RWT ATA), pause_authority (Team Multisig), rebalancer (bot wallet) - Call
initialize_nexuswith manager (Nexus bot wallet) - Create Nexus ATAs — create USDC ATA and RWT ATA owned by Nexus PDA (needed before OT destinations and RWT Engine config point to them)
- Add pool creators via
update_pool_creators - 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: passot_treasuryPDA and its RWT ATA — OT Treasury must already be initialized via the OT contract before pool creation - Transfer authority to Team Multisig via
propose_authority_transfer+accept_authority_transfer
Token Flow Summary
| From | To | Mechanism | Who triggers |
|---|---|---|---|
| User token_in | Pool vault_in | swap | User |
| Pool vault_out | User token_out | swap | User |
| Swap fee (LP share) | Stays in pool reserve vault on the fee-bearing side; tracked via cumulative_fees_per_share_<side> on PoolState | swap | Automatic |
| 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) | swap | Automatic |
| Swap fee (OT treasury) | OT Treasury RWT ATA (OT pairs only, always in RWT) | swap | Automatic |
| LP tokens A+B | Pool vaults | add_liquidity | LP provider |
| LP any ratio / single token | Pool vaults (via internal swap) | zap_liquidity | LP provider |
| Pool vaults | LP tokens A+B | remove_liquidity | LP provider |
| YD reward vault | Pool reserves (RWT) | compound_yield (CPI to YD) | Crank |
| OT Revenue (10% USDC) | Crank USDC ATA → Nexus USDC ATA | OT distribute_revenue → nexus_deposit (two-step: crank receives, then deposits) | Crank |
| RWT Engine yield (15% RWT) | LiquidityHolding RWT ATA → Nexus RWT ATA | YD 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 tokens | Pool vaults | nexus_add_liquidity | Nexus manager |
| Pool vaults | Nexus tokens | nexus_remove_liquidity | Nexus manager |
| Nexus token (USDC or RWT) | Areal Treasury ATA | nexus_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 contract —
LiquidityHoldingPDA andwithdraw_liquidity_holding(the RWT-lane atomic drain that CPIs intonexus_record_deposit) - RWT Engine contract —
claim_yield70 / 15 / 15 split that produces the 15% RWT slice routed through the RWT lane