Merge pull request #279 from propeller-heads/euler/dc/ENG-5076-fix-uniswapv4-executor

feat: Support Euler low balance swaps (univ4)
This commit is contained in:
dianacarvalho1
2025-10-09 12:30:59 +02:00
committed by GitHub
8 changed files with 318 additions and 147 deletions

View File

@@ -59,6 +59,8 @@ contract UniswapV4Executor is
address intermediaryToken;
uint24 fee;
int24 tickSpacing;
address hook;
bytes hookData;
}
constructor(IPoolManager _poolManager, address _permit2)
@@ -89,8 +91,6 @@ contract UniswapV4Executor is
bool zeroForOne,
TransferType transferType,
address receiver,
address hook,
bytes memory hookData,
UniswapV4Executor.UniswapV4Pool[] memory pools
) = _decodeData(data);
bytes memory swapData;
@@ -100,7 +100,7 @@ contract UniswapV4Executor is
currency1: Currency.wrap(zeroForOne ? tokenOut : tokenIn),
fee: pools[0].fee,
tickSpacing: pools[0].tickSpacing,
hooks: IHooks(hook)
hooks: IHooks(pools[0].hook)
});
swapData = abi.encodeWithSelector(
this.swapExactInputSingle.selector,
@@ -109,7 +109,7 @@ contract UniswapV4Executor is
amountIn,
transferType,
receiver,
hookData
pools[0].hookData
);
} else {
PathKey[] memory path = new PathKey[](pools.length);
@@ -118,8 +118,8 @@ contract UniswapV4Executor is
intermediateCurrency: Currency.wrap(pools[i].intermediaryToken),
fee: pools[i].fee,
tickSpacing: pools[i].tickSpacing,
hooks: IHooks(hook),
hookData: hookData
hooks: IHooks(pools[i].hook),
hookData: pools[i].hookData
});
}
@@ -149,8 +149,6 @@ contract UniswapV4Executor is
bool zeroForOne,
TransferType transferType,
address receiver,
address hook,
bytes memory hookData,
UniswapV4Pool[] memory pools
)
{
@@ -163,42 +161,71 @@ contract UniswapV4Executor is
zeroForOne = data[40] != 0;
transferType = TransferType(uint8(data[41]));
receiver = address(bytes20(data[42:62]));
hook = address(bytes20(data[62:82]));
bytes calldata remaining = data[82:];
bytes calldata remaining = data[62:];
// Decode first pool with hook data
if (remaining.length < 48) {
// 20 + 3 + 3 + 20 + 2 = 48 minimum
revert UniswapV4Executor__InvalidDataLength();
}
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);
address firstHook = address(bytes20(remaining[26:46]));
uint16 firstHookDataLength = uint16(bytes2(remaining[46:48]));
uint256 firstPoolTotalLength = 48 + firstHookDataLength;
if (remaining.length < firstPoolTotalLength) {
revert UniswapV4Executor__InvalidDataLength();
}
bytes memory firstHookData = remaining[48:48 + firstHookDataLength];
// Remaining after first pool are ple encoded
bytes[] memory encodedPools =
LibPrefixLengthEncodedByteArray.toArray(remaining[26:]);
bytes[] memory encodedPools = LibPrefixLengthEncodedByteArray.toArray(
remaining[firstPoolTotalLength:]
);
pools = new UniswapV4Pool[](1 + encodedPools.length);
pools[0] = firstPool;
uint256 encodedPoolsLength = 26;
uint256 plePoolsTotalLength;
pools[0] = UniswapV4Pool(
firstToken, firstFee, firstTickSpacing, firstHook, firstHookData
);
// Decode subsequent pools
for (uint256 i = 0; i < encodedPools.length; i++) {
bytes memory poolsData = encodedPools[i];
bytes memory poolData = encodedPools[i];
address intermediaryToken;
uint24 fee;
int24 tickSpacing;
address hook;
uint16 hookDataLength;
// slither-disable-next-line assembly
assembly {
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))))
let dataPtr := add(poolData, 0x20)
intermediaryToken := shr(96, mload(dataPtr))
fee := and(shr(232, mload(add(dataPtr, 20))), 0xffffff)
tickSpacing := and(shr(208, mload(add(dataPtr, 20))), 0xffffff)
hook := shr(96, mload(add(dataPtr, 26)))
hookDataLength := and(shr(240, mload(add(dataPtr, 46))), 0xffff)
}
pools[i + 1] = UniswapV4Pool(intermediaryToken, fee, tickSpacing);
plePoolsTotalLength += 2 + encodedPoolsLength; // 2 bytes prefix + data
}
hookData = remaining[26 + plePoolsTotalLength:];
if (poolData.length < 48 + hookDataLength) {
revert UniswapV4Executor__InvalidDataLength();
}
bytes memory hookData = new bytes(hookDataLength);
for (uint256 j = 0; j < hookDataLength; j++) {
hookData[j] = poolData[48 + j];
}
pools[i + 1] = UniswapV4Pool(
intermediaryToken, fee, tickSpacing, hook, hookData
);
}
}
/**
@@ -275,17 +302,12 @@ contract UniswapV4Executor is
address receiver,
bytes calldata hookData
) external returns (uint128) {
Currency currencyIn = zeroForOne ? poolKey.currency0 : poolKey.currency1;
_settle(currencyIn, amountIn, transferType);
uint128 amountOut = _swap(
poolKey, zeroForOne, -int256(uint256(amountIn)), hookData
).toUint128();
Currency currencyIn = zeroForOne ? poolKey.currency0 : poolKey.currency1;
uint256 amount = _getFullDebt(currencyIn);
if (amount > amountIn) {
revert UniswapV4Executor__V4TooMuchRequested(amountIn, amount);
}
_settle(currencyIn, amount, transferType);
Currency currencyOut =
zeroForOne ? poolKey.currency1 : poolKey.currency0;
_take(currencyOut, receiver, _mapTakeAmount(amountOut, currencyOut));
@@ -310,6 +332,7 @@ contract UniswapV4Executor is
uint128 amountOut = 0;
Currency swapCurrencyIn = currencyIn;
uint256 swapAmountIn = amountIn;
_settle(currencyIn, amountIn, transferType);
unchecked {
uint256 pathLength = path.length;
PathKey calldata pathKey;
@@ -331,12 +354,6 @@ contract UniswapV4Executor is
}
}
uint256 amount = _getFullDebt(currencyIn);
if (amount > amountIn) {
revert UniswapV4Executor__V4TooMuchRequested(amountIn, amount);
}
_settle(currencyIn, amount, transferType);
_take(
swapCurrencyIn, // at the end of the loop this is actually currency out
receiver,
@@ -387,21 +404,6 @@ contract UniswapV4Executor is
amount = uint256(_amount);
}
/// @notice Obtain the full amount owed by this contract (negative delta)
/// @param currency Currency to get the delta for
/// @return amount The amount owed by this contract as a uint256
function _getFullDebt(Currency currency)
internal
view
returns (uint256 amount)
{
int256 _amount = poolManager.currencyDelta(address(this), currency);
// If the amount is positive, it should be taken not settled.
if (_amount > 0) revert UniswapV4Executor__DeltaNotNegative(currency);
// Casting is safe due to limits on the total supply of a pool
amount = uint256(-_amount);
}
/**
* @notice Pays and settles a currency to the pool manager.
* @dev The implementing contract must ensure that the `payer` is a secure address.

View File

@@ -56,6 +56,7 @@ contract Constants is Test, BaseConstants {
address BSGG_ADDR = address(0xdA16Cf041E2780618c49Dbae5d734B89a6Bac9b3);
address GHO_ADDR = address(0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f);
address ONDO_ADDR = address(0xfAbA6f8e4a5E8Ab82F62fe7C39859FA577269BE3);
address RLUSD_ADDR = address(0x8292Bb45bf1Ee4d140127049757C2E0fF06317eD);
// Maverick v2
address MAVERICK_V2_FACTORY = 0x0A7e848Aca42d879EF06507Fca0E7b33A0a63c1e;
@@ -146,6 +147,9 @@ contract Constants is Test, BaseConstants {
// Curve meta registry
address CURVE_META_REGISTRY = 0xF98B45FA17DE75FB1aD0e7aFD971b0ca00e379fC;
// Uniswap v4 pool manager
address POOL_MANAGER = 0x000000000004444c5dc75cB358380D2e3dE08A90;
/**
* @dev Deploys a dummy contract with non-empty bytecode
*/

