Skip to content
8 changes: 4 additions & 4 deletions snapshots/PaymentsTests.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"Payments_swap_settleFromCaller_takeAllToMsgSender": "133006",
"Payments_swap_settleFromCaller_takeAllToSpecifiedAddress": "134475",
"Payments_swap_settleWithBalance_takeAllToMsgSender": "126957",
"Payments_swap_settleWithBalance_takeAllToSpecifiedAddress": "127095"
"Payments_swap_settleFromCaller_takeAllToMsgSender": "133163",
"Payments_swap_settleFromCaller_takeAllToSpecifiedAddress": "134632",
"Payments_swap_settleWithBalance_takeAllToMsgSender": "127114",
"Payments_swap_settleWithBalance_takeAllToSpecifiedAddress": "127252"
}
16 changes: 8 additions & 8 deletions snapshots/V4RouterTest.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"V4Router_Bytecode": "9606",
"V4Router_Bytecode": "9950",
"V4Router_ExactIn1Hop_nativeIn": "122222",
"V4Router_ExactIn1Hop_nativeOut": "120688",
"V4Router_ExactIn1Hop_oneForZero": "129560",
Expand All @@ -8,9 +8,9 @@
"V4Router_ExactIn2Hops_nativeIn": "179580",
"V4Router_ExactIn3Hops": "250829",
"V4Router_ExactIn3Hops_nativeIn": "236965",
"V4Router_ExactInputSingle": "134001",
"V4Router_ExactInputSingle_nativeIn": "120137",
"V4Router_ExactInputSingle_nativeOut": "118581",
"V4Router_ExactInputSingle": "134158",
"V4Router_ExactInputSingle_nativeIn": "120294",
"V4Router_ExactInputSingle_nativeOut": "118738",
"V4Router_ExactOut1Hop_nativeIn_sweepETH": "128509",
"V4Router_ExactOut1Hop_nativeOut": "121898",
"V4Router_ExactOut1Hop_oneForZero": "130770",
Expand All @@ -20,8 +20,8 @@
"V4Router_ExactOut3Hops": "248399",
"V4Router_ExactOut3Hops_nativeIn": "241481",
"V4Router_ExactOut3Hops_nativeOut": "225554",
"V4Router_ExactOutputSingle": "133337",
"V4Router_ExactOutputSingle_nativeIn_sweepETH": "126419",
"V4Router_ExactOutputSingle_nativeOut": "119821",
"router initcode hash (without constructor params, as uint256)": "109481674309128023454344526833703660232982807837386720789725053502055679819514"
"V4Router_ExactOutputSingle": "133494",
"V4Router_ExactOutputSingle_nativeIn_sweepETH": "126576",
"V4Router_ExactOutputSingle_nativeOut": "119978",
"router initcode hash (without constructor params, as uint256)": "57167443502318477526720794142954804203351366829026518962672658585322779768213"
}
38 changes: 25 additions & 13 deletions src/V4Router.sol
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ abstract contract V4Router is IV4Router, BaseActionsRouter, DeltaResolver {
using CalldataDecoder for bytes;
using BipsLibrary for uint256;

uint256 private constant PRECISION = 1e18;
uint256 private constant PRECISION = 1e36;

constructor(IPoolManager _poolManager) BaseActionsRouter(_poolManager) {}

Expand Down Expand Up @@ -90,6 +90,12 @@ abstract contract V4Router is IV4Router, BaseActionsRouter, DeltaResolver {
uint128 amountOut =
_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 priceX36 = uint256(amountOut) * PRECISION / amountIn;
if (priceX36 < params.minHopPriceX36) {
revert V4TooLittleReceivedPerHopSingle(params.minHopPriceX36, priceX36);
}
}
}

