In Uniswap v3, amountOutMinimum is a router check on total output, while sqrtPriceLimitX96 bounds how far the pool’s price may move during a swap. Reaching the price boundary can leave input unspent. The call succeeds only if the output it produced still meets the minimum. Choose both constraints together, then reconcile the tokens actually spent, received and left over.
The distinction matters when an instruction says “spend up to this amount without moving the pool beyond this price.” A price boundary can preserve that limit while leaving the spending plan unfinished. An output floor can then accept or reject the partial result.
This article follows exactInputSingle in Uniswap’s v3-periphery v1.3.0 SwapRouter, alongside v3-core v1.0.0. These are named implementations for an educational comparison. Match your actual router and interface before applying their behavior elsewhere.
Where each check runs
The released v3 single-swap interface places the input amount, minimum output and square-root price limit in one parameter structure. They answer different questions.
| Control | Unit | Question it answers |
|---|---|---|
amountIn | Raw input-token amount | What input amount is submitted to the exact-input swap? |
amountOutMinimum | Raw output-token amount | Is the returned total output sufficient? |
sqrtPriceLimitX96 | Encoded pool square-root price | How far may the pool price move in this direction? |
The router invokes the pool and receives the resulting output amount. It then checks whether that amount is at least amountOutMinimum; otherwise the swap call reverts. The output floor therefore judges the result of the attempted execution. It does not tell the pool where to stop along its price path. SwapRouter v1.3.0.
Inside the pool, the swap loop continues while an amount remains and the square-root price has not reached the specified limit. A reachable boundary can end that loop with input still available. That is an ordinary stopping condition in the v3-core v1.0.0 pool, rather than an automatic minimum-output failure.
Read the boundary in pool units
The encoded price uses the square root of the raw token1-to-token0 amount ratio, scaled by 2^96. It is not a percentage tolerance or a chart price pasted into an integer field. Token order and decimal scales determine how a human quote maps to it. The pool action interface defines this representation and the two swap directions.
With token0 as input and token1 as output, the encoded pool price decreases, so the boundary is a lower limit. With token1 as input, it increases, so the boundary is an upper limit. A displayed reciprocal quote reverses the human direction; it does not change the pool’s token order.
The pool also requires the limit to be strictly on the appropriate side of the starting encoded price and within its supported bounds. A boundary already passed when execution begins can therefore fail the pool’s validation, rather than produce a zero-sized successful swap. Those checks appear before the swap loop in the released pool source.
In the scoped router, supplying zero for sqrtPriceLimitX96 selects a direction-specific boundary near the protocol extreme. Zero means no custom price boundary through that router convention; it does not remove the pool’s bounds or make the output floor unnecessary. The router implementation performs that substitution before calling the pool.
One budget, three outcomes
Consider an original hypothetical pool with ordinary ERC-20 tokens A and B. A is token0, B is token1, and they have equal decimals. Define P as B per A. The trader supplies B to acquire A, so P rises.
Assume constant active liquidity L = 10,000 across the entire compared path, starting P = 1, and a 500 B gross input budget. L is the liquidity parameter in these normalized units, not dollar TVL. Use an illustrative fixed input fee of 0.30%. Ignore gas, token transfer taxes, competing transactions and integer rounding. This is a continuous teaching calculation; output floors below are whole-token display equivalents, not raw transaction parameters.
For a move from square-root price a to b, net B input is L × (b − a). A output is L × (1/a − 1/b). These within-range relationships correspond to the token deltas in SqrtPriceMath v1.0.0. The separate concentrated-liquidity path example explains what changes when active liquidity varies; here it stays fixed to isolate the two guards.
First impose an upper boundary at P = 1.050625, whose square root is 1.025. Reaching it requires 10,000 × (1.025 − 1) = 250 B of net input. The resulting output is 10,000 × (1 − 1/1.025) ≈ 243.902439 A.
With the illustrative input fee, the gross amount spent is 250 ÷ 0.997 ≈ 250.752257 B. The fee is approximately 0.752257 B, leaving 500 − 250.752257 ≈ 249.247743 B unspent. Fees are charged in the input calculation in the scoped SwapMath implementation; its actual arithmetic also rounds integer amounts.
For comparison, remove the custom boundary and spend the entire 500 B budget in the same model. Net input becomes 498.5 B. Square-root price reaches 1 + 498.5/10,000 = 1.04985, giving terminal P = 1.1021850225 and approximately 474.829738 A of output.
| Hypothetical instruction | Attempted A output | B input settled | Result |
|---|---|---|---|
| No custom boundary; minimum 470 A | 474.829738 | 500.000000 | Passes; budget spent |
| Boundary P = 1.050625; minimum 240 A | 243.902439 | 250.752257 | Passes; input remains |
| Same boundary; minimum 470 A | 243.902439 | 0; swap rolled back | Reverts; floor fails |
The third row does not keep the attempted 243.902439 A as a partial acquisition. The failing router check rolls back that swap call’s asset changes. If another contract catches the failure and continues, its surrounding behavior must be inspected separately. This call-level rollback follows Solidity’s exception semantics. The Universal Router allow-revert guide examines a different execution model.
In the second row, success confirms that the floor passed, while almost half the budget remains unused. Reporting only “swap successful” would omit a material part of the outcome.
Compare average cost with the terminal price
In the first row, a 470 A floor against a fully spent 500 B budget implies a maximum gross average acquisition price of approximately 500/470 = 1.063830 B per A. The actual gross average is approximately 1.053009 B per A, which satisfies that condition.
Yet terminal pool P is about 1.102185, above both numbers. The swap traverses a curve: early input exchanges at more favorable pool prices than late input. A constraint on aggregate output therefore cannot establish the last marginal price reached.
The partial case separates these measures again. Its gross average is approximately 250.752257/243.902439 = 1.028084 B per A, while terminal P is 1.050625. That terminal pool ratio excludes the input fee. Treating it as a fee-inclusive average would misread the example.
This distinction resembles the difference between average execution and individual price permissions in market-order slippage caps. The mechanisms differ: the present example stops a continuous pool path rather than matching eligible order-book levels.
Where the unspent 249.247743 B goes
The remainder is an accounting question as well as an execution question. Two integration designs can produce the same pool swap but place the leftover tokens in different locations.
If an ordinary ERC-20 payment is pulled directly from the payer for the amount owed, the unspent portion stays with that payer. If a wrapper first transfers the full budget into itself, that wrapper can still hold the remainder after the swap. The router’s callback and PeripheryPayments v1.3.0 distinguish payment from the payer from payment using contract-held tokens.
Uniswap’s current v3 single-swap guide explicitly warns integrations using a nonzero price limit to handle unswapped tokens. Follow the actual custody path; the output recipient alone does not identify who holds the unused input.
Also distinguish unspent tokens from remaining spending permission. The ERC-20 standard defines an allowance as the amount a spender is still authorized to withdraw. A balance can remain while that permission persists. If a signature or approval is involved, the Permit2 versus ERC-2612 comparison helps identify the spender and permission scope. Neither output acceptance nor a pool price boundary substitutes for that check.
Write a receipt for the floor, boundary and remainder
A practical test should exercise both the accepted partial result and the rejected result. In this fixture, changing only the floor from 240 A to 470 A changes the outcome from a retained partial swap to a reverted call. A test that always consumes the entire budget misses the distinction.
| Validation item | Evidence to retain |
|---|---|
| Match the implementation | Chain, target router, method and applicable interface |
| Preserve the quote state | Pool, block reference, direction, fee and observed price |
| Check the encoded boundary | Token order, decimals, intended human price and submitted integer |
| Check output acceptance | Minimum output and actual returned output in raw units |
| Reconcile the budget | Actual input spent, input fee and unspent amount |
| Follow custody and permission | Remainder holder, refund behavior and residual allowance |
| Interpret failure scope | Which call reverted and whether surrounding execution continued |
Do not copy the single-pool parameter structure into a multihop method. In this released router, exactInput has no per-hop custom price-limit field in its interface, and the method implementation passes the zero sentinel to each internal hop. Inspect the target method before assuming it enforces the same instructions.
For the worked boundary, write the acceptance rule as “receive at least 240 A, with pool P no higher than 1.050625,” then record the separate budget result: approximately 250.752257 B spent and 249.247743 B remaining. That receipt shows both the accepted output and the capital still available.
