From e6556937a9e7cd1ccbb8a0ce958e99e32108e47c Mon Sep 17 00:00:00 2001 From: ChefMist <133624774+ChefMist@users.noreply.github.com> Date: Fri, 13 Sep 2024 12:39:25 +0800 Subject: [PATCH] feat: #8 return early (gas optimization) (#13) * feat: updated pancake-v4-periphery * feat: update test cases based on latest v4-periphery * feat: updated v4-periphery * feat: update code based on latest commit * chore: clean up commented code * feat: #8 return early * feat: add missing early return for 1 command * chore: forge-test after merge * feat: prevent permit2.permit frontrun (#14) --- ...test_v4BinSwap_ExactInSingle_NativeIn.snap | 2 +- ...actInSingle_NativeOut_RouterRecipient.snap | 2 +- ...apV4Test#test_v4BinSwap_ExactInSingle.snap | 2 +- ...4Test#test_v4BinSwap_ExactIn_MultiHop.snap | 2 +- ...Test#test_v4BinSwap_ExactIn_SingleHop.snap | 2 +- ...Test#test_v4BinSwap_ExactOut_MultiHop.snap | 2 +- ...est#test_v4BinSwap_ExactOut_SingleHop.snap | 2 +- ...apV4Test#test_v4ClSwap_ExactOutSingle.snap | 2 +- ...#test_v4ClSwap_ExactInSingle_NativeIn.snap | 2 +- ...test_v4ClSwap_ExactInSingle_NativeOut.snap | 2 +- ...wapV4Test#test_v4ClSwap_ExactInSingle.snap | 2 +- ...V4Test#test_v4ClSwap_ExactIn_MultiHop.snap | 2 +- ...4Test#test_v4ClSwap_ExactIn_SingleHop.snap | 2 +- ...apV4Test#test_v4ClSwap_ExactOutSingle.snap | 2 +- ...4Test#test_v4ClSwap_ExactOut_MultiHop.snap | 2 +- ...Test#test_v4ClSwap_ExactOut_SingleHop.snap | 2 +- ...wapV2Test#test_v2Swap_exactInput0For1.snap | 2 +- ...apV2Test#test_v2Swap_exactOutput0For1.snap | 2 +- ...wapV3Test#test_v3Swap_ExactInput0For1.snap | 2 +- ...apV3Test#test_v3Swap_exactOutput0For1.snap | 2 +- ...pTest#test_stableSwap_ExactInput0For1.snap | 2 +- ...pTest#test_stableSwap_ExactInput1For0.snap | 2 +- .../UniversalRouterBytecodeSize.snap | 2 +- .../UniversalRouterTest#test_sweep_token.snap | 2 +- ...#test_v4CLPositionmanager_Mint_Native.snap | 2 +- ...ationTest#test_v3PositionManager_burn.snap | 2 +- ..._v4BinPositionmanager_BinAddLiquidity.snap | 2 +- ...ositionmanager_BinAddLiquidity_Native.snap | 2 +- ...ionTest#test_v4CLPositionmanager_Mint.snap | 2 +- src/base/Dispatcher.sol | 34 ++- test/UniversalRouter.t.sol | 207 +++++++++++++++++- 31 files changed, 266 insertions(+), 33 deletions(-) diff --git a/.forge-snapshots/BinNativePancakeSwapV4Test#test_v4BinSwap_ExactInSingle_NativeIn.snap b/.forge-snapshots/BinNativePancakeSwapV4Test#test_v4BinSwap_ExactInSingle_NativeIn.snap index 45d3689..1844a0a 100644 --- a/.forge-snapshots/BinNativePancakeSwapV4Test#test_v4BinSwap_ExactInSingle_NativeIn.snap +++ b/.forge-snapshots/BinNativePancakeSwapV4Test#test_v4BinSwap_ExactInSingle_NativeIn.snap @@ -1 +1 @@ -146358 \ No newline at end of file +146304 \ No newline at end of file diff --git a/.forge-snapshots/BinNativePancakeSwapV4Test#test_v4BinSwap_ExactInSingle_NativeOut_RouterRecipient.snap b/.forge-snapshots/BinNativePancakeSwapV4Test#test_v4BinSwap_ExactInSingle_NativeOut_RouterRecipient.snap index 8979a84..1b3cf48 100644 --- a/.forge-snapshots/BinNativePancakeSwapV4Test#test_v4BinSwap_ExactInSingle_NativeOut_RouterRecipient.snap +++ b/.forge-snapshots/BinNativePancakeSwapV4Test#test_v4BinSwap_ExactInSingle_NativeOut_RouterRecipient.snap @@ -1 +1 @@ -123415 \ No newline at end of file +123362 \ No newline at end of file diff --git a/.forge-snapshots/BinPancakeSwapV4Test#test_v4BinSwap_ExactInSingle.snap b/.forge-snapshots/BinPancakeSwapV4Test#test_v4BinSwap_ExactInSingle.snap index c293da0..fc5260b 100644 --- a/.forge-snapshots/BinPancakeSwapV4Test#test_v4BinSwap_ExactInSingle.snap +++ b/.forge-snapshots/BinPancakeSwapV4Test#test_v4BinSwap_ExactInSingle.snap @@ -1 +1 @@ -147320 \ No newline at end of file +147266 \ No newline at end of file diff --git a/.forge-snapshots/BinPancakeSwapV4Test#test_v4BinSwap_ExactIn_MultiHop.snap b/.forge-snapshots/BinPancakeSwapV4Test#test_v4BinSwap_ExactIn_MultiHop.snap index d0845ab..66daf69 100644 --- a/.forge-snapshots/BinPancakeSwapV4Test#test_v4BinSwap_ExactIn_MultiHop.snap +++ b/.forge-snapshots/BinPancakeSwapV4Test#test_v4BinSwap_ExactIn_MultiHop.snap @@ -1 +1 @@ -178729 \ No newline at end of file +178676 \ No newline at end of file diff --git a/.forge-snapshots/BinPancakeSwapV4Test#test_v4BinSwap_ExactIn_SingleHop.snap b/.forge-snapshots/BinPancakeSwapV4Test#test_v4BinSwap_ExactIn_SingleHop.snap index 1762821..413bc40 100644 --- a/.forge-snapshots/BinPancakeSwapV4Test#test_v4BinSwap_ExactIn_SingleHop.snap +++ b/.forge-snapshots/BinPancakeSwapV4Test#test_v4BinSwap_ExactIn_SingleHop.snap @@ -1 +1 @@ -149114 \ No newline at end of file +149062 \ No newline at end of file diff --git a/.forge-snapshots/BinPancakeSwapV4Test#test_v4BinSwap_ExactOut_MultiHop.snap b/.forge-snapshots/BinPancakeSwapV4Test#test_v4BinSwap_ExactOut_MultiHop.snap index fa6f8f5..f91f4ae 100644 --- a/.forge-snapshots/BinPancakeSwapV4Test#test_v4BinSwap_ExactOut_MultiHop.snap +++ b/.forge-snapshots/BinPancakeSwapV4Test#test_v4BinSwap_ExactOut_MultiHop.snap @@ -1 +1 @@ -182570 \ No newline at end of file +182517 \ No newline at end of file diff --git a/.forge-snapshots/BinPancakeSwapV4Test#test_v4BinSwap_ExactOut_SingleHop.snap b/.forge-snapshots/BinPancakeSwapV4Test#test_v4BinSwap_ExactOut_SingleHop.snap index 22cacea..333e317 100644 --- a/.forge-snapshots/BinPancakeSwapV4Test#test_v4BinSwap_ExactOut_SingleHop.snap +++ b/.forge-snapshots/BinPancakeSwapV4Test#test_v4BinSwap_ExactOut_SingleHop.snap @@ -1 +1 @@ -153487 \ No newline at end of file +153435 \ No newline at end of file diff --git a/.forge-snapshots/BinPancakeSwapV4Test#test_v4ClSwap_ExactOutSingle.snap b/.forge-snapshots/BinPancakeSwapV4Test#test_v4ClSwap_ExactOutSingle.snap index f6fccfa..39744d8 100644 --- a/.forge-snapshots/BinPancakeSwapV4Test#test_v4ClSwap_ExactOutSingle.snap +++ b/.forge-snapshots/BinPancakeSwapV4Test#test_v4ClSwap_ExactOutSingle.snap @@ -1 +1 @@ -151699 \ No newline at end of file +151645 \ No newline at end of file diff --git a/.forge-snapshots/CLNativePancakeSwapV4Test#test_v4ClSwap_ExactInSingle_NativeIn.snap b/.forge-snapshots/CLNativePancakeSwapV4Test#test_v4ClSwap_ExactInSingle_NativeIn.snap index 68bd635..94dde0f 100644 --- a/.forge-snapshots/CLNativePancakeSwapV4Test#test_v4ClSwap_ExactInSingle_NativeIn.snap +++ b/.forge-snapshots/CLNativePancakeSwapV4Test#test_v4ClSwap_ExactInSingle_NativeIn.snap @@ -1 +1 @@ -172360 \ No newline at end of file +172308 \ No newline at end of file diff --git a/.forge-snapshots/CLNativePancakeSwapV4Test#test_v4ClSwap_ExactInSingle_NativeOut.snap b/.forge-snapshots/CLNativePancakeSwapV4Test#test_v4ClSwap_ExactInSingle_NativeOut.snap index ce5c174..9b1ca5e 100644 --- a/.forge-snapshots/CLNativePancakeSwapV4Test#test_v4ClSwap_ExactInSingle_NativeOut.snap +++ b/.forge-snapshots/CLNativePancakeSwapV4Test#test_v4ClSwap_ExactInSingle_NativeOut.snap @@ -1 +1 @@ -174610 \ No newline at end of file +174558 \ No newline at end of file diff --git a/.forge-snapshots/CLPancakeSwapV4Test#test_v4ClSwap_ExactInSingle.snap b/.forge-snapshots/CLPancakeSwapV4Test#test_v4ClSwap_ExactInSingle.snap index f1a6d10..9640776 100644 --- a/.forge-snapshots/CLPancakeSwapV4Test#test_v4ClSwap_ExactInSingle.snap +++ b/.forge-snapshots/CLPancakeSwapV4Test#test_v4ClSwap_ExactInSingle.snap @@ -1 +1 @@ -181709 \ No newline at end of file +181657 \ No newline at end of file diff --git a/.forge-snapshots/CLPancakeSwapV4Test#test_v4ClSwap_ExactIn_MultiHop.snap b/.forge-snapshots/CLPancakeSwapV4Test#test_v4ClSwap_ExactIn_MultiHop.snap index 24abf7c..a797307 100644 --- a/.forge-snapshots/CLPancakeSwapV4Test#test_v4ClSwap_ExactIn_MultiHop.snap +++ b/.forge-snapshots/CLPancakeSwapV4Test#test_v4ClSwap_ExactIn_MultiHop.snap @@ -1 +1 @@ -247024 \ No newline at end of file +246971 \ No newline at end of file diff --git a/.forge-snapshots/CLPancakeSwapV4Test#test_v4ClSwap_ExactIn_SingleHop.snap b/.forge-snapshots/CLPancakeSwapV4Test#test_v4ClSwap_ExactIn_SingleHop.snap index e43e64d..3495a44 100644 --- a/.forge-snapshots/CLPancakeSwapV4Test#test_v4ClSwap_ExactIn_SingleHop.snap +++ b/.forge-snapshots/CLPancakeSwapV4Test#test_v4ClSwap_ExactIn_SingleHop.snap @@ -1 +1 @@ -183250 \ No newline at end of file +183197 \ No newline at end of file diff --git a/.forge-snapshots/CLPancakeSwapV4Test#test_v4ClSwap_ExactOutSingle.snap b/.forge-snapshots/CLPancakeSwapV4Test#test_v4ClSwap_ExactOutSingle.snap index c73c044..385c3b7 100644 --- a/.forge-snapshots/CLPancakeSwapV4Test#test_v4ClSwap_ExactOutSingle.snap +++ b/.forge-snapshots/CLPancakeSwapV4Test#test_v4ClSwap_ExactOutSingle.snap @@ -1 +1 @@ -186048 \ No newline at end of file +185996 \ No newline at end of file diff --git a/.forge-snapshots/CLPancakeSwapV4Test#test_v4ClSwap_ExactOut_MultiHop.snap b/.forge-snapshots/CLPancakeSwapV4Test#test_v4ClSwap_ExactOut_MultiHop.snap index 55d6385..dd5fdb0 100644 --- a/.forge-snapshots/CLPancakeSwapV4Test#test_v4ClSwap_ExactOut_MultiHop.snap +++ b/.forge-snapshots/CLPancakeSwapV4Test#test_v4ClSwap_ExactOut_MultiHop.snap @@ -1 +1 @@ -250783 \ No newline at end of file +250730 \ No newline at end of file diff --git a/.forge-snapshots/CLPancakeSwapV4Test#test_v4ClSwap_ExactOut_SingleHop.snap b/.forge-snapshots/CLPancakeSwapV4Test#test_v4ClSwap_ExactOut_SingleHop.snap index 4077297..d710aea 100644 --- a/.forge-snapshots/CLPancakeSwapV4Test#test_v4ClSwap_ExactOut_SingleHop.snap +++ b/.forge-snapshots/CLPancakeSwapV4Test#test_v4ClSwap_ExactOut_SingleHop.snap @@ -1 +1 @@ -187582 \ No newline at end of file +187529 \ No newline at end of file diff --git a/.forge-snapshots/PancakeSwapV2Test#test_v2Swap_exactInput0For1.snap b/.forge-snapshots/PancakeSwapV2Test#test_v2Swap_exactInput0For1.snap index 90b45b4..dfed1e4 100644 --- a/.forge-snapshots/PancakeSwapV2Test#test_v2Swap_exactInput0For1.snap +++ b/.forge-snapshots/PancakeSwapV2Test#test_v2Swap_exactInput0For1.snap @@ -1 +1 @@ -100147 \ No newline at end of file +100125 \ No newline at end of file diff --git a/.forge-snapshots/PancakeSwapV2Test#test_v2Swap_exactOutput0For1.snap b/.forge-snapshots/PancakeSwapV2Test#test_v2Swap_exactOutput0For1.snap index b0ac48d..5f918e2 100644 --- a/.forge-snapshots/PancakeSwapV2Test#test_v2Swap_exactOutput0For1.snap +++ b/.forge-snapshots/PancakeSwapV2Test#test_v2Swap_exactOutput0For1.snap @@ -1 +1 @@ -117028 \ No newline at end of file +100754 \ No newline at end of file diff --git a/.forge-snapshots/PancakeSwapV3Test#test_v3Swap_ExactInput0For1.snap b/.forge-snapshots/PancakeSwapV3Test#test_v3Swap_ExactInput0For1.snap index 78f1ad0..3d7ea4e 100644 --- a/.forge-snapshots/PancakeSwapV3Test#test_v3Swap_ExactInput0For1.snap +++ b/.forge-snapshots/PancakeSwapV3Test#test_v3Swap_ExactInput0For1.snap @@ -1 +1 @@ -151799 \ No newline at end of file +151862 \ No newline at end of file diff --git a/.forge-snapshots/PancakeSwapV3Test#test_v3Swap_exactOutput0For1.snap b/.forge-snapshots/PancakeSwapV3Test#test_v3Swap_exactOutput0For1.snap index fd87bb3..73d7c6c 100644 --- a/.forge-snapshots/PancakeSwapV3Test#test_v3Swap_exactOutput0For1.snap +++ b/.forge-snapshots/PancakeSwapV3Test#test_v3Swap_exactOutput0For1.snap @@ -1 +1 @@ -152677 \ No newline at end of file +152659 \ No newline at end of file diff --git a/.forge-snapshots/StableSwapTest#test_stableSwap_ExactInput0For1.snap b/.forge-snapshots/StableSwapTest#test_stableSwap_ExactInput0For1.snap index cf74b44..0386c0a 100644 --- a/.forge-snapshots/StableSwapTest#test_stableSwap_ExactInput0For1.snap +++ b/.forge-snapshots/StableSwapTest#test_stableSwap_ExactInput0For1.snap @@ -1 +1 @@ -193217 \ No newline at end of file +193198 \ No newline at end of file diff --git a/.forge-snapshots/StableSwapTest#test_stableSwap_ExactInput1For0.snap b/.forge-snapshots/StableSwapTest#test_stableSwap_ExactInput1For0.snap index 0055f2d..c823d60 100644 --- a/.forge-snapshots/StableSwapTest#test_stableSwap_ExactInput1For0.snap +++ b/.forge-snapshots/StableSwapTest#test_stableSwap_ExactInput1For0.snap @@ -1 +1 @@ -192673 \ No newline at end of file +192654 \ No newline at end of file diff --git a/.forge-snapshots/UniversalRouterBytecodeSize.snap b/.forge-snapshots/UniversalRouterBytecodeSize.snap index c522624..484ebbf 100644 --- a/.forge-snapshots/UniversalRouterBytecodeSize.snap +++ b/.forge-snapshots/UniversalRouterBytecodeSize.snap @@ -1 +1 @@ -24025 \ No newline at end of file +24160 \ No newline at end of file diff --git a/.forge-snapshots/UniversalRouterTest#test_sweep_token.snap b/.forge-snapshots/UniversalRouterTest#test_sweep_token.snap index 492b3fb..63f357d 100644 --- a/.forge-snapshots/UniversalRouterTest#test_sweep_token.snap +++ b/.forge-snapshots/UniversalRouterTest#test_sweep_token.snap @@ -1 +1 @@ -55442 \ No newline at end of file +55441 \ No newline at end of file diff --git a/.forge-snapshots/V3ToV4MigrationNativeTest#test_v4CLPositionmanager_Mint_Native.snap b/.forge-snapshots/V3ToV4MigrationNativeTest#test_v4CLPositionmanager_Mint_Native.snap index c6eae9f..8a3f431 100644 --- a/.forge-snapshots/V3ToV4MigrationNativeTest#test_v4CLPositionmanager_Mint_Native.snap +++ b/.forge-snapshots/V3ToV4MigrationNativeTest#test_v4CLPositionmanager_Mint_Native.snap @@ -1 +1 @@ -559387 \ No newline at end of file +559373 \ No newline at end of file diff --git a/.forge-snapshots/V3ToV4MigrationTest#test_v3PositionManager_burn.snap b/.forge-snapshots/V3ToV4MigrationTest#test_v3PositionManager_burn.snap index 7dce0ea..0b72187 100644 --- a/.forge-snapshots/V3ToV4MigrationTest#test_v3PositionManager_burn.snap +++ b/.forge-snapshots/V3ToV4MigrationTest#test_v3PositionManager_burn.snap @@ -1 +1 @@ -291669 \ No newline at end of file +291590 \ No newline at end of file diff --git a/.forge-snapshots/V3ToV4MigrationTest#test_v4BinPositionmanager_BinAddLiquidity.snap b/.forge-snapshots/V3ToV4MigrationTest#test_v4BinPositionmanager_BinAddLiquidity.snap index e1d204b..ad6e1a7 100644 --- a/.forge-snapshots/V3ToV4MigrationTest#test_v4BinPositionmanager_BinAddLiquidity.snap +++ b/.forge-snapshots/V3ToV4MigrationTest#test_v4BinPositionmanager_BinAddLiquidity.snap @@ -1 +1 @@ -594200 \ No newline at end of file +594191 \ No newline at end of file diff --git a/.forge-snapshots/V3ToV4MigrationTest#test_v4BinPositionmanager_BinAddLiquidity_Native.snap b/.forge-snapshots/V3ToV4MigrationTest#test_v4BinPositionmanager_BinAddLiquidity_Native.snap index fea99dc..23aaec4 100644 --- a/.forge-snapshots/V3ToV4MigrationTest#test_v4BinPositionmanager_BinAddLiquidity_Native.snap +++ b/.forge-snapshots/V3ToV4MigrationTest#test_v4BinPositionmanager_BinAddLiquidity_Native.snap @@ -1 +1 @@ -570042 \ No newline at end of file +570028 \ No newline at end of file diff --git a/.forge-snapshots/V3ToV4MigrationTest#test_v4CLPositionmanager_Mint.snap b/.forge-snapshots/V3ToV4MigrationTest#test_v4CLPositionmanager_Mint.snap index 8a2650e..4613764 100644 --- a/.forge-snapshots/V3ToV4MigrationTest#test_v4CLPositionmanager_Mint.snap +++ b/.forge-snapshots/V3ToV4MigrationTest#test_v4CLPositionmanager_Mint.snap @@ -1 +1 @@ -583553 \ No newline at end of file +583544 \ No newline at end of file diff --git a/src/base/Dispatcher.sol b/src/base/Dispatcher.sol index 9fbfb6f..5f7a035 100755 --- a/src/base/Dispatcher.sol +++ b/src/base/Dispatcher.sol @@ -81,6 +81,7 @@ abstract contract Dispatcher is bytes calldata path = inputs.toBytes(3); address payer = payerIsUser ? msgSender() : address(this); v3SwapExactInput(map(recipient), amountIn, amountOutMin, path, payer); + return (success, output); } else if (command == Commands.V3_SWAP_EXACT_OUT) { // equivalent: abi.decode(inputs, (address, uint256, uint256, bytes, bool)) address recipient; @@ -97,6 +98,7 @@ abstract contract Dispatcher is bytes calldata path = inputs.toBytes(3); address payer = payerIsUser ? msgSender() : address(this); v3SwapExactOutput(map(recipient), amountOut, amountInMax, path, payer); + return (success, output); } else if (command == Commands.PERMIT2_TRANSFER_FROM) { // equivalent: abi.decode(inputs, (address, address, uint160)) address token; @@ -108,6 +110,7 @@ abstract contract Dispatcher is amount := calldataload(add(inputs.offset, 0x40)) } permit2TransferFrom(token, msgSender(), map(recipient), amount); + return (success, output); } else if (command == Commands.PERMIT2_PERMIT_BATCH) { IAllowanceTransfer.PermitBatch calldata permitBatch; assembly { @@ -116,7 +119,12 @@ abstract contract Dispatcher is permitBatch := add(inputs.offset, calldataload(inputs.offset)) } bytes calldata data = inputs.toBytes(1); - PERMIT2.permit(msgSender(), permitBatch, data); + try PERMIT2.permit(msgSender(), permitBatch, data) {} + catch (bytes memory reason) { + output = reason; + success = false; + } + return (success, output); } else if (command == Commands.SWEEP) { // equivalent: abi.decode(inputs, (address, address, uint256)) address token; @@ -128,6 +136,7 @@ abstract contract Dispatcher is amountMin := calldataload(add(inputs.offset, 0x40)) } Payments.sweep(token, map(recipient), amountMin); + return (success, output); } else if (command == Commands.TRANSFER) { // equivalent: abi.decode(inputs, (address, address, uint256)) address token; @@ -139,6 +148,7 @@ abstract contract Dispatcher is value := calldataload(add(inputs.offset, 0x40)) } Payments.pay(token, map(recipient), value); + return (success, output); } else if (command == Commands.PAY_PORTION) { // equivalent: abi.decode(inputs, (address, address, uint256)) address token; @@ -150,6 +160,7 @@ abstract contract Dispatcher is bips := calldataload(add(inputs.offset, 0x40)) } Payments.payPortion(token, map(recipient), bips); + return (success, output); } else { // placeholder area for command 0x07 revert InvalidCommandType(command); @@ -172,6 +183,7 @@ abstract contract Dispatcher is address[] calldata path = inputs.toAddressArray(3); address payer = payerIsUser ? msgSender() : address(this); v2SwapExactInput(map(recipient), amountIn, amountOutMin, path, payer); + return (success, output); } else if (command == Commands.V2_SWAP_EXACT_OUT) { // equivalent: abi.decode(inputs, (address, uint256, uint256, bytes, bool)) address recipient; @@ -188,6 +200,7 @@ abstract contract Dispatcher is address[] calldata path = inputs.toAddressArray(3); address payer = payerIsUser ? msgSender() : address(this); v2SwapExactOutput(map(recipient), amountOut, amountInMax, path, payer); + return (success, output); } else if (command == Commands.PERMIT2_PERMIT) { // equivalent: abi.decode(inputs, (IAllowanceTransfer.PermitSingle, bytes)) IAllowanceTransfer.PermitSingle calldata permitSingle; @@ -195,7 +208,12 @@ abstract contract Dispatcher is permitSingle := inputs.offset } bytes calldata data = inputs.toBytes(6); // PermitSingle takes first 6 slots (0..5) - PERMIT2.permit(msgSender(), permitSingle, data); + try PERMIT2.permit(msgSender(), permitSingle, data) {} + catch (bytes memory reason) { + output = reason; + success = false; + } + return (success, output); } else if (command == Commands.WRAP_ETH) { // equivalent: abi.decode(inputs, (address, uint256)) address recipient; @@ -205,6 +223,7 @@ abstract contract Dispatcher is amount := calldataload(add(inputs.offset, 0x20)) } Payments.wrapETH(map(recipient), amount); + return (success, output); } else if (command == Commands.UNWRAP_WETH) { // equivalent: abi.decode(inputs, (address, uint256)) address recipient; @@ -214,6 +233,7 @@ abstract contract Dispatcher is amountMin := calldataload(add(inputs.offset, 0x20)) } Payments.unwrapWETH9(map(recipient), amountMin); + return (success, output); } else if (command == Commands.PERMIT2_TRANSFER_FROM_BATCH) { IAllowanceTransfer.AllowanceTransferDetails[] calldata batchDetails; (uint256 length, uint256 offset) = inputs.toLengthOffset(0); @@ -222,6 +242,7 @@ abstract contract Dispatcher is batchDetails.offset := offset } permit2TransferFrom(batchDetails, msgSender()); + return (success, output); } else if (command == Commands.BALANCE_CHECK_ERC20) { // equivalent: abi.decode(inputs, (address, address, uint256)) address owner; @@ -234,6 +255,7 @@ abstract contract Dispatcher is } success = (ERC20(token).balanceOf(owner) >= minBalance); if (!success) output = abi.encodePacked(BalanceTooLow.selector); + return (success, output); } else { // placeholder area for command 0x0f revert InvalidCommandType(command); @@ -244,6 +266,7 @@ abstract contract Dispatcher is if (command == Commands.V4_SWAP) { // pass the calldata provided to V4SwapRouter._executeActions (defined in BaseActionsRouter) _executeActions(inputs); + return (success, output); // This contract MUST be approved to spend the token since its going to be doing the call on the position manager } else if (command == Commands.V3_POSITION_MANAGER_PERMIT) { bytes4 selector; @@ -255,6 +278,7 @@ abstract contract Dispatcher is } (success, output) = address(V3_POSITION_MANAGER).call(inputs); + return (success, output); } else if (command == Commands.V3_POSITION_MANAGER_CALL) { bytes4 selector; assembly { @@ -279,14 +303,17 @@ abstract contract Dispatcher is } (success, output) = address(V3_POSITION_MANAGER).call(inputs); + return (success, output); } else if (command == Commands.V4_CL_POSITION_CALL) { // should only call modifyLiquidities() with Actions.CL_MINT_POSITION // do not permit or approve this contract over a v4 position or someone could use this command to decrease, burn, or transfer your position (success, output) = address(V4_CL_POSITION_MANAGER).call{value: address(this).balance}(inputs); + return (success, output); } else if (command == Commands.V4_BIN_POSITION_CALL) { // should only call modifyLiquidities() with Actions.BIN_ADD_LIQUIDITY // do not permit or approve this contract over a v4 position or someone could use this command to decrease, burn, or transfer your position (success, output) = address(V4_BIN_POSITION_MANAGER).call{value: address(this).balance}(inputs); + return (success, output); } else { // placeholder area for commands 0x15-0x20 revert InvalidCommandType(command); @@ -297,6 +324,7 @@ abstract contract Dispatcher is if (command == Commands.EXECUTE_SUB_PLAN) { (bytes calldata _commands, bytes[] calldata _inputs) = inputs.decodeCommandsAndInputs(); (success, output) = (address(this)).call(abi.encodeCall(Dispatcher.execute, (_commands, _inputs))); + return (success, output); } else if (command == Commands.STABLE_SWAP_EXACT_IN) { // equivalent: abi.decode(inputs, (address, uint256, uint256, bytes, bytes, bool)) address recipient; @@ -314,6 +342,7 @@ abstract contract Dispatcher is uint256[] calldata flag = inputs.toUintArray(4); address payer = payerIsUser ? msgSender() : address(this); stableSwapExactInput(map(recipient), amountIn, amountOutMin, path, flag, payer); + return (success, output); } else if (command == Commands.STABLE_SWAP_EXACT_OUT) { // equivalent: abi.decode(inputs, (address, uint256, uint256, bytes, bytes, bool)) address recipient; @@ -334,6 +363,7 @@ abstract contract Dispatcher is /// @dev structured this way as stack too deep by Yul uint256 amountIn = stableSwapExactOutputAmountIn(amountOut, amountInMax, path, flag); stableSwapExactOutput(map(recipient), amountIn, amountOut, path, flag, payer); + return (success, output); } else { // placeholder area for commands 0x24-0x3f revert InvalidCommandType(command); diff --git a/test/UniversalRouter.t.sol b/test/UniversalRouter.t.sol index fb86d11..300de47 100644 --- a/test/UniversalRouter.t.sol +++ b/test/UniversalRouter.t.sol @@ -6,7 +6,9 @@ import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; import {ERC20} from "solmate/src/tokens/ERC20.sol"; import {IERC165} from "@openzeppelin/contracts/interfaces/IERC165.sol"; import {ActionConstants} from "pancake-v4-periphery/src/libraries/ActionConstants.sol"; +import {Permit2SignatureHelpers} from "pancake-v4-periphery/test/shared/Permit2SignatureHelpers.sol"; import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol"; +import {DeployPermit2} from "permit2/test/utils/DeployPermit2.sol"; import {WETH} from "solmate/src/tokens/WETH.sol"; import {UniversalRouter} from "../src/UniversalRouter.sol"; @@ -19,21 +21,26 @@ import {MockERC721} from "./mock/MockERC721.sol"; import {MockERC1155} from "./mock/MockERC1155.sol"; import {RouterParameters} from "../src/base/RouterImmutables.sol"; -contract UniversalRouterTest is Test, GasSnapshot { +contract UniversalRouterTest is Test, GasSnapshot, Permit2SignatureHelpers, DeployPermit2 { error ContractSizeTooLarge(uint256 diff); + error InvalidNonce(); address RECIPIENT = makeAddr("alice"); uint256 constant AMOUNT = 10 ** 18; UniversalRouter router; MockERC20 erc20; + MockERC20 erc20_2; MockERC721 erc721; MockERC1155 erc1155; WETH weth9 = new WETH(); + IAllowanceTransfer permit2; function setUp() public { + permit2 = IAllowanceTransfer(deployPermit2()); + RouterParameters memory params = RouterParameters({ - permit2: address(0), + permit2: address(permit2), weth9: address(weth9), v2Factory: address(0), v3Factory: address(0), @@ -53,6 +60,7 @@ contract UniversalRouterTest is Test, GasSnapshot { router = new UniversalRouter(params); erc20 = new MockERC20(); + erc20_2 = new MockERC20(); erc721 = new MockERC721(); erc1155 = new MockERC1155(); } @@ -197,4 +205,199 @@ contract UniversalRouterTest is Test, GasSnapshot { vm.expectRevert(Payments.InsufficientETH.selector); router.execute{value: 1 ether}(commands, inputs); } + + function test_permit2Single() public { + // pre-req: + (address charlie, uint256 charliePK) = makeAddrAndKey("charlie"); + uint160 permitAmount = type(uint160).max; + uint48 permitExpiration = uint48(block.timestamp + 10e18); + uint48 permitNonce = 0; + + IAllowanceTransfer.PermitSingle memory permit = + defaultERC20PermitAllowance(address(erc20), permitAmount, permitExpiration, permitNonce); + permit.spender = address(router); + bytes memory sig = getPermitSignature(permit, charliePK, permit2.DOMAIN_SEPARATOR()); + + // before verify + (uint160 _amount, uint48 _expiration, uint48 _nonce) = + permit2.allowance(charlie, address(erc20), address(router)); + assertEq(_amount, 0); + assertEq(_expiration, 0); + assertEq(_nonce, 0); + + // execute + vm.startPrank(charlie); + bytes memory commands = abi.encodePacked(bytes1(uint8(Commands.PERMIT2_PERMIT))); + bytes[] memory inputs = new bytes[](1); + inputs[0] = abi.encode(permit, sig); + router.execute(commands, inputs); + + // after verify + (_amount, _expiration, _nonce) = permit2.allowance(charlie, address(erc20), address(router)); + assertEq(_amount, permitAmount); + assertEq(_expiration, permitExpiration); + assertEq(_nonce, permitNonce + 1); + } + + function test_permit2Batch() public { + // pre-req: + (address charlie, uint256 charliePK) = makeAddrAndKey("charlie"); + uint160 permitAmount = type(uint160).max; + uint48 permitExpiration = uint48(block.timestamp + 10e18); + uint48 permitNonce = 0; + + address[] memory tokens = new address[](2); + tokens[0] = address(erc20); + tokens[1] = address(erc20_2); + + IAllowanceTransfer.PermitBatch memory permit = + defaultERC20PermitBatchAllowance(tokens, permitAmount, permitExpiration, permitNonce); + permit.spender = address(router); + bytes memory sig = getPermitBatchSignature(permit, charliePK, permit2.DOMAIN_SEPARATOR()); + + // before verify + for (uint256 i; i < tokens.length; i++) { + (uint160 _amount, uint48 _expiration, uint48 _nonce) = + permit2.allowance(charlie, tokens[i], address(router)); + assertEq(_amount, 0); + assertEq(_expiration, 0); + assertEq(_nonce, 0); + } + + // execute + vm.startPrank(charlie); + bytes memory commands = abi.encodePacked(bytes1(uint8(Commands.PERMIT2_PERMIT_BATCH))); + bytes[] memory inputs = new bytes[](1); + inputs[0] = abi.encode(permit, sig); + router.execute(commands, inputs); + + // after verify + for (uint256 i; i < tokens.length; i++) { + (uint160 _amount, uint48 _expiration, uint48 _nonce) = + permit2.allowance(charlie, address(tokens[i]), address(router)); + assertEq(_amount, permitAmount); + assertEq(_expiration, permitExpiration); + assertEq(_nonce, permitNonce + 1); + } + } + + /// @dev test showing that if permit command have ALLOW_REVERT flag and was front-run, the next command can still execute + function test_permit2Single_frontrun() public { + // pre-req + address bob = makeAddr("bob"); + (address charlie, uint256 charliePK) = makeAddrAndKey("charlie"); + uint160 permitAmount = type(uint160).max; + uint48 permitExpiration = uint48(block.timestamp + 10e18); + uint48 permitNonce = 0; + + IAllowanceTransfer.PermitSingle memory permit = + defaultERC20PermitAllowance(address(erc20), permitAmount, permitExpiration, permitNonce); + permit.spender = address(router); + bytes memory sig = getPermitSignature(permit, charliePK, permit2.DOMAIN_SEPARATOR()); + + // bob front-runs the permits + vm.prank(bob); + permit2.permit(charlie, permit, sig); + + // bob's front-run was successful + (uint160 _amount, uint48 _expiration, uint48 _nonce) = + permit2.allowance(charlie, address(erc20), address(router)); + assertEq(_amount, permitAmount); + assertEq(_expiration, permitExpiration); + assertEq(_nonce, permitNonce + 1); + + // before + assertEq(weth9.balanceOf(address(router)), 0); + + // charlie tries to call universal router permit2_permit and wrap_eth command + vm.deal(charlie, 1 ether); + vm.startPrank(charlie); + + bytes[] memory inputs = new bytes[](2); + inputs[0] = abi.encode(permit, sig); + inputs[1] = abi.encode(ActionConstants.ADDRESS_THIS, ActionConstants.CONTRACT_BALANCE); + + bytes memory commands; + + // attempt 1: execute and expect revert + commands = abi.encodePacked(bytes1(uint8(Commands.PERMIT2_PERMIT)), bytes1(uint8(Commands.WRAP_ETH))); + vm.expectRevert( + abi.encodeWithSelector( + IUniversalRouter.ExecutionFailed.selector, 0, abi.encodePacked(InvalidNonce.selector) + ) + ); + router.execute{value: 1 ether}(commands, inputs); + + // attempt 2: execute with allow revert flag and no revert expected + commands = abi.encodePacked( + bytes1(uint8(Commands.PERMIT2_PERMIT)) | Commands.FLAG_ALLOW_REVERT, bytes1(uint8(Commands.WRAP_ETH)) + ); + router.execute{value: 1 ether}(commands, inputs); + + // after + assertEq(weth9.balanceOf(address(router)), 1 ether); + } + + /// @dev test showing that if permit command have ALLOW_REVERT flag and was front-run, the next command can still execute + function test_permit2Batch_frontrun() public { + // pre-req + address bob = makeAddr("bob"); + (address charlie, uint256 charliePK) = makeAddrAndKey("charlie"); + uint160 permitAmount = type(uint160).max; + uint48 permitExpiration = uint48(block.timestamp + 10e18); + uint48 permitNonce = 0; + + address[] memory tokens = new address[](2); + tokens[0] = address(erc20); + tokens[1] = address(erc20_2); + + IAllowanceTransfer.PermitBatch memory permit = + defaultERC20PermitBatchAllowance(tokens, permitAmount, permitExpiration, permitNonce); + permit.spender = address(router); + bytes memory sig = getPermitBatchSignature(permit, charliePK, permit2.DOMAIN_SEPARATOR()); + + // bob front-runs the permits + vm.prank(bob); + permit2.permit(charlie, permit, sig); + + // bob's front-run was successful + for (uint256 i; i < tokens.length; i++) { + (uint160 _amount, uint48 _expiration, uint48 _nonce) = + permit2.allowance(charlie, address(tokens[i]), address(router)); + assertEq(_amount, permitAmount); + assertEq(_expiration, permitExpiration); + assertEq(_nonce, permitNonce + 1); + } + + // before + assertEq(weth9.balanceOf(address(router)), 0); + + // charlie tries to call universal router permit2_permit and wrap_eth command + vm.deal(charlie, 1 ether); + vm.startPrank(charlie); + + bytes[] memory inputs = new bytes[](2); + inputs[0] = abi.encode(permit, sig); + inputs[1] = abi.encode(ActionConstants.ADDRESS_THIS, ActionConstants.CONTRACT_BALANCE); + + bytes memory commands; + + // attempt 1: execute and expect revert + commands = abi.encodePacked(bytes1(uint8(Commands.PERMIT2_PERMIT_BATCH)), bytes1(uint8(Commands.WRAP_ETH))); + vm.expectRevert( + abi.encodeWithSelector( + IUniversalRouter.ExecutionFailed.selector, 0, abi.encodePacked(InvalidNonce.selector) + ) + ); + router.execute{value: 1 ether}(commands, inputs); + + // attempt 2: execute with allow revert flag and no revert expected + commands = abi.encodePacked( + bytes1(uint8(Commands.PERMIT2_PERMIT_BATCH)) | Commands.FLAG_ALLOW_REVERT, bytes1(uint8(Commands.WRAP_ETH)) + ); + router.execute{value: 1 ether}(commands, inputs); + + // after + assertEq(weth9.balanceOf(address(router)), 1 ether); + } }