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; address intermediaryToken;
uint24 fee; uint24 fee;
int24 tickSpacing; int24 tickSpacing;
address hook;
bytes hookData;
} }
constructor(IPoolManager _poolManager, address _permit2) constructor(IPoolManager _poolManager, address _permit2)
@@ -89,8 +91,6 @@ 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;
@@ -100,7 +100,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(hook) hooks: IHooks(pools[0].hook)
}); });
swapData = abi.encodeWithSelector( swapData = abi.encodeWithSelector(
this.swapExactInputSingle.selector, this.swapExactInputSingle.selector,
@@ -109,7 +109,7 @@ contract UniswapV4Executor is
amountIn, amountIn,
transferType, transferType,
receiver, receiver,
hookData pools[0].hookData
); );
} else { } else {
PathKey[] memory path = new PathKey[](pools.length); PathKey[] memory path = new PathKey[](pools.length);
@@ -118,8 +118,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(hook), hooks: IHooks(pools[i].hook),
hookData: hookData hookData: pools[i].hookData
}); });
} }
@@ -149,8 +149,6 @@ 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
) )
{ {
@@ -163,42 +161,71 @@ 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]));
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])); address firstToken = address(bytes20(remaining[0:20]));
uint24 firstFee = uint24(bytes3(remaining[20:23])); uint24 firstFee = uint24(bytes3(remaining[20:23]));
int24 firstTickSpacing = int24(uint24(bytes3(remaining[23:26]))); int24 firstTickSpacing = int24(uint24(bytes3(remaining[23:26])));
UniswapV4Pool memory firstPool = address firstHook = address(bytes20(remaining[26:46]));
UniswapV4Pool(firstToken, firstFee, firstTickSpacing); 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 // Remaining after first pool are ple encoded
bytes[] memory encodedPools = bytes[] memory encodedPools = LibPrefixLengthEncodedByteArray.toArray(
LibPrefixLengthEncodedByteArray.toArray(remaining[26:]); remaining[firstPoolTotalLength:]
);
pools = new UniswapV4Pool[](1 + encodedPools.length); pools = new UniswapV4Pool[](1 + encodedPools.length);
pools[0] = firstPool; pools[0] = UniswapV4Pool(
firstToken, firstFee, firstTickSpacing, firstHook, firstHookData
uint256 encodedPoolsLength = 26; );
uint256 plePoolsTotalLength;
// Decode subsequent pools
for (uint256 i = 0; i < encodedPools.length; i++) { for (uint256 i = 0; i < encodedPools.length; i++) {
bytes memory poolsData = encodedPools[i]; bytes memory poolData = encodedPools[i];
address intermediaryToken; address intermediaryToken;
uint24 fee; uint24 fee;
int24 tickSpacing; int24 tickSpacing;
address hook;
uint16 hookDataLength;
// slither-disable-next-line assembly // slither-disable-next-line assembly
assembly { assembly {
intermediaryToken := mload(add(poolsData, add(0, 20))) let dataPtr := add(poolData, 0x20)
fee := shr(232, mload(add(poolsData, add(0, 52)))) intermediaryToken := shr(96, mload(dataPtr))
tickSpacing := shr(232, mload(add(poolsData, add(0, 55)))) fee := and(shr(232, mload(add(dataPtr, 20))), 0xffffff)
} tickSpacing := and(shr(208, mload(add(dataPtr, 20))), 0xffffff)
pools[i + 1] = UniswapV4Pool(intermediaryToken, fee, tickSpacing); hook := shr(96, mload(add(dataPtr, 26)))
plePoolsTotalLength += 2 + encodedPoolsLength; // 2 bytes prefix + data hookDataLength := and(shr(240, mload(add(dataPtr, 46))), 0xffff)
} }
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, address receiver,
bytes calldata hookData bytes calldata hookData
) external returns (uint128) { ) external returns (uint128) {
Currency currencyIn = zeroForOne ? poolKey.currency0 : poolKey.currency1;
_settle(currencyIn, amountIn, transferType);
uint128 amountOut = _swap( uint128 amountOut = _swap(
poolKey, zeroForOne, -int256(uint256(amountIn)), hookData poolKey, zeroForOne, -int256(uint256(amountIn)), hookData
).toUint128(); ).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 = Currency currencyOut =
zeroForOne ? poolKey.currency1 : poolKey.currency0; zeroForOne ? poolKey.currency1 : poolKey.currency0;
_take(currencyOut, receiver, _mapTakeAmount(amountOut, currencyOut)); _take(currencyOut, receiver, _mapTakeAmount(amountOut, currencyOut));
@@ -310,6 +332,7 @@ contract UniswapV4Executor is
uint128 amountOut = 0; uint128 amountOut = 0;
Currency swapCurrencyIn = currencyIn; Currency swapCurrencyIn = currencyIn;
uint256 swapAmountIn = amountIn; uint256 swapAmountIn = amountIn;
_settle(currencyIn, amountIn, transferType);
unchecked { unchecked {
uint256 pathLength = path.length; uint256 pathLength = path.length;
PathKey calldata pathKey; 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( _take(
swapCurrencyIn, // at the end of the loop this is actually currency out swapCurrencyIn, // at the end of the loop this is actually currency out
receiver, receiver,
@@ -387,21 +404,6 @@ contract UniswapV4Executor is
amount = uint256(_amount); 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. * @notice Pays and settles a currency to the pool manager.
* @dev The implementing contract must ensure that the `payer` is a secure address. * @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 BSGG_ADDR = address(0xdA16Cf041E2780618c49Dbae5d734B89a6Bac9b3);
address GHO_ADDR = address(0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f); address GHO_ADDR = address(0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f);
address ONDO_ADDR = address(0xfAbA6f8e4a5E8Ab82F62fe7C39859FA577269BE3); address ONDO_ADDR = address(0xfAbA6f8e4a5E8Ab82F62fe7C39859FA577269BE3);
address RLUSD_ADDR = address(0x8292Bb45bf1Ee4d140127049757C2E0fF06317eD);
// Maverick v2 // Maverick v2
address MAVERICK_V2_FACTORY = 0x0A7e848Aca42d879EF06507Fca0E7b33A0a63c1e; address MAVERICK_V2_FACTORY = 0x0A7e848Aca42d879EF06507Fca0E7b33A0a63c1e;
@@ -146,6 +147,9 @@ contract Constants is Test, BaseConstants {
// Curve meta registry // Curve meta registry
address CURVE_META_REGISTRY = 0xF98B45FA17DE75FB1aD0e7aFD971b0ca00e379fC; address CURVE_META_REGISTRY = 0xF98B45FA17DE75FB1aD0e7aFD971b0ca00e379fC;
// Uniswap v4 pool manager
address POOL_MANAGER = 0x000000000004444c5dc75cB358380D2e3dE08A90;
/** /**
* @dev Deploys a dummy contract with non-empty bytecode * @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"; import {Test} from "../../lib/forge-std/src/Test.sol";
contract UniswapV4ExecutorExposed is UniswapV4Executor { contract UniswapV4ExecutorExposed is UniswapV4Executor {
constructor(IPoolManager _poolManager, address _permit2) constructor(IPoolManager _POOL_MANAGER, address _permit2)
UniswapV4Executor(_poolManager, _permit2) UniswapV4Executor(_POOL_MANAGER, _permit2)
{} {}
function decodeData(bytes calldata data) function decodeData(bytes calldata data)
@@ -24,8 +24,6 @@ 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
) )
{ {
@@ -41,13 +39,11 @@ contract UniswapV4ExecutorTest is Constants, TestUtils {
IERC20 USDT = IERC20(USDT_ADDR); IERC20 USDT = IERC20(USDT_ADDR);
IERC20 USDC = IERC20(USDC_ADDR); IERC20 USDC = IERC20(USDC_ADDR);
address poolManager = 0x000000000004444c5dc75cB358380D2e3dE08A90;
function setUp() public { function setUp() public {
uint256 forkBlock = 22689128; 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(POOL_MANAGER), PERMIT2_ADDRESS
); );
} }
@@ -63,12 +59,16 @@ contract UniswapV4ExecutorTest is Constants, TestUtils {
pools[0] = UniswapV4Executor.UniswapV4Pool({ pools[0] = UniswapV4Executor.UniswapV4Pool({
intermediaryToken: USDT_ADDR, intermediaryToken: USDT_ADDR,
fee: pool1Fee, fee: pool1Fee,
tickSpacing: tickSpacing1 tickSpacing: tickSpacing1,
hook: address(0),
hookData: bytes("")
}); });
pools[1] = UniswapV4Executor.UniswapV4Pool({ pools[1] = UniswapV4Executor.UniswapV4Pool({
intermediaryToken: USDE_ADDR, intermediaryToken: USDE_ADDR,
fee: pool2Fee, fee: pool2Fee,
tickSpacing: tickSpacing2 tickSpacing: tickSpacing2,
hook: address(0),
hookData: bytes("0x12345")
}); });
bytes memory data = UniswapV4Utils.encodeExactInput( bytes memory data = UniswapV4Utils.encodeExactInput(
@@ -77,8 +77,6 @@ contract UniswapV4ExecutorTest is Constants, TestUtils {
zeroForOne, zeroForOne,
RestrictTransferFrom.TransferType.Transfer, RestrictTransferFrom.TransferType.Transfer,
ALICE, ALICE,
address(0),
bytes(""),
pools pools
); );
@@ -88,8 +86,6 @@ 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);
@@ -101,7 +97,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[0].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);
@@ -109,12 +105,13 @@ contract UniswapV4ExecutorTest is Constants, TestUtils {
assertEq(decodedPools[1].intermediaryToken, USDE_ADDR); assertEq(decodedPools[1].intermediaryToken, USDE_ADDR);
assertEq(decodedPools[1].fee, pool2Fee); assertEq(decodedPools[1].fee, pool2Fee);
assertEq(decodedPools[1].tickSpacing, tickSpacing2); assertEq(decodedPools[1].tickSpacing, tickSpacing2);
assertEq(decodedPools[1].hookData, bytes("0x12345"));
} }
function testSingleSwap() public { function testSingleSwap() public {
uint256 amountIn = 100 ether; uint256 amountIn = 100 ether;
deal(USDE_ADDR, address(uniswapV4Exposed), amountIn); deal(USDE_ADDR, address(uniswapV4Exposed), amountIn);
uint256 usdeBalanceBeforePool = USDE.balanceOf(poolManager); uint256 usdeBalanceBeforePool = USDE.balanceOf(POOL_MANAGER);
uint256 usdeBalanceBeforeSwapExecutor = uint256 usdeBalanceBeforeSwapExecutor =
USDE.balanceOf(address(uniswapV4Exposed)); USDE.balanceOf(address(uniswapV4Exposed));
@@ -123,7 +120,9 @@ contract UniswapV4ExecutorTest is Constants, TestUtils {
pools[0] = UniswapV4Executor.UniswapV4Pool({ pools[0] = UniswapV4Executor.UniswapV4Pool({
intermediaryToken: USDT_ADDR, intermediaryToken: USDT_ADDR,
fee: uint24(100), fee: uint24(100),
tickSpacing: int24(1) tickSpacing: int24(1),
hook: address(0),
hookData: bytes("")
}); });
bytes memory data = UniswapV4Utils.encodeExactInput( bytes memory data = UniswapV4Utils.encodeExactInput(
@@ -132,13 +131,11 @@ contract UniswapV4ExecutorTest is Constants, TestUtils {
true, true,
RestrictTransferFrom.TransferType.Transfer, RestrictTransferFrom.TransferType.Transfer,
ALICE, ALICE,
address(0),
bytes(""),
pools pools
); );
uint256 amountOut = uniswapV4Exposed.swap(amountIn, data); uint256 amountOut = uniswapV4Exposed.swap(amountIn, data);
assertEq(USDE.balanceOf(poolManager), usdeBalanceBeforePool + amountIn); assertEq(USDE.balanceOf(POOL_MANAGER), usdeBalanceBeforePool + amountIn);
assertEq( assertEq(
USDE.balanceOf(address(uniswapV4Exposed)), USDE.balanceOf(address(uniswapV4Exposed)),
usdeBalanceBeforeSwapExecutor - amountIn usdeBalanceBeforeSwapExecutor - amountIn
@@ -152,12 +149,12 @@ contract UniswapV4ExecutorTest is Constants, TestUtils {
loadCallDataFromFile("test_encode_uniswap_v4_simple_swap"); loadCallDataFromFile("test_encode_uniswap_v4_simple_swap");
uint256 amountIn = 100 ether; uint256 amountIn = 100 ether;
deal(USDE_ADDR, address(uniswapV4Exposed), amountIn); deal(USDE_ADDR, address(uniswapV4Exposed), amountIn);
uint256 usdeBalanceBeforePool = USDE.balanceOf(poolManager); uint256 usdeBalanceBeforePool = USDE.balanceOf(POOL_MANAGER);
uint256 usdeBalanceBeforeSwapExecutor = uint256 usdeBalanceBeforeSwapExecutor =
USDE.balanceOf(address(uniswapV4Exposed)); USDE.balanceOf(address(uniswapV4Exposed));
uint256 amountOut = uniswapV4Exposed.swap(amountIn, protocolData); uint256 amountOut = uniswapV4Exposed.swap(amountIn, protocolData);
assertEq(USDE.balanceOf(poolManager), usdeBalanceBeforePool + amountIn); assertEq(USDE.balanceOf(POOL_MANAGER), usdeBalanceBeforePool + amountIn);
assertEq( assertEq(
USDE.balanceOf(ALICE), usdeBalanceBeforeSwapExecutor - amountIn USDE.balanceOf(ALICE), usdeBalanceBeforeSwapExecutor - amountIn
); );
@@ -168,7 +165,7 @@ contract UniswapV4ExecutorTest is Constants, TestUtils {
// USDE -> USDT -> WBTC // USDE -> USDT -> WBTC
uint256 amountIn = 100 ether; uint256 amountIn = 100 ether;
deal(USDE_ADDR, address(uniswapV4Exposed), amountIn); deal(USDE_ADDR, address(uniswapV4Exposed), amountIn);
uint256 usdeBalanceBeforePool = USDE.balanceOf(poolManager); uint256 usdeBalanceBeforePool = USDE.balanceOf(POOL_MANAGER);
uint256 usdeBalanceBeforeSwapExecutor = uint256 usdeBalanceBeforeSwapExecutor =
USDE.balanceOf(address(uniswapV4Exposed)); USDE.balanceOf(address(uniswapV4Exposed));
@@ -177,12 +174,16 @@ contract UniswapV4ExecutorTest is Constants, TestUtils {
pools[0] = UniswapV4Executor.UniswapV4Pool({ pools[0] = UniswapV4Executor.UniswapV4Pool({
intermediaryToken: USDT_ADDR, intermediaryToken: USDT_ADDR,
fee: uint24(100), fee: uint24(100),
tickSpacing: int24(1) tickSpacing: int24(1),
hook: address(0),
hookData: bytes("")
}); });
pools[1] = UniswapV4Executor.UniswapV4Pool({ pools[1] = UniswapV4Executor.UniswapV4Pool({
intermediaryToken: WBTC_ADDR, intermediaryToken: WBTC_ADDR,
fee: uint24(3000), fee: uint24(3000),
tickSpacing: int24(60) tickSpacing: int24(60),
hook: address(0),
hookData: bytes("")
}); });
bytes memory data = UniswapV4Utils.encodeExactInput( bytes memory data = UniswapV4Utils.encodeExactInput(
@@ -191,13 +192,11 @@ contract UniswapV4ExecutorTest is Constants, TestUtils {
true, true,
RestrictTransferFrom.TransferType.Transfer, RestrictTransferFrom.TransferType.Transfer,
ALICE, ALICE,
address(0),
bytes(""),
pools pools
); );
uint256 amountOut = uniswapV4Exposed.swap(amountIn, data); uint256 amountOut = uniswapV4Exposed.swap(amountIn, data);
assertEq(USDE.balanceOf(poolManager), usdeBalanceBeforePool + amountIn); assertEq(USDE.balanceOf(POOL_MANAGER), usdeBalanceBeforePool + amountIn);
assertEq( assertEq(
USDE.balanceOf(address(uniswapV4Exposed)), USDE.balanceOf(address(uniswapV4Exposed)),
usdeBalanceBeforeSwapExecutor - amountIn usdeBalanceBeforeSwapExecutor - amountIn
@@ -212,12 +211,12 @@ contract UniswapV4ExecutorTest is Constants, TestUtils {
uint256 amountIn = 100 ether; uint256 amountIn = 100 ether;
deal(USDE_ADDR, address(uniswapV4Exposed), amountIn); deal(USDE_ADDR, address(uniswapV4Exposed), amountIn);
uint256 usdeBalanceBeforePool = USDE.balanceOf(poolManager); uint256 usdeBalanceBeforePool = USDE.balanceOf(POOL_MANAGER);
uint256 usdeBalanceBeforeSwapExecutor = uint256 usdeBalanceBeforeSwapExecutor =
USDE.balanceOf(address(uniswapV4Exposed)); USDE.balanceOf(address(uniswapV4Exposed));
uint256 amountOut = uniswapV4Exposed.swap(amountIn, protocolData); uint256 amountOut = uniswapV4Exposed.swap(amountIn, protocolData);
assertEq(USDE.balanceOf(poolManager), usdeBalanceBeforePool + amountIn); assertEq(USDE.balanceOf(POOL_MANAGER), usdeBalanceBeforePool + amountIn);
assertEq( assertEq(
USDE.balanceOf(address(uniswapV4Exposed)), USDE.balanceOf(address(uniswapV4Exposed)),
usdeBalanceBeforeSwapExecutor - amountIn usdeBalanceBeforeSwapExecutor - amountIn
@@ -239,7 +238,9 @@ contract UniswapV4ExecutorTest is Constants, TestUtils {
pools[0] = UniswapV4Executor.UniswapV4Pool({ pools[0] = UniswapV4Executor.UniswapV4Pool({
intermediaryToken: WETH_ADDR, intermediaryToken: WETH_ADDR,
fee: uint24(500), fee: uint24(500),
tickSpacing: int24(1) tickSpacing: int24(1),
hook: hook,
hookData: bytes("")
}); });
bytes memory data = UniswapV4Utils.encodeExactInput( bytes memory data = UniswapV4Utils.encodeExactInput(
@@ -248,8 +249,6 @@ contract UniswapV4ExecutorTest is Constants, TestUtils {
true, true,
RestrictTransferFrom.TransferType.Transfer, RestrictTransferFrom.TransferType.Transfer,
ALICE, ALICE,
hook,
bytes(""),
pools 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 { contract TychoRouterForBalancerV3Test is TychoRouterTestSetup {
function testSingleSwapUSV4CallbackPermit2() public { function testSingleSwapUSV4CallbackPermit2() public {
vm.startPrank(ALICE); vm.startPrank(ALICE);
@@ -282,7 +416,9 @@ contract TychoRouterForBalancerV3Test is TychoRouterTestSetup {
pools[0] = UniswapV4Executor.UniswapV4Pool({ pools[0] = UniswapV4Executor.UniswapV4Pool({
intermediaryToken: USDT_ADDR, intermediaryToken: USDT_ADDR,
fee: uint24(100), fee: uint24(100),
tickSpacing: int24(1) tickSpacing: int24(1),
hook: address(0),
hookData: bytes("")
}); });
bytes memory protocolData = UniswapV4Utils.encodeExactInput( bytes memory protocolData = UniswapV4Utils.encodeExactInput(
@@ -291,8 +427,6 @@ contract TychoRouterForBalancerV3Test is TychoRouterTestSetup {
true, true,
RestrictTransferFrom.TransferType.TransferFrom, RestrictTransferFrom.TransferType.TransferFrom,
ALICE, ALICE,
address(0),
bytes(""),
pools pools
); );
@@ -327,12 +461,16 @@ contract TychoRouterForBalancerV3Test is TychoRouterTestSetup {
pools[0] = UniswapV4Executor.UniswapV4Pool({ pools[0] = UniswapV4Executor.UniswapV4Pool({
intermediaryToken: USDT_ADDR, intermediaryToken: USDT_ADDR,
fee: uint24(100), fee: uint24(100),
tickSpacing: int24(1) tickSpacing: int24(1),
hook: address(0),
hookData: bytes("")
}); });
pools[1] = UniswapV4Executor.UniswapV4Pool({ pools[1] = UniswapV4Executor.UniswapV4Pool({
intermediaryToken: WBTC_ADDR, intermediaryToken: WBTC_ADDR,
fee: uint24(3000), fee: uint24(3000),
tickSpacing: int24(60) tickSpacing: int24(60),
hook: address(0),
hookData: bytes("")
}); });
bytes memory protocolData = UniswapV4Utils.encodeExactInput( bytes memory protocolData = UniswapV4Utils.encodeExactInput(
@@ -341,8 +479,6 @@ contract TychoRouterForBalancerV3Test is TychoRouterTestSetup {
true, true,
RestrictTransferFrom.TransferType.TransferFrom, RestrictTransferFrom.TransferType.TransferFrom,
ALICE, ALICE,
address(0),
bytes(""),
pools pools
); );

View File

@@ -10,8 +10,6 @@ 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) {
require(pools.length > 0, "Must have at least one pool"); require(pools.length > 0, "Must have at least one pool");
@@ -19,7 +17,10 @@ library UniswapV4Utils {
bytes memory firstPool = abi.encodePacked( bytes memory firstPool = abi.encodePacked(
pools[0].intermediaryToken, pools[0].intermediaryToken,
bytes3(pools[0].fee), 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); bytes[] memory encodedExtraPools = new bytes[](pools.length - 1);
@@ -27,7 +28,10 @@ library UniswapV4Utils {
encodedExtraPools[i - 1] = abi.encodePacked( 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,
pools[i].hook,
bytes2(uint16(pools[i].hookData.length)),
pools[i].hookData
); );
} }
@@ -37,10 +41,8 @@ library UniswapV4Utils {
zeroForOne, zeroForOne,
transferType, transferType,
receiver, receiver,
hook,
firstPool, firstPool,
pleEncode(encodedExtraPools), pleEncode(encodedExtraPools)
hookData
); );
} }

View File

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

View File

@@ -1247,22 +1247,28 @@ 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",
// hook address (not set, so zero)
"0000000000000000000000000000000000000000",
// hook data length (0)
"0000",
// ple encoding // ple encoding
"001a", "0030",
// second pool intermediary token (PEPE) // second pool intermediary token (PEPE)
"6982508145454ce325ddbe47a25d4ec3d2311933", "6982508145454ce325ddbe47a25d4ec3d2311933",
// fee // fee
"0061a8", "0061a8",
// tick spacing // 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!( let expected_swaps = String::from(concat!(
// length of ple encoded swaps without padding // length of ple encoded swaps without padding
"000000000000000000000000000000000000000000000000000000000000009c", "00000000000000000000000000000000000000000000000000000000000000b4",
// Swap data header // Swap data header
"f62849f9a0b5bf2913b396098f7c7019b51a820a", // executor address "f62849f9a0b5bf2913b396098f7c7019b51a820a", // executor address
// Protocol data // Protocol data
@@ -366,18 +366,21 @@ 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
"0000000000000000000000000000000000000000", // hook address
"0000", // hook data length
// ple encoding // ple encoding
"001a", "0030",
// Second pool params // Second pool params
"6982508145454ce325ddbe47a25d4ec3d2311933", // intermediary token (PEPE) "6982508145454ce325ddbe47a25d4ec3d2311933", // intermediary token (PEPE)
"0061a8", // fee "0061a8", // fee
"0001f4", // tick spacing "0001f4", // tick spacing
"00000000" // padding "0000000000000000000000000000000000000000", // hook address
"0000", // hook data length
"000000000000000000000000" // padding
)); ));
let hex_calldata = encode(&calldata); let hex_calldata = encode(&calldata);