feat: Support hooks (without special calldata)

Add hook address to encoded data and then use it in execution
Add execution test for Euler

Took 1 hour 19 minutes

Took 2 hours 40 minutes

Took 3 minutes


Took 2 minutes
This commit is contained in:
Diana Carvalho
2025-06-17 17:07:52 +01:00
committed by Diana Carvalho
parent 7b48cab3cd
commit a0581773cd
5 changed files with 73 additions and 7 deletions

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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
);
}
}

View File

@@ -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",

View File

@@ -1253,6 +1253,8 @@ mod tests {
"01",
// receiver
"cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2",
// hook address (not set, so zero)
"0000000000000000000000000000000000000000",
// first pool intermediary token (ETH)
"0000000000000000000000000000000000000000",
// fee