diff --git a/contracts/base/Dispatcher.sol b/contracts/base/Dispatcher.sol index fe3917f1..cb274812 100644 --- a/contracts/base/Dispatcher.sol +++ b/contracts/base/Dispatcher.sol @@ -65,7 +65,7 @@ abstract contract Dispatcher is // 0x00 <= command < 0x08 if (command < Commands.V2_SWAP_EXACT_IN) { if (command == Commands.V3_SWAP_EXACT_IN) { - // equivalent: abi.decode(inputs, (address, uint256, uint256, bytes, bool)) + // equivalent: abi.decode(inputs, (address, uint256, uint256, bytes, bool, uint256[])) address recipient; uint256 amountIn; uint256 amountOutMin; @@ -78,10 +78,11 @@ abstract contract Dispatcher is payerIsUser := calldataload(add(inputs.offset, 0x80)) } bytes calldata path = inputs.toBytes(3); + uint256[] calldata minHopPriceX36 = inputs.toUint256Array(5); address payer = payerIsUser ? msgSender() : address(this); - v3SwapExactInput(map(recipient), amountIn, amountOutMin, path, payer); + v3SwapExactInput(map(recipient), amountIn, amountOutMin, path, payer, minHopPriceX36); } else if (command == Commands.V3_SWAP_EXACT_OUT) { - // equivalent: abi.decode(inputs, (address, uint256, uint256, bytes, bool)) + // equivalent: abi.decode(inputs, (address, uint256, uint256, bytes, bool, uint256[])) address recipient; uint256 amountOut; uint256 amountInMax; @@ -94,8 +95,9 @@ abstract contract Dispatcher is payerIsUser := calldataload(add(inputs.offset, 0x80)) } bytes calldata path = inputs.toBytes(3); + uint256[] calldata minHopPriceX36 = inputs.toUint256Array(5); address payer = payerIsUser ? msgSender() : address(this); - v3SwapExactOutput(map(recipient), amountOut, amountInMax, path, payer); + v3SwapExactOutput(map(recipient), amountOut, amountInMax, path, payer, minHopPriceX36); } else if (command == Commands.PERMIT2_TRANSFER_FROM) { // equivalent: abi.decode(inputs, (address, address, uint160)) address token; @@ -157,14 +159,24 @@ abstract contract Dispatcher is bips := calldataload(add(inputs.offset, 0x40)) } Payments.payPortion(token, map(recipient), bips); + } else if (command == Commands.PAY_PORTION_FULL_PRECISION) { + // equivalent: abi.decode(inputs, (address, address, uint256)) + address token; + address recipient; + uint256 portion; + assembly { + token := calldataload(inputs.offset) + recipient := calldataload(add(inputs.offset, 0x20)) + portion := calldataload(add(inputs.offset, 0x40)) + } + Payments.payPortionFullPrecision(token, map(recipient), portion); } else { - // placeholder area for command 0x07 revert InvalidCommandType(command); } } else { // 0x08 <= command < 0x10 if (command == Commands.V2_SWAP_EXACT_IN) { - // equivalent: abi.decode(inputs, (address, uint256, uint256, bytes, bool)) + // equivalent: abi.decode(inputs, (address, uint256, uint256, address[], bool, uint256[])) address recipient; uint256 amountIn; uint256 amountOutMin; @@ -177,10 +189,11 @@ abstract contract Dispatcher is payerIsUser := calldataload(add(inputs.offset, 0x80)) } address[] calldata path = inputs.toAddressArray(3); + uint256[] calldata minHopPriceX36 = inputs.toUint256Array(5); address payer = payerIsUser ? msgSender() : address(this); - v2SwapExactInput(map(recipient), amountIn, amountOutMin, path, payer); + v2SwapExactInput(map(recipient), amountIn, amountOutMin, path, payer, minHopPriceX36); } else if (command == Commands.V2_SWAP_EXACT_OUT) { - // equivalent: abi.decode(inputs, (address, uint256, uint256, bytes, bool)) + // equivalent: abi.decode(inputs, (address, uint256, uint256, address[], bool, uint256[])) address recipient; uint256 amountOut; uint256 amountInMax; @@ -193,8 +206,9 @@ abstract contract Dispatcher is payerIsUser := calldataload(add(inputs.offset, 0x80)) } address[] calldata path = inputs.toAddressArray(3); + uint256[] calldata minHopPriceX36 = inputs.toUint256Array(5); address payer = payerIsUser ? msgSender() : address(this); - v2SwapExactOutput(map(recipient), amountOut, amountInMax, path, payer); + v2SwapExactOutput(map(recipient), amountOut, amountInMax, path, payer, minHopPriceX36); } else if (command == Commands.PERMIT2_PERMIT) { // equivalent: abi.decode(inputs, (IAllowanceTransfer.PermitSingle, bytes)) IAllowanceTransfer.PermitSingle calldata permitSingle; diff --git a/contracts/libraries/Commands.sol b/contracts/libraries/Commands.sol index 160e1d49..7861feac 100644 --- a/contracts/libraries/Commands.sol +++ b/contracts/libraries/Commands.sol @@ -19,7 +19,7 @@ library Commands { uint256 constant SWEEP = 0x04; uint256 constant TRANSFER = 0x05; uint256 constant PAY_PORTION = 0x06; - // COMMAND_PLACEHOLDER = 0x07; + uint256 constant PAY_PORTION_FULL_PRECISION = 0x07; // Command Types where 0x08<=value<=0x0f, executed in the second nested-if block uint256 constant V2_SWAP_EXACT_IN = 0x08; diff --git a/contracts/libraries/Constants.sol b/contracts/libraries/Constants.sol index 636da1d1..4ce0d342 100644 --- a/contracts/libraries/Constants.sol +++ b/contracts/libraries/Constants.sol @@ -25,4 +25,7 @@ library Constants { /// @dev The minimum length of an encoding that contains 2 or more pools uint256 internal constant MULTIPLE_V3_POOLS_MIN_LENGTH = V3_POP_OFFSET + NEXT_V3_POOL_OFFSET; + + /// @dev Precision multiplier for per-hop price calculations + uint256 internal constant PRICE_PRECISION = 1e36; } diff --git a/contracts/modules/Payments.sol b/contracts/modules/Payments.sol index 022e7cab..cca7f831 100644 --- a/contracts/modules/Payments.sol +++ b/contracts/modules/Payments.sol @@ -17,6 +17,7 @@ abstract contract Payments is PaymentsImmutables { error InsufficientToken(); error InsufficientETH(); + error InvalidPortion(); /// @notice Pays an amount of ETH or ERC20 to a recipient /// @param token The token to pay (can be ETH using Constants.ETH) @@ -50,6 +51,23 @@ abstract contract Payments is PaymentsImmutables { } } + /// @notice Pays a proportion of the contract's ETH or ERC20 to a recipient with 1e18 precision + /// @param token The token to pay (can be ETH using Constants.ETH) + /// @param recipient The address that will receive payment + /// @param portion Portion of whole balance of the contract, where 1e18 represents 100% + function payPortionFullPrecision(address token, address recipient, uint256 portion) internal { + if (portion > 1e18) revert InvalidPortion(); + if (token == Constants.ETH) { + uint256 balance = address(this).balance; + uint256 amount = balance * portion / 1e18; + recipient.safeTransferETH(amount); + } else { + uint256 balance = ERC20(token).balanceOf(address(this)); + uint256 amount = balance * portion / 1e18; + ERC20(token).safeTransfer(recipient, amount); + } + } + /// @notice Sweeps all of the contract's ERC20 or ETH to an address /// @param token The token to sweep (can be ETH using Constants.ETH) /// @param recipient The address that will receive payment diff --git a/contracts/modules/V3ToV4Migrator.sol b/contracts/modules/V3ToV4Migrator.sol index 725ab814..8604f302 100644 --- a/contracts/modules/V3ToV4Migrator.sol +++ b/contracts/modules/V3ToV4Migrator.sol @@ -93,8 +93,8 @@ abstract contract V3ToV4Migrator is MigratorImmutables { uint256 action = uint8(actions[actionIndex]); if ( - action == Actions.INCREASE_LIQUIDITY || action == Actions.DECREASE_LIQUIDITY - || action == Actions.BURN_POSITION + action == Actions.INCREASE_LIQUIDITY || action == Actions.INCREASE_LIQUIDITY_FROM_DELTAS + || action == Actions.DECREASE_LIQUIDITY || action == Actions.BURN_POSITION ) { revert OnlyMintAllowed(); } diff --git a/contracts/modules/uniswap/v2/V2SwapRouter.sol b/contracts/modules/uniswap/v2/V2SwapRouter.sol index 5a008d2a..23ca0cba 100644 --- a/contracts/modules/uniswap/v2/V2SwapRouter.sol +++ b/contracts/modules/uniswap/v2/V2SwapRouter.sol @@ -13,11 +13,13 @@ abstract contract V2SwapRouter is UniswapImmutables, Permit2Payments { error V2TooLittleReceived(); error V2TooMuchRequested(); error V2InvalidPath(); + error V2TooLittleReceivedPerHop(uint256 hopIndex, uint256 minPrice, uint256 price); + error V2InvalidHopPriceLength(); - function _v2Swap(address[] calldata path, address recipient, address pair) private { + function _v2Swap(address[] calldata path, address recipient, address pair, uint256[] calldata minHopPriceX36) + private + { unchecked { - if (path.length < 2) revert V2InvalidPath(); - // cached to save on duplicate operations (address token0,) = UniswapV2Library.sortTokens(path[0], path[1]); uint256 finalPairIndex = path.length - 1; @@ -29,6 +31,11 @@ abstract contract V2SwapRouter is UniswapImmutables, Permit2Payments { input == token0 ? (reserve0, reserve1) : (reserve1, reserve0); uint256 amountInput = ERC20(input).balanceOf(pair) - reserveInput; uint256 amountOutput = UniswapV2Library.getAmountOut(amountInput, reserveInput, reserveOutput); + if (minHopPriceX36.length != 0) { + uint256 price = amountOutput * Constants.PRICE_PRECISION / amountInput; + uint256 minPrice = minHopPriceX36[i]; + if (price < minPrice) revert V2TooLittleReceivedPerHop(i, minPrice, price); + } (uint256 amount0Out, uint256 amount1Out) = input == token0 ? (uint256(0), amountOutput) : (amountOutput, uint256(0)); address nextPair; @@ -49,16 +56,22 @@ abstract contract V2SwapRouter is UniswapImmutables, Permit2Payments { /// @param amountOutMinimum The minimum desired amount of output tokens /// @param path The path of the trade as an array of token addresses /// @param payer The address that will be paying the input + /// @param minHopPriceX36 Per-hop minimum price array in 1e36 precision (empty to disable) function v2SwapExactInput( address recipient, uint256 amountIn, uint256 amountOutMinimum, address[] calldata path, - address payer + address payer, + uint256[] calldata minHopPriceX36 ) internal { - address firstPair = UniswapV2Library.pairFor( - UNISWAP_V2_FACTORY, UNISWAP_V2_PAIR_INIT_CODE_HASH, path[0], path[1] - ); + if (path.length < 2) revert V2InvalidPath(); + if (minHopPriceX36.length != 0 && minHopPriceX36.length != path.length - 1) { + revert V2InvalidHopPriceLength(); + } + + address firstPair = + UniswapV2Library.pairFor(UNISWAP_V2_FACTORY, UNISWAP_V2_PAIR_INIT_CODE_HASH, path[0], path[1]); if ( amountIn != Constants.ALREADY_PAID // amountIn of 0 to signal that the pair already has the tokens ) { @@ -68,7 +81,7 @@ abstract contract V2SwapRouter is UniswapImmutables, Permit2Payments { ERC20 tokenOut = ERC20(path[path.length - 1]); uint256 balanceBefore = tokenOut.balanceOf(recipient); - _v2Swap(path, recipient, firstPair); + _v2Swap(path, recipient, firstPair, minHopPriceX36); uint256 amountOut = tokenOut.balanceOf(recipient) - balanceBefore; if (amountOut < amountOutMinimum) revert V2TooLittleReceived(); @@ -80,19 +93,25 @@ abstract contract V2SwapRouter is UniswapImmutables, Permit2Payments { /// @param amountInMaximum The maximum desired amount of input tokens /// @param path The path of the trade as an array of token addresses /// @param payer The address that will be paying the input + /// @param minHopPriceX36 Per-hop minimum price array in 1e36 precision (empty to disable) function v2SwapExactOutput( address recipient, uint256 amountOut, uint256 amountInMaximum, address[] calldata path, - address payer + address payer, + uint256[] calldata minHopPriceX36 ) internal { - (uint256 amountIn, address firstPair) = UniswapV2Library.getAmountInMultihop( - UNISWAP_V2_FACTORY, UNISWAP_V2_PAIR_INIT_CODE_HASH, amountOut, path - ); + if (path.length < 2) revert V2InvalidPath(); + if (minHopPriceX36.length != 0 && minHopPriceX36.length != path.length - 1) { + revert V2InvalidHopPriceLength(); + } + + (uint256 amountIn, address firstPair) = + UniswapV2Library.getAmountInMultihop(UNISWAP_V2_FACTORY, UNISWAP_V2_PAIR_INIT_CODE_HASH, amountOut, path); if (amountIn > amountInMaximum) revert V2TooMuchRequested(); payOrPermit2Transfer(path[0], payer, firstPair, amountIn); - _v2Swap(path, recipient, firstPair); + _v2Swap(path, recipient, firstPair, minHopPriceX36); } } diff --git a/contracts/modules/uniswap/v3/BytesLib.sol b/contracts/modules/uniswap/v3/BytesLib.sol index dd938a28..3e1f89a3 100644 --- a/contracts/modules/uniswap/v3/BytesLib.sol +++ b/contracts/modules/uniswap/v3/BytesLib.sol @@ -50,16 +50,18 @@ library BytesLib { pure returns (uint256 length, uint256 offset) { - uint256 relativeOffset; assembly { // The offset of the `_arg`-th element is `32 * arg`, which stores the offset of the length pointer. // shl(5, x) is equivalent to mul(32, x) let lengthPtr := add(_bytes.offset, calldataload(add(_bytes.offset, shl(5, _arg)))) length := calldataload(lengthPtr) offset := add(lengthPtr, 0x20) - relativeOffset := sub(offset, _bytes.offset) + let relativeOffset := sub(offset, _bytes.offset) + if lt(_bytes.length, add(shl(5, length), relativeOffset)) { + mstore(0, 0x3b99b53d) // SliceOutOfBounds() + revert(0x1c, 0x04) + } } - if (_bytes.length < length + relativeOffset) revert SliceOutOfBounds(); } /// @notice Decode the `_arg`-th element in `_bytes` as `address[]` @@ -73,6 +75,17 @@ library BytesLib { } } + /// @notice Decode the `_arg`-th element in `_bytes` as `uint256[]` + /// @param _bytes The input bytes string to extract a uint256 array from + /// @param _arg The index of the argument to extract + function toUint256Array(bytes calldata _bytes, uint256 _arg) internal pure returns (uint256[] calldata res) { + (uint256 length, uint256 offset) = toLengthOffset(_bytes, _arg); + assembly { + res.length := length + res.offset := offset + } + } + /// @notice Equivalent to abi.decode(bytes, bytes[]) /// @param _bytes The input bytes string to extract an parameters from function decodeCommandsAndInputs(bytes calldata _bytes) internal pure returns (bytes calldata, bytes[] calldata) { diff --git a/contracts/modules/uniswap/v3/V3SwapRouter.sol b/contracts/modules/uniswap/v3/V3SwapRouter.sol index 7073049d..b7acddce 100644 --- a/contracts/modules/uniswap/v3/V3SwapRouter.sol +++ b/contracts/modules/uniswap/v3/V3SwapRouter.sol @@ -7,6 +7,7 @@ import {SafeCast} from '@uniswap/v3-core/contracts/libraries/SafeCast.sol'; import {IUniswapV3Pool} from '@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol'; import {IUniswapV3SwapCallback} from '@uniswap/v3-core/contracts/interfaces/callback/IUniswapV3SwapCallback.sol'; import {ActionConstants} from '@uniswap/v4-periphery/src/libraries/ActionConstants.sol'; +import {Constants} from '../../../libraries/Constants.sol'; import {CalldataDecoder} from '@uniswap/v4-periphery/src/libraries/CalldataDecoder.sol'; import {Permit2Payments} from '../../Permit2Payments.sol'; import {UniswapImmutables} from '../UniswapImmutables.sol'; @@ -25,6 +26,9 @@ abstract contract V3SwapRouter is UniswapImmutables, Permit2Payments, IUniswapV3 error V3TooMuchRequested(); error V3InvalidAmountOut(); error V3InvalidCaller(); + error V3TooLittleReceivedPerHop(uint256 hopIndex, uint256 minPrice, uint256 price); + error V3TooMuchRequestedPerHop(uint256 hopIndex, uint256 minPrice, uint256 price); + error V3HopPriceAndPathLengthMismatch(); /// @dev The minimum value that can be returned from #getSqrtRatioAtTick. Equivalent to getSqrtRatioAtTick(MIN_TICK) uint160 internal constant MIN_SQRT_RATIO = 4295128739; @@ -34,7 +38,8 @@ abstract contract V3SwapRouter is UniswapImmutables, Permit2Payments, IUniswapV3 function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata data) external { if (amount0Delta <= 0 && amount1Delta <= 0) revert V3InvalidSwap(); // swaps entirely within 0-liquidity regions are not supported - (, address payer) = abi.decode(data, (bytes, address)); + (, address payer, uint256[] memory minHopPriceX36, uint256 hopIndex) = + abi.decode(data, (bytes, address, uint256[], uint256)); bytes calldata path = data.toBytes(0); // because exact output swaps are executed in reverse order, in this case tokenOut is actually tokenIn @@ -51,11 +56,33 @@ abstract contract V3SwapRouter is UniswapImmutables, Permit2Payments, IUniswapV3 } else { // either initiate the next swap or pay if (path.hasMultiplePools()) { + // Per-hop price check for exact-output intermediate hops + if (minHopPriceX36.length != 0) { + uint256 amountOut = uint256(-(amount0Delta > 0 ? amount1Delta : amount0Delta)); + uint256 price = amountOut * Constants.PRICE_PRECISION / amountToPay; + uint256 minPrice = minHopPriceX36[hopIndex]; + if (price < minPrice) revert V3TooMuchRequestedPerHop(hopIndex, minPrice, price); + } // this is an intermediate step so the payer is actually this contract path = path.skipToken(); - _swap(-amountToPay.toInt256(), msg.sender, path, payer, false); + _swap( + -amountToPay.toInt256(), + msg.sender, + path, + payer, + false, + minHopPriceX36, + hopIndex > 0 ? hopIndex - 1 : 0 + ); } else { if (amountToPay > MaxInputAmount.get()) revert V3TooMuchRequested(); + // Per-hop price check for the first trading hop (last executed in exact-output) + if (minHopPriceX36.length != 0) { + uint256 amountOut = uint256(-(amount0Delta > 0 ? amount1Delta : amount0Delta)); + uint256 price = amountOut * Constants.PRICE_PRECISION / amountToPay; + uint256 minPrice = minHopPriceX36[hopIndex]; + if (price < minPrice) revert V3TooMuchRequestedPerHop(hopIndex, minPrice, price); + } // note that because exact output swaps are executed in reverse order, tokenOut is actually tokenIn payOrPermit2Transfer(tokenOut, payer, msg.sender, amountToPay); } @@ -68,13 +95,22 @@ abstract contract V3SwapRouter is UniswapImmutables, Permit2Payments, IUniswapV3 /// @param amountOutMinimum The minimum desired amount of output tokens /// @param path The path of the trade as a bytes string /// @param payer The address that will be paying the input + /// @param minHopPriceX36 Per-hop minimum price array in 1e36 precision (empty to disable) function v3SwapExactInput( address recipient, uint256 amountIn, uint256 amountOutMinimum, bytes calldata path, - address payer + address payer, + uint256[] calldata minHopPriceX36 ) internal { + // Validate hop price array length + // V3 path: token(20) + [fee(3) + token(20)] * numHops => path.length = (minHopPriceX36.length * 23) + 20 + if ( + minHopPriceX36.length != 0 + && path.length != (minHopPriceX36.length * Constants.NEXT_V3_POOL_OFFSET) + Constants.ADDR_SIZE + ) revert V3HopPriceAndPathLengthMismatch(); + // use amountIn == ActionConstants.CONTRACT_BALANCE as a flag to swap the entire balance of the contract if (amountIn == ActionConstants.CONTRACT_BALANCE) { address tokenIn = path.decodeFirstToken(); @@ -82,6 +118,9 @@ abstract contract V3SwapRouter is UniswapImmutables, Permit2Payments, IUniswapV3 } uint256 amountOut; + uint256 hopIndex; + uint256 previousAmountIn = amountIn; + uint256[] memory emptyHopPrice = new uint256[](0); while (true) { bool hasMultiplePools = path.hasMultiplePools(); @@ -91,15 +130,26 @@ abstract contract V3SwapRouter is UniswapImmutables, Permit2Payments, IUniswapV3 hasMultiplePools ? address(this) : recipient, // for intermediate swaps, this contract custodies path.getFirstPool(), // only the first pool is needed payer, // for intermediate swaps, this contract custodies - true + true, + emptyHopPrice, // exact-in callbacks don't need price checks + 0 ); amountIn = uint256(-(zeroForOne ? amount1Delta : amount0Delta)); + // Per-hop price check for exact-input + if (minHopPriceX36.length != 0) { + uint256 price = amountIn * Constants.PRICE_PRECISION / previousAmountIn; + uint256 minPrice = minHopPriceX36[hopIndex]; + if (price < minPrice) revert V3TooLittleReceivedPerHop(hopIndex, minPrice, price); + } + // decide whether to continue or terminate if (hasMultiplePools) { payer = address(this); path = path.skipToken(); + previousAmountIn = amountIn; + hopIndex++; } else { amountOut = amountIn; break; @@ -115,16 +165,35 @@ abstract contract V3SwapRouter is UniswapImmutables, Permit2Payments, IUniswapV3 /// @param amountInMaximum The maximum desired amount of input tokens /// @param path The path of the trade as a bytes string /// @param payer The address that will be paying the input + /// @param minHopPriceX36 Per-hop minimum price array in 1e36 precision (empty to disable) function v3SwapExactOutput( address recipient, uint256 amountOut, uint256 amountInMaximum, bytes calldata path, - address payer + address payer, + uint256[] calldata minHopPriceX36 ) internal { + // Validate hop price array length + // V3 path: token(20) + [fee(3) + token(20)] * numHops => path.length = (minHopPriceX36.length * 23) + 20 + if ( + minHopPriceX36.length != 0 + && path.length != (minHopPriceX36.length * Constants.NEXT_V3_POOL_OFFSET) + Constants.ADDR_SIZE + ) revert V3HopPriceAndPathLengthMismatch(); + + // Convert calldata to memory for abi.encode in _swap + uint256[] memory minHopPriceX36Memory = minHopPriceX36; + MaxInputAmount.set(amountInMaximum); + + // For exact-output, the first _swap handles the LAST trading hop. + // Trading direction: hop 0 (A->B), hop 1 (B->C), ... + // Execution: last hop first, then callbacks handle earlier hops. + // So start hopIndex at minHopPriceX36Memory.length - 1 and decrement in callbacks. + uint256 startHopIndex = minHopPriceX36Memory.length > 0 ? minHopPriceX36Memory.length - 1 : 0; + (int256 amount0Delta, int256 amount1Delta, bool zeroForOne) = - _swap(-amountOut.toInt256(), recipient, path, payer, false); + _swap(-amountOut.toInt256(), recipient, path, payer, false, minHopPriceX36Memory, startHopIndex); uint256 amountOutReceived = zeroForOne ? uint256(-amount1Delta) : uint256(-amount0Delta); @@ -135,10 +204,15 @@ abstract contract V3SwapRouter is UniswapImmutables, Permit2Payments, IUniswapV3 /// @dev Performs a single swap for both exactIn and exactOut /// For exactIn, `amount` is `amountIn`. For exactOut, `amount` is `-amountOut` - function _swap(int256 amount, address recipient, bytes calldata path, address payer, bool isExactIn) - private - returns (int256 amount0Delta, int256 amount1Delta, bool zeroForOne) - { + function _swap( + int256 amount, + address recipient, + bytes calldata path, + address payer, + bool isExactIn, + uint256[] memory minHopPriceX36, + uint256 hopIndex + ) private returns (int256 amount0Delta, int256 amount1Delta, bool zeroForOne) { (address tokenIn, uint24 fee, address tokenOut) = path.decodeFirstPool(); zeroForOne = isExactIn ? tokenIn < tokenOut : tokenOut < tokenIn; @@ -149,7 +223,7 @@ abstract contract V3SwapRouter is UniswapImmutables, Permit2Payments, IUniswapV3 zeroForOne, amount, (zeroForOne ? MIN_SQRT_RATIO + 1 : MAX_SQRT_RATIO - 1), - abi.encode(path, payer) + abi.encode(path, payer, minHopPriceX36, hopIndex) ); } diff --git a/foundry.toml b/foundry.toml index 38dc6255..572bf416 100644 --- a/foundry.toml +++ b/foundry.toml @@ -5,6 +5,7 @@ libs = ['lib'] via_ir = true solc_version = '0.8.26' evm_version = "cancun" +bytecode_hash = "none" optimizer_runs = 4444 fs_permissions = [{ access = "read", path = "./script/deployParameters/"}] diff --git a/lib/forge-std b/lib/forge-std index 73d44ec7..1de6eecf 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 73d44ec7d124e3831bc5f832267889ffb6f9bc3f +Subproject commit 1de6eecf821de7fe2c908cc48d3ab3dced20717f diff --git a/lib/v4-periphery b/lib/v4-periphery index 3779387e..25b3144d 160000 --- a/lib/v4-periphery +++ b/lib/v4-periphery @@ -1 +1 @@ -Subproject commit 3779387e5d296f39df543d23524b050f89a62917 +Subproject commit 25b3144ddb5bad3af06a19f39cc912b405f592ca diff --git a/snapshots/UniversalRouterTest.json b/snapshots/UniversalRouterTest.json new file mode 100644 index 00000000..73a54398 --- /dev/null +++ b/snapshots/UniversalRouterTest.json @@ -0,0 +1,3 @@ +{ + "poolManager bytecode size": "24200" +} \ No newline at end of file diff --git a/test/foundry-tests/UniswapV2.t.sol b/test/foundry-tests/UniswapV2.t.sol index ddd78c14..9cd48ee4 100644 --- a/test/foundry-tests/UniswapV2.t.sol +++ b/test/foundry-tests/UniswapV2.t.sol @@ -8,6 +8,7 @@ import {IUniswapV2Factory} from '@uniswap/v2-core/contracts/interfaces/IUniswapV import {IUniswapV2Pair} from '@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol'; import {UniversalRouter} from '../../contracts/UniversalRouter.sol'; import {Payments} from '../../contracts/modules/Payments.sol'; +import {V2SwapRouter} from '../../contracts/modules/uniswap/v2/V2SwapRouter.sol'; import {ActionConstants} from '@uniswap/v4-periphery/src/libraries/ActionConstants.sol'; import {Commands} from '../../contracts/libraries/Commands.sol'; import {RouterParameters} from '../../contracts/types/RouterParameters.sol'; @@ -65,7 +66,7 @@ abstract contract UniswapV2Test is Test { path[0] = token0(); path[1] = token1(); bytes[] memory inputs = new bytes[](1); - inputs[0] = abi.encode(ActionConstants.MSG_SENDER, AMOUNT, 0, path, true); + inputs[0] = abi.encode(ActionConstants.MSG_SENDER, AMOUNT, 0, path, true, new uint256[](0)); router.execute(commands, inputs); assertEq(ERC20(token0()).balanceOf(FROM), BALANCE - AMOUNT); @@ -78,7 +79,7 @@ abstract contract UniswapV2Test is Test { path[0] = token1(); path[1] = token0(); bytes[] memory inputs = new bytes[](1); - inputs[0] = abi.encode(ActionConstants.MSG_SENDER, AMOUNT, 0, path, true); + inputs[0] = abi.encode(ActionConstants.MSG_SENDER, AMOUNT, 0, path, true, new uint256[](0)); router.execute(commands, inputs); assertEq(ERC20(token1()).balanceOf(FROM), BALANCE - AMOUNT); @@ -92,7 +93,7 @@ abstract contract UniswapV2Test is Test { path[0] = token0(); path[1] = token1(); bytes[] memory inputs = new bytes[](1); - inputs[0] = abi.encode(ActionConstants.MSG_SENDER, AMOUNT, 0, path, false); + inputs[0] = abi.encode(ActionConstants.MSG_SENDER, AMOUNT, 0, path, false, new uint256[](0)); router.execute(commands, inputs); assertGt(ERC20(token1()).balanceOf(FROM), BALANCE); @@ -105,7 +106,7 @@ abstract contract UniswapV2Test is Test { path[0] = token1(); path[1] = token0(); bytes[] memory inputs = new bytes[](1); - inputs[0] = abi.encode(ActionConstants.MSG_SENDER, AMOUNT, 0, path, false); + inputs[0] = abi.encode(ActionConstants.MSG_SENDER, AMOUNT, 0, path, false, new uint256[](0)); router.execute(commands, inputs); assertGt(ERC20(token0()).balanceOf(FROM), BALANCE); @@ -117,7 +118,7 @@ abstract contract UniswapV2Test is Test { path[0] = token0(); path[1] = token1(); bytes[] memory inputs = new bytes[](1); - inputs[0] = abi.encode(ActionConstants.MSG_SENDER, AMOUNT, type(uint256).max, path, true); + inputs[0] = abi.encode(ActionConstants.MSG_SENDER, AMOUNT, type(uint256).max, path, true, new uint256[](0)); router.execute(commands, inputs); assertLt(ERC20(token0()).balanceOf(FROM), BALANCE); @@ -130,7 +131,7 @@ abstract contract UniswapV2Test is Test { path[0] = token1(); path[1] = token0(); bytes[] memory inputs = new bytes[](1); - inputs[0] = abi.encode(ActionConstants.MSG_SENDER, AMOUNT, type(uint256).max, path, true); + inputs[0] = abi.encode(ActionConstants.MSG_SENDER, AMOUNT, type(uint256).max, path, true, new uint256[](0)); router.execute(commands, inputs); assertLt(ERC20(token1()).balanceOf(FROM), BALANCE); @@ -144,7 +145,7 @@ abstract contract UniswapV2Test is Test { path[0] = token0(); path[1] = token1(); bytes[] memory inputs = new bytes[](1); - inputs[0] = abi.encode(ActionConstants.MSG_SENDER, AMOUNT, type(uint256).max, path, false); + inputs[0] = abi.encode(ActionConstants.MSG_SENDER, AMOUNT, type(uint256).max, path, false, new uint256[](0)); router.execute(commands, inputs); assertGe(ERC20(token1()).balanceOf(FROM), BALANCE + AMOUNT); @@ -157,12 +158,75 @@ abstract contract UniswapV2Test is Test { path[0] = token1(); path[1] = token0(); bytes[] memory inputs = new bytes[](1); - inputs[0] = abi.encode(ActionConstants.MSG_SENDER, AMOUNT, type(uint256).max, path, false); + inputs[0] = abi.encode(ActionConstants.MSG_SENDER, AMOUNT, type(uint256).max, path, false, new uint256[](0)); router.execute(commands, inputs); assertGe(ERC20(token0()).balanceOf(FROM), BALANCE + AMOUNT); } + function testExactInputInvalidHopSlippageLength() public { + bytes memory commands = abi.encodePacked(bytes1(uint8(Commands.V2_SWAP_EXACT_IN))); + address[] memory path = new address[](2); + path[0] = token0(); + path[1] = token1(); + // path has 2 tokens = 1 hop, but we pass 2 slippage values (wrong length) + uint256[] memory hopSlippage = new uint256[](2); + hopSlippage[0] = 0; + hopSlippage[1] = 0; + bytes[] memory inputs = new bytes[](1); + inputs[0] = abi.encode(ActionConstants.MSG_SENDER, AMOUNT, 0, path, true, hopSlippage); + + vm.expectRevert(V2SwapRouter.V2InvalidHopPriceLength.selector); + router.execute(commands, inputs); + } + + function testExactOutputInvalidHopSlippageLength() public { + bytes memory commands = abi.encodePacked(bytes1(uint8(Commands.V2_SWAP_EXACT_OUT))); + address[] memory path = new address[](2); + path[0] = token0(); + path[1] = token1(); + // path has 2 tokens = 1 hop, but we pass 2 slippage values (wrong length) + uint256[] memory hopSlippage = new uint256[](2); + hopSlippage[0] = 0; + hopSlippage[1] = 0; + bytes[] memory inputs = new bytes[](1); + inputs[0] = abi.encode(ActionConstants.MSG_SENDER, AMOUNT, type(uint256).max, path, true, hopSlippage); + + vm.expectRevert(V2SwapRouter.V2InvalidHopPriceLength.selector); + router.execute(commands, inputs); + } + + function testExactInputWithHopSlippagePass() public { + bytes memory commands = abi.encodePacked(bytes1(uint8(Commands.V2_SWAP_EXACT_IN))); + address[] memory path = new address[](2); + path[0] = token0(); + path[1] = token1(); + // Set minPrice to 0 (any price is acceptable) + uint256[] memory hopSlippage = new uint256[](1); + hopSlippage[0] = 0; + bytes[] memory inputs = new bytes[](1); + inputs[0] = abi.encode(ActionConstants.MSG_SENDER, AMOUNT, 0, path, true, hopSlippage); + + router.execute(commands, inputs); + assertEq(ERC20(token0()).balanceOf(FROM), BALANCE - AMOUNT); + assertGt(ERC20(token1()).balanceOf(FROM), BALANCE); + } + + function testExactInputWithHopSlippageFail() public { + bytes memory commands = abi.encodePacked(bytes1(uint8(Commands.V2_SWAP_EXACT_IN))); + address[] memory path = new address[](2); + path[0] = token0(); + path[1] = token1(); + // Set an impossibly high minPrice so it reverts + uint256[] memory hopSlippage = new uint256[](1); + hopSlippage[0] = type(uint256).max; + bytes[] memory inputs = new bytes[](1); + inputs[0] = abi.encode(ActionConstants.MSG_SENDER, AMOUNT, 0, path, true, hopSlippage); + + vm.expectRevert(); + router.execute(commands, inputs); + } + function token0() internal virtual returns (address); function token1() internal virtual returns (address); diff --git a/test/foundry-tests/UniswapV3.t.sol b/test/foundry-tests/UniswapV3.t.sol new file mode 100644 index 00000000..6294b738 --- /dev/null +++ b/test/foundry-tests/UniswapV3.t.sol @@ -0,0 +1,284 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.15; + +import 'forge-std/Test.sol'; +import {UniversalRouter} from '../../contracts/UniversalRouter.sol'; +import {V3SwapRouter} from '../../contracts/modules/uniswap/v3/V3SwapRouter.sol'; +import {Commands} from '../../contracts/libraries/Commands.sol'; +import {RouterParameters} from '../../contracts/types/RouterParameters.sol'; +import {ActionConstants} from '@uniswap/v4-periphery/src/libraries/ActionConstants.sol'; +import {MockERC20} from './mock/MockERC20.sol'; +import {MockV3Pool} from './mock/MockV3Pool.sol'; + +contract UniswapV3Test is Test { + // 90% exchange rate: input 100 → output 90 + uint256 constant RATE = 0.9e18; + uint256 constant PRECISION = 1e36; + uint24 constant FEE = 3000; + + address constant V3_FACTORY = address(0xBEEF); + bytes32 constant POOL_INIT_CODE_HASH = bytes32(uint256(0xDEAD)); + address constant RECIPIENT = address(0x1234); + + UniversalRouter router; + MockERC20 tokenA; // lowest address + MockERC20 tokenB; + MockERC20 tokenC; // highest address + + function setUp() public { + // Deploy tokens and sort by address + address[3] memory tokens; + for (uint256 i = 0; i < 3; i++) { + tokens[i] = address(new MockERC20()); + } + if (tokens[0] > tokens[1]) (tokens[0], tokens[1]) = (tokens[1], tokens[0]); + if (tokens[1] > tokens[2]) (tokens[1], tokens[2]) = (tokens[2], tokens[1]); + if (tokens[0] > tokens[1]) (tokens[0], tokens[1]) = (tokens[1], tokens[0]); + tokenA = MockERC20(tokens[0]); + tokenB = MockERC20(tokens[1]); + tokenC = MockERC20(tokens[2]); + + // Deploy router + RouterParameters memory params = RouterParameters({ + permit2: address(0), + weth9: address(0), + v2Factory: address(0), + v3Factory: V3_FACTORY, + pairInitCodeHash: bytes32(0), + poolInitCodeHash: POOL_INIT_CODE_HASH, + v4PoolManager: address(0), + v3NFTPositionManager: address(0), + v4PositionManager: address(0), + spokePool: address(0) + }); + router = new UniversalRouter(params); + + // Deploy mock pools at deterministic addresses + _deployMockPool(address(tokenA), address(tokenB)); + _deployMockPool(address(tokenB), address(tokenC)); + } + + // ────────────────────────────────────────────── + // Exact Input - Single Hop + // ────────────────────────────────────────────── + + function testV3ExactInputSingleHopNoSlippage() public { + deal(address(tokenA), address(router), 100 ether); + + bytes memory path = abi.encodePacked(address(tokenA), FEE, address(tokenB)); + _executeV3ExactIn(RECIPIENT, 100 ether, 0, path, new uint256[](0)); + + assertEq(tokenB.balanceOf(RECIPIENT), 90 ether); + } + + function testV3ExactInputSingleHopSlippagePass() public { + deal(address(tokenA), address(router), 100 ether); + + bytes memory path = abi.encodePacked(address(tokenA), FEE, address(tokenB)); + uint256[] memory slippage = new uint256[](1); + slippage[0] = 0.8e36; // 80% threshold, actual is 90% + _executeV3ExactIn(RECIPIENT, 100 ether, 0, path, slippage); + + assertEq(tokenB.balanceOf(RECIPIENT), 90 ether); + } + + function testV3ExactInputSingleHopSlippageFail() public { + deal(address(tokenA), address(router), 100 ether); + + bytes memory path = abi.encodePacked(address(tokenA), FEE, address(tokenB)); + uint256[] memory slippage = new uint256[](1); + slippage[0] = 0.95e36; // 95% threshold, actual is 90% + + vm.expectRevert(); + _executeV3ExactIn(RECIPIENT, 100 ether, 0, path, slippage); + } + + // ────────────────────────────────────────────── + // Exact Input - Multi Hop + // ────────────────────────────────────────────── + + function testV3ExactInputMultiHopSlippagePass() public { + deal(address(tokenA), address(router), 100 ether); + + bytes memory path = abi.encodePacked(address(tokenA), FEE, address(tokenB), FEE, address(tokenC)); + uint256[] memory slippage = new uint256[](2); + slippage[0] = 0.8e36; // hop 0: A→B + slippage[1] = 0.8e36; // hop 1: B→C + _executeV3ExactIn(RECIPIENT, 100 ether, 0, path, slippage); + + assertEq(tokenC.balanceOf(RECIPIENT), 81 ether); + } + + function testV3ExactInputMultiHopSlippageFailFirstHop() public { + deal(address(tokenA), address(router), 100 ether); + + bytes memory path = abi.encodePacked(address(tokenA), FEE, address(tokenB), FEE, address(tokenC)); + uint256[] memory slippage = new uint256[](2); + slippage[0] = 0.95e36; // hop 0 too tight + slippage[1] = 0.8e36; + + vm.expectRevert(); + _executeV3ExactIn(RECIPIENT, 100 ether, 0, path, slippage); + } + + function testV3ExactInputMultiHopSlippageFailSecondHop() public { + deal(address(tokenA), address(router), 100 ether); + + bytes memory path = abi.encodePacked(address(tokenA), FEE, address(tokenB), FEE, address(tokenC)); + uint256[] memory slippage = new uint256[](2); + slippage[0] = 0.8e36; + slippage[1] = 0.95e36; // hop 1 too tight + + vm.expectRevert(); + _executeV3ExactIn(RECIPIENT, 100 ether, 0, path, slippage); + } + + // ────────────────────────────────────────────── + // Exact Output - Single Hop + // ────────────────────────────────────────────── + + function testV3ExactOutputSingleHopSlippagePass() public { + deal(address(tokenA), address(router), 200 ether); + + // Exact output path is reversed: B → A (to trade A→B) + bytes memory path = abi.encodePacked(address(tokenB), FEE, address(tokenA)); + uint256[] memory slippage = new uint256[](1); + slippage[0] = 0.8e36; // 80% threshold, actual is 90% + _executeV3ExactOut(RECIPIENT, 90 ether, type(uint256).max, path, slippage); + + assertEq(tokenB.balanceOf(RECIPIENT), 90 ether); + } + + function testV3ExactOutputSingleHopSlippageFail() public { + deal(address(tokenA), address(router), 200 ether); + + bytes memory path = abi.encodePacked(address(tokenB), FEE, address(tokenA)); + uint256[] memory slippage = new uint256[](1); + slippage[0] = 0.95e36; // 95% threshold, actual is 90% + + vm.expectRevert(); + _executeV3ExactOut(RECIPIENT, 90 ether, type(uint256).max, path, slippage); + } + + // ────────────────────────────────────────────── + // Exact Output - Multi Hop (the bug-catching tests) + // ────────────────────────────────────────────── + + function testV3ExactOutputMultiHopSlippagePass() public { + deal(address(tokenA), address(router), 200 ether); + + // Trade A→B→C, exact output 81 C. Path reversed: C-B-A + bytes memory path = abi.encodePacked(address(tokenC), FEE, address(tokenB), FEE, address(tokenA)); + uint256[] memory slippage = new uint256[](2); + slippage[0] = 0.8e36; // hop 0: A→B + slippage[1] = 0.8e36; // hop 1: B→C + _executeV3ExactOut(RECIPIENT, 81 ether, type(uint256).max, path, slippage); + + assertEq(tokenC.balanceOf(RECIPIENT), 81 ether); + } + + /// @notice This test catches the bug where hop 0 slippage was skipped in exact-output. + /// Hop 0 (A→B) is the first trading hop but the last executed in exact-output. + /// With the bug, minHopPriceX36[0] was never checked. + function testV3ExactOutputMultiHopSlippageFailHop0() public { + deal(address(tokenA), address(router), 200 ether); + + bytes memory path = abi.encodePacked(address(tokenC), FEE, address(tokenB), FEE, address(tokenA)); + uint256[] memory slippage = new uint256[](2); + slippage[0] = 0.95e36; // hop 0 (A→B) too tight - this MUST be enforced + slippage[1] = 0.8e36; // hop 1 (B→C) fine + + vm.expectRevert(); + _executeV3ExactOut(RECIPIENT, 81 ether, type(uint256).max, path, slippage); + } + + function testV3ExactOutputMultiHopSlippageFailHop1() public { + deal(address(tokenA), address(router), 200 ether); + + bytes memory path = abi.encodePacked(address(tokenC), FEE, address(tokenB), FEE, address(tokenA)); + uint256[] memory slippage = new uint256[](2); + slippage[0] = 0.8e36; // hop 0 fine + slippage[1] = 0.95e36; // hop 1 (B→C) too tight + + vm.expectRevert(); + _executeV3ExactOut(RECIPIENT, 81 ether, type(uint256).max, path, slippage); + } + + // ────────────────────────────────────────────── + // Invalid hop slippage length + // ────────────────────────────────────────────── + + function testV3ExactInputInvalidHopSlippageLength() public { + deal(address(tokenA), address(router), 100 ether); + + bytes memory path = abi.encodePacked(address(tokenA), FEE, address(tokenB)); + uint256[] memory slippage = new uint256[](2); // wrong: 1 hop but 2 values + + vm.expectRevert(V3SwapRouter.V3HopPriceAndPathLengthMismatch.selector); + _executeV3ExactIn(RECIPIENT, 100 ether, 0, path, slippage); + } + + function testV3ExactOutputInvalidHopSlippageLength() public { + deal(address(tokenA), address(router), 200 ether); + + bytes memory path = abi.encodePacked(address(tokenB), FEE, address(tokenA)); + uint256[] memory slippage = new uint256[](2); // wrong: 1 hop but 2 values + + vm.expectRevert(V3SwapRouter.V3HopPriceAndPathLengthMismatch.selector); + _executeV3ExactOut(RECIPIENT, 90 ether, type(uint256).max, path, slippage); + } + + // ────────────────────────────────────────────── + // Helpers + // ────────────────────────────────────────────── + + function _executeV3ExactIn( + address recipient, + uint256 amountIn, + uint256 amountOutMin, + bytes memory path, + uint256[] memory minHopPriceX36 + ) internal { + bytes memory commands = abi.encodePacked(bytes1(uint8(Commands.V3_SWAP_EXACT_IN))); + bytes[] memory inputs = new bytes[](1); + inputs[0] = abi.encode(recipient, amountIn, amountOutMin, path, false, minHopPriceX36); + router.execute(commands, inputs); + } + + function _executeV3ExactOut( + address recipient, + uint256 amountOut, + uint256 amountInMax, + bytes memory path, + uint256[] memory minHopPriceX36 + ) internal { + bytes memory commands = abi.encodePacked(bytes1(uint8(Commands.V3_SWAP_EXACT_OUT))); + bytes[] memory inputs = new bytes[](1); + inputs[0] = abi.encode(recipient, amountOut, amountInMax, path, false, minHopPriceX36); + router.execute(commands, inputs); + } + + function _deployMockPool(address token0, address token1) internal { + MockV3Pool mock = new MockV3Pool(token0, token1, RATE); + address expected = _computePoolAddress(token0, token1, FEE); + vm.etch(expected, address(mock).code); + // Give pool tokens to transfer during swaps + deal(token0, expected, 10000 ether); + deal(token1, expected, 10000 ether); + } + + function _computePoolAddress(address _tokenA, address _tokenB, uint24 fee) internal pure returns (address) { + if (_tokenA > _tokenB) (_tokenA, _tokenB) = (_tokenB, _tokenA); + return address( + uint160( + uint256( + keccak256( + abi.encodePacked( + hex'ff', V3_FACTORY, keccak256(abi.encode(_tokenA, _tokenB, fee)), POOL_INIT_CODE_HASH + ) + ) + ) + ) + ); + } +} diff --git a/test/foundry-tests/UniversalRouter.t.sol b/test/foundry-tests/UniversalRouter.t.sol index e290b14f..5b02d435 100644 --- a/test/foundry-tests/UniversalRouter.t.sol +++ b/test/foundry-tests/UniversalRouter.t.sol @@ -41,6 +41,10 @@ contract UniversalRouterTest is Test { event ExampleModuleEvent(string message); + function test_bytecodeSize() public { + vm.snapshotValue('poolManager bytecode size', address(router).code.length); + } + function testCallModule() public { uint256 bytecodeSize; address theRouter = address(router); @@ -97,4 +101,58 @@ contract UniversalRouterTest is Test { vm.expectRevert(Payments.InsufficientETH.selector); router.execute(commands, inputs); } + + function testPayPortionFullPrecisionToken() public { + bytes memory commands = abi.encodePacked(bytes1(uint8(Commands.PAY_PORTION_FULL_PRECISION))); + bytes[] memory inputs = new bytes[](1); + // 50% = 0.5e18 + inputs[0] = abi.encode(address(erc20), RECIPIENT, 5e17); + + erc20.mint(address(router), AMOUNT); + assertEq(erc20.balanceOf(RECIPIENT), 0); + + router.execute(commands, inputs); + + assertEq(erc20.balanceOf(RECIPIENT), AMOUNT / 2); + } + + function testPayPortionFullPrecisionETH() public { + bytes memory commands = abi.encodePacked(bytes1(uint8(Commands.PAY_PORTION_FULL_PRECISION))); + bytes[] memory inputs = new bytes[](1); + // 100% = 1e18 + inputs[0] = abi.encode(Constants.ETH, RECIPIENT, 1e18); + + assertEq(RECIPIENT.balance, 0); + + router.execute{value: AMOUNT}(commands, inputs); + + assertEq(RECIPIENT.balance, AMOUNT); + } + + function testPayPortionFullPrecisionInvalidPortion() public { + bytes memory commands = abi.encodePacked(bytes1(uint8(Commands.PAY_PORTION_FULL_PRECISION))); + bytes[] memory inputs = new bytes[](1); + // portion > 1e18 should revert + inputs[0] = abi.encode(address(erc20), RECIPIENT, 1e18 + 1); + + erc20.mint(address(router), AMOUNT); + + vm.expectRevert(Payments.InvalidPortion.selector); + router.execute(commands, inputs); + } + + function testPayPortionFullPrecisionSmallFraction() public { + bytes memory commands = abi.encodePacked(bytes1(uint8(Commands.PAY_PORTION_FULL_PRECISION))); + bytes[] memory inputs = new bytes[](1); + // 0.0001% = 1e12 (precision beyond bips) + inputs[0] = abi.encode(address(erc20), RECIPIENT, 1e12); + + erc20.mint(address(router), AMOUNT); + assertEq(erc20.balanceOf(RECIPIENT), 0); + + router.execute(commands, inputs); + + // 1e18 * 1e12 / 1e18 = 1e12 + assertEq(erc20.balanceOf(RECIPIENT), 1e12); + } } diff --git a/test/foundry-tests/mock/MockV3Pool.sol b/test/foundry-tests/mock/MockV3Pool.sol new file mode 100644 index 00000000..e0ea31f7 --- /dev/null +++ b/test/foundry-tests/mock/MockV3Pool.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import {ERC20} from 'solmate/src/tokens/ERC20.sol'; +import {IUniswapV3SwapCallback} from '@uniswap/v3-core/contracts/interfaces/callback/IUniswapV3SwapCallback.sol'; + +/// @notice Mock V3 pool with a fixed exchange rate for testing per-hop slippage +contract MockV3Pool { + address public immutable token0; + address public immutable token1; + uint256 public immutable rate; // output per input, 1e18 scale (0.9e18 = 90%) + + constructor(address _tokenA, address _tokenB, uint256 _rate) { + (token0, token1) = _tokenA < _tokenB ? (_tokenA, _tokenB) : (_tokenB, _tokenA); + rate = _rate; + } + + function swap( + address recipient, + bool zeroForOne, + int256 amountSpecified, + uint160, /* sqrtPriceLimitX96 */ + bytes calldata data + ) + external + returns (int256 amount0Delta, int256 amount1Delta) + { + if (amountSpecified > 0) { + // Exact input + uint256 input = uint256(amountSpecified); + uint256 output = input * rate / 1e18; + (amount0Delta, amount1Delta) = + zeroForOne ? (int256(input), -int256(output)) : (-int256(output), int256(input)); + } else { + // Exact output + uint256 output = uint256(-amountSpecified); + uint256 input = (output * 1e18 + rate - 1) / rate; // round up + (amount0Delta, amount1Delta) = + zeroForOne ? (int256(input), -int256(output)) : (-int256(output), int256(input)); + } + + // Transfer output tokens to recipient + if (amount0Delta < 0) ERC20(token0).transfer(recipient, uint256(-amount0Delta)); + if (amount1Delta < 0) ERC20(token1).transfer(recipient, uint256(-amount1Delta)); + + // Callback to receive payment + IUniswapV3SwapCallback(msg.sender).uniswapV3SwapCallback(amount0Delta, amount1Delta, data); + } +} diff --git a/test/integration-tests/UniswapMixed.test.ts b/test/integration-tests/UniswapMixed.test.ts index 7250deda..83d906ea 100644 --- a/test/integration-tests/UniswapMixed.test.ts +++ b/test/integration-tests/UniswapMixed.test.ts @@ -121,9 +121,10 @@ describe('Uniswap V2, V3, and V4 Tests:', () => { v3AmountOutMin, encodePathExactInput(v3Tokens), SOURCE_MSG_SENDER, + [], ]) // amountIn of 0 because the USDC is already in the pair - planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [MSG_SENDER, 0, v2AmountOutMin, v2Tokens, SOURCE_MSG_SENDER]) + planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [MSG_SENDER, 0, v2AmountOutMin, v2Tokens, SOURCE_MSG_SENDER, []]) const { wethBalanceBefore, wethBalanceAfter, v2SwapEventArgs } = await executeRouter( planner, @@ -150,6 +151,7 @@ describe('Uniswap V2, V3, and V4 Tests:', () => { v2AmountOutMin, v2Tokens, SOURCE_MSG_SENDER, + [], ]) planner.addCommand(CommandType.V3_SWAP_EXACT_IN, [ MSG_SENDER, @@ -157,6 +159,7 @@ describe('Uniswap V2, V3, and V4 Tests:', () => { v3AmountOutMin, encodePathExactInput(v3Tokens), SOURCE_ROUTER, + [], ]) const { wethBalanceBefore, wethBalanceAfter, v3SwapEventArgs } = await executeRouter( @@ -186,9 +189,9 @@ describe('Uniswap V2, V3, and V4 Tests:', () => { planner.addCommand(CommandType.PERMIT2_TRANSFER_FROM, [DAI.address, Pair.getAddress(DAI, USDT), v2AmountIn2]) // 2) trade route1 and return tokens to bob - planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [MSG_SENDER, 0, minAmountOut1, route1, SOURCE_MSG_SENDER]) + planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [MSG_SENDER, 0, minAmountOut1, route1, SOURCE_MSG_SENDER, []]) // 3) trade route2 and return tokens to bob - planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [MSG_SENDER, 0, minAmountOut2, route2, SOURCE_MSG_SENDER]) + planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [MSG_SENDER, 0, minAmountOut2, route2, SOURCE_MSG_SENDER, []]) const { wethBalanceBefore, wethBalanceAfter } = await executeRouter( planner, @@ -228,9 +231,9 @@ describe('Uniswap V2, V3, and V4 Tests:', () => { planner.addCommand(CommandType.PERMIT2_TRANSFER_FROM_BATCH, [BATCH_TRANSFER]) // 2) trade route1 and return tokens to bob - planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [MSG_SENDER, 0, minAmountOut1, route1, SOURCE_MSG_SENDER]) + planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [MSG_SENDER, 0, minAmountOut1, route1, SOURCE_MSG_SENDER, []]) // 3) trade route2 and return tokens to bob - planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [MSG_SENDER, 0, minAmountOut2, route2, SOURCE_MSG_SENDER]) + planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [MSG_SENDER, 0, minAmountOut2, route2, SOURCE_MSG_SENDER, []]) const { wethBalanceBefore, wethBalanceAfter } = await executeRouter( planner, @@ -258,6 +261,7 @@ describe('Uniswap V2, V3, and V4 Tests:', () => { minAmountOut1, route1, SOURCE_MSG_SENDER, + [], ]) // 2) trade route2 and return tokens to bob planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [ @@ -266,6 +270,7 @@ describe('Uniswap V2, V3, and V4 Tests:', () => { minAmountOut2, route2, SOURCE_MSG_SENDER, + [], ]) const { wethBalanceBefore, wethBalanceAfter } = await executeRouter( @@ -359,6 +364,7 @@ describe('Uniswap V2, V3, and V4 Tests:', () => { minAmountOut1, route1, SOURCE_MSG_SENDER, + [], ]) // 3) trade route2 and return tokens to bob planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [ @@ -367,6 +373,7 @@ describe('Uniswap V2, V3, and V4 Tests:', () => { minAmountOut2, route2, SOURCE_MSG_SENDER, + [], ]) const { usdcBalanceBefore, usdcBalanceAfter } = await executeRouter( @@ -440,6 +447,7 @@ describe('Uniswap V2, V3, and V4 Tests:', () => { minAmountOut1WETH, encodePathExactInput(route1), SOURCE_ROUTER, + [], ]) // 3) trade route2 and return tokens to bob planner.addCommand(CommandType.V3_SWAP_EXACT_IN, [ @@ -448,6 +456,7 @@ describe('Uniswap V2, V3, and V4 Tests:', () => { minAmountOut1USDC.add(minAmountOut2USDC), encodePathExactInput(route2), SOURCE_ROUTER, + [], ]) const { usdcBalanceBefore, usdcBalanceAfter } = await executeRouter( @@ -468,7 +477,7 @@ describe('Uniswap V2, V3, and V4 Tests:', () => { const minAmountOut = expandTo18DecimalsBN(0.0005) // V2 trades DAI for USDC, sending the tokens back to the router for v3 trade - planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [ADDRESS_THIS, v2AmountIn, 0, tokens, SOURCE_MSG_SENDER]) + planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [ADDRESS_THIS, v2AmountIn, 0, tokens, SOURCE_MSG_SENDER, []]) // V3 trades USDC for WETH, trading the whole balance, with a recipient of Alice planner.addCommand(CommandType.V3_SWAP_EXACT_IN, [ ADDRESS_THIS, @@ -476,6 +485,7 @@ describe('Uniswap V2, V3, and V4 Tests:', () => { 0, encodePathExactInput(tokens), SOURCE_MSG_SENDER, + [], ]) // aggregate slippage check planner.addCommand(CommandType.SWEEP, [WETH.address, MSG_SENDER, minAmountOut]) @@ -502,13 +512,14 @@ describe('Uniswap V2, V3, and V4 Tests:', () => { const value = v2AmountIn.add(v3AmountIn) planner.addCommand(CommandType.WRAP_ETH, [ADDRESS_THIS, value]) - planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [ADDRESS_THIS, v2AmountIn, 0, tokens, SOURCE_ROUTER]) + planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [ADDRESS_THIS, v2AmountIn, 0, tokens, SOURCE_ROUTER, []]) planner.addCommand(CommandType.V3_SWAP_EXACT_IN, [ ADDRESS_THIS, v3AmountIn, 0, encodePathExactInput(tokens), SOURCE_MSG_SENDER, + [], ]) // aggregate slippage check planner.addCommand(CommandType.SWEEP, [USDC.address, MSG_SENDER, 0.0005 * 10 ** 6]) @@ -533,13 +544,14 @@ describe('Uniswap V2, V3, and V4 Tests:', () => { const v2AmountIn: BigNumber = expandTo18DecimalsBN(20) const v3AmountIn: BigNumber = expandTo18DecimalsBN(30) - planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [ADDRESS_THIS, v2AmountIn, 0, tokens, SOURCE_MSG_SENDER]) + planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [ADDRESS_THIS, v2AmountIn, 0, tokens, SOURCE_MSG_SENDER, []]) planner.addCommand(CommandType.V3_SWAP_EXACT_IN, [ ADDRESS_THIS, v3AmountIn, 0, encodePathExactInput(tokens), SOURCE_MSG_SENDER, + [], ]) // aggregate slippage check planner.addCommand(CommandType.UNWRAP_WETH, [MSG_SENDER, expandTo18DecimalsBN(0.0005)]) @@ -573,6 +585,7 @@ describe('Uniswap V2, V3, and V4 Tests:', () => { maxAmountIn, [DAI.address, WETH.address], SOURCE_MSG_SENDER, + [], ]) planner.addCommand(CommandType.V3_SWAP_EXACT_OUT, [ ADDRESS_THIS, @@ -580,6 +593,7 @@ describe('Uniswap V2, V3, and V4 Tests:', () => { maxAmountIn, path, SOURCE_MSG_SENDER, + [], ]) // aggregate slippage check planner.addCommand(CommandType.UNWRAP_WETH, [MSG_SENDER, fullAmountOut]) @@ -612,7 +626,7 @@ describe('Uniswap V2, V3, and V4 Tests:', () => { { currencyIn, path: encodeMultihopExactInPath(route1, currencyIn), - maxHopSlippage: [], + minHopPriceX36: [], amountIn: v4AmountIn1, amountOutMinimum: 0, }, @@ -622,7 +636,7 @@ describe('Uniswap V2, V3, and V4 Tests:', () => { { currencyIn, path: encodeMultihopExactInPath(route2, currencyIn), - maxHopSlippage: [], + minHopPriceX36: [], amountIn: v4AmountIn2, amountOutMinimum: 0, }, @@ -674,6 +688,7 @@ describe('Uniswap V2, V3, and V4 Tests:', () => { 0, planOneTokens, SOURCE_MSG_SENDER, + [], ]) // V3 trades USDC for WETH, trading the whole balance, with a recipient of Alice subplan.addCommand(CommandType.V3_SWAP_EXACT_IN, [ @@ -682,6 +697,7 @@ describe('Uniswap V2, V3, and V4 Tests:', () => { 0, encodePathExactInput(planOneTokens), SOURCE_MSG_SENDER, + [], ]) // aggregate slippage check subplan.addCommand(CommandType.SWEEP, [WETH.address, MSG_SENDER, planOneWethMinOut]) @@ -700,6 +716,7 @@ describe('Uniswap V2, V3, and V4 Tests:', () => { wethMinAmountOut2, encodePathExactInput(planTwoTokens), SOURCE_MSG_SENDER, + [], ]) // add the second subplan to the main planner @@ -730,6 +747,7 @@ describe('Uniswap V2, V3, and V4 Tests:', () => { 0, planOneTokens, SOURCE_MSG_SENDER, + [], ]) // V3 trades USDC for WETH, trading the whole balance, with a recipient of Alice subplan.addCommand(CommandType.V3_SWAP_EXACT_IN, [ @@ -738,6 +756,7 @@ describe('Uniswap V2, V3, and V4 Tests:', () => { 0, encodePathExactInput(planOneTokens), SOURCE_MSG_SENDER, + [], ]) // aggregate slippage check subplan.addCommand(CommandType.SWEEP, [WETH.address, MSG_SENDER, planOneWethMinOut]) @@ -756,6 +775,7 @@ describe('Uniswap V2, V3, and V4 Tests:', () => { wethMinAmountOut2, encodePathExactInput(planTwoTokens), SOURCE_MSG_SENDER, + [], ]) // add the second subplan to the main planner @@ -789,6 +809,7 @@ describe('Uniswap V2, V3, and V4 Tests:', () => { 0, planOneTokens, SOURCE_MSG_SENDER, + [], ]) // V3 trades USDC for WETH, trading the whole balance, with a recipient of Alice subplan.addCommand(CommandType.V3_SWAP_EXACT_IN, [ @@ -797,6 +818,7 @@ describe('Uniswap V2, V3, and V4 Tests:', () => { 0, encodePathExactInput(planOneTokens), SOURCE_MSG_SENDER, + [], ]) // aggregate slippage check subplan.addCommand(CommandType.SWEEP, [WETH.address, MSG_SENDER, planOneWethMinOut]) @@ -816,6 +838,7 @@ describe('Uniswap V2, V3, and V4 Tests:', () => { wethMinAmountOut2, encodePathExactInput(planTwoTokens), SOURCE_MSG_SENDER, + [], ]) // add the second subplan to the main planner @@ -846,6 +869,7 @@ describe('Uniswap V2, V3, and V4 Tests:', () => { 0, planOneTokens, SOURCE_MSG_SENDER, + [], ]) // V3 trades USDC for WETH, trading the whole balance, with a recipient of Alice subplan.addCommand(CommandType.V3_SWAP_EXACT_IN, [ @@ -854,6 +878,7 @@ describe('Uniswap V2, V3, and V4 Tests:', () => { 0, encodePathExactInput(planOneTokens), SOURCE_MSG_SENDER, + [], ]) // aggregate slippage check subplan.addCommand(CommandType.SWEEP, [WETH.address, MSG_SENDER, planOneWethMinOut]) @@ -873,6 +898,7 @@ describe('Uniswap V2, V3, and V4 Tests:', () => { wethMinAmountOut2, encodePathExactInput(planTwoTokens), SOURCE_MSG_SENDER, + [], ]) // add the second subplan to the main planner diff --git a/test/integration-tests/UniswapV2.test.ts b/test/integration-tests/UniswapV2.test.ts index 8a9aa714..e3efefc3 100644 --- a/test/integration-tests/UniswapV2.test.ts +++ b/test/integration-tests/UniswapV2.test.ts @@ -133,6 +133,7 @@ describe('Uniswap V2 Tests:', () => { minAmountOutWETH, [DAI.address, WETH.address], SOURCE_MSG_SENDER, + [], ]) const { wethBalanceBefore, wethBalanceAfter, daiBalanceAfter, daiBalanceBefore } = await executeRouter( planner, @@ -171,6 +172,7 @@ describe('Uniswap V2 Tests:', () => { maxAmountInDAI, [DAI.address, WETH.address], SOURCE_MSG_SENDER, + [], ]) const { wethBalanceBefore, wethBalanceAfter, daiBalanceAfter, daiBalanceBefore } = await executeRouter( planner, @@ -209,6 +211,7 @@ describe('Uniswap V2 Tests:', () => { minAmountOutWETH, [DAI.address, WETH.address], SOURCE_MSG_SENDER, + [], ]) const testCustomErrors = await (await ethers.getContractFactory('TestCustomErrors')).deploy() @@ -227,6 +230,7 @@ describe('Uniswap V2 Tests:', () => { minAmountOut, [DAI.address, WETH.address], SOURCE_MSG_SENDER, + [], ]) const { wethBalanceBefore, wethBalanceAfter } = await executeRouter( planner, @@ -247,6 +251,7 @@ describe('Uniswap V2 Tests:', () => { expandTo18DecimalsBN(10000), [WETH.address, DAI.address], SOURCE_MSG_SENDER, + [], ]) planner.addCommand(CommandType.SWEEP, [WETH.address, MSG_SENDER, 0]) const { daiBalanceBefore, daiBalanceAfter } = await executeRouter( @@ -268,6 +273,7 @@ describe('Uniswap V2 Tests:', () => { 1, [DAI.address, WETH.address], SOURCE_MSG_SENDER, + [], ]) planner.addCommand(CommandType.PAY_PORTION, [WETH.address, alice.address, ONE_PERCENT_BIPS]) planner.addCommand(CommandType.SWEEP, [WETH.address, MSG_SENDER, 1]) @@ -299,6 +305,7 @@ describe('Uniswap V2 Tests:', () => { minAmountOut, [DAI.address, USDC.address, WETH.address], SOURCE_MSG_SENDER, + [], ]) const { wethBalanceBefore, wethBalanceAfter } = await executeRouter( @@ -321,6 +328,7 @@ describe('Uniswap V2 Tests:', () => { 1, [DAI.address, WETH.address], SOURCE_MSG_SENDER, + [], ]) planner.addCommand(CommandType.UNWRAP_WETH, [MSG_SENDER, 0]) @@ -345,6 +353,7 @@ describe('Uniswap V2 Tests:', () => { expandTo18DecimalsBN(10000), [DAI.address, WETH.address], SOURCE_MSG_SENDER, + [], ]) planner.addCommand(CommandType.UNWRAP_WETH, [MSG_SENDER, amountOut]) planner.addCommand(CommandType.SWEEP, [DAI.address, MSG_SENDER, 0]) @@ -373,6 +382,7 @@ describe('Uniswap V2 Tests:', () => { expandTo18DecimalsBN(10000), [DAI.address, WETH.address], SOURCE_MSG_SENDER, + [], ]) planner.addCommand(CommandType.UNWRAP_WETH, [ADDRESS_THIS, amountOut]) planner.addCommand(CommandType.PAY_PORTION, [ETH_ADDRESS, alice.address, ONE_PERCENT_BIPS]) @@ -398,6 +408,7 @@ describe('Uniswap V2 Tests:', () => { minAmountOut, [WETH.address, DAI.address], SOURCE_MSG_SENDER, + [], ]) const { daiBalanceBefore, daiBalanceAfter, v2SwapEventArgs } = await executeRouter( @@ -426,6 +437,7 @@ describe('Uniswap V2 Tests:', () => { expandTo18DecimalsBN(1), [WETH.address, DAI.address], SOURCE_ROUTER, + [], ]) planner.addCommand(CommandType.UNWRAP_WETH, [MSG_SENDER, 0]) diff --git a/test/integration-tests/UniswapV3.test.ts b/test/integration-tests/UniswapV3.test.ts index b387718a..af8433a5 100644 --- a/test/integration-tests/UniswapV3.test.ts +++ b/test/integration-tests/UniswapV3.test.ts @@ -85,6 +85,7 @@ describe('Uniswap V3 Tests:', () => { amountOutMin, path, tokenSource, + [], ]) } } @@ -127,6 +128,7 @@ describe('Uniswap V3 Tests:', () => { minAmountOutWETH, path, SOURCE_MSG_SENDER, + [], ]) const { wethBalanceBefore, wethBalanceAfter, daiBalanceAfter, daiBalanceBefore } = await executeRouter( planner, @@ -170,6 +172,7 @@ describe('Uniswap V3 Tests:', () => { maxAmountInDAI, path, SOURCE_MSG_SENDER, + [], ]) const { wethBalanceBefore, wethBalanceAfter, daiBalanceAfter, daiBalanceBefore } = await executeRouter( planner, @@ -232,7 +235,14 @@ describe('Uniswap V3 Tests:', () => { const tokens = [DAI.address, WETH.address] const path = encodePathExactOutput(tokens) - planner.addCommand(CommandType.V3_SWAP_EXACT_OUT, [MSG_SENDER, amountOut, amountInMax, path, SOURCE_MSG_SENDER]) + planner.addCommand(CommandType.V3_SWAP_EXACT_OUT, [ + MSG_SENDER, + amountOut, + amountInMax, + path, + SOURCE_MSG_SENDER, + [], + ]) const { wethBalanceBefore, wethBalanceAfter, v3SwapEventArgs } = await executeRouter( planner, @@ -252,7 +262,14 @@ describe('Uniswap V3 Tests:', () => { const tokens = [DAI.address, USDC.address, WETH.address] const path = encodePathExactOutput(tokens) - planner.addCommand(CommandType.V3_SWAP_EXACT_OUT, [MSG_SENDER, amountOut, amountInMax, path, SOURCE_MSG_SENDER]) + planner.addCommand(CommandType.V3_SWAP_EXACT_OUT, [ + MSG_SENDER, + amountOut, + amountInMax, + path, + SOURCE_MSG_SENDER, + [], + ]) const { commands, inputs } = planner const balanceWethBefore = await wethContract.balanceOf(bob.address) @@ -287,7 +304,14 @@ describe('Uniswap V3 Tests:', () => { const tokens = [DAI.address, WETH.address] const path = encodePathExactOutput(tokens) - planner.addCommand(CommandType.V3_SWAP_EXACT_OUT, [ADDRESS_THIS, amountOut, amountInMax, path, SOURCE_MSG_SENDER]) + planner.addCommand(CommandType.V3_SWAP_EXACT_OUT, [ + ADDRESS_THIS, + amountOut, + amountInMax, + path, + SOURCE_MSG_SENDER, + [], + ]) planner.addCommand(CommandType.UNWRAP_WETH, [MSG_SENDER, amountOut]) const { ethBalanceBefore, ethBalanceAfter, gasSpent } = await executeRouter( @@ -330,7 +354,7 @@ describe('Uniswap V3 Tests:', () => { const path = encodePathExactOutput(tokens) planner.addCommand(CommandType.WRAP_ETH, [ADDRESS_THIS, amountInMax]) - planner.addCommand(CommandType.V3_SWAP_EXACT_OUT, [MSG_SENDER, amountOut, amountInMax, path, SOURCE_ROUTER]) + planner.addCommand(CommandType.V3_SWAP_EXACT_OUT, [MSG_SENDER, amountOut, amountInMax, path, SOURCE_ROUTER, []]) planner.addCommand(CommandType.UNWRAP_WETH, [MSG_SENDER, 0]) const { ethBalanceBefore, ethBalanceAfter, daiBalanceBefore, daiBalanceAfter, gasSpent, v3SwapEventArgs } = diff --git a/test/integration-tests/UniswapV4.test.ts b/test/integration-tests/UniswapV4.test.ts index a16de361..19fcb71c 100644 --- a/test/integration-tests/UniswapV4.test.ts +++ b/test/integration-tests/UniswapV4.test.ts @@ -134,6 +134,7 @@ describe('Uniswap V4 Tests:', () => { zeroForOne: true, amountIn: amountInUSDC, amountOutMinimum: minAmountOutNative, + minHopPriceX36: 0, hookData: '0x', }, ]) @@ -161,7 +162,7 @@ describe('Uniswap V4 Tests:', () => { { currencyIn, path: encodeMultihopExactInPath([USDC_WETH.poolKey], currencyIn), - maxHopSlippage: [], + minHopPriceX36: [], amountIn: amountInUSDC, amountOutMinimum: minAmountOutNative, }, @@ -191,7 +192,7 @@ describe('Uniswap V4 Tests:', () => { { currencyIn, path: encodeMultihopExactInPath([DAI_USDC.poolKey, USDC_WETH.poolKey], currencyIn), - maxHopSlippage: [], + minHopPriceX36: [], amountIn: amountInDAI, amountOutMinimum: minAmountOutNative, }, @@ -221,7 +222,7 @@ describe('Uniswap V4 Tests:', () => { { currencyIn, path: encodeMultihopExactInPath([DAI_USDC.poolKey, USDC_WETH.poolKey], currencyIn), - maxHopSlippage: [], + minHopPriceX36: [], amountIn: amountInDAI, amountOutMinimum: minAmountOutNative, }, @@ -269,7 +270,7 @@ describe('Uniswap V4 Tests:', () => { { currencyIn, path: encodeMultihopExactInPath([DAI_USDC.poolKey, USDC_WETH.poolKey], currencyIn), - maxHopSlippage: [], + minHopPriceX36: [], amountIn: OPEN_DELTA, amountOutMinimum: minOut, }, @@ -305,7 +306,7 @@ describe('Uniswap V4 Tests:', () => { { currencyIn, path: encodeMultihopExactInPath([DAI_USDC.poolKey, ETH_USDC.poolKey], currencyIn), - maxHopSlippage: [], + minHopPriceX36: [], amountIn: amountInDAI, amountOutMinimum: minAmountOutNative, }, @@ -345,6 +346,7 @@ describe('Uniswap V4 Tests:', () => { zeroForOne: true, amountOut: amountOutNative, amountInMaximum: maxAmountInUSDC, + minHopPriceX36: 0, hookData: '0x', }, ]) @@ -373,7 +375,7 @@ describe('Uniswap V4 Tests:', () => { { currencyOut, path: encodeMultihopExactOutPath([USDC_WETH.poolKey], currencyOut), - maxHopSlippage: [], + minHopPriceX36: [], amountOut: amountOutNative, amountInMaximum: maxAmountInUSDC, }, @@ -403,7 +405,7 @@ describe('Uniswap V4 Tests:', () => { { currencyOut, path: encodeMultihopExactOutPath([DAI_USDC.poolKey, USDC_WETH.poolKey], currencyOut), - maxHopSlippage: [], + minHopPriceX36: [], amountOut: amountOutNative, amountInMaximum: maxAmountInDAI, }, @@ -435,6 +437,7 @@ describe('Uniswap V4 Tests:', () => { zeroForOne: true, amountIn: amountInNative, amountOutMinimum: minAmountOutUSDC, + minHopPriceX36: 0, hookData: '0x', }, ]) @@ -464,7 +467,7 @@ describe('Uniswap V4 Tests:', () => { { currencyIn, path: encodeMultihopExactInPath([ETH_USDC.poolKey], currencyIn), - maxHopSlippage: [], + minHopPriceX36: [], amountIn: amountInNative, amountOutMinimum: minAmountOutUSDC, }, @@ -496,7 +499,7 @@ describe('Uniswap V4 Tests:', () => { { currencyIn, path: encodeMultihopExactInPath([ETH_USDC.poolKey, DAI_USDC.poolKey], currencyIn), - maxHopSlippage: [], + minHopPriceX36: [], amountIn: amountInNative, amountOutMinimum: minAmountOutDAI, }, @@ -529,6 +532,7 @@ describe('Uniswap V4 Tests:', () => { zeroForOne: true, amountOut: amountOutUSDC, amountInMaximum: maxAmountInNative, + minHopPriceX36: 0, hookData: '0x', }, ]) @@ -561,7 +565,7 @@ describe('Uniswap V4 Tests:', () => { { currencyOut, path: encodeMultihopExactOutPath([ETH_USDC.poolKey], currencyOut), - maxHopSlippage: [], + minHopPriceX36: [], amountOut: amountOutUSDC, amountInMaximum: maxAmountInNative, }, @@ -596,7 +600,7 @@ describe('Uniswap V4 Tests:', () => { { currencyOut, path: encodeMultihopExactOutPath([ETH_USDC.poolKey, DAI_USDC.poolKey], currencyOut), - maxHopSlippage: [], + minHopPriceX36: [], amountOut: amountOutDAI, amountInMaximum: maxAmountInNative, }, @@ -634,6 +638,7 @@ describe('Uniswap V4 Tests:', () => { zeroForOne: false, amountIn: amountInUSDC, amountOutMinimum: minAmountOutNative, + minHopPriceX36: 0, hookData: '0x', }, ]) @@ -662,7 +667,7 @@ describe('Uniswap V4 Tests:', () => { { currencyIn, path: encodeMultihopExactInPath([ETH_USDC.poolKey], currencyIn), - maxHopSlippage: [], + minHopPriceX36: [], amountIn: amountInUSDC, amountOutMinimum: minAmountOutNative, }, @@ -693,7 +698,7 @@ describe('Uniswap V4 Tests:', () => { { currencyIn, path: encodeMultihopExactInPath([DAI_USDC.poolKey, ETH_USDC.poolKey], currencyIn), - maxHopSlippage: [], + minHopPriceX36: [], amountIn: amountInDAI, amountOutMinimum: minAmountOutNative, }, @@ -725,6 +730,7 @@ describe('Uniswap V4 Tests:', () => { zeroForOne: false, amountOut: amountOutNative, amountInMaximum: maxAmountInUSDC, + minHopPriceX36: 0, hookData: '0x', }, ]) @@ -753,7 +759,7 @@ describe('Uniswap V4 Tests:', () => { { currencyOut, path: encodeMultihopExactOutPath([ETH_USDC.poolKey], currencyOut), - maxHopSlippage: [], + minHopPriceX36: [], amountOut: amountOutNative, amountInMaximum: maxAmountInUSDC, }, @@ -784,7 +790,7 @@ describe('Uniswap V4 Tests:', () => { { currencyOut, path: encodeMultihopExactOutPath([DAI_USDC.poolKey, ETH_USDC.poolKey], currencyOut), - maxHopSlippage: [], + minHopPriceX36: [], amountOut: amountOutNative, amountInMaximum: maxAmountInDAI, }, diff --git a/test/integration-tests/UniversalRouter.test.ts b/test/integration-tests/UniversalRouter.test.ts index 555fc834..217a3017 100644 --- a/test/integration-tests/UniversalRouter.test.ts +++ b/test/integration-tests/UniversalRouter.test.ts @@ -67,6 +67,7 @@ describe('UniversalRouter', () => { 1, [DAI.address, WETH.address], SOURCE_MSG_SENDER, + [], ]) const invalidDeadline = 10 @@ -113,6 +114,72 @@ describe('UniversalRouter', () => { ) }) + describe('PAY_PORTION_FULL_PRECISION', () => { + it('pays a portion of router balance with 1e18 precision', async () => { + const amount = expandTo18DecimalsBN(100) + // Transfer DAI to the router first (simulating tokens already in the router) + await daiContract.transfer(router.address, amount) + const balanceBefore = await daiContract.balanceOf(alice.address) + + // Pay 33.333...% (1/3) of balance to alice — impossible with 10_000 bips + const oneThird = ethers.BigNumber.from('333333333333333333') // 0.333...e18 + planner.addCommand(CommandType.PAY_PORTION_FULL_PRECISION, [DAI.address, alice.address, oneThird]) + const { commands, inputs } = planner + await router['execute(bytes,bytes[])'](commands, inputs) + + const balanceAfter = await daiContract.balanceOf(alice.address) + const received = balanceAfter.sub(balanceBefore) + // 100e18 * 333333333333333333 / 1e18 = 33.333333333333333300e18 + expect(received).to.eq(ethers.BigNumber.from('33333333333333333300')) + }) + + it('pays full balance when portion is 1e18 (100%)', async () => { + const amount = expandTo18DecimalsBN(50) + await daiContract.transfer(router.address, amount) + const balanceBefore = await daiContract.balanceOf(alice.address) + + const fullPortion = ethers.BigNumber.from('1000000000000000000') // 1e18 + planner.addCommand(CommandType.PAY_PORTION_FULL_PRECISION, [DAI.address, alice.address, fullPortion]) + const { commands, inputs } = planner + await router['execute(bytes,bytes[])'](commands, inputs) + + const balanceAfter = await daiContract.balanceOf(alice.address) + expect(balanceAfter.sub(balanceBefore)).to.eq(amount) + }) + + it('reverts if portion exceeds 1e18', async () => { + await daiContract.transfer(router.address, expandTo18DecimalsBN(1)) + const overPortion = ethers.BigNumber.from('1000000000000000001') // 1e18 + 1 + planner.addCommand(CommandType.PAY_PORTION_FULL_PRECISION, [DAI.address, alice.address, overPortion]) + const { commands, inputs } = planner + await expect(router['execute(bytes,bytes[])'](commands, inputs)).to.be.revertedWithCustomError( + router, + 'InvalidPortion' + ) + }) + + it('pays ETH with full precision', async () => { + const value = expandTo18DecimalsBN(1) + planner.addCommand(CommandType.WRAP_ETH, [ADDRESS_THIS, value]) + planner.addCommand(CommandType.UNWRAP_WETH, [ADDRESS_THIS, 0]) + // Pay 50% of ETH balance + const halfPortion = ethers.BigNumber.from('500000000000000000') // 0.5e18 + planner.addCommand(CommandType.PAY_PORTION_FULL_PRECISION, [ETH_ADDRESS, alice.address, halfPortion]) + const { commands, inputs } = planner + + const balanceBefore = await ethers.provider.getBalance(alice.address) + const tx = await router['execute(bytes,bytes[])'](commands, inputs, { value }) + const receipt = await tx.wait() + const gasCost = receipt.gasUsed.mul(receipt.effectiveGasPrice) + const balanceAfter = await ethers.provider.getBalance(alice.address) + + // Sent 1 ETH, got back 0.5 ETH, minus gas + const netChange = balanceAfter.sub(balanceBefore).add(gasCost) + // netChange = received - sent = 0.5 - 1.0 = -0.5 + expect(netChange).to.eq(expandTo18DecimalsBN(1).div(2).sub(value)) + }) + }) + it('reverts if a malicious contract tries to reenter', async () => { // create malicious calldata to sweep ETH out of the router planner.addCommand(CommandType.SWEEP, [ETH_ADDRESS, alice.address, 0]) diff --git a/test/integration-tests/gas-tests/Uniswap.gas.test.ts b/test/integration-tests/gas-tests/Uniswap.gas.test.ts index da5655df..f18d91b1 100644 --- a/test/integration-tests/gas-tests/Uniswap.gas.test.ts +++ b/test/integration-tests/gas-tests/Uniswap.gas.test.ts @@ -194,6 +194,7 @@ describe('Uniswap Gas Tests', () => { minAmountOut, [DAI.address, WETH.address], SOURCE_MSG_SENDER, + [], ]) const { commands, inputs } = planner await snapshotGasCost(router['execute(bytes,bytes[],uint256)'](commands, inputs, DEADLINE)) @@ -206,6 +207,7 @@ describe('Uniswap Gas Tests', () => { minAmountOut, [DAI.address, USDC.address, WETH.address], SOURCE_MSG_SENDER, + [], ]) const { commands, inputs } = planner @@ -219,6 +221,7 @@ describe('Uniswap Gas Tests', () => { minAmountOut, [DAI.address, USDC.address, WETH.address], SOURCE_MSG_SENDER, + [], ]) const { commands, inputs } = planner @@ -232,6 +235,7 @@ describe('Uniswap Gas Tests', () => { minAmountOut, [DAI.address, USDC.address, USDT.address, WETH.address], SOURCE_MSG_SENDER, + [], ]) const { commands, inputs } = planner @@ -245,6 +249,7 @@ describe('Uniswap Gas Tests', () => { 1, [DAI.address, USDC.address, USDT.address, WETH.address], SOURCE_MSG_SENDER, + [], ]) const { commands, inputs } = planner @@ -259,6 +264,7 @@ describe('Uniswap Gas Tests', () => { 1, [DAI.address, WETH.address], SOURCE_MSG_SENDER, + [], ]) planner.addCommand(CommandType.PAY_PORTION, [WETH.address, alice.address, ONE_PERCENT_BIPS]) planner.addCommand(CommandType.SWEEP, [WETH.address, MSG_SENDER, 1]) @@ -274,6 +280,7 @@ describe('Uniswap Gas Tests', () => { expandTo18DecimalsBN(100), [WETH.address, DAI.address], SOURCE_MSG_SENDER, + [], ]) const { commands, inputs } = planner @@ -287,6 +294,7 @@ describe('Uniswap Gas Tests', () => { expandTo18DecimalsBN(100), [WETH.address, USDC.address, DAI.address], SOURCE_MSG_SENDER, + [], ]) const { commands, inputs } = planner @@ -300,6 +308,7 @@ describe('Uniswap Gas Tests', () => { expandTo18DecimalsBN(100), [WETH.address, USDT.address, USDC.address, DAI.address], SOURCE_MSG_SENDER, + [], ]) const { commands, inputs } = planner @@ -315,6 +324,7 @@ describe('Uniswap Gas Tests', () => { 1, [DAI.address, WETH.address], SOURCE_MSG_SENDER, + [], ]) planner.addCommand(CommandType.UNWRAP_WETH, [MSG_SENDER, 0]) @@ -330,6 +340,7 @@ describe('Uniswap Gas Tests', () => { expandTo18DecimalsBN(10000), [DAI.address, WETH.address], SOURCE_MSG_SENDER, + [], ]) planner.addCommand(CommandType.UNWRAP_WETH, [MSG_SENDER, amountOut]) planner.addCommand(CommandType.SWEEP, [DAI.address, MSG_SENDER, 0]) @@ -346,6 +357,7 @@ describe('Uniswap Gas Tests', () => { expandTo18DecimalsBN(10000), [DAI.address, WETH.address], SOURCE_MSG_SENDER, + [], ]) planner.addCommand(CommandType.UNWRAP_WETH, [ADDRESS_THIS, amountOut]) planner.addCommand(CommandType.PAY_PORTION, [ETH_ADDRESS, MSG_SENDER, 50]) @@ -370,6 +382,7 @@ describe('Uniswap Gas Tests', () => { minAmountOut, [WETH.address, DAI.address], SOURCE_MSG_SENDER, + [], ]) const { commands, inputs } = planner @@ -389,6 +402,7 @@ describe('Uniswap Gas Tests', () => { expandTo18DecimalsBN(1), [WETH.address, DAI.address], SOURCE_ROUTER, + [], ]) planner.addCommand(CommandType.UNWRAP_WETH, [MSG_SENDER, 0]) @@ -521,6 +535,7 @@ describe('Uniswap Gas Tests', () => { amountOutMin, path, sourceOfTokens, + [], ]) } } @@ -572,6 +587,7 @@ describe('Uniswap Gas Tests', () => { amountInMax, path, SOURCE_MSG_SENDER, + [], ]) const { commands, inputs } = planner @@ -589,6 +605,7 @@ describe('Uniswap Gas Tests', () => { amountInMax, path, SOURCE_MSG_SENDER, + [], ]) const { commands, inputs } = planner @@ -606,6 +623,7 @@ describe('Uniswap Gas Tests', () => { amountInMax, path, SOURCE_MSG_SENDER, + [], ]) const { commands, inputs } = planner @@ -633,6 +651,7 @@ describe('Uniswap Gas Tests', () => { amountInMax, path, SOURCE_MSG_SENDER, + [], ]) planner.addCommand(CommandType.UNWRAP_WETH, [MSG_SENDER, amountOut]) @@ -660,7 +679,14 @@ describe('Uniswap Gas Tests', () => { const path = encodePathExactOutput(tokens) planner.addCommand(CommandType.WRAP_ETH, [ADDRESS_THIS, amountInMax]) - planner.addCommand(CommandType.V3_SWAP_EXACT_OUT, [MSG_SENDER, amountOut, amountInMax, path, SOURCE_ROUTER]) + planner.addCommand(CommandType.V3_SWAP_EXACT_OUT, [ + MSG_SENDER, + amountOut, + amountInMax, + path, + SOURCE_ROUTER, + [], + ]) planner.addCommand(CommandType.UNWRAP_WETH, [MSG_SENDER, 0]) const { commands, inputs } = planner @@ -696,9 +722,17 @@ describe('Uniswap Gas Tests', () => { v3AmountOutMin, encodePathExactInput(v3Tokens), SOURCE_MSG_SENDER, + [], ]) // the tokens are already int he v2 pair, so amountIn is 0 - planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [MSG_SENDER, 0, v2AmountOutMin, v2Tokens, SOURCE_MSG_SENDER]) + planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [ + MSG_SENDER, + 0, + v2AmountOutMin, + v2Tokens, + SOURCE_MSG_SENDER, + [], + ]) const { commands, inputs } = planner await snapshotGasCost(router['execute(bytes,bytes[],uint256)'](commands, inputs, DEADLINE)) @@ -717,6 +751,7 @@ describe('Uniswap Gas Tests', () => { v2AmountOutMin, v2Tokens, SOURCE_MSG_SENDER, + [], ]) planner.addCommand(CommandType.V3_SWAP_EXACT_IN, [ MSG_SENDER, @@ -724,6 +759,7 @@ describe('Uniswap Gas Tests', () => { v3AmountOutMin, encodePathExactInput(v3Tokens), SOURCE_ROUTER, + [], ]) const { commands, inputs } = planner @@ -746,9 +782,23 @@ describe('Uniswap Gas Tests', () => { planner.addCommand(CommandType.PERMIT2_TRANSFER_FROM, [DAI.address, Pair.getAddress(DAI, USDT), v2AmountIn2]) // 2) trade route1 and return tokens to bob - planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [MSG_SENDER, 0, minAmountOut1, route1, SOURCE_MSG_SENDER]) + planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [ + MSG_SENDER, + 0, + minAmountOut1, + route1, + SOURCE_MSG_SENDER, + [], + ]) // 3) trade route2 and return tokens to bob - planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [MSG_SENDER, 0, minAmountOut2, route2, SOURCE_MSG_SENDER]) + planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [ + MSG_SENDER, + 0, + minAmountOut2, + route2, + SOURCE_MSG_SENDER, + [], + ]) const { commands, inputs } = planner await snapshotGasCost(router['execute(bytes,bytes[],uint256)'](commands, inputs, DEADLINE)) @@ -781,9 +831,23 @@ describe('Uniswap Gas Tests', () => { planner.addCommand(CommandType.PERMIT2_TRANSFER_FROM_BATCH, [BATCH_TRANSFER]) // 2) trade route1 and return tokens to bob - planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [MSG_SENDER, 0, minAmountOut1, route1, SOURCE_MSG_SENDER]) + planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [ + MSG_SENDER, + 0, + minAmountOut1, + route1, + SOURCE_MSG_SENDER, + [], + ]) // 3) trade route2 and return tokens to bob - planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [MSG_SENDER, 0, minAmountOut2, route2, SOURCE_MSG_SENDER]) + planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [ + MSG_SENDER, + 0, + minAmountOut2, + route2, + SOURCE_MSG_SENDER, + [], + ]) const { commands, inputs } = planner await snapshotGasCost(router['execute(bytes,bytes[],uint256)'](commands, inputs, DEADLINE)) @@ -805,6 +869,7 @@ describe('Uniswap Gas Tests', () => { minAmountOut1, route1, SOURCE_MSG_SENDER, + [], ]) // 2) trade route2 and return tokens to bob planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [ @@ -813,6 +878,7 @@ describe('Uniswap Gas Tests', () => { minAmountOut2, route2, SOURCE_MSG_SENDER, + [], ]) const { commands, inputs } = planner @@ -858,6 +924,7 @@ describe('Uniswap Gas Tests', () => { minAmountOut1, route1, SOURCE_MSG_SENDER, + [], ]) // 3) trade route2 and return tokens to bob planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [ @@ -866,6 +933,7 @@ describe('Uniswap Gas Tests', () => { minAmountOut2, route2, SOURCE_MSG_SENDER, + [], ]) const { commands, inputs } = planner @@ -878,7 +946,14 @@ describe('Uniswap Gas Tests', () => { const v3AmountIn: BigNumber = expandTo18DecimalsBN(3) // V2 trades DAI for USDC, sending the tokens back to the router for v3 trade - planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [router.address, v2AmountIn, 0, tokens, SOURCE_MSG_SENDER]) + planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [ + router.address, + v2AmountIn, + 0, + tokens, + SOURCE_MSG_SENDER, + [], + ]) // V3 trades USDC for WETH, trading the whole balance, with a recipient of Alice planner.addCommand(CommandType.V3_SWAP_EXACT_IN, [ router.address, @@ -886,6 +961,7 @@ describe('Uniswap Gas Tests', () => { 0, encodePathExactInput(tokens), SOURCE_MSG_SENDER, + [], ]) // aggregate slippate check planner.addCommand(CommandType.SWEEP, [WETH.address, MSG_SENDER, expandTo18DecimalsBN(0.0005)]) @@ -900,7 +976,7 @@ describe('Uniswap Gas Tests', () => { const v3AmountIn: BigNumber = expandTo18DecimalsBN(3) // V2 trades DAI for USDC, sending the tokens back to the router for v3 trade - planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [ADDRESS_THIS, v2AmountIn, 0, tokens, SOURCE_MSG_SENDER]) + planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [ADDRESS_THIS, v2AmountIn, 0, tokens, SOURCE_MSG_SENDER, []]) // V3 trades USDC for WETH, trading the whole balance, with a recipient of Alice planner.addCommand(CommandType.V3_SWAP_EXACT_IN, [ router.address, @@ -908,6 +984,7 @@ describe('Uniswap Gas Tests', () => { 0, encodePathExactInput(tokens), SOURCE_MSG_SENDER, + [], ]) // aggregate slippate check planner.addCommand(CommandType.SWEEP, [WETH.address, MSG_SENDER, expandTo18DecimalsBN(0.0005)]) @@ -923,13 +1000,14 @@ describe('Uniswap Gas Tests', () => { const value = v2AmountIn.add(v3AmountIn) planner.addCommand(CommandType.WRAP_ETH, [ADDRESS_THIS, value]) - planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [router.address, v2AmountIn, 0, tokens, SOURCE_ROUTER]) + planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [router.address, v2AmountIn, 0, tokens, SOURCE_ROUTER, []]) planner.addCommand(CommandType.V3_SWAP_EXACT_IN, [ router.address, v3AmountIn, 0, encodePathExactInput(tokens), SOURCE_ROUTER, + [], ]) // aggregate slippate check planner.addCommand(CommandType.SWEEP, [USDC.address, MSG_SENDER, 0.0005 * 10 ** 6]) @@ -943,13 +1021,21 @@ describe('Uniswap Gas Tests', () => { const v2AmountIn: BigNumber = expandTo18DecimalsBN(20) const v3AmountIn: BigNumber = expandTo18DecimalsBN(30) - planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [router.address, v2AmountIn, 0, tokens, SOURCE_MSG_SENDER]) + planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [ + router.address, + v2AmountIn, + 0, + tokens, + SOURCE_MSG_SENDER, + [], + ]) planner.addCommand(CommandType.V3_SWAP_EXACT_IN, [ router.address, v3AmountIn, 0, encodePathExactInput(tokens), SOURCE_MSG_SENDER, + [], ]) // aggregate slippate check planner.addCommand(CommandType.UNWRAP_WETH, [MSG_SENDER, expandTo18DecimalsBN(0.0005)]) @@ -972,6 +1058,7 @@ describe('Uniswap Gas Tests', () => { maxAmountIn, [DAI.address, WETH.address], SOURCE_MSG_SENDER, + [], ]) planner.addCommand(CommandType.V3_SWAP_EXACT_OUT, [ router.address, @@ -979,6 +1066,7 @@ describe('Uniswap Gas Tests', () => { maxAmountIn, path, SOURCE_MSG_SENDER, + [], ]) // aggregate slippate check planner.addCommand(CommandType.UNWRAP_WETH, [MSG_SENDER, fullAmountOut]) @@ -1011,6 +1099,7 @@ describe('Uniswap Gas Tests', () => { 0, planOneTokens, SOURCE_MSG_SENDER, + [], ]) // V3 trades USDC for WETH, trading the whole balance, with a recipient of Alice subplan.addCommand(CommandType.V3_SWAP_EXACT_IN, [ @@ -1019,6 +1108,7 @@ describe('Uniswap Gas Tests', () => { 0, encodePathExactInput(planOneTokens), SOURCE_MSG_SENDER, + [], ]) // aggregate slippage check subplan.addCommand(CommandType.SWEEP, [WETH.address, MSG_SENDER, planOneWethMinOut]) @@ -1037,6 +1127,7 @@ describe('Uniswap Gas Tests', () => { wethMinAmountOut2, encodePathExactInput(planTwoTokens), SOURCE_MSG_SENDER, + [], ]) // add the second subplan to the main planner @@ -1058,6 +1149,7 @@ describe('Uniswap Gas Tests', () => { 0, planOneTokens, SOURCE_MSG_SENDER, + [], ]) // V3 trades USDC for WETH, trading the whole balance, with a recipient of Alice subplan.addCommand(CommandType.V3_SWAP_EXACT_IN, [ @@ -1066,6 +1158,7 @@ describe('Uniswap Gas Tests', () => { 0, encodePathExactInput(planOneTokens), SOURCE_MSG_SENDER, + [], ]) // aggregate slippage check subplan.addCommand(CommandType.SWEEP, [WETH.address, MSG_SENDER, planOneWethMinOut]) @@ -1084,6 +1177,7 @@ describe('Uniswap Gas Tests', () => { wethMinAmountOut2, encodePathExactInput(planTwoTokens), SOURCE_MSG_SENDER, + [], ]) // add the second subplan to the main planner @@ -1105,6 +1199,7 @@ describe('Uniswap Gas Tests', () => { 0, planOneTokens, SOURCE_MSG_SENDER, + [], ]) // V3 trades USDC for WETH, trading the whole balance, with a recipient of Alice subplan.addCommand(CommandType.V3_SWAP_EXACT_IN, [ @@ -1113,6 +1208,7 @@ describe('Uniswap Gas Tests', () => { 0, encodePathExactInput(planOneTokens), SOURCE_MSG_SENDER, + [], ]) // aggregate slippage check subplan.addCommand(CommandType.SWEEP, [WETH.address, MSG_SENDER, planOneWethMinOut]) @@ -1132,6 +1228,7 @@ describe('Uniswap Gas Tests', () => { wethMinAmountOut2, encodePathExactInput(planTwoTokens), SOURCE_MSG_SENDER, + [], ]) // add the second subplan to the main planner @@ -1152,6 +1249,7 @@ describe('Uniswap Gas Tests', () => { 0, planOneTokens, SOURCE_MSG_SENDER, + [], ]) // V3 trades USDC for WETH, trading the whole balance, with a recipient of Alice subplan.addCommand(CommandType.V3_SWAP_EXACT_IN, [ @@ -1160,6 +1258,7 @@ describe('Uniswap Gas Tests', () => { 0, encodePathExactInput(planOneTokens), SOURCE_MSG_SENDER, + [], ]) // aggregate slippage check subplan.addCommand(CommandType.SWEEP, [WETH.address, MSG_SENDER, planOneWethMinOut]) @@ -1179,6 +1278,7 @@ describe('Uniswap Gas Tests', () => { wethMinAmountOut2, encodePathExactInput(planTwoTokens), SOURCE_MSG_SENDER, + [], ]) // add the second subplan to the main planner diff --git a/test/integration-tests/gas-tests/UniversalRouter.gas.test.ts b/test/integration-tests/gas-tests/UniversalRouter.gas.test.ts deleted file mode 100644 index 6dc567c2..00000000 --- a/test/integration-tests/gas-tests/UniversalRouter.gas.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { UniversalRouter, IWETH9, ERC20 } from '../../../typechain' -import { expect } from '../shared/expect' -import { ALICE_ADDRESS } from '../shared/constants' -import { abi as TOKEN_ABI } from '../../../artifacts/solmate/src/tokens/ERC20.sol/ERC20.json' -import { abi as WETH_ABI } from '../../../artifacts/@uniswap/v4-periphery/src/interfaces/external/IWETH9.sol/IWETH9.json' -import { resetFork, WETH, DAI } from '../shared/mainnetForkHelpers' -import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' -import hre from 'hardhat' -import deployUniversalRouter from '../shared/deployUniversalRouter' -import { RoutePlanner } from '../shared/planner' - -const { ethers } = hre - -describe('UniversalRouter Gas Tests', () => { - let alice: SignerWithAddress - let planner: RoutePlanner - let router: UniversalRouter - let daiContract: ERC20 - let wethContract: IWETH9 - - beforeEach(async () => { - await resetFork() - alice = await ethers.getSigner(ALICE_ADDRESS) - await hre.network.provider.request({ - method: 'hardhat_impersonateAccount', - params: [ALICE_ADDRESS], - }) - daiContract = new ethers.Contract(DAI.address, TOKEN_ABI, alice) as ERC20 - wethContract = new ethers.Contract(WETH.address, WETH_ABI, alice) as IWETH9 - router = (await deployUniversalRouter(alice.address)).connect(alice) as UniversalRouter - planner = new RoutePlanner() - }) - - it('gas: bytecode size', async () => { - expect(((await router.provider.getCode(router.address)).length - 2) / 2).to.matchSnapshot() - }) -}) diff --git a/test/integration-tests/gas-tests/UniversalVSSwapRouter.gas.test.ts b/test/integration-tests/gas-tests/UniversalVSSwapRouter.gas.test.ts index 017841f2..71324b05 100644 --- a/test/integration-tests/gas-tests/UniversalVSSwapRouter.gas.test.ts +++ b/test/integration-tests/gas-tests/UniversalVSSwapRouter.gas.test.ts @@ -168,10 +168,17 @@ describe('Uniswap UX Tests gas:', () => { if (swap.route.protocol == 'V2') { let pathAddresses = routeToAddresses(route) - planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [bob.address, amountIn, 0, pathAddresses, SOURCE_MSG_SENDER]) + planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [ + bob.address, + amountIn, + 0, + pathAddresses, + SOURCE_MSG_SENDER, + [], + ]) } else if (swap.route.protocol == 'V3') { let path = encodePathExactInput(route) - planner.addCommand(CommandType.V3_SWAP_EXACT_IN, [bob.address, amountIn, 0, path, SOURCE_MSG_SENDER]) + planner.addCommand(CommandType.V3_SWAP_EXACT_IN, [bob.address, amountIn, 0, path, SOURCE_MSG_SENDER, []]) } else { console.log('invalid protocol') } diff --git a/test/integration-tests/gas-tests/__snapshots__/CheckOwnership.gas.test.ts.snap b/test/integration-tests/gas-tests/__snapshots__/CheckOwnership.gas.test.ts.snap index be7aae4b..45c827d4 100644 --- a/test/integration-tests/gas-tests/__snapshots__/CheckOwnership.gas.test.ts.snap +++ b/test/integration-tests/gas-tests/__snapshots__/CheckOwnership.gas.test.ts.snap @@ -3,6 +3,6 @@ exports[`Check Ownership Gas gas: balance check ERC20 1`] = ` Object { "calldataByteLength": 356, - "gasUsed": 38007, + "gasUsed": 37904, } `; diff --git a/test/integration-tests/gas-tests/__snapshots__/Payments.gas.test.ts.snap b/test/integration-tests/gas-tests/__snapshots__/Payments.gas.test.ts.snap index 8e894239..f4419bd9 100644 --- a/test/integration-tests/gas-tests/__snapshots__/Payments.gas.test.ts.snap +++ b/test/integration-tests/gas-tests/__snapshots__/Payments.gas.test.ts.snap @@ -10,41 +10,41 @@ Object { exports[`Payments Gas Tests Individual Command Tests gas: SWEEP_WITH_FEE 1`] = ` Object { "calldataByteLength": 516, - "gasUsed": 66117, + "gasUsed": 66081, } `; exports[`Payments Gas Tests Individual Command Tests gas: TRANSFER with ERC20 1`] = ` Object { "calldataByteLength": 356, - "gasUsed": 36319, + "gasUsed": 36205, } `; exports[`Payments Gas Tests Individual Command Tests gas: TRANSFER with ETH 1`] = ` Object { "calldataByteLength": 356, - "gasUsed": 31872, + "gasUsed": 31764, } `; exports[`Payments Gas Tests Individual Command Tests gas: UNWRAP_WETH 1`] = ` Object { "calldataByteLength": 324, - "gasUsed": 44938, + "gasUsed": 44998, } `; exports[`Payments Gas Tests Individual Command Tests gas: UNWRAP_WETH_WITH_FEE 1`] = ` Object { "calldataByteLength": 644, - "gasUsed": 51501, + "gasUsed": 51432, } `; exports[`Payments Gas Tests Individual Command Tests gas: WRAP_ETH 1`] = ` Object { "calldataByteLength": 324, - "gasUsed": 53716, + "gasUsed": 53675, } `; diff --git a/test/integration-tests/gas-tests/__snapshots__/Uniswap.gas.test.ts.snap b/test/integration-tests/gas-tests/__snapshots__/Uniswap.gas.test.ts.snap index d6fcab36..e96e2632 100644 --- a/test/integration-tests/gas-tests/__snapshots__/Uniswap.gas.test.ts.snap +++ b/test/integration-tests/gas-tests/__snapshots__/Uniswap.gas.test.ts.snap @@ -2,106 +2,106 @@ exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Batch reverts gas: 2 sub-plans, both fail but the transaction succeeds 1`] = ` Object { - "calldataByteLength": 1764, - "gasUsed": 273531, + "calldataByteLength": 1956, + "gasUsed": 278967, } `; exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Batch reverts gas: 2 sub-plans, neither fails 1`] = ` Object { - "calldataByteLength": 1764, - "gasUsed": 249218, + "calldataByteLength": 1956, + "gasUsed": 254625, } `; exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Batch reverts gas: 2 sub-plans, second sub plan fails 1`] = ` Object { - "calldataByteLength": 1764, - "gasUsed": 249218, + "calldataByteLength": 1956, + "gasUsed": 254625, } `; exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Batch reverts gas: 2 sub-plans, the first fails 1`] = ` Object { - "calldataByteLength": 1764, - "gasUsed": 273531, + "calldataByteLength": 1956, + "gasUsed": 278967, } `; exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Interleaving routes gas: V2, then V3 1`] = ` Object { - "calldataByteLength": 836, - "gasUsed": 191332, + "calldataByteLength": 964, + "gasUsed": 194906, } `; exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Interleaving routes gas: V3, then V2 1`] = ` Object { - "calldataByteLength": 836, - "gasUsed": 178869, + "calldataByteLength": 964, + "gasUsed": 182171, } `; exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Split routes gas: ERC20 --> ERC20 split V2 and V2 different routes, different input tokens, each two hop, with batch permit 1`] = ` Object { - "calldataByteLength": 1540, - "gasUsed": 299714, + "calldataByteLength": 1668, + "gasUsed": 303148, } `; exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Split routes gas: ERC20 --> ERC20 split V2 and V2 different routes, each two hop, with explicit permit 1`] = ` Object { - "calldataByteLength": 1220, - "gasUsed": 310403, + "calldataByteLength": 1348, + "gasUsed": 313539, } `; exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Split routes gas: ERC20 --> ERC20 split V2 and V2 different routes, each two hop, with explicit permit transfer from batch 1`] = ` Object { - "calldataByteLength": 1284, - "gasUsed": 311659, + "calldataByteLength": 1412, + "gasUsed": 314886, } `; exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Split routes gas: ERC20 --> ERC20 split V2 and V2 different routes, each two hop, without explicit permit 1`] = ` Object { - "calldataByteLength": 900, - "gasUsed": 306723, + "calldataByteLength": 1028, + "gasUsed": 310121, } `; exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Split routes gas: ERC20 --> ERC20 split V2 and V3, one hop 1`] = ` Object { - "calldataByteLength": 996, - "gasUsed": 178918, + "calldataByteLength": 1124, + "gasUsed": 182345, } `; exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Split routes gas: ERC20 --> ERC20 split V2 and V3, one hop, ADDRESS_THIS flag 1`] = ` Object { - "calldataByteLength": 996, - "gasUsed": 178693, + "calldataByteLength": 1124, + "gasUsed": 182120, } `; exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Split routes gas: ERC20 --> ETH split V2 and V3, exactOut, one hop 1`] = ` Object { - "calldataByteLength": 964, - "gasUsed": 194232, + "calldataByteLength": 1092, + "gasUsed": 197446, } `; exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Split routes gas: ERC20 --> ETH split V2 and V3, one hop 1`] = ` Object { - "calldataByteLength": 964, - "gasUsed": 186862, + "calldataByteLength": 1092, + "gasUsed": 190349, } `; exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Split routes gas: ETH --> ERC20 split V2 and V3, one hop 1`] = ` Object { - "calldataByteLength": 1124, - "gasUsed": 193755, + "calldataByteLength": 1252, + "gasUsed": 197061, } `; @@ -142,99 +142,99 @@ Object { exports[`Uniswap Gas Tests Trade on UniswapV2 with Universal Router. ERC20 --> ERC20 gas: exactIn trade, where an output fee is taken 1`] = ` Object { - "calldataByteLength": 836, - "gasUsed": 127798, + "calldataByteLength": 900, + "gasUsed": 129130, } `; exports[`Uniswap Gas Tests Trade on UniswapV2 with Universal Router. ERC20 --> ERC20 gas: exactIn, one trade, one hop 1`] = ` Object { - "calldataByteLength": 516, - "gasUsed": 107767, + "calldataByteLength": 580, + "gasUsed": 109109, } `; exports[`Uniswap Gas Tests Trade on UniswapV2 with Universal Router. ERC20 --> ERC20 gas: exactIn, one trade, three hops 1`] = ` Object { - "calldataByteLength": 580, - "gasUsed": 243074, + "calldataByteLength": 644, + "gasUsed": 245129, } `; exports[`Uniswap Gas Tests Trade on UniswapV2 with Universal Router. ERC20 --> ERC20 gas: exactIn, one trade, three hops, no deadline 1`] = ` Object { - "calldataByteLength": 548, - "gasUsed": 242816, + "calldataByteLength": 612, + "gasUsed": 244871, } `; exports[`Uniswap Gas Tests Trade on UniswapV2 with Universal Router. ERC20 --> ERC20 gas: exactIn, one trade, two hops 1`] = ` Object { - "calldataByteLength": 548, - "gasUsed": 175466, + "calldataByteLength": 612, + "gasUsed": 177158, } `; exports[`Uniswap Gas Tests Trade on UniswapV2 with Universal Router. ERC20 --> ERC20 gas: exactIn, one trade, two hops, MSG_SENDER flag 1`] = ` Object { - "calldataByteLength": 548, - "gasUsed": 175466, + "calldataByteLength": 612, + "gasUsed": 177158, } `; exports[`Uniswap Gas Tests Trade on UniswapV2 with Universal Router. ERC20 --> ERC20 gas: exactOut, one trade, one hop 1`] = ` Object { - "calldataByteLength": 516, - "gasUsed": 107355, + "calldataByteLength": 580, + "gasUsed": 108263, } `; exports[`Uniswap Gas Tests Trade on UniswapV2 with Universal Router. ERC20 --> ERC20 gas: exactOut, one trade, three hops 1`] = ` Object { - "calldataByteLength": 580, - "gasUsed": 248413, + "calldataByteLength": 644, + "gasUsed": 250138, } `; exports[`Uniswap Gas Tests Trade on UniswapV2 with Universal Router. ERC20 --> ERC20 gas: exactOut, one trade, two hops 1`] = ` Object { - "calldataByteLength": 548, - "gasUsed": 177957, + "calldataByteLength": 612, + "gasUsed": 179266, } `; exports[`Uniswap Gas Tests Trade on UniswapV2 with Universal Router. ERC20 --> ETH gas: exactIn, one trade, one hop 1`] = ` Object { - "calldataByteLength": 644, - "gasUsed": 124213, + "calldataByteLength": 708, + "gasUsed": 125629, } `; exports[`Uniswap Gas Tests Trade on UniswapV2 with Universal Router. ERC20 --> ETH gas: exactOut, one trade, one hop 1`] = ` Object { - "calldataByteLength": 804, - "gasUsed": 129151, + "calldataByteLength": 868, + "gasUsed": 130176, } `; exports[`Uniswap Gas Tests Trade on UniswapV2 with Universal Router. ERC20 --> ETH gas: exactOut, with ETH fee 1`] = ` Object { - "calldataByteLength": 964, - "gasUsed": 137185, + "calldataByteLength": 1028, + "gasUsed": 138054, } `; exports[`Uniswap Gas Tests Trade on UniswapV2 with Universal Router. ETH --> ERC20 gas: exactIn, one trade, one hop 1`] = ` Object { - "calldataByteLength": 644, - "gasUsed": 107744, + "calldataByteLength": 708, + "gasUsed": 108949, } `; exports[`Uniswap Gas Tests Trade on UniswapV2 with Universal Router. ETH --> ERC20 gas: exactOut, one trade, one hop 1`] = ` Object { - "calldataByteLength": 772, - "gasUsed": 126268, + "calldataByteLength": 836, + "gasUsed": 127173, } `; @@ -282,70 +282,70 @@ Object { exports[`Uniswap Gas Tests Trade on UniswapV3 with Universal Router. ERC20 --> ERC20 gas: exactIn, one trade, one hop 1`] = ` Object { - "calldataByteLength": 516, - "gasUsed": 106693, + "calldataByteLength": 580, + "gasUsed": 108749, } `; exports[`Uniswap Gas Tests Trade on UniswapV3 with Universal Router. ERC20 --> ERC20 gas: exactIn, one trade, three hops 1`] = ` Object { - "calldataByteLength": 548, - "gasUsed": 256740, + "calldataByteLength": 612, + "gasUsed": 261838, } `; exports[`Uniswap Gas Tests Trade on UniswapV3 with Universal Router. ERC20 --> ERC20 gas: exactIn, one trade, two hops 1`] = ` Object { - "calldataByteLength": 548, - "gasUsed": 179102, + "calldataByteLength": 612, + "gasUsed": 182672, } `; exports[`Uniswap Gas Tests Trade on UniswapV3 with Universal Router. ERC20 --> ERC20 gas: exactOut, one trade, one hop 1`] = ` Object { - "calldataByteLength": 516, - "gasUsed": 114182, + "calldataByteLength": 580, + "gasUsed": 116395, } `; exports[`Uniswap Gas Tests Trade on UniswapV3 with Universal Router. ERC20 --> ERC20 gas: exactOut, one trade, three hops 1`] = ` Object { - "calldataByteLength": 548, - "gasUsed": 251696, + "calldataByteLength": 612, + "gasUsed": 256693, } `; exports[`Uniswap Gas Tests Trade on UniswapV3 with Universal Router. ERC20 --> ERC20 gas: exactOut, one trade, two hops 1`] = ` Object { - "calldataByteLength": 548, - "gasUsed": 174643, + "calldataByteLength": 612, + "gasUsed": 178243, } `; exports[`Uniswap Gas Tests Trade on UniswapV3 with Universal Router. ERC20 --> ETH gas: exactIn swap 1`] = ` Object { - "calldataByteLength": 644, - "gasUsed": 123187, + "calldataByteLength": 708, + "gasUsed": 125317, } `; exports[`Uniswap Gas Tests Trade on UniswapV3 with Universal Router. ERC20 --> ETH gas: exactOut swap 1`] = ` Object { - "calldataByteLength": 644, - "gasUsed": 130748, + "calldataByteLength": 708, + "gasUsed": 133035, } `; exports[`Uniswap Gas Tests Trade on UniswapV3 with Universal Router. ETH --> ERC20 gas: exactIn swap 1`] = ` Object { - "calldataByteLength": 644, - "gasUsed": 216591, + "calldataByteLength": 708, + "gasUsed": 218569, } `; exports[`Uniswap Gas Tests Trade on UniswapV3 with Universal Router. ETH --> ERC20 gas: exactOut swap 1`] = ` Object { - "calldataByteLength": 772, - "gasUsed": 125977, + "calldataByteLength": 836, + "gasUsed": 128183, } `; diff --git a/test/integration-tests/gas-tests/__snapshots__/UniversalRouter.gas.test.ts.snap b/test/integration-tests/gas-tests/__snapshots__/UniversalRouter.gas.test.ts.snap deleted file mode 100644 index ba695fba..00000000 --- a/test/integration-tests/gas-tests/__snapshots__/UniversalRouter.gas.test.ts.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`UniversalRouter Gas Tests gas: bytecode size 1`] = `18469`; diff --git a/test/integration-tests/gas-tests/__snapshots__/UniversalVSSwapRouter.gas.test.ts.snap b/test/integration-tests/gas-tests/__snapshots__/UniversalVSSwapRouter.gas.test.ts.snap index 0e48406b..26ba0d8f 100644 --- a/test/integration-tests/gas-tests/__snapshots__/UniversalVSSwapRouter.gas.test.ts.snap +++ b/test/integration-tests/gas-tests/__snapshots__/UniversalVSSwapRouter.gas.test.ts.snap @@ -7,32 +7,32 @@ Object { } `; -exports[`Uniswap UX Tests gas: Comparisons Casual Swapper - 3 swaps Permit2 Max Approval Swap 1`] = `1115454`; +exports[`Uniswap UX Tests gas: Comparisons Casual Swapper - 3 swaps Permit2 Max Approval Swap 1`] = `1136031`; -exports[`Uniswap UX Tests gas: Comparisons Casual Swapper - 3 swaps Permit2 Sign Per Swap 1`] = `1150159`; +exports[`Uniswap UX Tests gas: Comparisons Casual Swapper - 3 swaps Permit2 Sign Per Swap 1`] = `1170723`; exports[`Uniswap UX Tests gas: Comparisons Casual Swapper - 3 swaps SwapRouter02 1`] = `1124979`; -exports[`Uniswap UX Tests gas: Comparisons Frequent Swapper - 10 swaps Permit2 Max Approval Swap 1`] = `3114152`; +exports[`Uniswap UX Tests gas: Comparisons Frequent Swapper - 10 swaps Permit2 Max Approval Swap 1`] = `3174531`; -exports[`Uniswap UX Tests gas: Comparisons Frequent Swapper - 10 swaps Permit2 Sign Per Swap 1`] = `3269019`; +exports[`Uniswap UX Tests gas: Comparisons Frequent Swapper - 10 swaps Permit2 Sign Per Swap 1`] = `3329339`; exports[`Uniswap UX Tests gas: Comparisons Frequent Swapper - 10 swaps SwapRouter02 1`] = `3195011`; -exports[`Uniswap UX Tests gas: Comparisons Frequent Swapper across 3 swap router versions - 15 swaps across 3 versions Permit2 Max Approval Swap 1`] = `4146229`; +exports[`Uniswap UX Tests gas: Comparisons Frequent Swapper across 3 swap router versions - 15 swaps across 3 versions Permit2 Max Approval Swap 1`] = `4224448`; -exports[`Uniswap UX Tests gas: Comparisons Frequent Swapper across 3 swap router versions - 15 swaps across 3 versions Permit2 Sign Per Swap 1`] = `4352007`; +exports[`Uniswap UX Tests gas: Comparisons Frequent Swapper across 3 swap router versions - 15 swaps across 3 versions Permit2 Sign Per Swap 1`] = `4430146`; exports[`Uniswap UX Tests gas: Comparisons Frequent Swapper across 3 swap router versions - 15 swaps across 3 versions SwapRouter02 1`] = `4282374`; -exports[`Uniswap UX Tests gas: Comparisons One Time Swapper - Complex Swap Permit2 Max Approval Swap 1`] = `513474`; +exports[`Uniswap UX Tests gas: Comparisons One Time Swapper - Complex Swap Permit2 Max Approval Swap 1`] = `521974`; -exports[`Uniswap UX Tests gas: Comparisons One Time Swapper - Complex Swap Permit2 Sign Per Swap 1`] = `513792`; +exports[`Uniswap UX Tests gas: Comparisons One Time Swapper - Complex Swap Permit2 Sign Per Swap 1`] = `522292`; exports[`Uniswap UX Tests gas: Comparisons One Time Swapper - Complex Swap SwapRouter02 1`] = `500008`; -exports[`Uniswap UX Tests gas: Comparisons One Time Swapper - Simple Swap Permit2 Max Approval Swap 1`] = `301777`; +exports[`Uniswap UX Tests gas: Comparisons One Time Swapper - Simple Swap Permit2 Max Approval Swap 1`] = `305341`; -exports[`Uniswap UX Tests gas: Comparisons One Time Swapper - Simple Swap Permit2 Sign Per Swap 1`] = `301713`; +exports[`Uniswap UX Tests gas: Comparisons One Time Swapper - Simple Swap Permit2 Sign Per Swap 1`] = `305277`; exports[`Uniswap UX Tests gas: Comparisons One Time Swapper - Simple Swap SwapRouter02 1`] = `270033`; diff --git a/test/integration-tests/gas-tests/__snapshots__/V3ToV4Migration.gas.test.ts.snap b/test/integration-tests/gas-tests/__snapshots__/V3ToV4Migration.gas.test.ts.snap index e3c288d7..cde6c57a 100644 --- a/test/integration-tests/gas-tests/__snapshots__/V3ToV4Migration.gas.test.ts.snap +++ b/test/integration-tests/gas-tests/__snapshots__/V3ToV4Migration.gas.test.ts.snap @@ -3,55 +3,55 @@ exports[`V3 to V4 Migration Gas Tests V3 Commands burn gas: erc721permit + decreaseLiquidity + collect + burn 1`] = ` Object { "calldataByteLength": 1092, - "gasUsed": 256864, + "gasUsed": 257003, } `; exports[`V3 to V4 Migration Gas Tests V3 Commands collect gas: erc721permit + decreaseLiquidity + collect 1`] = ` Object { "calldataByteLength": 964, - "gasUsed": 224512, + "gasUsed": 224616, } `; exports[`V3 to V4 Migration Gas Tests V3 Commands decrease liquidity gas: erc721permit + decreaseLiquidity 1`] = ` Object { "calldataByteLength": 740, - "gasUsed": 202607, + "gasUsed": 202641, } `; exports[`V3 to V4 Migration Gas Tests V3 Commands erc721permit gas: erc721permit 1`] = ` Object { "calldataByteLength": 484, - "gasUsed": 66280, + "gasUsed": 66244, } `; exports[`V3 to V4 Migration Gas Tests V4 Commands initialize pool gas: initialize a pool 1`] = ` Object { "calldataByteLength": 452, - "gasUsed": 59163, + "gasUsed": 59119, } `; exports[`V3 to V4 Migration Gas Tests V4 Commands mint gas: migrate and mint 1`] = ` Object { "calldataByteLength": 2500, - "gasUsed": 597216, + "gasUsed": 598034, } `; exports[`V3 to V4 Migration Gas Tests V4 Commands mint gas: migrate weth position into eth position with forwarding 1`] = ` Object { "calldataByteLength": 2788, - "gasUsed": 574445, + "gasUsed": 575125, } `; exports[`V3 to V4 Migration Gas Tests V4 Commands mint gas: mint 1`] = ` Object { "calldataByteLength": 1604, - "gasUsed": 443107, + "gasUsed": 443737, } `; diff --git a/test/integration-tests/shared/planner.ts b/test/integration-tests/shared/planner.ts index e90dfbb3..0c1bb64f 100644 --- a/test/integration-tests/shared/planner.ts +++ b/test/integration-tests/shared/planner.ts @@ -13,6 +13,7 @@ export enum CommandType { SWEEP = 0x04, TRANSFER = 0x05, PAY_PORTION = 0x06, + PAY_PORTION_FULL_PRECISION = 0x07, V2_SWAP_EXACT_IN = 0x08, V2_SWAP_EXACT_OUT = 0x09, @@ -61,10 +62,10 @@ const ABI_DEFINITION: { [key in CommandType]: string[] } = { [CommandType.PERMIT2_TRANSFER_FROM_BATCH]: [PERMIT2_TRANSFER_FROM_BATCH_STRUCT], // Uniswap Actions - [CommandType.V3_SWAP_EXACT_IN]: ['address', 'uint256', 'uint256', 'bytes', 'bool'], - [CommandType.V3_SWAP_EXACT_OUT]: ['address', 'uint256', 'uint256', 'bytes', 'bool'], - [CommandType.V2_SWAP_EXACT_IN]: ['address', 'uint256', 'uint256', 'address[]', 'bool'], - [CommandType.V2_SWAP_EXACT_OUT]: ['address', 'uint256', 'uint256', 'address[]', 'bool'], + [CommandType.V3_SWAP_EXACT_IN]: ['address', 'uint256', 'uint256', 'bytes', 'bool', 'uint256[]'], + [CommandType.V3_SWAP_EXACT_OUT]: ['address', 'uint256', 'uint256', 'bytes', 'bool', 'uint256[]'], + [CommandType.V2_SWAP_EXACT_IN]: ['address', 'uint256', 'uint256', 'address[]', 'bool', 'uint256[]'], + [CommandType.V2_SWAP_EXACT_OUT]: ['address', 'uint256', 'uint256', 'address[]', 'bool', 'uint256[]'], // Token Actions and Checks [CommandType.WRAP_ETH]: ['address', 'uint256'], @@ -72,6 +73,7 @@ const ABI_DEFINITION: { [key in CommandType]: string[] } = { [CommandType.SWEEP]: ['address', 'address', 'uint256'], [CommandType.TRANSFER]: ['address', 'address', 'uint256'], [CommandType.PAY_PORTION]: ['address', 'address', 'uint256'], + [CommandType.PAY_PORTION_FULL_PRECISION]: ['address', 'address', 'uint256'], [CommandType.BALANCE_CHECK_ERC20]: ['address', 'address', 'uint256'], [CommandType.V4_SWAP]: ['bytes', 'bytes[]'], diff --git a/test/integration-tests/shared/v4Planner.ts b/test/integration-tests/shared/v4Planner.ts index fe55d278..3c7a1890 100644 --- a/test/integration-tests/shared/v4Planner.ts +++ b/test/integration-tests/shared/v4Planner.ts @@ -51,20 +51,24 @@ const POOL_KEY_STRUCT = '(address currency0,address currency1,uint24 fee,int24 t const PATH_KEY_STRUCT = '(address intermediateCurrency,uint256 fee,int24 tickSpacing,address hooks,bytes hookData)' const SWAP_EXACT_IN_SINGLE_STRUCT = - '(' + POOL_KEY_STRUCT + ' poolKey,bool zeroForOne,uint128 amountIn,uint128 amountOutMinimum,bytes hookData)' + '(' + + POOL_KEY_STRUCT + + ' poolKey,bool zeroForOne,uint128 amountIn,uint128 amountOutMinimum,uint256 minHopPriceX36,bytes hookData)' const SWAP_EXACT_IN_STRUCT = '(address currencyIn,' + PATH_KEY_STRUCT + - '[] path,uint256[] maxHopSlippage,uint128 amountIn,uint128 amountOutMinimum)' + '[] path,uint256[] minHopPriceX36,uint128 amountIn,uint128 amountOutMinimum)' const SWAP_EXACT_OUT_SINGLE_STRUCT = - '(' + POOL_KEY_STRUCT + ' poolKey,bool zeroForOne,uint128 amountOut,uint128 amountInMaximum,bytes hookData)' + '(' + + POOL_KEY_STRUCT + + ' poolKey,bool zeroForOne,uint128 amountOut,uint128 amountInMaximum,uint256 minHopPriceX36,bytes hookData)' const SWAP_EXACT_OUT_STRUCT = '(address currencyOut,' + PATH_KEY_STRUCT + - '[] path,uint256[] maxHopSlippage,uint128 amountOut,uint128 amountInMaximum)' + '[] path,uint256[] minHopPriceX36,uint128 amountOut,uint128 amountInMaximum)' const ABI_DEFINITION: { [key in Actions]: string[] } = { // Liquidity commands