Exact-in and exact-out
Both are single-hop, single-pool swaps, exposed as two separate on-chain instructions rather
than one instruction with a mode flag — the per-bin math genuinely branches on which side is
fixed:
swap_exact_in: fixes the gross input, drains bins until it’s fully consumed or the
pool runs out. min_amount_out is the slippage floor.
swap_exact_out: fixes the exact net output the trader wants to receive, drains bins
until that target is reached. max_amount_in is the slippage ceiling.
x_to_y = true: caller pays X, receives Y, price moves down, traversal walks bins in
descending id order from the active bin. x_to_y = false: the mirror — caller pays Y, receives
X, price moves up, traversal ascends.
Traversal
Bins below the active bin are Y-only, bins above are X-only (see
Positions and liquidity) — a swap simply walks that
partition in the appropriate direction, consuming the output side of each bin it passes
through, until the trade is filled.
Traversal uses sparse bin arrays with no bitmap. The client supplies the relevant
BinArray accounts directly (including verified gaps for any uninitialized stretch); the
program has no on-chain index of which price regions are populated. This is the load-bearing
design trade-off behind the protocol’s scale limit:
MAX_BIN_ARRAYS_PER_TRAVERSAL = 6, 100 bins per array, so a single swap can cross at
most 600 bins. At the tightest supported bin step (1 bp), that’s roughly a 6.2% worst-case
price drift ceiling per swap — the floor, since every coarser bin step covers proportionally
more price range per bin. A trade that would need to walk further must be split across
multiple transactions. This is a deliberate trade-off for the no-bitmap design, not an
oversight — it keeps a swap’s compute and account-loading cost bounded regardless of how much
of the price curve is populated.
The fold terminates the instant the trade is filled — it does not necessarily touch every
account the client provided. If the supplied bin arrays run out before the trade fills, the
instruction fails with InsufficientLiquidity: a correct signal that the trade needs more
account coverage or must be split, not a bug.
Fees are per-bin, and rise mid-swap
Each bin crossed pays a fee: gross_amount_in − net_amount_in for that bin, split between LPs
and the protocol (see Fees). Critically, the fee rate itself isn’t constant
across one swap — advancing the active bin also accumulates the dynamic fee’s
volatility state, so later bins in the same large swap are priced at a higher rate than
earlier ones. A quote that doesn’t simulate this bin-by-bin will systematically underprice a
swap that crosses more than one bin.
Every rounding decision favors the pool
Whichever side of a fill is the “cost” (the input on a drain, the input on an exact-out fill)
rounds up; whichever side is the free variable rounds down. This holds symmetrically for both
directions and both modes — a trader can never extract more value than the bin’s true reserves
represent, and the accumulated rounding always lands with LPs, never against them.
Token-2022 transfer fees
Bins never see a transfer-fee-extension mint directly — the gross/net conversion happens
outside the bin fold entirely:
- Exact-in: nets the input down before folding, then nets the fold’s raw output down before
checking it against
min_amount_out.
- Exact-out: grosses the output target up before folding, then grosses the fold’s raw input
up before checking it against
max_amount_in.
So slippage protection always applies to what the trader actually receives or actually pays,
not the pre-transfer-fee intermediate value — a fee-bearing mint can’t be used to bypass it.
Transfer hooks
If either mint carries an active Token-2022 TransferHook extension, the swap instruction
resolves and appends its extra accounts automatically as part of the settlement CPI — nothing
extra to configure from the SDK’s perspective. Building the instruction by hand (without the
SDK) requires supplying those extra accounts yourself via the TransferHookX/TransferHookY
remaining-accounts group.