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.
In plain terms, for a trader selling X for Y:
- The trade starts in the lowest-priced bin that still holds Y, and swaps X for Y along that bin’s curve.
- When that bin has no Y left, the trade moves to the next bin up. Moving costs nothing; untouched bins keep their reserves.
- The trade stops when the input is spent, or just before the price limit, and any unspent input stays in the trader’s wallet.
- 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
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
| Type | Contract | Function | Behavior |
|---|---|---|---|
| Single bin, IOC | clmm-pool-v1 | swap-x-for-y-ioc, swap-y-for-x-ioc | Spends what fits in one bin; the rest of the input stays in the wallet |
| Single bin, FOK | clmm-router-v1 | swap-x-for-y-fok, swap-y-for-x-fok | Reverts unless the whole input fits in one bin |
| Multi-bin route | clmm-router-v1 | swap-x-for-y, swap-y-for-x | Walks up to 32 bins of one pool |
| Multi-pool route | clmm-router-v1 | swap-routes | Chains 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.
Price Protection
Every swap carries these protections, checked on-chain:
- Price limit.
max-price(selling X) ormin-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-dyormin-dxbounds 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
deadlinein 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-xis charged on X when selling X for Y;fee-rate-yis 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-yandquote-swap-y-for-xquote a single-bin swap at the pool’s fee rates;quote-swap-x-for-y-asandquote-swap-y-for-x-asquote at the rates a given address pays;simulate-swap-x-for-yandsimulate-swap-y-for-xquote 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.