Merge pull request #221 from propeller-heads/hooks/dc/ENG-4624-pass-hook-data

feat: Univ4 hooks
This commit is contained in:
Tamara
2025-08-27 23:31:04 -04:00
committed by GitHub
13 changed files with 270 additions and 73 deletions

View File

@@ -5,7 +5,8 @@
"pancakeswap_v2": "0xC9db3FEB380E4fd9af239e2595ECdEcE3b5c34A4", "pancakeswap_v2": "0xC9db3FEB380E4fd9af239e2595ECdEcE3b5c34A4",
"uniswap_v3": "0xbab7124C9662B15C6b9AF0b1f329907dD55a24FC", "uniswap_v3": "0xbab7124C9662B15C6b9AF0b1f329907dD55a24FC",
"pancakeswap_v3": "0x9D32e9F569B22Ae8d8C6f788037C1CD53632A059", "pancakeswap_v3": "0x9D32e9F569B22Ae8d8C6f788037C1CD53632A059",
"uniswap_v4": "0xD11496EAb53A9521f0bC1e5c1098Ecb467103Ad9", "uniswap_v4": "0x2C2EaB81Cf983602153E67b1890164BC4CABC6ed",
"uniswap_v4_hooks": "0x2C2EaB81Cf983602153E67b1890164BC4CABC6ed",
"vm:balancer_v2": "0xB5b8dc3F0a1Be99685a0DEd015Af93bFBB55C411", "vm:balancer_v2": "0xB5b8dc3F0a1Be99685a0DEd015Af93bFBB55C411",
"ekubo_v2": "0x263DD7AD20983b5E0392bf1F09C4493500EDb333", "ekubo_v2": "0x263DD7AD20983b5E0392bf1F09C4493500EDb333",
"vm:curve": "0x879F3008D96EBea0fc584aD684c7Df31777F3165", "vm:curve": "0x879F3008D96EBea0fc584aD684c7Df31777F3165",
@@ -17,12 +18,14 @@
"base": { "base": {
"uniswap_v2": "0xF744EBfaA580cF3fFc25aD046E92BD8B770a0700", "uniswap_v2": "0xF744EBfaA580cF3fFc25aD046E92BD8B770a0700",
"uniswap_v3": "0x647bffbf8bd72bf6341ecba8b0279e090313a40d", "uniswap_v3": "0x647bffbf8bd72bf6341ecba8b0279e090313a40d",
"uniswap_v4": "0xb5628b97f491f85766673ac4d5d47bb1af7fcc4a", "uniswap_v4": "0x7Dfa502736C7bd84DA1402F7524214215BC9534d",
"uniswap_v4_hooks": "0x7Dfa502736C7bd84DA1402F7524214215BC9534d",
"rfq:bebop": "0x489A3f531dA3873D6585BF3f8E0dEE48CAC6F7BC" "rfq:bebop": "0x489A3f531dA3873D6585BF3f8E0dEE48CAC6F7BC"
}, },
"unichain": { "unichain": {
"uniswap_v2": "0x00C1b81e3C8f6347E69e2DDb90454798A6Be975E", "uniswap_v2": "0x00C1b81e3C8f6347E69e2DDb90454798A6Be975E",
"uniswap_v3": "0xD26A838A41af3d4815DfD745a080B2062c4124d1", "uniswap_v3": "0xD26A838A41af3d4815DfD745a080B2062c4124d1",
"uniswap_v4": "0xF744EBfaA580cF3fFc25aD046E92BD8B770a0700" "uniswap_v4": "0x647bfFbF8Bd72bF6341ECBa8B0279e090313A40D",
"uniswap_v4_hooks": "0x647bfFbF8Bd72bF6341ECBa8B0279e090313A40D"
} }
} }

View File