File diff suppressed because one or more lines are too long

View File

@@ -11,8 +11,8 @@ import {SafeCallback} from "@uniswap/v4-periphery/src/base/SafeCallback.sol";
import {Test} from "../../lib/forge-std/src/Test.sol";
contract UniswapV4ExecutorExposed is UniswapV4Executor {
constructor(IPoolManager _poolManager, address _permit2)
UniswapV4Executor(_poolManager, _permit2)
constructor(IPoolManager _POOL_MANAGER, address _permit2)
UniswapV4Executor(_POOL_MANAGER, _permit2)
{}
function decodeData(bytes calldata data)
@@ -24,8 +24,6 @@ contract UniswapV4ExecutorExposed is UniswapV4Executor {
bool zeroForOne,
RestrictTransferFrom.TransferType transferType,
address receiver,
address hook,
bytes memory hookData,
UniswapV4Pool[] memory pools
)
{
@@ -41,13 +39,11 @@ contract UniswapV4ExecutorTest is Constants, TestUtils {
IERC20 USDT = IERC20(USDT_ADDR);
IERC20 USDC = IERC20(USDC_ADDR);
address poolManager = 0x000000000004444c5dc75cB358380D2e3dE08A90;
function setUp() public {
uint256 forkBlock = 22689128;
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
uniswapV4Exposed = new UniswapV4ExecutorExposed(
IPoolManager(poolManager), PERMIT2_ADDRESS
IPoolManager(POOL_MANAGER), PERMIT2_ADDRESS
);
}
@@ -63,12 +59,16 @@ contract UniswapV4ExecutorTest is Constants, TestUtils {
pools[0] = UniswapV4Executor.UniswapV4Pool({
intermediaryToken: USDT_ADDR,
fee: pool1Fee,
tickSpacing: tickSpacing1
tickSpacing: tickSpacing1,
hook: address(0),
hookData: bytes("")
});
pools[1] = UniswapV4Executor.UniswapV4Pool({
intermediaryToken: USDE_ADDR,
fee: pool2Fee,
tickSpacing: tickSpacing2
tickSpacing: tickSpacing2,
hook: address(0),
hookData: bytes("0x12345")
});
bytes memory data = UniswapV4Utils.encodeExactInput(
@@ -77,8 +77,6 @@ contract UniswapV4ExecutorTest is Constants, TestUtils {
zeroForOne,
RestrictTransferFrom.TransferType.Transfer,
ALICE,
address(0),
bytes(""),
pools
);
@@ -88,8 +86,6 @@ contract UniswapV4ExecutorTest is Constants, TestUtils {
bool zeroForOneDecoded,
RestrictTransferFrom.TransferType transferType,
address receiver,
address hook,
bytes memory hookData,
UniswapV4Executor.UniswapV4Pool[] memory decodedPools
) = uniswapV4Exposed.decodeData(data);
@@ -101,7 +97,7 @@ contract UniswapV4ExecutorTest is Constants, TestUtils {
uint8(RestrictTransferFrom.TransferType.Transfer)
);
assertEq(receiver, ALICE);
assertEq(hook, address(0));
assertEq(decodedPools[0].hook, address(0));
assertEq(decodedPools.length, 2);
assertEq(decodedPools[0].intermediaryToken, USDT_ADDR);
assertEq(decodedPools[0].fee, pool1Fee);
@@ -109,12 +105,13 @@ contract UniswapV4ExecutorTest is Constants, TestUtils {
assertEq(decodedPools[1].intermediaryToken, USDE_ADDR);
assertEq(decodedPools[1].fee, pool2Fee);
assertEq(decodedPools[1].tickSpacing, tickSpacing2);
assertEq(decodedPools[1].hookData, bytes("0x12345"));
}
function testSingleSwap() public {
uint256 amountIn = 100 ether;
deal(USDE_ADDR, address(uniswapV4Exposed), amountIn);
uint256 usdeBalanceBeforePool = USDE.balanceOf(poolManager);
uint256 usdeBalanceBeforePool = USDE.balanceOf(POOL_MANAGER);
uint256 usdeBalanceBeforeSwapExecutor =
USDE.balanceOf(address(uniswapV4Exposed));
@@ -123,7 +120,9 @@ contract UniswapV4ExecutorTest is Constants, TestUtils {
pools[0] = UniswapV4Executor.UniswapV4Pool({
intermediaryToken: USDT_ADDR,
fee: uint24(100),
tickSpacing: int24(1)
tickSpacing: int24(1),
hook: address(0),
hookData: bytes("")
});
bytes memory data = UniswapV4Utils.encodeExactInput(
@@ -132,13 +131,11 @@ contract UniswapV4ExecutorTest is Constants, TestUtils {
true,
RestrictTransferFrom.TransferType.Transfer,
ALICE,
address(0),
bytes(""),
pools
);
uint256 amountOut = uniswapV4Exposed.swap(amountIn, data);
assertEq(USDE.balanceOf(poolManager), usdeBalanceBeforePool + amountIn);
assertEq(USDE.balanceOf(POOL_MANAGER), usdeBalanceBeforePool + amountIn);
assertEq(
USDE.balanceOf(address(uniswapV4Exposed)),
usdeBalanceBeforeSwapExecutor - amountIn
@@ -152,12 +149,12 @@ contract UniswapV4ExecutorTest is Constants, TestUtils {
loadCallDataFromFile("test_encode_uniswap_v4_simple_swap");
uint256 amountIn = 100 ether;
deal(USDE_ADDR, address(uniswapV4Exposed), amountIn);
uint256 usdeBalanceBeforePool = USDE.balanceOf(poolManager);
uint256 usdeBalanceBeforePool = USDE.balanceOf(POOL_MANAGER);
uint256 usdeBalanceBeforeSwapExecutor =
USDE.balanceOf(address(uniswapV4Exposed));
uint256 amountOut = uniswapV4Exposed.swap(amountIn, protocolData);
assertEq(USDE.balanceOf(poolManager), usdeBalanceBeforePool + amountIn);
assertEq(USDE.balanceOf(POOL_MANAGER), usdeBalanceBeforePool + amountIn);
assertEq(
USDE.balanceOf(ALICE), usdeBalanceBeforeSwapExecutor - amountIn
);
@@ -168,7 +165,7 @@ contract UniswapV4ExecutorTest is Constants, TestUtils {
// USDE -> USDT -> WBTC
uint256 amountIn = 100 ether;
deal(USDE_ADDR, address(uniswapV4Exposed), amountIn);
uint256 usdeBalanceBeforePool = USDE.balanceOf(poolManager);
uint256 usdeBalanceBeforePool = USDE.balanceOf(POOL_MANAGER);
uint256 usdeBalanceBeforeSwapExecutor =
USDE.balanceOf(address(uniswapV4Exposed));
@@ -177,12 +174,16 @@ contract UniswapV4ExecutorTest is Constants, TestUtils {
pools[0] = UniswapV4Executor.UniswapV4Pool({
intermediaryToken: USDT_ADDR,
fee: uint24(100),
tickSpacing: int24(1)
tickSpacing: int24(1),
hook: address(0),
hookData: bytes("")
});
pools[1] = UniswapV4Executor.UniswapV4Pool({
intermediaryToken: WBTC_ADDR,
fee: uint24(3000),
tickSpacing: int24(60)
tickSpacing: int24(60),
hook: address(0),
hookData: bytes("")
});
bytes memory data = UniswapV4Utils.encodeExactInput(
@@ -191,13 +192,11 @@ contract UniswapV4ExecutorTest is Constants, TestUtils {
true,
RestrictTransferFrom.TransferType.Transfer,
ALICE,
address(0),
bytes(""),
pools
);
uint256 amountOut = uniswapV4Exposed.swap(amountIn, data);
assertEq(USDE.balanceOf(poolManager), usdeBalanceBeforePool + amountIn);
assertEq(USDE.balanceOf(POOL_MANAGER), usdeBalanceBeforePool + amountIn);
assertEq(
USDE.balanceOf(address(uniswapV4Exposed)),
usdeBalanceBeforeSwapExecutor - amountIn
@@ -212,12 +211,12 @@ contract UniswapV4ExecutorTest is Constants, TestUtils {
uint256 amountIn = 100 ether;
deal(USDE_ADDR, address(uniswapV4Exposed), amountIn);
uint256 usdeBalanceBeforePool = USDE.balanceOf(poolManager);
uint256 usdeBalanceBeforePool = USDE.balanceOf(POOL_MANAGER);
uint256 usdeBalanceBeforeSwapExecutor =
USDE.balanceOf(address(uniswapV4Exposed));
uint256 amountOut = uniswapV4Exposed.swap(amountIn, protocolData);
assertEq(USDE.balanceOf(poolManager), usdeBalanceBeforePool + amountIn);
assertEq(USDE.balanceOf(POOL_MANAGER), usdeBalanceBeforePool + amountIn);
assertEq(
USDE.balanceOf(address(uniswapV4Exposed)),
usdeBalanceBeforeSwapExecutor - amountIn
@@ -239,7 +238,9 @@ contract UniswapV4ExecutorTest is Constants, TestUtils {
pools[0] = UniswapV4Executor.UniswapV4Pool({
intermediaryToken: WETH_ADDR,
fee: uint24(500),
tickSpacing: int24(1)
tickSpacing: int24(1),
hook: hook,
hookData: bytes("")
});
bytes memory data = UniswapV4Utils.encodeExactInput(
@@ -248,8 +249,6 @@ contract UniswapV4ExecutorTest is Constants, TestUtils {
true,
RestrictTransferFrom.TransferType.Transfer,
ALICE,
hook,
bytes(""),
pools
);
@@ -267,6 +266,141 @@ contract UniswapV4ExecutorTest is Constants, TestUtils {
}
}
contract UniswapV4ExecutorTestForEuler is Constants, TestUtils {
/* These tests are necessary because Euler works a little differently from general UniswapV4 logic.
In the previous version of the UniswapV4Executor we are only sending the user's tokens into the Pool Manager
after we call swap on it. This is ok because the Pool Manager tracks the debts and accepts everything as long
as the tokens are transfers inside of the unlock callback. However, Euler expects the funds to already be
in the Pool Manager when beforeSwap is called. This is not a problem for tokens that the Pool Manager has a
lot of, but for tokens with low balances this makes the tx fail. We need to transfer the tokens into
the Pool Manager before we call swap on it.
The only risk here is that we are assuming that the amount_in will never change. In the previous version, we
were confirming this amount with the currencyDelta of the Pool Manager. Now we pray.
*/
using SafeERC20 for IERC20;
UniswapV4ExecutorExposed uniswapV4Exposed;
IERC20 USDT = IERC20(USDT_ADDR);
IERC20 RLUSD = IERC20(RLUSD_ADDR);
IERC20 WBTC = IERC20(WBTC_ADDR);
function setUp() public {
uint256 forkBlock = 23535338;
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
uniswapV4Exposed = new UniswapV4ExecutorExposed(
IPoolManager(POOL_MANAGER), PERMIT2_ADDRESS
);
}
function testSingleSwapEulerLowBalance() public {
uint256 amountIn = 134187695711754971245517404;
deal(RLUSD_ADDR, address(uniswapV4Exposed), amountIn);
address eulerProxy = 0xe1Ce9AF672f8854845E5474400B6ddC7AE458a10;
uint256 rlusdEulerBalanceBefore = RLUSD.balanceOf(eulerProxy);
UniswapV4Executor.UniswapV4Pool[] memory pools =
new UniswapV4Executor.UniswapV4Pool[](1);
pools[0] = UniswapV4Executor.UniswapV4Pool({
intermediaryToken: USDT_ADDR,
fee: uint24(50),
tickSpacing: int24(1),
hook: address(0xF87ACF8428F2f9403AAA0256A7272d6549ECa8A8),
hookData: bytes("")
});
bytes memory data = UniswapV4Utils.encodeExactInput(
RLUSD_ADDR,
USDT_ADDR,
true,
RestrictTransferFrom.TransferType.Transfer,
ALICE,
pools
);
uint256 amountOut = uniswapV4Exposed.swap(amountIn, data);
assertEq(
RLUSD.balanceOf(eulerProxy), rlusdEulerBalanceBefore + amountIn
);
assertTrue(USDT.balanceOf(ALICE) == amountOut);
}
function testMultipleSwapEulerLowBalance() public {
// RLUSD -(euler)-> USDT -> WBTC
uint256 amountIn = 134187695711754971245517404;
deal(RLUSD_ADDR, address(uniswapV4Exposed), amountIn);
address eulerProxy = 0xe1Ce9AF672f8854845E5474400B6ddC7AE458a10;
uint256 rlusdEulerBalanceBefore = RLUSD.balanceOf(eulerProxy);
UniswapV4Executor.UniswapV4Pool[] memory pools =
new UniswapV4Executor.UniswapV4Pool[](2);
pools[0] = UniswapV4Executor.UniswapV4Pool({
intermediaryToken: USDT_ADDR,
fee: uint24(50),
tickSpacing: int24(1),
hook: address(0xF87ACF8428F2f9403AAA0256A7272d6549ECa8A8),
hookData: bytes("")
});
pools[1] = UniswapV4Executor.UniswapV4Pool({
intermediaryToken: WBTC_ADDR,
fee: uint24(3000),
tickSpacing: int24(60),
hook: address(0),
hookData: bytes("")
});
bytes memory data = UniswapV4Utils.encodeExactInput(
RLUSD_ADDR,
WBTC_ADDR,
true,
RestrictTransferFrom.TransferType.Transfer,
ALICE,
pools
);
uint256 amountOut = uniswapV4Exposed.swap(amountIn, data);
assertEq(
RLUSD.balanceOf(eulerProxy), rlusdEulerBalanceBefore + amountIn
);
assertTrue(WBTC.balanceOf(ALICE) == amountOut);
}
function testMultipleSwapLastSwapEuler() public {
// USDC -> RLUSD -(euler)- > USDT
// Sanity check to see if a grouped swap with Euler in the last hop works
uint256 amountIn = 134187695711754971245517404;
deal(USDC_ADDR, address(uniswapV4Exposed), amountIn);
UniswapV4Executor.UniswapV4Pool[] memory pools =
new UniswapV4Executor.UniswapV4Pool[](2);
pools[0] = UniswapV4Executor.UniswapV4Pool({
intermediaryToken: RLUSD_ADDR,
fee: uint24(500),
tickSpacing: int24(10),
hook: address(0),
hookData: bytes("")
});
pools[1] = UniswapV4Executor.UniswapV4Pool({
intermediaryToken: USDT_ADDR,
fee: uint24(50),
tickSpacing: int24(1),
hook: address(0xF87ACF8428F2f9403AAA0256A7272d6549ECa8A8),
hookData: bytes("")
});
bytes memory data = UniswapV4Utils.encodeExactInput(
USDC_ADDR,
USDT_ADDR,
false,
RestrictTransferFrom.TransferType.Transfer,
ALICE,
pools
);
uint256 amountOut = uniswapV4Exposed.swap(amountIn, data);
assertTrue(USDT.balanceOf(ALICE) == amountOut);
}
}
contract TychoRouterForBalancerV3Test is TychoRouterTestSetup {
function testSingleSwapUSV4CallbackPermit2() public {
vm.startPrank(ALICE);
@@ -282,7 +416,9 @@ contract TychoRouterForBalancerV3Test is TychoRouterTestSetup {
pools[0] = UniswapV4Executor.UniswapV4Pool({
intermediaryToken: USDT_ADDR,
fee: uint24(100),
tickSpacing: int24(1)
tickSpacing: int24(1),
hook: address(0),
hookData: bytes("")
});
bytes memory protocolData = UniswapV4Utils.encodeExactInput(
@@ -291,8 +427,6 @@ contract TychoRouterForBalancerV3Test is TychoRouterTestSetup {
true,
RestrictTransferFrom.TransferType.TransferFrom,
ALICE,
address(0),
bytes(""),
pools
);
@@ -327,12 +461,16 @@ contract TychoRouterForBalancerV3Test is TychoRouterTestSetup {
pools[0] = UniswapV4Executor.UniswapV4Pool({
intermediaryToken: USDT_ADDR,
fee: uint24(100),
tickSpacing: int24(1)
tickSpacing: int24(1),
hook: address(0),
hookData: bytes("")
});
pools[1] = UniswapV4Executor.UniswapV4Pool({
intermediaryToken: WBTC_ADDR,
fee: uint24(3000),
tickSpacing: int24(60)
tickSpacing: int24(60),
hook: address(0),
hookData: bytes("")
});
bytes memory protocolData = UniswapV4Utils.encodeExactInput(
@@ -341,8 +479,6 @@ contract TychoRouterForBalancerV3Test is TychoRouterTestSetup {
true,
RestrictTransferFrom.TransferType.TransferFrom,
ALICE,
address(0),
bytes(""),
pools
);

View File

@@ -10,8 +10,6 @@ library UniswapV4Utils {
bool zeroForOne,
RestrictTransferFrom.TransferType transferType,
address receiver,
address hook,
bytes memory hookData,
UniswapV4Executor.UniswapV4Pool[] memory pools
) public pure returns (bytes memory) {
require(pools.length > 0, "Must have at least one pool");
@@ -19,7 +17,10 @@ library UniswapV4Utils {
bytes memory firstPool = abi.encodePacked(
pools[0].intermediaryToken,
bytes3(pools[0].fee),
pools[0].tickSpacing
pools[0].tickSpacing,
pools[0].hook,
bytes2(uint16(pools[0].hookData.length)),
pools[0].hookData
);
bytes[] memory encodedExtraPools = new bytes[](pools.length - 1);
@@ -27,7 +28,10 @@ library UniswapV4Utils {
encodedExtraPools[i - 1] = abi.encodePacked(
pools[i].intermediaryToken,
bytes3(pools[i].fee),
pools[i].tickSpacing
pools[i].tickSpacing,
pools[i].hook,
bytes2(uint16(pools[i].hookData.length)),
pools[i].hookData
);
}
@@ -37,10 +41,8 @@ library UniswapV4Utils {
zeroForOne,
transferType,
receiver,
hook,
firstPool,
pleEncode(encodedExtraPools),
hookData
pleEncode(encodedExtraPools)
);
}

View File

@@ -189,23 +189,24 @@ 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
.clone()
.unwrap_or_default()
.to_vec(),
);
}
let hook_data = swap
.user_data
.clone()
.unwrap_or_default()
.to_vec();
let hook_data_length = (hook_data.len() as u16).to_be_bytes();
// 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,
hook_data,
hook_address,
hook_data_length,
AlloyBytes::from(hook_data),
)
.abi_encode_packed());
}
@@ -218,8 +219,15 @@ impl SwapEncoder for UniswapV4SwapEncoder {
let zero_to_one = Self::get_zero_to_one(token_in_address, token_out_address);
let pool_params =
(token_out_address, pool_fee_u24, pool_tick_spacing_u24).abi_encode_packed();
let pool_params = (
token_out_address,
pool_fee_u24,
pool_tick_spacing_u24,
hook_address,
hook_data_length,
AlloyBytes::from(hook_data),
)
.abi_encode_packed();
let args = (
group_token_in_address,
@@ -227,9 +235,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,
hook_data,
);
Ok(args.abi_encode_packed())
@@ -1249,15 +1255,17 @@ mod tests {
"01",
// receiver
"cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2",
// hook address (not set, so zero)
"0000000000000000000000000000000000000000",
// pool params:
// - intermediary token
"dac17f958d2ee523a2206206994597c13d831ec7",
// - fee
"000064",
// - tick spacing
"000001"
"000001",
// hook address (not set, so zero)
"0000000000000000000000000000000000000000",
// hook data length (0)
"0000"
))
);
write_calldata_to_file("test_encode_uniswap_v4_simple_swap", hex_swap.as_str());
@@ -1315,7 +1323,11 @@ mod tests {
// - fee (3 bytes)
"000bb8",
// - tick spacing (3 bytes)
"00003c"
"00003c",
// hook address (not set, so zero)
"0000000000000000000000000000000000000000",
// hook data length (0)
"0000"
))
);
}
@@ -1414,8 +1426,6 @@ mod tests {
"01",
// receiver
"cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2",
// hook address (not set, so zero)
"0000000000000000000000000000000000000000",
// pool params:
// - intermediary token USDT
"dac17f958d2ee523a2206206994597c13d831ec7",
@@ -1423,15 +1433,23 @@ mod tests {
"000064",
// - tick spacing
"000001",
// hook address (not set, so zero)
"0000000000000000000000000000000000000000",
// hook data length (0)
"0000",
// Second swap
// ple encoding
"001a",
"0030",
// - intermediary token WBTC
"2260fac5e5542a773aa44fbcfedf7c193bc2c599",
// - fee
"000bb8",
// - tick spacing
"00003c"
"00003c",
// hook address (not set, so zero)
"0000000000000000000000000000000000000000",
// hook data length (0)
"0000"
))
);
write_calldata_to_file("test_encode_uniswap_v4_sequential_swap", combined_hex.as_str());

View File

@@ -1247,22 +1247,28 @@ mod tests {
"01",
// receiver
"cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2",
// hook address (not set, so zero)
"0000000000000000000000000000000000000000",
// first pool intermediary token (ETH)
"0000000000000000000000000000000000000000",
// fee
"000bb8",
// tick spacing
"00003c",
// hook address (not set, so zero)
"0000000000000000000000000000000000000000",
// hook data length (0)
"0000",
// ple encoding
"001a",
"0030",
// second pool intermediary token (PEPE)
"6982508145454ce325ddbe47a25d4ec3d2311933",
// fee
"0061a8",
// tick spacing
"0001f4"
"0001f4",
// hook address (not set, so zero)
"0000000000000000000000000000000000000000",
// hook data length (0)
"0000",
))
);
}

View File

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