Skip to Content
Stacks PoolsConcentrated Liquidity PoolsSwaps, Fees and Price Protection
Stacks Pools

Swaps, Fees and Price Protection

Learn how swaps walk through Zest Protocol concentrated liquidity bins, how fees are charged and shared, and how price limits, minimum outputs and deadlines protect every trade.

How a Swap Executes

A swap spends its input bin by bin, in price order. In each bin it spends as much as the bin can take before one of three things happens: the bin runs out of the token being bought, the price reaches the trader’s limit, or the bin reaches its size bound, the largest reserve a bin can hold, far beyond any real market. Whatever input is left moves on to the next bin.

Anatomy of a swap: selling X for Y walks the bins in rising price order and fills each at most once. Moving to the next bin uses no input, and the trade stops when its input is spent or just before its price limit.

In plain terms, for a trader selling X for Y:

  1. The trade starts in the lowest-priced bin that still holds Y, and swaps X for Y along that bin’s curve.
  2. When that bin has no Y left, the trade moves to the next bin up. Moving costs nothing; untouched bins keep their reserves.
  3. The trade stops when the input is spent, or just before the price limit, and any unspent input stays in the trader’s wallet.
  4. Each bin it trades through keeps the liquidity providers’ share of the fee; the rest accrues to the protocol.

How the Price Moves Through Bins

One trade, one curve: the price rises smoothly inside each bin and continues into the next from the same price. The line bends at a bin edge, where the liquidity changes, and moving to the next bin uses no input.

Inside a bin, each unit of input buys a little less than the one before, so the price rises smoothly. In a pool’s normal state the bins above the price hold only Y and sit at their bottom edge, so when a trade empties one bin, the next starts at exactly the price where the last one ended. The price line continues without a break and only bends where the liquidity changes, as a Uniswap v3 price path does at a tick.

Every bin keeps its own price, and that price always lies inside the bin. Because a trade walks bins in price order, a bin that is out of line with its neighbors starts a little higher, never lower. Arbitrage brings such bins back to their edges. The price limit applies in every bin, and the minimum output to the route as a whole.

The caller chooses the bins. Front-ends and aggregators read the pool’s bins, pick the ones the trade should cross, and pass them as a list:

  • When selling X for Y, the price rises, so ticks must be strictly ascending.
  • When selling Y for X, the price falls, so ticks must be strictly descending.

Empty bins and bins beyond the price limit are read but left unchanged, so a bin emptied between quote and execution cannot make a route fail.

Swap Types

TypeContractFunctionBehavior
Single bin, IOCclmm-pool-v1swap-x-for-y-ioc, swap-y-for-x-iocSpends what fits in one bin; the rest of the input stays in the wallet
Single bin, FOKclmm-router-v1swap-x-for-y-fok, swap-y-for-x-fokReverts unless the whole input fits in one bin
Multi-bin routeclmm-router-v1swap-x-for-y, swap-y-for-xWalks up to 32 bins of one pool
Multi-pool routeclmm-router-v1swap-routesChains up to 9 pools; each hop must fill completely

A multi-bin route can allow a partial fill. With allow-partial-fill false, the route reverts unless it spends the whole input; with it true, the unspent input stays in the wallet. A whole multi-bin route pays its input and output in one transfer each way.

A multi-pool route swaps pool by pool: each pool takes its own transfer in and pays its own transfer out, every hop must fill completely, and one minimum applies to the final output.

One route, one check: each pool in a route settles its own hop, and the route checks the minimum only on what you finally receive.

Price Protection

Every swap carries these protections, checked on-chain:

  • Price limit. max-price (selling X) or min-price (selling Y) bounds the marginal price, in WAD (10¹⁸ = price 1, quoted as X per Y). The swap stops before the price passes it.
  • Minimum output. min-dy or min-dx bounds the total output. The price limit bounds the curve price, not the fee; the minimum output is what guarantees the amount received.
  • Deadline. Every swap takes a deadline in seconds. A transaction mined in a block whose timestamp is later than the deadline reverts.

If any of these is violated, the whole transaction reverts and no tokens move. Multi-pool routes take one deadline for every hop and one minimum for the final output.

Stacks wallet post-conditions add a further check: they cap which tokens can leave the signer’s account and in what amount.

Fees

Swap Fees

Each pool has two fee rates, one for each input token:

  • fee-rate-x is charged on X when selling X for Y;
  • fee-rate-y is charged on Y when selling Y for X.

Rates are in units of 10⁸ (u1000000 = 1%), and no pool can charge more than 10%. The fee is taken from the input and rounds up, so splitting a trade into tiny pieces cannot avoid it.

Where Fees Go

Each pool has an LP fee share, set in its fee-rebate field: the part of every fee that goes to liquidity providers. That part is added to the bin the fee was paid in and compounds into its shares. The remainder accrues to the protocol, per pool, and governance can claim it.

The same split applies to the implicit swap fee paid by an off-ratio deposit.

Partner Discounts

Governance can give a partner address a percentage off a public pool’s fees, on its swaps, routes and deposits alike. A discount lowers what that pool’s liquidity providers earn from the partner’s trades. Private pools take no discounts. get-fee-rates returns the rates a given address pays, and the quote-*-as functions quote at those rates.

Fee Changes

Fee rates can be changed by governance, and a change takes effect from the transaction that executes it. A swap already signed but not yet mined pays the rate in force when it is mined, up to the 10% cap. The minimum output protects against this: set it from the quote, not from zero.

Quotes

The pool’s quote functions run the same arithmetic as the swaps against a bin’s current state:

  • quote-swap-x-for-y and quote-swap-y-for-x quote a single-bin swap at the pool’s fee rates;
  • quote-swap-x-for-y-as and quote-swap-y-for-x-as quote at the rates a given address pays;
  • simulate-swap-x-for-y and simulate-swap-y-for-x quote against any reserves, for route planning off-chain.

Quotes do not check the pool’s state or the input size limit, so a quote can succeed for a paused pool, or for an amount the swap itself would reject.

Last updated on