@@ -6,6 +6,7 @@
"uniswap_v3": "0x2e234DAe75C793f67A35089C9d99245E1C58470b", "uniswap_v3": "0x2e234DAe75C793f67A35089C9d99245E1C58470b",
"pancakeswap_v3": "0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9", "pancakeswap_v3": "0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9",
"uniswap_v4": "0xF62849F9A0B5Bf2913b396098F7c7019b51A820a", "uniswap_v4": "0xF62849F9A0B5Bf2913b396098F7c7019b51A820a",
"uniswap_v4_hooks": "0xF62849F9A0B5Bf2913b396098F7c7019b51A820a",
"vm:balancer_v2": "0xc7183455a4C133Ae270771860664b6B7ec320bB1", "vm:balancer_v2": "0xc7183455a4C133Ae270771860664b6B7ec320bB1",
"ekubo_v2": "0xa0Cb889707d426A7A386870A03bc70d1b0697598", "ekubo_v2": "0xa0Cb889707d426A7A386870A03bc70d1b0697598",
"vm:curve": "0x1d1499e622D69689cdf9004d05Ec547d650Ff211", "vm:curve": "0x1d1499e622D69689cdf9004d05Ec547d650Ff211",

View File

@@ -65,7 +65,7 @@ For each of the following, you must select one of `tenderly_ethereum`, `tenderly
1. If the wallet that has the role, is a Gnosis Safe, you need to set the `SAFE_ADDRESS` env var. 1. If the wallet that has the role, is a Gnosis Safe, you need to set the `SAFE_ADDRESS` env var.
2. The scripts deploy-executors, remove-executor, set-roles and revoke-role all support this. 2. The scripts deploy-executors, remove-executor, set-roles and revoke-role all support this.
1. If `SAFE_ADDRESS` is set, then it will propose a transaction to the safe wallet and later on it needs to be 1. If `SAFE_ADDRESS` is set, then it will propose a transaction to the safe wallet and later on it needs to be
approved in their UI to execute on chain. approved in their UI to execute on chain. Be sure to change the PRIVATE_KEY to that which has permissions on the safe wallet.
2. If it's not set, it will submit the transaction directly to the chain. 2. If it's not set, it will submit the transaction directly to the chain.
## Deploy Uniswap X filler ## Deploy Uniswap X filler

View File

@@ -25,6 +25,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();
@@ -45,6 +46,7 @@ contract UniswapV4Executor is
using CurrencyLibrary for Currency; using CurrencyLibrary for Currency;
using V4SafeCast for *; using V4SafeCast 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,8 @@ contract UniswapV4Executor is
bool zeroForOne, bool zeroForOne,
TransferType transferType, TransferType transferType,
address receiver, address receiver,
address hook,
bytes memory hookData,
UniswapV4Executor.UniswapV4Pool[] memory pools UniswapV4Executor.UniswapV4Pool[] memory pools
) = _decodeData(data); ) = _decodeData(data);
bytes memory swapData; bytes memory swapData;
@@ -95,7 +99,7 @@ contract UniswapV4Executor is
currency1: Currency.wrap(zeroForOne ? tokenOut : tokenIn), currency1: Currency.wrap(zeroForOne ? tokenOut : tokenIn),
fee: pools[0].fee, fee: pools[0].fee,
tickSpacing: pools[0].tickSpacing, tickSpacing: pools[0].tickSpacing,
hooks: IHooks(address(0)) hooks: IHooks(hook)
}); });
swapData = abi.encodeWithSelector( swapData = abi.encodeWithSelector(
this.swapExactInputSingle.selector, this.swapExactInputSingle.selector,
@@ -104,7 +108,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);
@@ -113,8 +117,8 @@ contract UniswapV4Executor is
intermediateCurrency: Currency.wrap(pools[i].intermediaryToken), intermediateCurrency: Currency.wrap(pools[i].intermediaryToken),
fee: pools[i].fee, fee: pools[i].fee,
tickSpacing: pools[i].tickSpacing, tickSpacing: pools[i].tickSpacing,
hooks: IHooks(address(0)), hooks: IHooks(hook),
hookData: bytes("") hookData: hookData
}); });
} }
@@ -144,10 +148,12 @@ contract UniswapV4Executor is
bool zeroForOne, bool zeroForOne,
TransferType transferType, TransferType transferType,
address receiver, address receiver,
address hook,
bytes memory hookData,
UniswapV4Pool[] memory pools UniswapV4Pool[] memory pools
) )
{ {
if (data.length < 88) { if (data.length < 108) {
revert UniswapV4Executor__InvalidDataLength(); revert UniswapV4Executor__InvalidDataLength();
} }
@@ -156,25 +162,42 @@ contract UniswapV4Executor is
zeroForOne = data[40] != 0; zeroForOne = data[40] != 0;
transferType = TransferType(uint8(data[41])); transferType = TransferType(uint8(data[41]));
receiver = address(bytes20(data[42:62])); receiver = address(bytes20(data[42:62]));
hook = address(bytes20(data[62:82]));
uint256 poolsLength = (data.length - 62) / 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[62:]; 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:];
} }
/** /**

File diff suppressed because one or more lines are too long

View File

@@ -24,6 +24,8 @@ contract UniswapV4ExecutorExposed is UniswapV4Executor {
bool zeroForOne, bool zeroForOne,
RestrictTransferFrom.TransferType transferType, RestrictTransferFrom.TransferType transferType,
address receiver, address receiver,
address hook,
bytes memory hookData,
UniswapV4Pool[] memory pools UniswapV4Pool[] memory pools
) )
{ {
@@ -37,10 +39,12 @@ contract UniswapV4ExecutorTest is Constants, TestUtils {
UniswapV4ExecutorExposed uniswapV4Exposed; UniswapV4ExecutorExposed uniswapV4Exposed;
IERC20 USDE = IERC20(USDE_ADDR); IERC20 USDE = IERC20(USDE_ADDR);
IERC20 USDT = IERC20(USDT_ADDR); IERC20 USDT = IERC20(USDT_ADDR);
IERC20 USDC = IERC20(USDC_ADDR);
address poolManager = 0x000000000004444c5dc75cB358380D2e3dE08A90; address poolManager = 0x000000000004444c5dc75cB358380D2e3dE08A90;
function setUp() public { function setUp() public {
uint256 forkBlock = 21817316; uint256 forkBlock = 22689128;
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock); vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
uniswapV4Exposed = new UniswapV4ExecutorExposed( uniswapV4Exposed = new UniswapV4ExecutorExposed(
IPoolManager(poolManager), PERMIT2_ADDRESS IPoolManager(poolManager), PERMIT2_ADDRESS
@@ -73,6 +77,8 @@ contract UniswapV4ExecutorTest is Constants, TestUtils {
zeroForOne, zeroForOne,
RestrictTransferFrom.TransferType.Transfer, RestrictTransferFrom.TransferType.Transfer,
ALICE, ALICE,
address(0),
bytes(""),
pools pools
); );
@@ -82,6 +88,8 @@ contract UniswapV4ExecutorTest is Constants, TestUtils {
bool zeroForOneDecoded, bool zeroForOneDecoded,
RestrictTransferFrom.TransferType transferType, RestrictTransferFrom.TransferType transferType,
address receiver, address receiver,
address hook,
bytes memory hookData,
UniswapV4Executor.UniswapV4Pool[] memory decodedPools UniswapV4Executor.UniswapV4Pool[] memory decodedPools
) = uniswapV4Exposed.decodeData(data); ) = uniswapV4Exposed.decodeData(data);
@@ -93,6 +101,7 @@ contract UniswapV4ExecutorTest is Constants, TestUtils {
uint8(RestrictTransferFrom.TransferType.Transfer) uint8(RestrictTransferFrom.TransferType.Transfer)
); );
assertEq(receiver, ALICE); assertEq(receiver, ALICE);
assertEq(hook, address(0));
assertEq(decodedPools.length, 2); assertEq(decodedPools.length, 2);
assertEq(decodedPools[0].intermediaryToken, USDT_ADDR); assertEq(decodedPools[0].intermediaryToken, USDT_ADDR);
assertEq(decodedPools[0].fee, pool1Fee); assertEq(decodedPools[0].fee, pool1Fee);
@@ -123,6 +132,8 @@ contract UniswapV4ExecutorTest is Constants, TestUtils {
true, true,
RestrictTransferFrom.TransferType.Transfer, RestrictTransferFrom.TransferType.Transfer,
ALICE, ALICE,
address(0),
bytes(""),
pools pools
); );
@@ -180,6 +191,8 @@ contract UniswapV4ExecutorTest is Constants, TestUtils {
true, true,
RestrictTransferFrom.TransferType.Transfer, RestrictTransferFrom.TransferType.Transfer,
ALICE, ALICE,
address(0),
bytes(""),
pools pools
); );
@@ -211,6 +224,43 @@ contract UniswapV4ExecutorTest is Constants, TestUtils {
); );
assertTrue(IERC20(WBTC_ADDR).balanceOf(ALICE) == amountOut); 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,
bytes(""),
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 { contract TychoRouterForBalancerV3Test is TychoRouterTestSetup {
@@ -237,6 +287,8 @@ contract TychoRouterForBalancerV3Test is TychoRouterTestSetup {
true, true,
RestrictTransferFrom.TransferType.TransferFrom, RestrictTransferFrom.TransferType.TransferFrom,
ALICE, ALICE,
address(0),
bytes(""),
pools pools
); );
@@ -285,6 +337,8 @@ contract TychoRouterForBalancerV3Test is TychoRouterTestSetup {
true, true,
RestrictTransferFrom.TransferType.TransferFrom, RestrictTransferFrom.TransferType.TransferFrom,
ALICE, ALICE,
address(0),
bytes(""),
pools pools
); );

View File

@@ -10,13 +10,21 @@ library UniswapV4Utils {
bool zeroForOne, bool zeroForOne,
RestrictTransferFrom.TransferType transferType, RestrictTransferFrom.TransferType transferType,
address receiver, address receiver,
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
@@ -24,7 +32,28 @@ library UniswapV4Utils {
} }
return abi.encodePacked( return abi.encodePacked(
tokenIn, tokenOut, zeroForOne, transferType, receiver, encodedPools tokenIn,
tokenOut,
zeroForOne,
transferType,
receiver,
hook,
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

@@ -13,6 +13,7 @@ pub const PROTOCOL_SPECIFIC_CONFIG: &str =
pub static GROUPABLE_PROTOCOLS: LazyLock<HashSet<&'static str>> = LazyLock::new(|| { pub static GROUPABLE_PROTOCOLS: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
let mut set = HashSet::new(); let mut set = HashSet::new();
set.insert("uniswap_v4"); set.insert("uniswap_v4");
set.insert("uniswap_v4_hooks");
set.insert("vm:balancer_v3"); set.insert("vm:balancer_v3");
set.insert("ekubo_v2"); set.insert("ekubo_v2");
set set
@@ -29,6 +30,7 @@ pub static IN_TRANSFER_REQUIRED_PROTOCOLS: LazyLock<HashSet<&'static str>> = Laz
set.insert("uniswap_v3"); set.insert("uniswap_v3");
set.insert("pancakeswap_v3"); set.insert("pancakeswap_v3");
set.insert("uniswap_v4"); set.insert("uniswap_v4");
set.insert("uniswap_v4_hooks");
set.insert("ekubo_v2"); set.insert("ekubo_v2");
set.insert("vm:maverick_v2"); set.insert("vm:maverick_v2");
set.insert("vm:balancer_v3"); set.insert("vm:balancer_v3");
@@ -46,6 +48,7 @@ pub static CALLBACK_CONSTRAINED_PROTOCOLS: LazyLock<HashSet<&'static str>> = Laz
set.insert("uniswap_v3"); set.insert("uniswap_v3");
set.insert("pancakeswap_v3"); set.insert("pancakeswap_v3");
set.insert("uniswap_v4"); set.insert("uniswap_v4");
set.insert("uniswap_v4_hooks");
set.insert("ekubo_v2"); set.insert("ekubo_v2");
set.insert("vm:balancer_v3"); set.insert("vm:balancer_v3");
set set

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(),
@@ -272,17 +281,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);
} }
@@ -463,10 +481,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(
@@ -476,7 +503,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

@@ -72,6 +72,11 @@ impl SwapEncoderBuilder {
self.chain, self.chain,
self.config, self.config,
)?)), )?)),
"uniswap_v4_hooks" => Ok(Box::new(UniswapV4SwapEncoder::new(
self.executor_address,
self.chain,
self.config,
)?)),
"ekubo_v2" => { "ekubo_v2" => {
Ok(Box::new(EkuboSwapEncoder::new(self.executor_address, self.chain, self.config)?)) Ok(Box::new(EkuboSwapEncoder::new(self.executor_address, self.chain, self.config)?))
} }

View File

@@ -185,9 +185,28 @@ impl SwapEncoder for UniswapV4SwapEncoder {
EncodingError::FatalError("Failed to pad tick spacing bytes".to_string()) EncodingError::FatalError("Failed to pad tick spacing bytes".to_string())
})?; })?;
let hook_address = match get_static_attribute(swap, "hooks") {
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
.clone()
.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());
} }
@@ -208,7 +227,9 @@ impl SwapEncoder for UniswapV4SwapEncoder {
zero_to_one, zero_to_one,
(encoding_context.transfer_type as u8).to_be_bytes(), (encoding_context.transfer_type as u8).to_be_bytes(),
bytes_to_address(&encoding_context.receiver)?, bytes_to_address(&encoding_context.receiver)?,
hook_address,
pool_params, pool_params,
hook_data,
); );
Ok(args.abi_encode_packed()) Ok(args.abi_encode_packed())
@@ -1155,6 +1176,7 @@ mod tests {
mod uniswap_v4 { mod uniswap_v4 {
use super::*; use super::*;
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() {
@@ -1210,6 +1232,8 @@ mod tests {
"01", "01",
// receiver // receiver
"cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2", "cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2",
// hook address (not set, so zero)
"0000000000000000000000000000000000000000",
// pool params: // pool params:
// - intermediary token // - intermediary token
"dac17f958d2ee523a2206206994597c13d831ec7", "dac17f958d2ee523a2206206994597c13d831ec7",
@@ -1352,8 +1376,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,
@@ -1368,6 +1395,8 @@ mod tests {
"01", "01",
// receiver // receiver
"cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2", "cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2",
// hook address (not set, so zero)
"0000000000000000000000000000000000000000",
// pool params: // pool params:
// - intermediary token USDT // - intermediary token USDT
"dac17f958d2ee523a2206206994597c13d831ec7", "dac17f958d2ee523a2206206994597c13d831ec7",
@@ -1375,6 +1404,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::{
EncodedSolution, EncodingContext, NativeAction, Solution, Transaction, TransferType, EncodedSolution, EncodingContext, NativeAction, Solution, Transaction, TransferType,
@@ -313,32 +314,44 @@ impl TychoExecutorEncoder {
)) ))
})?; })?;
let mut grouped_protocol_data: Vec<u8> = 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<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 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)?; 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(),
@@ -1233,12 +1246,16 @@ mod tests {
"01", "01",
// receiver // receiver
"cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2", "cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2",
// hook address (not set, so zero)
"0000000000000000000000000000000000000000",
// first pool intermediary token (ETH) // first pool intermediary token (ETH)
"0000000000000000000000000000000000000000", "0000000000000000000000000000000000000000",
// fee // fee
"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

View File

@@ -373,7 +373,7 @@ fn test_single_encoding_strategy_usv4_grouped_swap() {
let expected_swaps = String::from(concat!( let expected_swaps = String::from(concat!(
// length of ple encoded swaps without padding // length of ple encoded swaps without padding
"0000000000000000000000000000000000000000000000000000000000000086", "000000000000000000000000000000000000000000000000000000000000009c",
// Swap data header // Swap data header
"f62849f9a0b5bf2913b396098f7c7019b51a820a", // executor address "f62849f9a0b5bf2913b396098f7c7019b51a820a", // executor address
// Protocol data // Protocol data
@@ -382,15 +382,18 @@ fn test_single_encoding_strategy_usv4_grouped_swap() {
"00", // zero2one "00", // zero2one
"00", // transfer type TransferFrom "00", // transfer type TransferFrom
"cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2", // receiver "cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2", // receiver
"0000000000000000000000000000000000000000", // hook address
// First pool params // First pool params
"0000000000000000000000000000000000000000", // intermediary token (ETH) "0000000000000000000000000000000000000000", // intermediary token (ETH)
"000bb8", // fee "000bb8", // fee
"00003c", // tick spacing "00003c", // tick spacing
// ple encoding
"001a",
// Second pool params // Second pool params
"6982508145454ce325ddbe47a25d4ec3d2311933", // intermediary token (PEPE) "6982508145454ce325ddbe47a25d4ec3d2311933", // intermediary token (PEPE)
"0061a8", // fee "0061a8", // fee
"0001f4", // tick spacing "0001f4", // tick spacing
"0000000000000000000000000000000000000000000000000000" // padding "00000000" // padding
)); ));
let hex_calldata = encode(&calldata); let hex_calldata = encode(&calldata);