feat: add per-hop slippage to single swaps and flip to output/input ratio#516
Open
feat: add per-hop slippage to single swaps and flip to output/input ratio#516
Conversation
The 1e18 precision caused the price calculation to truncate to 0 for token pairs with extreme value ratios (e.g. WBTC/PEPE), silently disabling the slippage check. 1e36 provides sufficient precision while remaining overflow-safe (uint128_max * 1e36 < uint256_max). Adds regression test with an extreme price pool verifying the check still fires for high ratio pairs.
Adds uint256 maxHopSlippage field to ExactInputSingleParams and ExactOutputSingleParams. When non-zero, enforces a price-based (amountIn * PRECISION / amountOut) slippage check. This prevents sandwich attacks when single swaps follow V2/V3 hops with positive slippage in the Universal Router. Introduces V4TooLittleReceivedPerHopSingle and V4TooMuchRequestedPerHopSingle errors for the single swap case.
Changes price formula from amountIn/amountOut (revert if > max) to amountOut/amountIn (revert if < min). This makes 0 a natural disabled value for single swap maxHopSlippage since any real output/input ratio is always > 0, removing the need for an explicit != 0 guard.
zhongeric
reviewed
Mar 13, 2026
src/V4Router.sol
Outdated
| _swap(params.poolKey, params.zeroForOne, -int256(uint256(amountIn)), params.hookData).toUint128(); | ||
| if (amountOut < params.amountOutMinimum) revert V4TooLittleReceived(params.amountOutMinimum, amountOut); | ||
| if (params.minHopPriceX36 != 0) { | ||
| uint256 price = uint256(amountOut) * PRECISION / amountIn; |
test/router/V4Router.t.sol
Outdated
| uint256 amountIn = 1 ether; | ||
| uint256 expectedAmountOut = 992054607780215625; | ||
|
|
||
| uint256 expectedPrice = expectedAmountOut * 1e36 / amountIn; |
test/router/V4Router.t.sol
Outdated
Comment on lines
+328
to
+331
| // With output/input formula: price is large for this direction, no truncation concern. | ||
| // The precision concern applies in the reverse direction (tiny output, huge input). | ||
| uint256 expectedPrice = actualAmountOut * 1e36 / amountIn; | ||
| assertGt(expectedPrice, 0, "price should be non-zero"); |
There was a problem hiding this comment.
wait maybe im reading this wrong but shouldn't we also check that using 1e18 for precision would result in an expected price of 0?
also we are doing output/input so the comment doesn't make sense to me as it seems like it shouldn't be an issue in the current implementation?
|
Could you update the PR description too plz? |
zhongeric
approved these changes
Mar 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Add minHopPriceX36 field to ExactInputSingleParams and ExactOutputSingleParams for price-based per-hop slippage protection on single swaps -- prevents sandwich attacks when single swaps follow V2/V3 hops with
positive slippage in the Universal Router
Flip the per-hop slippage formula from amountIn * PRECISION / amountOut (revert if > max) to amountOut * PRECISION / amountIn (revert if < min) across all swap types (single + multi-hop)
The flipped formula makes 0 a natural disabled value (any real output/input ratio is > 0), removing the need for an explicit != 0 guard on single swaps
Add V4TooLittleReceivedPerHopSingle and V4TooMuchRequestedPerHopSingle errors for single swap per-hop slippage violations
Update CalldataDecoder minimum length checks for the new struct field
Increase PRECISION from 1e18 to 1e36 to handle extreme value ratio pairs (e.g. WBTC/PEPE) where the old precision truncated to 0
Rename local price variables to priceX36 for clarity on the scaling factor
Replace raw 1e36 literals in tests with a PRECISION constant
Test plan
Revert + success tests for ExactInputSingle per-hop slippage
Revert + success tests for ExactOutputSingle per-hop slippage
Existing multi-hop per-hop slippage tests updated for flipped formula
Extreme ratio regression test with ~1e22 price pool -- asserts 1e18 precision truncates to 0 while 1e36 preserves resolution
CalldataDecoder fuzz tests assert new minHopPriceX36 field
Full suite passes (486/486) with --isolate
🤖 Generated with Claude Code