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