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
This commit is contained in:
Diana Carvalho
2025-06-18 15:53:30 +01:00
committed by Diana Carvalho
parent a0581773cd
commit 93678d9d19
6 changed files with 159 additions and 49 deletions

View File

@@ -24,6 +24,7 @@ import {TransientStateLibrary} from
"@uniswap/v4-core/src/libraries/TransientStateLibrary.sol"; "@uniswap/v4-core/src/libraries/TransientStateLibrary.sol";
import "../RestrictTransferFrom.sol"; import "../RestrictTransferFrom.sol";
import "@openzeppelin/contracts/utils/Address.sol"; import "@openzeppelin/contracts/utils/Address.sol";
import "../../lib/bytes/LibPrefixLengthEncodedByteArray.sol";
error UniswapV4Executor__InvalidDataLength(); error UniswapV4Executor__InvalidDataLength();
error UniswapV4Executor__NotPoolManager(); error UniswapV4Executor__NotPoolManager();
@@ -44,6 +45,7 @@ contract UniswapV4Executor is
using CurrencyLibrary for Currency; using CurrencyLibrary for Currency;
using SafeCast for *; using SafeCast for *;
using TransientStateLibrary for IPoolManager; using TransientStateLibrary for IPoolManager;
using LibPrefixLengthEncodedByteArray for bytes;
IPoolManager public immutable poolManager; IPoolManager public immutable poolManager;
address private immutable _self; address private immutable _self;
@@ -86,6 +88,7 @@ contract UniswapV4Executor is
TransferType transferType, TransferType transferType,
address receiver, address receiver,
address hook, address hook,
bytes memory hookData,
UniswapV4Executor.UniswapV4Pool[] memory pools UniswapV4Executor.UniswapV4Pool[] memory pools
) = _decodeData(data); ) = _decodeData(data);
bytes memory swapData; bytes memory swapData;
@@ -104,7 +107,7 @@ contract UniswapV4Executor is
amountIn, amountIn,
transferType, transferType,
receiver, receiver,
bytes("") hookData
); );
} else { } else {
PathKey[] memory path = new PathKey[](pools.length); PathKey[] memory path = new PathKey[](pools.length);
@@ -114,7 +117,7 @@ contract UniswapV4Executor is
fee: pools[i].fee, fee: pools[i].fee,
tickSpacing: pools[i].tickSpacing, tickSpacing: pools[i].tickSpacing,
hooks: IHooks(hook), hooks: IHooks(hook),
hookData: bytes("") hookData: hookData
}); });
} }
@@ -145,6 +148,7 @@ contract UniswapV4Executor is
TransferType transferType, TransferType transferType,
address receiver, address receiver,
address hook, address hook,
bytes memory hookData,
UniswapV4Pool[] memory pools UniswapV4Pool[] memory pools
) )
{ {
@@ -159,24 +163,40 @@ contract UniswapV4Executor is
receiver = address(bytes20(data[42:62])); receiver = address(bytes20(data[42:62]));
hook = address(bytes20(data[62:82])); hook = address(bytes20(data[62:82]));
uint256 poolsLength = (data.length - 82) / 26; // 26 bytes per pool object bytes calldata remaining = data[82:];
pools = new UniswapV4Pool[](poolsLength); address firstToken = address(bytes20(remaining[0:20]));
bytes memory poolsData = data[82:]; uint24 firstFee = uint24(bytes3(remaining[20:23]));
uint256 offset = 0; int24 firstTickSpacing = int24(uint24(bytes3(remaining[23:26])));
for (uint256 i = 0; i < poolsLength; i++) { 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; address intermediaryToken;
uint24 fee; uint24 fee;
int24 tickSpacing; int24 tickSpacing;
// slither-disable-next-line assembly // slither-disable-next-line assembly
assembly { assembly {
intermediaryToken := mload(add(poolsData, add(offset, 20))) intermediaryToken := mload(add(poolsData, add(0, 20)))
fee := shr(232, mload(add(poolsData, add(offset, 52)))) fee := shr(232, mload(add(poolsData, add(0, 52))))
tickSpacing := shr(232, mload(add(poolsData, add(offset, 55)))) tickSpacing := shr(232, mload(add(poolsData, add(0, 55))))
} }
pools[i] = UniswapV4Pool(intermediaryToken, fee, tickSpacing); pools[i + 1] = UniswapV4Pool(intermediaryToken, fee, tickSpacing);
offset += 26; plePoolsTotalLength += 2 + encodedPoolsLength; // 2 bytes prefix + data
} }
hookData = remaining[26 + plePoolsTotalLength:];
} }
/** /**

View File

@@ -25,6 +25,7 @@ contract UniswapV4ExecutorExposed is UniswapV4Executor {
RestrictTransferFrom.TransferType transferType, RestrictTransferFrom.TransferType transferType,
address receiver, address receiver,
address hook, address hook,
bytes memory hookData,
UniswapV4Pool[] memory pools UniswapV4Pool[] memory pools
) )
{ {
@@ -77,6 +78,7 @@ contract UniswapV4ExecutorTest is Constants, TestUtils {
RestrictTransferFrom.TransferType.Transfer, RestrictTransferFrom.TransferType.Transfer,
ALICE, ALICE,
address(0), address(0),
bytes(""),
pools pools
); );
@@ -87,6 +89,7 @@ contract UniswapV4ExecutorTest is Constants, TestUtils {
RestrictTransferFrom.TransferType transferType, RestrictTransferFrom.TransferType transferType,
address receiver, address receiver,
address hook, address hook,
bytes memory hookData,
UniswapV4Executor.UniswapV4Pool[] memory decodedPools UniswapV4Executor.UniswapV4Pool[] memory decodedPools
) = uniswapV4Exposed.decodeData(data); ) = uniswapV4Exposed.decodeData(data);
@@ -130,6 +133,7 @@ contract UniswapV4ExecutorTest is Constants, TestUtils {
RestrictTransferFrom.TransferType.Transfer, RestrictTransferFrom.TransferType.Transfer,
ALICE, ALICE,
address(0), address(0),
bytes(""),
pools pools
); );
@@ -188,6 +192,7 @@ contract UniswapV4ExecutorTest is Constants, TestUtils {
RestrictTransferFrom.TransferType.Transfer, RestrictTransferFrom.TransferType.Transfer,
ALICE, ALICE,
address(0), address(0),
bytes(""),
pools pools
); );
@@ -244,6 +249,7 @@ contract UniswapV4ExecutorTest is Constants, TestUtils {
RestrictTransferFrom.TransferType.Transfer, RestrictTransferFrom.TransferType.Transfer,
ALICE, ALICE,
hook, hook,
bytes(""),
pools pools
); );

View File

@@ -11,13 +11,20 @@ library UniswapV4Utils {
RestrictTransferFrom.TransferType transferType, RestrictTransferFrom.TransferType transferType,
address receiver, address receiver,
address hook, address hook,
bytes memory hookData,
UniswapV4Executor.UniswapV4Pool[] memory pools UniswapV4Executor.UniswapV4Pool[] memory pools
) public pure returns (bytes memory) { ) 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++) { bytes memory firstPool = abi.encodePacked(
encodedPools = abi.encodePacked( pools[0].intermediaryToken,
encodedPools, 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, pools[i].intermediaryToken,
bytes3(pools[i].fee), bytes3(pools[i].fee),
pools[i].tickSpacing pools[i].tickSpacing
@@ -31,7 +38,22 @@ library UniswapV4Utils {
transferType, transferType,
receiver, receiver,
hook, 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])
); );
} }
} }
}

View File

@@ -121,16 +121,25 @@ impl StrategyEncoder for SingleSwapStrategyEncoder {
transfer_type: transfer, transfer_type: transfer,
}; };
let mut grouped_protocol_data: Vec<u8> = vec![]; let mut grouped_protocol_data: Vec<Vec<u8>> = vec![];
let mut initial_protocol_data: Vec<u8> = vec![];
for swap in grouped_swap.swaps.iter() { for swap in grouped_swap.swaps.iter() {
let protocol_data = swap_encoder.encode_swap(swap, &encoding_context)?; 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( let swap_data = self.encode_swap_header(
Bytes::from_str(swap_encoder.executor_address()) Bytes::from_str(swap_encoder.executor_address())
.map_err(|_| EncodingError::FatalError("Invalid executor address".to_string()))?, .map_err(|_| EncodingError::FatalError("Invalid executor address".to_string()))?,
grouped_protocol_data, initial_protocol_data,
); );
Ok(EncodedSolution { Ok(EncodedSolution {
function_signature: self.function_signature.clone(), function_signature: self.function_signature.clone(),
@@ -269,17 +278,26 @@ impl StrategyEncoder for SequentialSwapStrategyEncoder {
transfer_type: transfer, transfer_type: transfer,
}; };
let mut grouped_protocol_data: Vec<u8> = vec![]; let mut grouped_protocol_data: Vec<Vec<u8>> = vec![];
let mut initial_protocol_data: Vec<u8> = vec![];
for swap in grouped_swap.swaps.iter() { for swap in grouped_swap.swaps.iter() {
let protocol_data = swap_encoder.encode_swap(swap, &encoding_context)?; 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( let swap_data = self.encode_swap_header(
Bytes::from_str(swap_encoder.executor_address()).map_err(|_| { Bytes::from_str(swap_encoder.executor_address()).map_err(|_| {
EncodingError::FatalError("Invalid executor address".to_string()) EncodingError::FatalError("Invalid executor address".to_string())
})?, })?,
grouped_protocol_data, initial_protocol_data,
); );
swaps.push(swap_data); swaps.push(swap_data);
} }
@@ -458,10 +476,19 @@ impl StrategyEncoder for SplitSwapStrategyEncoder {
transfer_type: transfer, transfer_type: transfer,
}; };
let mut grouped_protocol_data: Vec<u8> = vec![]; let mut grouped_protocol_data: Vec<Vec<u8>> = vec![];
let mut initial_protocol_data: Vec<u8> = vec![];
for swap in grouped_swap.swaps.iter() { for swap in grouped_swap.swaps.iter() {
let protocol_data = swap_encoder.encode_swap(swap, &encoding_context)?; 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( let swap_data = self.encode_swap_header(
@@ -471,7 +498,7 @@ impl StrategyEncoder for SplitSwapStrategyEncoder {
Bytes::from_str(swap_encoder.executor_address()).map_err(|_| { Bytes::from_str(swap_encoder.executor_address()).map_err(|_| {
EncodingError::FatalError("Invalid executor address".to_string()) EncodingError::FatalError("Invalid executor address".to_string())
})?, })?,
grouped_protocol_data, initial_protocol_data,
); );
swaps.push(swap_data); swaps.push(swap_data);
} }

View File

@@ -180,10 +180,23 @@ impl SwapEncoder for UniswapV4SwapEncoder {
Ok(hook) => Address::from_slice(&hook), Ok(hook) => Address::from_slice(&hook),
Err(_) => Address::ZERO, 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 // Early check if this is not the first swap
if encoding_context.group_token_in != swap.token_in { 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()); .abi_encode_packed());
} }
@@ -206,6 +219,7 @@ impl SwapEncoder for UniswapV4SwapEncoder {
bytes_to_address(&encoding_context.receiver)?, bytes_to_address(&encoding_context.receiver)?,
hook_address, hook_address,
pool_params, pool_params,
hook_data,
); );
Ok(args.abi_encode_packed()) Ok(args.abi_encode_packed())
@@ -841,7 +855,7 @@ mod tests {
mod uniswap_v4 { mod uniswap_v4 {
use super::*; use super::*;
use crate::encoding::evm::utils::write_calldata_to_file; use crate::encoding::evm::utils::{ple_encode, write_calldata_to_file};
#[test] #[test]
fn test_encode_uniswap_v4_simple_swap() { fn test_encode_uniswap_v4_simple_swap() {
@@ -1062,8 +1076,11 @@ mod tests {
.encode_swap(&second_swap, &context) .encode_swap(&second_swap, &context)
.unwrap(); .unwrap();
let combined_hex = let combined_hex = format!(
format!("{}{}", encode(&initial_encoded_swap), encode(&second_encoded_swap)); "{}{}",
encode(&initial_encoded_swap),
encode(ple_encode(vec![second_encoded_swap]))
);
assert_eq!( assert_eq!(
combined_hex, combined_hex,
@@ -1087,6 +1104,9 @@ mod tests {
"000064", "000064",
// - tick spacing // - tick spacing
"000001", "000001",
// Second swap
// ple encoding
"001a",
// - intermediary token WBTC // - intermediary token WBTC
"2260fac5e5542a773aa44fbcfedf7c193bc2c599", "2260fac5e5542a773aa44fbcfedf7c193bc2c599",
// - fee // - fee

View File

@@ -14,6 +14,7 @@ use crate::encoding::{
SequentialSwapStrategyEncoder, SingleSwapStrategyEncoder, SplitSwapStrategyEncoder, SequentialSwapStrategyEncoder, SingleSwapStrategyEncoder, SplitSwapStrategyEncoder,
}, },
swap_encoder::swap_encoder_registry::SwapEncoderRegistry, swap_encoder::swap_encoder_registry::SwapEncoderRegistry,
utils::ple_encode,
}, },
models::{ models::{
Chain, EncodedSolution, EncodingContext, NativeAction, Solution, Transaction, TransferType, Chain, EncodedSolution, EncodingContext, NativeAction, Solution, Transaction, TransferType,
@@ -310,11 +311,12 @@ impl TychoExecutorEncoder {
)) ))
})?; })?;
let mut grouped_protocol_data: Vec<u8> = vec![]; let transfer = if IN_TRANSFER_REQUIRED_PROTOCOLS.contains(
for swap in grouped_swap.swaps.iter() { &grouped_swap.swaps[0]
let transfer = if IN_TRANSFER_REQUIRED_PROTOCOLS .component
.contains(&swap.component.protocol_system.as_str()) .protocol_system
{ .as_str(),
) {
TransferType::Transfer TransferType::Transfer
} else { } else {
TransferType::None TransferType::None
@@ -327,15 +329,26 @@ impl TychoExecutorEncoder {
group_token_out: grouped_swap.token_out.clone(), group_token_out: grouped_swap.token_out.clone(),
transfer_type: transfer, transfer_type: transfer,
}; };
let mut grouped_protocol_data: Vec<Vec<u8>> = vec![];
let mut initial_protocol_data: Vec<u8> = vec![];
for swap in grouped_swap.swaps.iter() {
let protocol_data = swap_encoder.encode_swap(swap, &encoding_context)?; 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()) let executor_address = Bytes::from_str(swap_encoder.executor_address())
.map_err(|_| EncodingError::FatalError("Invalid executor address".to_string()))?; .map_err(|_| EncodingError::FatalError("Invalid executor address".to_string()))?;
Ok(EncodedSolution { Ok(EncodedSolution {
swaps: grouped_protocol_data, swaps: initial_protocol_data,
interacting_with: executor_address, interacting_with: executor_address,
permit: None, permit: None,
function_signature: "".to_string(), function_signature: "".to_string(),
@@ -1261,6 +1274,8 @@ mod tests {
"000bb8", "000bb8",
// tick spacing // tick spacing
"00003c", "00003c",
// ple encoding
"001a",
// second pool intermediary token (PEPE) // second pool intermediary token (PEPE)
"6982508145454ce325ddbe47a25d4ec3d2311933", "6982508145454ce325ddbe47a25d4ec3d2311933",
// fee // fee