diff --git a/foundry/interfaces/IUniversalRouter.sol b/foundry/interfaces/IUniversalRouter.sol new file mode 100644 index 0000000..472e579 --- /dev/null +++ b/foundry/interfaces/IUniversalRouter.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.24; + +interface IUniversalRouter { + /// @notice Thrown when a required command has failed + error ExecutionFailed(uint256 commandIndex, bytes message); + + /// @notice Thrown when attempting to send ETH directly to the contract + error ETHNotAccepted(); + + /// @notice Thrown when executing commands with an expired deadline + error TransactionDeadlinePassed(); + + /// @notice Thrown when attempting to execute commands and an incorrect number of inputs are provided + error LengthMismatch(); + + // @notice Thrown when an address that isn't WETH tries to send ETH to the router without calldata + error InvalidEthSender(); + + /// @notice Executes encoded commands along with provided inputs. Reverts if deadline has expired. + /// @param commands A set of concatenated commands, each 1 byte in length + /// @param inputs An array of byte strings containing abi encoded inputs for each command + /// @param deadline The deadline by which the transaction must be executed + function execute(bytes calldata commands, bytes[] calldata inputs, uint256 deadline) external payable; +} \ No newline at end of file diff --git a/foundry/test/Constants.sol b/foundry/test/Constants.sol index 67b0f29..6a0eb01 100644 --- a/foundry/test/Constants.sol +++ b/foundry/test/Constants.sol @@ -7,7 +7,7 @@ contract BaseConstants { address BASE_USDC = 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913; address BASE_MAG7 = 0x9E6A46f294bB67c20F1D1E7AfB0bBEf614403B55; - // uniswap v2 + // Uniswap v2 address USDC_MAG7_POOL = 0x739c2431670A12E2cF8e11E3603eB96e6728a789; } @@ -40,17 +40,23 @@ contract Constants is Test, BaseConstants { address USDT_ADDR = address(0xdAC17F958D2ee523a2206206994597C13D831ec7); address PEPE_ADDR = address(0x6982508145454Ce325dDbE47a25d4ec3d2311933); - // uniswap v2 + // Uniswap v2 address WETH_DAI_POOL = 0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11; address DAI_USDC_POOL = 0xAE461cA67B15dc8dc81CE7615e0320dA1A9aB8D5; address WETH_WBTC_POOL = 0xBb2b8038a1640196FbE3e38816F3e67Cba72D940; address USDC_WBTC_POOL = 0x004375Dff511095CC5A197A54140a24eFEF3A416; - // uniswap v3 + // Uniswap v3 address USV3_FACTORY_ETHEREUM = 0x1F98431c8aD98523631AE4a59f267346ea31F984; address USV2_FACTORY_ETHEREUM = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f; address DAI_WETH_USV3 = 0xC2e9F25Be6257c210d7Adf0D4Cd6E3E881ba25f8; + // Uniswap universal router + address UNIVERSAL_ROUTER = 0x66a9893cC07D91D95644AEDD05D03f95e1dBA8Af; + + // Permit2 + address PERMIT2_ADDRESS = 0x000000000022D473030F116dDEE9F6B43aC78BA3; + /** * @dev Deploys a dummy contract with non-empty bytecode */ diff --git a/foundry/test/GasTest.t.sol b/foundry/test/GasTest.t.sol new file mode 100644 index 0000000..2bf9bdd --- /dev/null +++ b/foundry/test/GasTest.t.sol @@ -0,0 +1,232 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.24; + +import {IUniversalRouter} from "../interfaces/IUniversalRouter.sol"; +import {IPermit2} from "../lib/permit2/src/interfaces/IPermit2.sol"; +import {Constants} from "./Constants.sol"; +import {Actions} from "../lib/v4-periphery/src/libraries/Actions.sol"; +import {PoolKey} from "../lib/v4-core/src/types/PoolKey.sol"; +import {IV4Router} from "../lib/v4-periphery/src/interfaces/IV4Router.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {Currency} from "../lib/v4-core/src/types/Currency.sol"; +import {IHooks} from "../lib/v4-core/src/interfaces/IHooks.sol"; +import "forge-std/Test.sol"; +import "@permit2/src/interfaces/IAllowanceTransfer.sol"; + +contract Commands { + uint256 constant V2_SWAP_EXACT_IN = 0x08; + uint256 constant V3_SWAP_EXACT_IN = 0x00; + uint256 constant V4_SWAP = 0x10; + uint256 constant PERMIT2_PERMIT = 0x0a; +} + +// A gas test to compare the gas usage of the UniversalRouter with the TychoRouter + +contract GasTest is Commands, Test, Constants { + IUniversalRouter universalRouter = IUniversalRouter(UNIVERSAL_ROUTER); + IPermit2 permit2 = IPermit2(PERMIT2_ADDRESS); + + function setUp() public { + uint256 forkBlock = 21817316; + vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock); + } + + function testUniversalRouterUniswapV2() public { + bool isPermit2 = false; + uint256 amountIn = 10 ** 18; + + bytes memory commands = + abi.encodePacked(uint8(Commands.V2_SWAP_EXACT_IN)); + + address[] memory path = new address[](2); + path[0] = WETH_ADDR; + path[1] = DAI_ADDR; + + bytes[] memory inputs = new bytes[](1); + inputs[0] = abi.encode(BOB, amountIn, uint256(0), path, isPermit2); + + deal(WETH_ADDR, address(universalRouter), amountIn); + universalRouter.execute(commands, inputs, block.timestamp + 1000); + } + + function testUniversalRouterUniswapV2Permit2() public { + bool isPermit2 = true; + uint256 amountIn = 10 ** 18; + + bytes memory commands = abi.encodePacked( + uint8(Commands.PERMIT2_PERMIT), uint8(Commands.V2_SWAP_EXACT_IN) + ); + + vm.startPrank(ALICE); + ( + IAllowanceTransfer.PermitSingle memory permitSingle, + bytes memory signature + ) = handlePermit2Approval(WETH_ADDR, amountIn); + + address[] memory path = new address[](2); + path[0] = WETH_ADDR; + path[1] = DAI_ADDR; + + bytes[] memory inputs = new bytes[](2); + inputs[0] = abi.encode(permitSingle, signature); + inputs[1] = abi.encode(ALICE, amountIn, uint256(0), path, isPermit2); + + deal(WETH_ADDR, ALICE, amountIn); + + universalRouter.execute(commands, inputs, block.timestamp + 1000); + } + + function testUniversalRouterUniswapV3() public { + bool isPermit2 = false; + uint256 amountIn = 10 ** 18; + + bytes memory commands = + abi.encodePacked(uint8(Commands.V3_SWAP_EXACT_IN)); + + uint24 poolFee = 3000; + bytes memory path = abi.encodePacked(WETH_ADDR, poolFee, DAI_ADDR); + + bytes[] memory inputs = new bytes[](1); + inputs[0] = abi.encode(BOB, amountIn, uint256(0), path, isPermit2); + + deal(WETH_ADDR, address(universalRouter), amountIn); + universalRouter.execute(commands, inputs, block.timestamp + 1000); + } + + function testUniversalRouterUniswapV3Permit2() public { + bool isPermit2 = true; + uint256 amountIn = 10 ** 18; + + bytes memory commands = abi.encodePacked( + uint8(Commands.PERMIT2_PERMIT), uint8(Commands.V3_SWAP_EXACT_IN) + ); + + vm.startPrank(ALICE); + ( + IAllowanceTransfer.PermitSingle memory permitSingle, + bytes memory signature + ) = handlePermit2Approval(WETH_ADDR, amountIn); + + uint24 poolFee = 3000; + bytes memory path = abi.encodePacked(WETH_ADDR, poolFee, DAI_ADDR); + + bytes[] memory inputs = new bytes[](2); + inputs[0] = abi.encode(permitSingle, signature); + inputs[1] = abi.encode(ALICE, amountIn, uint256(0), path, isPermit2); + + deal(WETH_ADDR, ALICE, amountIn); + + universalRouter.execute(commands, inputs, block.timestamp + 1000); + } + + function testUniversalRouterUniswapV4Permit2() public { + uint128 amountIn = uint128(100 ether); + uint128 amountOutMinimum = uint128(0); + uint256 deadline = block.timestamp + 1000; + + bytes memory commands = abi.encodePacked( + uint8(Commands.PERMIT2_PERMIT), uint8(Commands.V4_SWAP) + ); + + bytes memory actions = abi.encodePacked( + uint8(Actions.SWAP_EXACT_IN_SINGLE), + uint8(Actions.SETTLE_ALL), + uint8(Actions.TAKE_ALL) + ); + + vm.startPrank(ALICE); + ( + IAllowanceTransfer.PermitSingle memory permitSingle, + bytes memory signature + ) = handlePermit2Approval(USDE_ADDR, amountIn); + + PoolKey memory key = PoolKey({ + currency0: Currency.wrap(USDE_ADDR), + currency1: Currency.wrap(USDT_ADDR), + fee: 100, + tickSpacing: int24(1), + hooks: IHooks(address(0)) + }); + + bytes[] memory params = new bytes[](3); + params[0] = abi.encode( + IV4Router.ExactInputSingleParams({ + poolKey: key, + zeroForOne: true, + amountIn: amountIn, + amountOutMinimum: amountOutMinimum, + hookData: bytes("") + }) + ); + + params[1] = abi.encode(key.currency0, amountIn); + params[2] = abi.encode(key.currency1, amountOutMinimum); + + bytes[] memory inputs = new bytes[](2); + inputs[0] = abi.encode(permitSingle, signature); + inputs[1] = abi.encode(actions, params); + + deal(USDE_ADDR, ALICE, amountIn); + + universalRouter.execute(commands, inputs, deadline); + } + + function handlePermit2Approval(address tokenIn, uint256 amount_in) + internal + returns (IAllowanceTransfer.PermitSingle memory, bytes memory) + { + IERC20(tokenIn).approve(PERMIT2_ADDRESS, amount_in); + IAllowanceTransfer.PermitSingle memory permitSingle = IAllowanceTransfer + .PermitSingle({ + details: IAllowanceTransfer.PermitDetails({ + token: tokenIn, + amount: uint160(amount_in), + expiration: uint48(block.timestamp + 1 days), + nonce: 0 + }), + spender: UNIVERSAL_ROUTER, + sigDeadline: block.timestamp + 1 days + }); + + bytes memory signature = signPermit2(permitSingle, ALICE_PK); + return (permitSingle, signature); + } + + function signPermit2( + IAllowanceTransfer.PermitSingle memory permit, + uint256 privateKey + ) internal view returns (bytes memory) { + bytes32 _PERMIT_DETAILS_TYPEHASH = keccak256( + "PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)" + ); + bytes32 _PERMIT_SINGLE_TYPEHASH = keccak256( + "PermitSingle(PermitDetails details,address spender,uint256 sigDeadline)PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)" + ); + bytes32 domainSeparator = keccak256( + abi.encode( + keccak256( + "EIP712Domain(string name,uint256 chainId,address verifyingContract)" + ), + keccak256("Permit2"), + block.chainid, + PERMIT2_ADDRESS + ) + ); + bytes32 detailsHash = + keccak256(abi.encode(_PERMIT_DETAILS_TYPEHASH, permit.details)); + bytes32 permitHash = keccak256( + abi.encode( + _PERMIT_SINGLE_TYPEHASH, + detailsHash, + permit.spender, + permit.sigDeadline + ) + ); + + bytes32 digest = + keccak256(abi.encodePacked("\x19\x01", domainSeparator, permitHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest); + + return abi.encodePacked(r, s, v); + } +} diff --git a/foundry/test/TychoRouter.t.sol b/foundry/test/TychoRouter.t.sol index 08a069e..571a060 100644 --- a/foundry/test/TychoRouter.t.sol +++ b/foundry/test/TychoRouter.t.sol @@ -231,6 +231,50 @@ contract TychoRouterTest is TychoRouterTestSetup { assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0); } + function testSwapSimplePermit2() public { + // Trade 1 WETH for DAI with 1 swap on Uniswap V2 using Permit2 + // 1 WETH -> DAI + // (USV2) + vm.startPrank(ALICE); + + uint256 amountIn = 1 ether; + deal(WETH_ADDR, ALICE, amountIn); + ( + IAllowanceTransfer.PermitSingle memory permitSingle, + bytes memory signature + ) = handlePermit2Approval(WETH_ADDR, amountIn); + + bytes memory protocolData = encodeUniswapV2Swap( + WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false + ); + + bytes memory swap = encodeSwap( + uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData + ); + bytes[] memory swaps = new bytes[](1); + swaps[0] = swap; + + tychoRouter.swapPermit2( + amountIn, + WETH_ADDR, + DAI_ADDR, + 0, + false, + false, + 2, + ALICE, + permitSingle, + signature, + pleEncode(swaps) + ); + + uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(ALICE); + assertEq(daiBalance, 2659881924818443699787); + assertEq(IERC20(WETH_ADDR).balanceOf(ALICE), 0); + + vm.stopPrank(); + } + function testSwapMultipleHops() public { // Trade 1 WETH for USDC through DAI with 2 swaps on Uniswap V2 // 1 WETH -> DAI -> USDC @@ -633,6 +677,50 @@ contract TychoRouterTest is TychoRouterTestSetup { assertGe(finalBalance, expAmountOut); } + function testSwapSingleUSV3Permit2() public { + // Trade 1 WETH for DAI with 1 swap on Uniswap V3 using Permit2 + // 1 WETH -> DAI + // (USV3) + vm.startPrank(ALICE); + uint256 amountIn = 10 ** 18; + deal(WETH_ADDR, ALICE, amountIn); + ( + IAllowanceTransfer.PermitSingle memory permitSingle, + bytes memory signature + ) = handlePermit2Approval(WETH_ADDR, amountIn); + + uint256 expAmountOut = 1205_128428842122129186; //Swap 1 WETH for 1205.12 DAI + bool zeroForOne = false; + bytes memory protocolData = encodeUniswapV3Swap( + WETH_ADDR, DAI_ADDR, tychoRouterAddr, DAI_WETH_USV3, zeroForOne + ); + bytes memory swap = encodeSwap( + uint8(0), uint8(1), uint24(0), address(usv3Executor), protocolData + ); + + bytes[] memory swaps = new bytes[](1); + swaps[0] = swap; + + tychoRouter.swapPermit2( + amountIn, + WETH_ADDR, + DAI_ADDR, + 0, + false, + false, + 2, + ALICE, + permitSingle, + signature, + pleEncode(swaps) + ); + + uint256 finalBalance = IERC20(DAI_ADDR).balanceOf(ALICE); + assertGe(finalBalance, expAmountOut); + + vm.stopPrank(); + } + function testEmptySwapsRevert() public { uint256 amountIn = 10 ** 18; bytes memory swaps = ""; @@ -650,7 +738,7 @@ contract TychoRouterTest is TychoRouterTestSetup { // Approve permit2 vm.startPrank(ALICE); - IERC20(WETH_ADDR).approve(address(permit2Address), type(uint256).max); + IERC20(WETH_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max); // Encoded solution generated using `test_split_swap_strategy_encoder_simple` // but manually replacing the executor address // `5c2f5a71f67c01775180adc06909288b4c329308` with the one in this test @@ -688,7 +776,7 @@ contract TychoRouterTest is TychoRouterTestSetup { assertEq(balancerAfter - balancerBefore, 2659881924818443699787); } - function testUSV4Integration4() public { + function testUSV4Integration() public { // Test created with calldata from our router encoder. // Performs a sequential swap from USDC to PEPE though ETH using two @@ -701,7 +789,7 @@ contract TychoRouterTest is TychoRouterTestSetup { // Approve permit2 vm.startPrank(ALICE); - IERC20(USDC_ADDR).approve(address(permit2Address), type(uint256).max); + IERC20(USDC_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max); // Encoded solution generated using `test_split_encoding_strategy_usv4` // and ensuring that the encoded executor address is the one in this test // `f62849f9a0b5bf2913b396098f7c7019b51a820a` @@ -754,7 +842,7 @@ contract TychoRouterTest is TychoRouterTestSetup { // Approve permit2 vm.startPrank(ALICE); - IERC20(USDC_ADDR).approve(address(permit2Address), type(uint256).max); + IERC20(USDC_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max); // Encoded solution generated using `test_split_encoding_strategy_usv4_eth_out` // and ensuring that the encoded executor address is the one in this test @@ -783,7 +871,7 @@ contract TychoRouterTest is TychoRouterTestSetup { // Approve permit2 vm.startPrank(ALICE); - // IERC20(WETH_ADDR).approve(address(permit2Address), type(uint256).max); + // IERC20(WETH_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max); // Encoded solution generated using // `test_split_swap_strategy_encoder_simple_route_wrap` // but manually replacing the executor address @@ -812,7 +900,7 @@ contract TychoRouterTest is TychoRouterTestSetup { // Approve permit2 vm.startPrank(ALICE); - IERC20(DAI_ADDR).approve(address(permit2Address), type(uint256).max); + IERC20(DAI_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max); // Encoded solution generated using // `test_split_swap_strategy_encoder_simple_route_unwrap` // but manually replacing the executor address @@ -844,7 +932,7 @@ contract TychoRouterTest is TychoRouterTestSetup { // Approve permit2 vm.startPrank(ALICE); - IERC20(WETH_ADDR).approve(address(permit2Address), type(uint256).max); + IERC20(WETH_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max); // Encoded solution generated using `test_split_swap_strategy_encoder_complex` // but manually replacing the executor address // `5c2f5a71f67c01775180adc06909288b4c329308` with the one in this test @@ -946,6 +1034,52 @@ contract TychoRouterTest is TychoRouterTestSetup { assertEq(IERC20(USDT_ADDR).balanceOf(tychoRouterAddr), 99943852); } + function testSwapSingleUSV4CallbackPermit2() public { + vm.startPrank(ALICE); + uint256 amountIn = 100 ether; + deal(USDE_ADDR, ALICE, amountIn); + ( + IAllowanceTransfer.PermitSingle memory permitSingle, + bytes memory signature + ) = handlePermit2Approval(USDE_ADDR, amountIn); + + UniswapV4Executor.UniswapV4Pool[] memory pools = + new UniswapV4Executor.UniswapV4Pool[](1); + pools[0] = UniswapV4Executor.UniswapV4Pool({ + intermediaryToken: USDT_ADDR, + fee: uint24(100), + tickSpacing: int24(1) + }); + + bytes memory protocolData = UniswapV4Utils.encodeExactInput( + USDE_ADDR, USDT_ADDR, true, address(usv4Executor), pools + ); + + bytes memory swap = encodeSwap( + uint8(0), uint8(1), uint24(0), address(usv4Executor), protocolData + ); + + bytes[] memory swaps = new bytes[](1); + swaps[0] = swap; + + tychoRouter.swapPermit2( + amountIn, + USDE_ADDR, + USDT_ADDR, + 0, + false, + false, + 2, + ALICE, + permitSingle, + signature, + pleEncode(swaps) + ); + + assertEq(IERC20(USDT_ADDR).balanceOf(ALICE), 99943852); + vm.stopPrank(); + } + function testSwapMultipleUSV4Callback() public { // This test has two uniswap v4 hops that will be executed inside of the V4 pool manager // USDE -> USDT -> WBTC diff --git a/foundry/test/TychoRouterTestSetup.sol b/foundry/test/TychoRouterTestSetup.sol index 8fbf8c5..634f814 100644 --- a/foundry/test/TychoRouterTestSetup.sol +++ b/foundry/test/TychoRouterTestSetup.sol @@ -34,7 +34,6 @@ contract TychoRouterExposed is TychoRouter { contract TychoRouterTestSetup is Test, Constants { TychoRouterExposed tychoRouter; address tychoRouterAddr; - address permit2Address = address(0x000000000022D473030F116dDEE9F6B43aC78BA3); UniswapV2Executor public usv2Executor; UniswapV3Executor public usv3Executor; UniswapV4Executor public usv4Executor; @@ -49,7 +48,7 @@ contract TychoRouterTestSetup is Test, Constants { address factoryV2 = USV2_FACTORY_ETHEREUM; address poolManagerAddress = 0x000000000004444c5dc75cB358380D2e3dE08A90; IPoolManager poolManager = IPoolManager(poolManagerAddress); - tychoRouter = new TychoRouterExposed(permit2Address, WETH_ADDR); + tychoRouter = new TychoRouterExposed(PERMIT2_ADDRESS, WETH_ADDR); tychoRouterAddr = address(tychoRouter); tychoRouter.grantRole(keccak256("FUND_RESCUER_ROLE"), FUND_RESCUER); tychoRouter.grantRole(keccak256("FEE_SETTER_ROLE"), FEE_SETTER); @@ -108,7 +107,7 @@ contract TychoRouterTestSetup is Test, Constants { internal returns (IAllowanceTransfer.PermitSingle memory, bytes memory) { - IERC20(tokenIn).approve(permit2Address, amount_in); + IERC20(tokenIn).approve(PERMIT2_ADDRESS, amount_in); IAllowanceTransfer.PermitSingle memory permitSingle = IAllowanceTransfer .PermitSingle({ details: IAllowanceTransfer.PermitDetails({ @@ -148,7 +147,7 @@ contract TychoRouterTestSetup is Test, Constants { ), keccak256("Permit2"), block.chainid, - permit2Address + PERMIT2_ADDRESS ) ); bytes32 detailsHash =