From ec87969aa6e938a4e0d5bdfa0d6aaae1c49b56fd Mon Sep 17 00:00:00 2001 From: Diana Carvalho Date: Tue, 15 Apr 2025 16:21:23 +0100 Subject: [PATCH] feat: Add transfer out for Uniswap V4 Add transfer in Executor and pass receiver address in encoding This is done by using the TAKE action instead of TAKE_ALL --- don't change below this line --- ENG-4315 Took 1 hour 36 minutes Took 2 minutes --- foundry/src/executors/UniswapV4Executor.sol | 25 +++++++++++-------- foundry/test/TychoRouterSplitSwap.t.sol | 5 ++-- .../test/executors/UniswapV4Executor.t.sol | 19 ++++++++------ foundry/test/executors/UniswapV4Utils.sol | 3 ++- .../evm/strategy_encoder/strategy_encoders.rs | 1 + .../evm/swap_encoder/swap_encoders.rs | 15 +++++++---- src/encoding/evm/tycho_encoders.rs | 2 ++ 7 files changed, 43 insertions(+), 27 deletions(-) diff --git a/foundry/src/executors/UniswapV4Executor.sol b/foundry/src/executors/UniswapV4Executor.sol index aafbd7d..6f32012 100644 --- a/foundry/src/executors/UniswapV4Executor.sol +++ b/foundry/src/executors/UniswapV4Executor.sol @@ -46,6 +46,7 @@ contract UniswapV4Executor is IExecutor, V4Router, ICallback, TokenTransfer { address tokenOut, bool zeroForOne, TransferType transferType, + address receiver, UniswapV4Executor.UniswapV4Pool[] memory pools ) = _decodeData(data); @@ -72,7 +73,7 @@ contract UniswapV4Executor is IExecutor, V4Router, ICallback, TokenTransfer { bytes memory actions = abi.encodePacked( uint8(Actions.SWAP_EXACT_IN_SINGLE), uint8(Actions.SETTLE_ALL), - uint8(Actions.TAKE_ALL) + uint8(Actions.TAKE) ); bytes[] memory params = new bytes[](3); @@ -87,7 +88,7 @@ contract UniswapV4Executor is IExecutor, V4Router, ICallback, TokenTransfer { }) ); params[1] = abi.encode(tokenIn, amountIn); // currency to settle - params[2] = abi.encode(tokenOut, uint256(0)); // currency to take + params[2] = abi.encode(tokenOut, receiver, uint256(0)); // currency to take. 0 means to take the full amount swapData = abi.encode(actions, params); } else { PathKey[] memory path = new PathKey[](pools.length); @@ -104,7 +105,7 @@ contract UniswapV4Executor is IExecutor, V4Router, ICallback, TokenTransfer { bytes memory actions = abi.encodePacked( uint8(Actions.SWAP_EXACT_IN), uint8(Actions.SETTLE_ALL), - uint8(Actions.TAKE_ALL) + uint8(Actions.TAKE) ); bytes[] memory params = new bytes[](3); @@ -119,22 +120,22 @@ contract UniswapV4Executor is IExecutor, V4Router, ICallback, TokenTransfer { }) ); params[1] = abi.encode(currencyIn, amountIn); - params[2] = abi.encode(Currency.wrap(tokenOut), uint256(0)); + params[2] = abi.encode(Currency.wrap(tokenOut), receiver, uint256(0)); swapData = abi.encode(actions, params); } uint256 tokenOutBalanceBefore; tokenOutBalanceBefore = tokenOut == address(0) - ? address(this).balance - : IERC20(tokenOut).balanceOf(address(this)); + ? receiver.balance + : IERC20(tokenOut).balanceOf(receiver); executeActions(swapData); uint256 tokenOutBalanceAfter; tokenOutBalanceAfter = tokenOut == address(0) - ? address(this).balance - : IERC20(tokenOut).balanceOf(address(this)); + ? receiver.balance + : IERC20(tokenOut).balanceOf(receiver); calculatedAmount = tokenOutBalanceAfter - tokenOutBalanceBefore; @@ -155,10 +156,11 @@ contract UniswapV4Executor is IExecutor, V4Router, ICallback, TokenTransfer { address tokenOut, bool zeroForOne, TransferType transferType, + address receiver, UniswapV4Pool[] memory pools ) { - if (data.length < 67) { + if (data.length < 88) { revert UniswapV4Executor__InvalidDataLength(); } @@ -166,10 +168,11 @@ contract UniswapV4Executor is IExecutor, V4Router, ICallback, TokenTransfer { tokenOut = address(bytes20(data[20:40])); zeroForOne = (data[40] != 0); transferType = TransferType(uint8(data[41])); + receiver = address(bytes20(data[42:62])); - uint256 poolsLength = (data.length - 42) / 26; // 26 bytes per pool object + uint256 poolsLength = (data.length - 62) / 26; // 26 bytes per pool object pools = new UniswapV4Pool[](poolsLength); - bytes memory poolsData = data[42:]; + bytes memory poolsData = data[62:]; uint256 offset = 0; for (uint256 i = 0; i < poolsLength; i++) { address intermediaryToken; diff --git a/foundry/test/TychoRouterSplitSwap.t.sol b/foundry/test/TychoRouterSplitSwap.t.sol index 255c76b..97083d7 100644 --- a/foundry/test/TychoRouterSplitSwap.t.sol +++ b/foundry/test/TychoRouterSplitSwap.t.sol @@ -423,6 +423,7 @@ contract TychoRouterSplitSwapTest is TychoRouterTestSetup { USDT_ADDR, true, TokenTransfer.TransferType.TRANSFER_PERMIT2_TO_ROUTER, + ALICE, pools ); @@ -471,7 +472,7 @@ contract TychoRouterSplitSwapTest is TychoRouterTestSetup { }); bytes memory protocolData = UniswapV4Utils.encodeExactInput( - USDE_ADDR, WBTC_ADDR, true, TokenTransfer.TransferType.NONE, pools + USDE_ADDR, WBTC_ADDR, true, TokenTransfer.TransferType.NONE, ALICE, pools ); bytes memory swap = encodeSplitSwap( @@ -483,7 +484,7 @@ contract TychoRouterSplitSwapTest is TychoRouterTestSetup { tychoRouter.exposedSplitSwap(amountIn, 2, pleEncode(swaps)); - assertEq(IERC20(WBTC_ADDR).balanceOf(tychoRouterAddr), 102718); + assertEq(IERC20(WBTC_ADDR).balanceOf(ALICE), 102718); } function testSplitInputCyclicSwapInternalMethod() public { diff --git a/foundry/test/executors/UniswapV4Executor.t.sol b/foundry/test/executors/UniswapV4Executor.t.sol index 62f61c9..2518774 100644 --- a/foundry/test/executors/UniswapV4Executor.t.sol +++ b/foundry/test/executors/UniswapV4Executor.t.sol @@ -22,6 +22,7 @@ contract UniswapV4ExecutorExposed is UniswapV4Executor { address tokenOut, bool zeroForOne, TokenTransfer.TransferType transferType, + address receiver, UniswapV4Pool[] memory pools ) { @@ -68,7 +69,7 @@ contract UniswapV4ExecutorTest is Test, Constants { }); bytes memory data = UniswapV4Utils.encodeExactInput( - USDE_ADDR, USDT_ADDR, zeroForOne, transferType, pools + USDE_ADDR, USDT_ADDR, zeroForOne, transferType, ALICE, pools ); ( @@ -76,6 +77,7 @@ contract UniswapV4ExecutorTest is Test, Constants { address tokenOut, bool zeroForOneDecoded, TokenTransfer.TransferType transferTypeDecoded, + address receiver, UniswapV4Executor.UniswapV4Pool[] memory decodedPools ) = uniswapV4Exposed.decodeData(data); @@ -83,6 +85,7 @@ contract UniswapV4ExecutorTest is Test, Constants { assertEq(tokenOut, USDT_ADDR); assertEq(zeroForOneDecoded, zeroForOne); assertEq(uint8(transferTypeDecoded), uint8(transferType)); + assertEq(receiver, ALICE); assertEq(decodedPools.length, 2); assertEq(decodedPools[0].intermediaryToken, USDT_ADDR); assertEq(decodedPools[0].fee, pool1Fee); @@ -108,7 +111,7 @@ contract UniswapV4ExecutorTest is Test, Constants { }); bytes memory data = UniswapV4Utils.encodeExactInput( - USDE_ADDR, USDT_ADDR, true, TokenTransfer.TransferType.NONE, pools + USDE_ADDR, USDT_ADDR, true, TokenTransfer.TransferType.NONE, ALICE, pools ); uint256 amountOut = uniswapV4Exposed.swap(amountIn, data); @@ -117,7 +120,7 @@ contract UniswapV4ExecutorTest is Test, Constants { USDE.balanceOf(address(uniswapV4Exposed)), usdeBalanceBeforeSwapExecutor - amountIn ); - assertTrue(USDT.balanceOf(address(uniswapV4Exposed)) == amountOut); + assertTrue(USDT.balanceOf(ALICE) == amountOut); } function testSingleSwapIntegration() public { @@ -134,10 +137,10 @@ contract UniswapV4ExecutorTest is Test, Constants { uint256 amountOut = uniswapV4Exposed.swap(amountIn, protocolData); assertEq(USDE.balanceOf(poolManager), usdeBalanceBeforePool + amountIn); assertEq( - USDE.balanceOf(address(uniswapV4Exposed)), + USDE.balanceOf(ALICE), usdeBalanceBeforeSwapExecutor - amountIn ); - assertTrue(USDT.balanceOf(address(uniswapV4Exposed)) == amountOut); + assertTrue(USDT.balanceOf(ALICE) == amountOut); } function testMultipleSwap() public { @@ -162,7 +165,7 @@ contract UniswapV4ExecutorTest is Test, Constants { }); bytes memory data = UniswapV4Utils.encodeExactInput( - USDE_ADDR, WBTC_ADDR, true, TokenTransfer.TransferType.NONE, pools + USDE_ADDR, WBTC_ADDR, true, TokenTransfer.TransferType.NONE, ALICE, pools ); uint256 amountOut = uniswapV4Exposed.swap(amountIn, data); @@ -172,7 +175,7 @@ contract UniswapV4ExecutorTest is Test, Constants { usdeBalanceBeforeSwapExecutor - amountIn ); assertTrue( - IERC20(WBTC_ADDR).balanceOf(address(uniswapV4Exposed)) == amountOut + IERC20(WBTC_ADDR).balanceOf(ALICE) == amountOut ); } @@ -196,7 +199,7 @@ contract UniswapV4ExecutorTest is Test, Constants { usdeBalanceBeforeSwapExecutor - amountIn ); assertTrue( - IERC20(WBTC_ADDR).balanceOf(address(uniswapV4Exposed)) == amountOut + IERC20(WBTC_ADDR).balanceOf(ALICE) == amountOut ); } } diff --git a/foundry/test/executors/UniswapV4Utils.sol b/foundry/test/executors/UniswapV4Utils.sol index 1ccc29c..c96465c 100644 --- a/foundry/test/executors/UniswapV4Utils.sol +++ b/foundry/test/executors/UniswapV4Utils.sol @@ -9,6 +9,7 @@ library UniswapV4Utils { address tokenOut, bool zeroForOne, UniswapV4Executor.TransferType transferType, + address receiver, UniswapV4Executor.UniswapV4Pool[] memory pools ) public pure returns (bytes memory) { bytes memory encodedPools; @@ -23,7 +24,7 @@ library UniswapV4Utils { } return abi.encodePacked( - tokenIn, tokenOut, zeroForOne, transferType, encodedPools + tokenIn, tokenOut, zeroForOne, transferType, receiver, encodedPools ); } } diff --git a/src/encoding/evm/strategy_encoder/strategy_encoders.rs b/src/encoding/evm/strategy_encoder/strategy_encoders.rs index 4a34850..29f085c 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoders.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoders.rs @@ -1433,6 +1433,7 @@ mod tests { "6982508145454ce325ddbe47a25d4ec3d2311933", // group token in "00", // zero2one "04", // transfer type (transfer to router) + "cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2", // receiver // First pool params "0000000000000000000000000000000000000000", // intermediary token (ETH) "000bb8", // fee diff --git a/src/encoding/evm/swap_encoder/swap_encoders.rs b/src/encoding/evm/swap_encoder/swap_encoders.rs index a538e5d..decaeab 100644 --- a/src/encoding/evm/swap_encoder/swap_encoders.rs +++ b/src/encoding/evm/swap_encoder/swap_encoders.rs @@ -207,6 +207,7 @@ impl SwapEncoder for UniswapV4SwapEncoder { group_token_out_address, zero_to_one, (encoding_context.transfer_type as u8).to_be_bytes(), + bytes_to_address(&encoding_context.receiver)?, pool_params, ); @@ -766,9 +767,8 @@ mod tests { split: 0f64, }; let encoding_context = EncodingContext { - // The receiver address was taken from `address(uniswapV4Exposed)` in the - // UniswapV4Executor.t.sol - receiver: Bytes::from("0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f"), + // The receiver is ALICE to match the solidity tests + receiver: Bytes::from("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2"), exact_out: false, // Same as the executor address router_address: Some(Bytes::from("0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f")), @@ -800,6 +800,8 @@ mod tests { "01", // transfer type "00", + // receiver + "cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2", // pool params: // - intermediary token "dac17f958d2ee523a2206206994597c13d831ec7", @@ -878,11 +880,11 @@ mod tests { let usdt_address = Bytes::from("0xdAC17F958D2ee523a2206206994597C13D831ec7"); let wbtc_address = Bytes::from("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"); let router_address = Bytes::from("0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f"); - let receiver_address = router_address.clone(); // The context is the same for both swaps, since the group token in and out are the same let context = EncodingContext { - receiver: receiver_address.clone(), + // The receiver is ALICE to match the solidity tests + receiver: Bytes::from("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2"), exact_out: false, router_address: Some(router_address.clone()), group_token_in: usde_address.clone(), @@ -968,6 +970,8 @@ mod tests { "01", // transfer type "00", + // receiver + "cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2", // pool params: // - intermediary token USDT "dac17f958d2ee523a2206206994597c13d831ec7", @@ -983,6 +987,7 @@ mod tests { "00003c" )) ); + println!("{}", combined_hex) } mod ekubo { diff --git a/src/encoding/evm/tycho_encoders.rs b/src/encoding/evm/tycho_encoders.rs index 1ecf41f..5d285e0 100644 --- a/src/encoding/evm/tycho_encoders.rs +++ b/src/encoding/evm/tycho_encoders.rs @@ -1050,6 +1050,8 @@ mod tests { "00", // transfer type "00", + // receiver + "cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2", // first pool intermediary token (ETH) "0000000000000000000000000000000000000000", // fee