From 93678d9d1987eaa9fff8c19a6fe69e7771ed582c Mon Sep 17 00:00:00 2001 From: Diana Carvalho Date: Wed, 18 Jun 2025 15:53:30 +0100 Subject: [PATCH] feat(univ4): Pass user_data as hook_data in execution Because we don't know the size of hook data, it needs to be at the end of the protocol data. But we also don't know the size of the intermediary swaps. To solve this, we are now ple encoding the intermediary swaps and only then appending the hook data Took 2 hours 50 minutes Took 40 seconds --- foundry/src/executors/UniswapV4Executor.sol | 44 +++++++++++----- foundry/test/protocols/UniswapV4.t.sol | 6 +++ foundry/test/protocols/UniswapV4Utils.sol | 32 ++++++++++-- .../evm/strategy_encoder/strategy_encoders.rs | 45 ++++++++++++---- .../evm/swap_encoder/swap_encoders.rs | 30 +++++++++-- src/encoding/evm/tycho_encoders.rs | 51 ++++++++++++------- 6 files changed, 159 insertions(+), 49 deletions(-) diff --git a/foundry/src/executors/UniswapV4Executor.sol b/foundry/src/executors/UniswapV4Executor.sol index fcbf6ec..887e81a 100644 --- a/foundry/src/executors/UniswapV4Executor.sol +++ b/foundry/src/executors/UniswapV4Executor.sol @@ -24,6 +24,7 @@ import {TransientStateLibrary} from "@uniswap/v4-core/src/libraries/TransientStateLibrary.sol"; import "../RestrictTransferFrom.sol"; import "@openzeppelin/contracts/utils/Address.sol"; +import "../../lib/bytes/LibPrefixLengthEncodedByteArray.sol"; error UniswapV4Executor__InvalidDataLength(); error UniswapV4Executor__NotPoolManager(); @@ -44,6 +45,7 @@ contract UniswapV4Executor is using CurrencyLibrary for Currency; using SafeCast for *; using TransientStateLibrary for IPoolManager; + using LibPrefixLengthEncodedByteArray for bytes; IPoolManager public immutable poolManager; address private immutable _self; @@ -86,6 +88,7 @@ contract UniswapV4Executor is TransferType transferType, address receiver, address hook, + bytes memory hookData, UniswapV4Executor.UniswapV4Pool[] memory pools ) = _decodeData(data); bytes memory swapData; @@ -104,7 +107,7 @@ contract UniswapV4Executor is amountIn, transferType, receiver, - bytes("") + hookData ); } else { PathKey[] memory path = new PathKey[](pools.length); @@ -114,7 +117,7 @@ contract UniswapV4Executor is fee: pools[i].fee, tickSpacing: pools[i].tickSpacing, hooks: IHooks(hook), - hookData: bytes("") + hookData: hookData }); } @@ -145,6 +148,7 @@ contract UniswapV4Executor is TransferType transferType, address receiver, address hook, + bytes memory hookData, UniswapV4Pool[] memory pools ) { @@ -159,24 +163,40 @@ contract UniswapV4Executor is receiver = address(bytes20(data[42:62])); hook = address(bytes20(data[62:82])); - uint256 poolsLength = (data.length - 82) / 26; // 26 bytes per pool object - pools = new UniswapV4Pool[](poolsLength); - bytes memory poolsData = data[82:]; - uint256 offset = 0; - for (uint256 i = 0; i < poolsLength; i++) { + bytes calldata remaining = data[82:]; + address firstToken = address(bytes20(remaining[0:20])); + uint24 firstFee = uint24(bytes3(remaining[20:23])); + int24 firstTickSpacing = int24(uint24(bytes3(remaining[23:26]))); + UniswapV4Pool memory firstPool = + UniswapV4Pool(firstToken, firstFee, firstTickSpacing); + + // Remaining after first pool are ple encoded + bytes[] memory encodedPools = + LibPrefixLengthEncodedByteArray.toArray(remaining[26:]); + + pools = new UniswapV4Pool[](1 + encodedPools.length); + pools[0] = firstPool; + + uint256 encodedPoolsLength = 26; + uint256 plePoolsTotalLength; + + for (uint256 i = 0; i < encodedPools.length; i++) { + bytes memory poolsData = encodedPools[i]; address intermediaryToken; uint24 fee; int24 tickSpacing; // slither-disable-next-line assembly assembly { - intermediaryToken := mload(add(poolsData, add(offset, 20))) - fee := shr(232, mload(add(poolsData, add(offset, 52)))) - tickSpacing := shr(232, mload(add(poolsData, add(offset, 55)))) + intermediaryToken := mload(add(poolsData, add(0, 20))) + fee := shr(232, mload(add(poolsData, add(0, 52)))) + tickSpacing := shr(232, mload(add(poolsData, add(0, 55)))) } - pools[i] = UniswapV4Pool(intermediaryToken, fee, tickSpacing); - offset += 26; + pools[i + 1] = UniswapV4Pool(intermediaryToken, fee, tickSpacing); + plePoolsTotalLength += 2 + encodedPoolsLength; // 2 bytes prefix + data } + + hookData = remaining[26 + plePoolsTotalLength:]; } /** diff --git a/foundry/test/protocols/UniswapV4.t.sol b/foundry/test/protocols/UniswapV4.t.sol index ea48fc3..19cf453 100644 --- a/foundry/test/protocols/UniswapV4.t.sol +++ b/foundry/test/protocols/UniswapV4.t.sol @@ -25,6 +25,7 @@ contract UniswapV4ExecutorExposed is UniswapV4Executor { RestrictTransferFrom.TransferType transferType, address receiver, address hook, + bytes memory hookData, UniswapV4Pool[] memory pools ) { @@ -77,6 +78,7 @@ contract UniswapV4ExecutorTest is Constants, TestUtils { RestrictTransferFrom.TransferType.Transfer, ALICE, address(0), + bytes(""), pools ); @@ -87,6 +89,7 @@ contract UniswapV4ExecutorTest is Constants, TestUtils { RestrictTransferFrom.TransferType transferType, address receiver, address hook, + bytes memory hookData, UniswapV4Executor.UniswapV4Pool[] memory decodedPools ) = uniswapV4Exposed.decodeData(data); @@ -130,6 +133,7 @@ contract UniswapV4ExecutorTest is Constants, TestUtils { RestrictTransferFrom.TransferType.Transfer, ALICE, address(0), + bytes(""), pools ); @@ -188,6 +192,7 @@ contract UniswapV4ExecutorTest is Constants, TestUtils { RestrictTransferFrom.TransferType.Transfer, ALICE, address(0), + bytes(""), pools ); @@ -244,6 +249,7 @@ contract UniswapV4ExecutorTest is Constants, TestUtils { RestrictTransferFrom.TransferType.Transfer, ALICE, hook, + bytes(""), pools ); diff --git a/foundry/test/protocols/UniswapV4Utils.sol b/foundry/test/protocols/UniswapV4Utils.sol index cb58a8e..9051f12 100644 --- a/foundry/test/protocols/UniswapV4Utils.sol +++ b/foundry/test/protocols/UniswapV4Utils.sol @@ -11,13 +11,20 @@ library UniswapV4Utils { RestrictTransferFrom.TransferType transferType, address receiver, address hook, + bytes memory hookData, UniswapV4Executor.UniswapV4Pool[] memory pools ) public pure returns (bytes memory) { - bytes memory encodedPools; + require(pools.length > 0, "Must have at least one pool"); - for (uint256 i = 0; i < pools.length; i++) { - encodedPools = abi.encodePacked( - encodedPools, + bytes memory firstPool = abi.encodePacked( + pools[0].intermediaryToken, + bytes3(pools[0].fee), + pools[0].tickSpacing + ); + + bytes[] memory encodedExtraPools = new bytes[](pools.length - 1); + for (uint256 i = 1; i < pools.length; i++) { + encodedExtraPools[i - 1] = abi.encodePacked( pools[i].intermediaryToken, bytes3(pools[i].fee), pools[i].tickSpacing @@ -31,7 +38,22 @@ library UniswapV4Utils { transferType, receiver, hook, - encodedPools + firstPool, + pleEncode(encodedExtraPools), + hookData ); } + + function pleEncode(bytes[] memory data) + public + pure + returns (bytes memory encoded) + { + for (uint256 i = 0; i < data.length; i++) { + encoded = bytes.concat( + encoded, + abi.encodePacked(bytes2(uint16(data[i].length)), data[i]) + ); + } + } } diff --git a/src/encoding/evm/strategy_encoder/strategy_encoders.rs b/src/encoding/evm/strategy_encoder/strategy_encoders.rs index 7c551e0..6470c54 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoders.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoders.rs @@ -121,16 +121,25 @@ impl StrategyEncoder for SingleSwapStrategyEncoder { transfer_type: transfer, }; - let mut grouped_protocol_data: Vec = vec![]; + let mut grouped_protocol_data: Vec> = vec![]; + let mut initial_protocol_data: Vec = vec![]; for swap in grouped_swap.swaps.iter() { let protocol_data = swap_encoder.encode_swap(swap, &encoding_context)?; - grouped_protocol_data.extend(protocol_data); + if encoding_context.group_token_in == swap.token_in { + initial_protocol_data = protocol_data; + } else { + grouped_protocol_data.push(protocol_data); + } + } + + if !grouped_protocol_data.is_empty() { + initial_protocol_data.extend(ple_encode(grouped_protocol_data)); } let swap_data = self.encode_swap_header( Bytes::from_str(swap_encoder.executor_address()) .map_err(|_| EncodingError::FatalError("Invalid executor address".to_string()))?, - grouped_protocol_data, + initial_protocol_data, ); Ok(EncodedSolution { function_signature: self.function_signature.clone(), @@ -269,17 +278,26 @@ impl StrategyEncoder for SequentialSwapStrategyEncoder { transfer_type: transfer, }; - let mut grouped_protocol_data: Vec = vec![]; + let mut grouped_protocol_data: Vec> = vec![]; + let mut initial_protocol_data: Vec = vec![]; for swap in grouped_swap.swaps.iter() { let protocol_data = swap_encoder.encode_swap(swap, &encoding_context)?; - grouped_protocol_data.extend(protocol_data); + if encoding_context.group_token_in == swap.token_in { + initial_protocol_data = protocol_data; + } else { + grouped_protocol_data.push(protocol_data); + } + } + + if !grouped_protocol_data.is_empty() { + initial_protocol_data.extend(ple_encode(grouped_protocol_data)); } let swap_data = self.encode_swap_header( Bytes::from_str(swap_encoder.executor_address()).map_err(|_| { EncodingError::FatalError("Invalid executor address".to_string()) })?, - grouped_protocol_data, + initial_protocol_data, ); swaps.push(swap_data); } @@ -458,10 +476,19 @@ impl StrategyEncoder for SplitSwapStrategyEncoder { transfer_type: transfer, }; - let mut grouped_protocol_data: Vec = vec![]; + let mut grouped_protocol_data: Vec> = vec![]; + let mut initial_protocol_data: Vec = vec![]; for swap in grouped_swap.swaps.iter() { let protocol_data = swap_encoder.encode_swap(swap, &encoding_context)?; - grouped_protocol_data.extend(protocol_data); + if encoding_context.group_token_in == swap.token_in { + initial_protocol_data = protocol_data; + } else { + grouped_protocol_data.push(protocol_data); + } + } + + if !grouped_protocol_data.is_empty() { + initial_protocol_data.extend(ple_encode(grouped_protocol_data)); } let swap_data = self.encode_swap_header( @@ -471,7 +498,7 @@ impl StrategyEncoder for SplitSwapStrategyEncoder { Bytes::from_str(swap_encoder.executor_address()).map_err(|_| { EncodingError::FatalError("Invalid executor address".to_string()) })?, - grouped_protocol_data, + initial_protocol_data, ); swaps.push(swap_data); } diff --git a/src/encoding/evm/swap_encoder/swap_encoders.rs b/src/encoding/evm/swap_encoder/swap_encoders.rs index 5466e6c..cb9a651 100644 --- a/src/encoding/evm/swap_encoder/swap_encoders.rs +++ b/src/encoding/evm/swap_encoder/swap_encoders.rs @@ -180,10 +180,23 @@ impl SwapEncoder for UniswapV4SwapEncoder { Ok(hook) => Address::from_slice(&hook), Err(_) => Address::ZERO, }; - + let mut hook_data = AlloyBytes::new(); + if encoding_context.group_token_out == swap.token_out { + // Add hook data if it's only the last swap + hook_data = AlloyBytes::from( + swap.user_data + .unwrap_or_default() + .to_vec(), + ); + } // 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) + return Ok(( + bytes_to_address(&swap.token_out)?, + pool_fee_u24, + pool_tick_spacing_u24, + hook_data, + ) .abi_encode_packed()); } @@ -206,6 +219,7 @@ impl SwapEncoder for UniswapV4SwapEncoder { bytes_to_address(&encoding_context.receiver)?, hook_address, pool_params, + hook_data, ); Ok(args.abi_encode_packed()) @@ -841,7 +855,7 @@ mod tests { mod uniswap_v4 { use super::*; - use crate::encoding::evm::utils::write_calldata_to_file; + use crate::encoding::evm::utils::{ple_encode, write_calldata_to_file}; #[test] fn test_encode_uniswap_v4_simple_swap() { @@ -1062,8 +1076,11 @@ mod tests { .encode_swap(&second_swap, &context) .unwrap(); - let combined_hex = - format!("{}{}", encode(&initial_encoded_swap), encode(&second_encoded_swap)); + let combined_hex = format!( + "{}{}", + encode(&initial_encoded_swap), + encode(ple_encode(vec![second_encoded_swap])) + ); assert_eq!( combined_hex, @@ -1087,6 +1104,9 @@ mod tests { "000064", // - tick spacing "000001", + // Second swap + // ple encoding + "001a", // - intermediary token WBTC "2260fac5e5542a773aa44fbcfedf7c193bc2c599", // - fee diff --git a/src/encoding/evm/tycho_encoders.rs b/src/encoding/evm/tycho_encoders.rs index b108478..008b974 100644 --- a/src/encoding/evm/tycho_encoders.rs +++ b/src/encoding/evm/tycho_encoders.rs @@ -14,6 +14,7 @@ use crate::encoding::{ SequentialSwapStrategyEncoder, SingleSwapStrategyEncoder, SplitSwapStrategyEncoder, }, swap_encoder::swap_encoder_registry::SwapEncoderRegistry, + utils::ple_encode, }, models::{ Chain, EncodedSolution, EncodingContext, NativeAction, Solution, Transaction, TransferType, @@ -310,32 +311,44 @@ impl TychoExecutorEncoder { )) })?; - let mut grouped_protocol_data: Vec = vec![]; + let transfer = if IN_TRANSFER_REQUIRED_PROTOCOLS.contains( + &grouped_swap.swaps[0] + .component + .protocol_system + .as_str(), + ) { + TransferType::Transfer + } else { + TransferType::None + }; + let encoding_context = EncodingContext { + receiver: solution.receiver.clone(), + exact_out: solution.exact_out, + router_address: None, + group_token_in: grouped_swap.token_in.clone(), + group_token_out: grouped_swap.token_out.clone(), + transfer_type: transfer, + }; + let mut grouped_protocol_data: Vec> = vec![]; + let mut initial_protocol_data: Vec = vec![]; for swap in grouped_swap.swaps.iter() { - let transfer = if IN_TRANSFER_REQUIRED_PROTOCOLS - .contains(&swap.component.protocol_system.as_str()) - { - TransferType::Transfer - } else { - TransferType::None - }; - let encoding_context = EncodingContext { - receiver: solution.receiver.clone(), - exact_out: solution.exact_out, - router_address: None, - group_token_in: grouped_swap.token_in.clone(), - group_token_out: grouped_swap.token_out.clone(), - transfer_type: transfer, - }; let protocol_data = swap_encoder.encode_swap(swap, &encoding_context)?; - grouped_protocol_data.extend(protocol_data); + if encoding_context.group_token_in == swap.token_in { + initial_protocol_data = protocol_data; + } else { + grouped_protocol_data.push(protocol_data); + } + } + + if !grouped_protocol_data.is_empty() { + initial_protocol_data.extend(ple_encode(grouped_protocol_data)); } let executor_address = Bytes::from_str(swap_encoder.executor_address()) .map_err(|_| EncodingError::FatalError("Invalid executor address".to_string()))?; Ok(EncodedSolution { - swaps: grouped_protocol_data, + swaps: initial_protocol_data, interacting_with: executor_address, permit: None, function_signature: "".to_string(), @@ -1261,6 +1274,8 @@ mod tests { "000bb8", // tick spacing "00003c", + // ple encoding + "001a", // second pool intermediary token (PEPE) "6982508145454ce325ddbe47a25d4ec3d2311933", // fee