diff --git a/foundry/src/executors/UniswapV4Executor.sol b/foundry/src/executors/UniswapV4Executor.sol index b8fd87d..fcbf6ec 100644 --- a/foundry/src/executors/UniswapV4Executor.sol +++ b/foundry/src/executors/UniswapV4Executor.sol @@ -85,6 +85,7 @@ contract UniswapV4Executor is bool zeroForOne, TransferType transferType, address receiver, + address hook, UniswapV4Executor.UniswapV4Pool[] memory pools ) = _decodeData(data); bytes memory swapData; @@ -94,7 +95,7 @@ contract UniswapV4Executor is currency1: Currency.wrap(zeroForOne ? tokenOut : tokenIn), fee: pools[0].fee, tickSpacing: pools[0].tickSpacing, - hooks: IHooks(address(0)) + hooks: IHooks(hook) }); swapData = abi.encodeWithSelector( this.swapExactInputSingle.selector, @@ -112,7 +113,7 @@ contract UniswapV4Executor is intermediateCurrency: Currency.wrap(pools[i].intermediaryToken), fee: pools[i].fee, tickSpacing: pools[i].tickSpacing, - hooks: IHooks(address(0)), + hooks: IHooks(hook), hookData: bytes("") }); } @@ -143,10 +144,11 @@ contract UniswapV4Executor is bool zeroForOne, TransferType transferType, address receiver, + address hook, UniswapV4Pool[] memory pools ) { - if (data.length < 88) { + if (data.length < 108) { revert UniswapV4Executor__InvalidDataLength(); } @@ -155,10 +157,11 @@ contract UniswapV4Executor is zeroForOne = data[40] != 0; transferType = TransferType(uint8(data[41])); receiver = address(bytes20(data[42:62])); + hook = address(bytes20(data[62:82])); - uint256 poolsLength = (data.length - 62) / 26; // 26 bytes per pool object + uint256 poolsLength = (data.length - 82) / 26; // 26 bytes per pool object pools = new UniswapV4Pool[](poolsLength); - bytes memory poolsData = data[62:]; + bytes memory poolsData = data[82:]; uint256 offset = 0; for (uint256 i = 0; i < poolsLength; i++) { address intermediaryToken; diff --git a/foundry/test/protocols/UniswapV4.t.sol b/foundry/test/protocols/UniswapV4.t.sol index b69efc3..ea48fc3 100644 --- a/foundry/test/protocols/UniswapV4.t.sol +++ b/foundry/test/protocols/UniswapV4.t.sol @@ -24,6 +24,7 @@ contract UniswapV4ExecutorExposed is UniswapV4Executor { bool zeroForOne, RestrictTransferFrom.TransferType transferType, address receiver, + address hook, UniswapV4Pool[] memory pools ) { @@ -37,10 +38,12 @@ contract UniswapV4ExecutorTest is Constants, TestUtils { UniswapV4ExecutorExposed uniswapV4Exposed; IERC20 USDE = IERC20(USDE_ADDR); IERC20 USDT = IERC20(USDT_ADDR); + IERC20 USDC = IERC20(USDC_ADDR); + address poolManager = 0x000000000004444c5dc75cB358380D2e3dE08A90; function setUp() public { - uint256 forkBlock = 21817316; + uint256 forkBlock = 22689128; vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock); uniswapV4Exposed = new UniswapV4ExecutorExposed( IPoolManager(poolManager), PERMIT2_ADDRESS @@ -73,6 +76,7 @@ contract UniswapV4ExecutorTest is Constants, TestUtils { zeroForOne, RestrictTransferFrom.TransferType.Transfer, ALICE, + address(0), pools ); @@ -82,6 +86,7 @@ contract UniswapV4ExecutorTest is Constants, TestUtils { bool zeroForOneDecoded, RestrictTransferFrom.TransferType transferType, address receiver, + address hook, UniswapV4Executor.UniswapV4Pool[] memory decodedPools ) = uniswapV4Exposed.decodeData(data); @@ -93,6 +98,7 @@ contract UniswapV4ExecutorTest is Constants, TestUtils { uint8(RestrictTransferFrom.TransferType.Transfer) ); assertEq(receiver, ALICE); + assertEq(hook, address(0)); assertEq(decodedPools.length, 2); assertEq(decodedPools[0].intermediaryToken, USDT_ADDR); assertEq(decodedPools[0].fee, pool1Fee); @@ -123,6 +129,7 @@ contract UniswapV4ExecutorTest is Constants, TestUtils { true, RestrictTransferFrom.TransferType.Transfer, ALICE, + address(0), pools ); @@ -180,6 +187,7 @@ contract UniswapV4ExecutorTest is Constants, TestUtils { true, RestrictTransferFrom.TransferType.Transfer, ALICE, + address(0), pools ); @@ -211,6 +219,42 @@ contract UniswapV4ExecutorTest is Constants, TestUtils { ); assertTrue(IERC20(WBTC_ADDR).balanceOf(ALICE) == amountOut); } + + function testSingleSwapEulerHook() public { + // Replicating tx: 0xb372306a81c6e840f4ec55f006da6b0b097f435802a2e6fd216998dd12fb4aca + address hook = address(0x69058613588536167BA0AA94F0CC1Fe420eF28a8); + + uint256 amountIn = 7407000000; + deal(USDC_ADDR, address(uniswapV4Exposed), amountIn); + uint256 usdcBalanceBeforeSwapExecutor = + USDC.balanceOf(address(uniswapV4Exposed)); + + UniswapV4Executor.UniswapV4Pool[] memory pools = + new UniswapV4Executor.UniswapV4Pool[](1); + pools[0] = UniswapV4Executor.UniswapV4Pool({ + intermediaryToken: WETH_ADDR, + fee: uint24(500), + tickSpacing: int24(1) + }); + + bytes memory data = UniswapV4Utils.encodeExactInput( + USDC_ADDR, + WETH_ADDR, + true, + RestrictTransferFrom.TransferType.Transfer, + ALICE, + hook, + pools + ); + + uint256 amountOut = uniswapV4Exposed.swap(amountIn, data); + assertEq(amountOut, 2681115183499232721); + assertEq( + USDC.balanceOf(address(uniswapV4Exposed)), + usdcBalanceBeforeSwapExecutor - amountIn + ); + assertTrue(IERC20(WETH_ADDR).balanceOf(ALICE) == amountOut); + } } contract TychoRouterForBalancerV3Test is TychoRouterTestSetup { diff --git a/foundry/test/protocols/UniswapV4Utils.sol b/foundry/test/protocols/UniswapV4Utils.sol index c280fcb..cb58a8e 100644 --- a/foundry/test/protocols/UniswapV4Utils.sol +++ b/foundry/test/protocols/UniswapV4Utils.sol @@ -10,6 +10,7 @@ library UniswapV4Utils { bool zeroForOne, RestrictTransferFrom.TransferType transferType, address receiver, + address hook, UniswapV4Executor.UniswapV4Pool[] memory pools ) public pure returns (bytes memory) { bytes memory encodedPools; @@ -24,7 +25,13 @@ library UniswapV4Utils { } return abi.encodePacked( - tokenIn, tokenOut, zeroForOne, transferType, receiver, encodedPools + tokenIn, + tokenOut, + zeroForOne, + transferType, + receiver, + hook, + encodedPools ); } } diff --git a/src/encoding/evm/swap_encoder/swap_encoders.rs b/src/encoding/evm/swap_encoder/swap_encoders.rs index cbf33e9..5466e6c 100644 --- a/src/encoding/evm/swap_encoder/swap_encoders.rs +++ b/src/encoding/evm/swap_encoder/swap_encoders.rs @@ -176,6 +176,11 @@ impl SwapEncoder for UniswapV4SwapEncoder { EncodingError::FatalError("Failed to pad tick spacing bytes".to_string()) })?; + let hook_address = match get_static_attribute(&swap, "hook") { + Ok(hook) => Address::from_slice(&hook), + Err(_) => Address::ZERO, + }; + // Early check if this is not the first swap if encoding_context.group_token_in != swap.token_in { return Ok((bytes_to_address(&swap.token_out)?, pool_fee_u24, pool_tick_spacing_u24) @@ -199,6 +204,7 @@ impl SwapEncoder for UniswapV4SwapEncoder { zero_to_one, (encoding_context.transfer_type as u8).to_be_bytes(), bytes_to_address(&encoding_context.receiver)?, + hook_address, pool_params, ); @@ -897,6 +903,8 @@ mod tests { "01", // receiver "cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2", + // hook address (not set, so zero) + "0000000000000000000000000000000000000000", // pool params: // - intermediary token "dac17f958d2ee523a2206206994597c13d831ec7", @@ -1070,6 +1078,8 @@ mod tests { "01", // receiver "cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2", + // hook address (not set, so zero) + "0000000000000000000000000000000000000000", // pool params: // - intermediary token USDT "dac17f958d2ee523a2206206994597c13d831ec7", diff --git a/src/encoding/evm/tycho_encoders.rs b/src/encoding/evm/tycho_encoders.rs index 4ecc366..b108478 100644 --- a/src/encoding/evm/tycho_encoders.rs +++ b/src/encoding/evm/tycho_encoders.rs @@ -1253,6 +1253,8 @@ mod tests { "01", // receiver "cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2", + // hook address (not set, so zero) + "0000000000000000000000000000000000000000", // first pool intermediary token (ETH) "0000000000000000000000000000000000000000", // fee