function _swapExactInput(IV4Router.ExactInputParams calldata params) private {
Expand All @@ -102,19 +108,19 @@ abstract contract V4Router is IV4Router, BaseActionsRouter, DeltaResolver {
if (amountIn == ActionConstants.OPEN_DELTA) amountIn = _getFullCredit(currencyIn).toUint128();
PathKey calldata pathKey;

uint256 perHopSlippageLength = params.maxHopSlippage.length;
if (perHopSlippageLength != 0 && perHopSlippageLength != pathLength) revert InvalidHopSlippageLength();
uint256 perHopPriceLength = params.minHopPriceX36.length;
if (perHopPriceLength != 0 && perHopPriceLength != pathLength) revert InvalidHopPriceLength();

for (uint256 i = 0; i < pathLength; i++) {
pathKey = params.path[i];
(PoolKey memory poolKey, bool zeroForOne) = pathKey.getPoolAndSwapDirection(currencyIn);
// The output delta will always be positive, except for when interacting with certain hook pools
amountOut = _swap(poolKey, zeroForOne, -int256(uint256(amountIn)), pathKey.hookData).toUint128();

if (perHopSlippageLength != 0) {
uint256 price = amountIn * PRECISION / amountOut;
uint256 maxSlippage = params.maxHopSlippage[i];
if (price > maxSlippage) revert V4TooLittleReceivedPerHop(i, maxSlippage, price);
if (perHopPriceLength != 0) {
uint256 priceX36 = amountOut * PRECISION / amountIn;
uint256 minPrice = params.minHopPriceX36[i];
if (priceX36 < minPrice) revert V4TooLittleReceivedPerHop(i, minPrice, priceX36);
}

amountIn = amountOut;
Expand All @@ -136,6 +142,12 @@ abstract contract V4Router is IV4Router, BaseActionsRouter, DeltaResolver {
))
.toUint128();
if (amountIn > params.amountInMaximum) revert V4TooMuchRequested(params.amountInMaximum, amountIn);
if (params.minHopPriceX36 != 0) {
uint256 priceX36 = uint256(amountOut) * PRECISION / amountIn;
if (priceX36 < params.minHopPriceX36) {
revert V4TooMuchRequestedPerHopSingle(params.minHopPriceX36, priceX36);
}
}
}

function _swapExactOutput(IV4Router.ExactOutputParams calldata params) private {
Expand All @@ -151,8 +163,8 @@ abstract contract V4Router is IV4Router, BaseActionsRouter, DeltaResolver {
amountOut = _getFullDebt(currencyOut).toUint128();
}

uint256 perHopSlippageLength = params.maxHopSlippage.length;
if (perHopSlippageLength != 0 && perHopSlippageLength != pathLength) revert InvalidHopSlippageLength();
uint256 perHopPriceLength = params.minHopPriceX36.length;
if (perHopPriceLength != 0 && perHopPriceLength != pathLength) revert InvalidHopPriceLength();

for (uint256 i = pathLength; i > 0; i--) {
pathKey = params.path[i - 1];
Expand All @@ -161,10 +173,10 @@ abstract contract V4Router is IV4Router, BaseActionsRouter, DeltaResolver {
amountIn = (uint256(-int256(_swap(poolKey, !oneForZero, int256(uint256(amountOut)), pathKey.hookData))))
.toUint128();

if (perHopSlippageLength != 0) {
uint256 price = amountIn * PRECISION / amountOut;
uint256 maxSlippage = params.maxHopSlippage[i - 1];
if (price > maxSlippage) revert V4TooMuchRequestedPerHop(i - 1, maxSlippage, price);
if (perHopPriceLength != 0) {
uint256 priceX36 = amountOut * PRECISION / amountIn;
uint256 minPrice = params.minHopPriceX36[i - 1];
if (priceX36 < minPrice) revert V4TooMuchRequestedPerHop(i - 1, minPrice, priceX36);
}
amountOut = amountIn;
currencyOut = pathKey.intermediateCurrency;
Expand Down
22 changes: 14 additions & 8 deletions src/interfaces/IV4Router.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,27 +13,32 @@ interface IV4Router is IImmutableState {
error V4TooLittleReceived(uint256 minAmountOutReceived, uint256 amountReceived);
/// @notice Emitted when an exactOutput is asked for more than its maxAmountIn
error V4TooMuchRequested(uint256 maxAmountInRequested, uint256 amountRequested);
/// @notice Emitted when an exactInput swap does not receive its relative minAmountOut per hop (max price)
error V4TooLittleReceivedPerHop(uint256 hopIndex, uint256 maxPrice, uint256 price);
/// @notice Emitted when an exactOutput is asked for more than its relative maxAmountIn per hop (max price)
error V4TooMuchRequestedPerHop(uint256 hopIndex, uint256 maxPrice, uint256 price);
/// @notice Emitted when the length of the maxHopSlippage array is not zero and not equal to the path length
error InvalidHopSlippageLength();
/// @notice Emitted when an exactInput swap does not receive its relative minAmountOut per hop (min price)
error V4TooLittleReceivedPerHop(uint256 hopIndex, uint256 minPrice, uint256 price);
/// @notice Emitted when an exactOutput is asked for more than its relative maxAmountIn per hop (min price)
error V4TooMuchRequestedPerHop(uint256 hopIndex, uint256 minPrice, uint256 price);
/// @notice Emitted when a single exactInput swap does not meet its relative price limit
error V4TooLittleReceivedPerHopSingle(uint256 minPrice, uint256 price);
/// @notice Emitted when a single exactOutput swap exceeds its relative price limit
error V4TooMuchRequestedPerHopSingle(uint256 minPrice, uint256 price);
/// @notice Emitted when the length of the per-hop minimum price array is not zero and not equal to the path length
error InvalidHopPriceLength();

/// @notice Parameters for a single-hop exact-input swap
struct ExactInputSingleParams {
PoolKey poolKey;
bool zeroForOne;
uint128 amountIn;
uint128 amountOutMinimum;
uint256 minHopPriceX36;
bytes hookData;
}

/// @notice Parameters for a multi-hop exact-input swap
struct ExactInputParams {
Currency currencyIn;
PathKey[] path;
uint256[] maxHopSlippage;
uint256[] minHopPriceX36;
uint128 amountIn;
uint128 amountOutMinimum;
}
Expand All @@ -44,14 +49,15 @@ interface IV4Router is IImmutableState {
bool zeroForOne;
uint128 amountOut;
uint128 amountInMaximum;
uint256 minHopPriceX36;
bytes hookData;
}

/// @notice Parameters for a multi-hop exact-output swap
struct ExactOutputParams {
Currency currencyOut;
PathKey[] path;
uint256[] maxHopSlippage;
uint256[] minHopPriceX36;
uint128 amountOut;
uint128 amountInMaximum;
}
Expand Down
8 changes: 4 additions & 4 deletions src/libraries/CalldataDecoder.sol
Original file line number Diff line number Diff line change
Expand Up @@ -201,8 +201,8 @@ library CalldataDecoder {
// ExactInputSingleParams is a variable length struct so we just have to look up its location
assembly ("memory-safe") {
// only safety checks for the minimum length, where hookData is empty
// 0x140 = 10 * 0x20 -> 8 elements, bytes offset, and bytes length 0
if lt(params.length, 0x140) {
// 0x160 = 11 * 0x20 -> 9 elements, bytes offset, and bytes length 0
if lt(params.length, 0x160) {
mstore(0, SLICE_ERROR_SELECTOR)
revert(0x1c, 4)
}
Expand Down Expand Up @@ -237,8 +237,8 @@ library CalldataDecoder {
// ExactOutputSingleParams is a variable length struct so we just have to look up its location
assembly ("memory-safe") {
// only safety checks for the minimum length, where hookData is empty
// 0x140 = 10 * 0x20 -> 8 elements, bytes offset, and bytes length 0
if lt(params.length, 0x140) {
// 0x160 = 11 * 0x20 -> 9 elements, bytes offset, and bytes length 0
if lt(params.length, 0x160) {
mstore(0, SLICE_ERROR_SELECTOR)
revert(0x1c, 4)
}
Expand Down
6 changes: 4 additions & 2 deletions test/libraries/CalldataDecoder.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ contract CalldataDecoderTest is Test {
assertEq(swapParams.amountIn, _swapParams.amountIn);
assertEq(swapParams.amountOutMinimum, _swapParams.amountOutMinimum);
_assertEq(swapParams.path, _swapParams.path);
_assertEq(swapParams.maxHopSlippage, _swapParams.maxHopSlippage);
_assertEq(swapParams.minHopPriceX36, _swapParams.minHopPriceX36);
}

function test_fuzz_decodeSwapExactInSingleParams(IV4Router.ExactInputSingleParams calldata _swapParams)
Expand All @@ -126,6 +126,7 @@ contract CalldataDecoderTest is Test {
assertEq(swapParams.zeroForOne, _swapParams.zeroForOne);
assertEq(swapParams.amountIn, _swapParams.amountIn);
assertEq(swapParams.amountOutMinimum, _swapParams.amountOutMinimum);
assertEq(swapParams.minHopPriceX36, _swapParams.minHopPriceX36);
assertEq(swapParams.hookData, _swapParams.hookData);
_assertEq(swapParams.poolKey, _swapParams.poolKey);
}
Expand All @@ -138,7 +139,7 @@ contract CalldataDecoderTest is Test {
assertEq(swapParams.amountOut, _swapParams.amountOut);
assertEq(swapParams.amountInMaximum, _swapParams.amountInMaximum);
_assertEq(swapParams.path, _swapParams.path);
_assertEq(swapParams.maxHopSlippage, _swapParams.maxHopSlippage);
_assertEq(swapParams.minHopPriceX36, _swapParams.minHopPriceX36);
}

function test_fuzz_decodeSwapExactOutSingleParams(IV4Router.ExactOutputSingleParams calldata _swapParams)
Expand All @@ -151,6 +152,7 @@ contract CalldataDecoderTest is Test {
assertEq(swapParams.zeroForOne, _swapParams.zeroForOne);
assertEq(swapParams.amountOut, _swapParams.amountOut);
assertEq(swapParams.amountInMaximum, _swapParams.amountInMaximum);
assertEq(swapParams.minHopPriceX36, _swapParams.minHopPriceX36);
assertEq(swapParams.hookData, _swapParams.hookData);
_assertEq(swapParams.poolKey, _swapParams.poolKey);
}
Expand Down
8 changes: 4 additions & 4 deletions test/router/Payments.gas.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ contract PaymentsTests is RoutingTestHelpers {
function test_gas_swap_settleFromCaller_takeAllToSpecifiedAddress() public {
uint256 amountIn = 1 ether;
IV4Router.ExactInputSingleParams memory params =
IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, bytes(""));
IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, 0, bytes(""));

plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params));
plan = plan.add(Actions.SETTLE_ALL, abi.encode(key0.currency0, MAX_SETTLE_AMOUNT));
Expand All @@ -32,7 +32,7 @@ contract PaymentsTests is RoutingTestHelpers {
function test_gas_swap_settleFromCaller_takeAllToMsgSender() public {
uint256 amountIn = 1 ether;
IV4Router.ExactInputSingleParams memory params =
IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, bytes(""));
IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, 0, bytes(""));

plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params));
plan = plan.add(Actions.SETTLE, abi.encode(key0.currency0, amountIn, true));
Expand All @@ -46,7 +46,7 @@ contract PaymentsTests is RoutingTestHelpers {
function test_gas_swap_settleWithBalance_takeAllToSpecifiedAddress() public {
uint256 amountIn = 1 ether;
IV4Router.ExactInputSingleParams memory params =
IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, bytes(""));
IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, 0, bytes(""));

// seed the router with tokens
key0.currency0.transfer(address(router), amountIn);
Expand All @@ -63,7 +63,7 @@ contract PaymentsTests is RoutingTestHelpers {
function test_gas_swap_settleWithBalance_takeAllToMsgSender() public {
uint256 amountIn = 1 ether;
IV4Router.ExactInputSingleParams memory params =
IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, bytes(""));
IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, 0, bytes(""));

// seed the router with tokens
key0.currency0.transfer(address(router), amountIn);
Expand Down
16 changes: 8 additions & 8 deletions test/router/Payments.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ contract PaymentsTests is RoutingTestHelpers {
function test_exactIn_settleAll_revertsSlippage() public {
uint256 amountIn = 1 ether;
IV4Router.ExactInputSingleParams memory params =
IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, bytes(""));
IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, 0, bytes(""));

plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params));
plan = plan.add(Actions.SETTLE_ALL, abi.encode(key0.currency0, amountIn - 1));
Expand All @@ -36,7 +36,7 @@ contract PaymentsTests is RoutingTestHelpers {
uint256 amountIn = 1 ether;
uint256 expectedAmountOut = 992054607780215625;
IV4Router.ExactInputSingleParams memory params =
IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, bytes(""));
IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, 0, bytes(""));

plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params));
plan = plan.add(Actions.SETTLE_ALL, abi.encode(key0.currency0, MAX_SETTLE_AMOUNT));
Expand All @@ -54,7 +54,7 @@ contract PaymentsTests is RoutingTestHelpers {
uint256 expectedAmountIn = 1008049273448486163;

IV4Router.ExactOutputSingleParams memory params =
IV4Router.ExactOutputSingleParams(key0, true, uint128(amountOut), uint128(expectedAmountIn), bytes(""));
IV4Router.ExactOutputSingleParams(key0, true, uint128(amountOut), uint128(expectedAmountIn), 0, bytes(""));

plan = plan.add(Actions.SWAP_EXACT_OUT_SINGLE, abi.encode(params));
plan = plan.add(Actions.SETTLE_ALL, abi.encode(key0.currency0, expectedAmountIn - 1));
Expand All @@ -72,7 +72,7 @@ contract PaymentsTests is RoutingTestHelpers {
uint256 expectedAmountIn = 1008049273448486163;

IV4Router.ExactOutputSingleParams memory params =
IV4Router.ExactOutputSingleParams(key0, true, uint128(amountOut), uint128(expectedAmountIn), bytes(""));
IV4Router.ExactOutputSingleParams(key0, true, uint128(amountOut), uint128(expectedAmountIn), 0, bytes(""));

plan = plan.add(Actions.SWAP_EXACT_OUT_SINGLE, abi.encode(params));
plan = plan.add(Actions.SETTLE_ALL, abi.encode(key0.currency0, MAX_SETTLE_AMOUNT));
Expand All @@ -88,7 +88,7 @@ contract PaymentsTests is RoutingTestHelpers {
uint256 expectedAmountIn = 1008049273448486163;

IV4Router.ExactOutputSingleParams memory params =
IV4Router.ExactOutputSingleParams(key0, true, uint128(amountOut), uint128(expectedAmountIn), bytes(""));
IV4Router.ExactOutputSingleParams(key0, true, uint128(amountOut), uint128(expectedAmountIn), 0, bytes(""));

plan = plan.add(Actions.SWAP_EXACT_OUT_SINGLE, abi.encode(params));
plan = plan.add(Actions.SETTLE_ALL, abi.encode(key0.currency0, expectedAmountIn));
Expand All @@ -102,7 +102,7 @@ contract PaymentsTests is RoutingTestHelpers {
uint256 amountIn = 1 ether;
uint256 expectedAmountOut = 992054607780215625;
IV4Router.ExactInputSingleParams memory params =
IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, bytes(""));
IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, 0, bytes(""));

// seed the router with tokens
key0.currency0.transfer(address(router), amountIn);
Expand Down Expand Up @@ -136,7 +136,7 @@ contract PaymentsTests is RoutingTestHelpers {
uint256 amountIn = 1 ether;
uint256 expectedAmountOut = 992054607780215625;
IV4Router.ExactInputSingleParams memory params =
IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, bytes(""));
IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, 0, bytes(""));

plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params));
plan = plan.add(Actions.SETTLE, abi.encode(key0.currency0, amountIn, true));
Expand Down Expand Up @@ -173,7 +173,7 @@ contract PaymentsTests is RoutingTestHelpers {
function test_settle_takePortion_reverts() public {
uint256 amountIn = 1 ether;
IV4Router.ExactInputSingleParams memory params =
IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, bytes(""));
IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, 0, bytes(""));

plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params));
plan = plan.add(Actions.SETTLE, abi.encode(key0.currency0, amountIn, true));
Expand Down
Loading
Loading