From f82ae3b92a82b27981f7178ec5d3c4cc7ec1329a Mon Sep 17 00:00:00 2001 From: Diana Carvalho Date: Thu, 9 Oct 2025 10:26:28 +0200 Subject: [PATCH] feat: Support Euler low balance single swaps (univ4) Took 1 hour 34 minutes Took 4 minutes --- foundry/src/executors/UniswapV4Executor.sol | 9 +-- foundry/test/Constants.sol | 4 + foundry/test/protocols/UniswapV4.t.sol | 84 +++++++++++++++++---- 3 files changed, 77 insertions(+), 20 deletions(-) diff --git a/foundry/src/executors/UniswapV4Executor.sol b/foundry/src/executors/UniswapV4Executor.sol index 8f245f6..3cdee4f 100644 --- a/foundry/src/executors/UniswapV4Executor.sol +++ b/foundry/src/executors/UniswapV4Executor.sol @@ -275,17 +275,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)); diff --git a/foundry/test/Constants.sol b/foundry/test/Constants.sol index f078f36..0053124 100644 --- a/foundry/test/Constants.sol +++ b/foundry/test/Constants.sol @@ -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 */ diff --git a/foundry/test/protocols/UniswapV4.t.sol b/foundry/test/protocols/UniswapV4.t.sol index 656fd65..0706ae9 100644 --- a/foundry/test/protocols/UniswapV4.t.sol +++ b/foundry/test/protocols/UniswapV4.t.sol @@ -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) @@ -41,13 +41,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 ); } @@ -114,7 +112,7 @@ contract UniswapV4ExecutorTest is Constants, TestUtils { 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)); @@ -138,7 +136,7 @@ contract UniswapV4ExecutorTest is Constants, TestUtils { ); 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 +150,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 +166,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)); @@ -197,7 +195,7 @@ contract UniswapV4ExecutorTest is Constants, TestUtils { ); 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 +210,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 @@ -267,6 +265,66 @@ 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); + + 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); + uint256 rlusdBalanceBeforeSwapExecutor = + RLUSD.balanceOf(address(uniswapV4Exposed)); + + UniswapV4Executor.UniswapV4Pool[] memory pools = + new UniswapV4Executor.UniswapV4Pool[](1); + pools[0] = UniswapV4Executor.UniswapV4Pool({ + intermediaryToken: USDT_ADDR, + fee: uint24(50), + tickSpacing: int24(1) + }); + + bytes memory data = UniswapV4Utils.encodeExactInput( + RLUSD_ADDR, + USDT_ADDR, + true, + RestrictTransferFrom.TransferType.Transfer, + ALICE, + address(0xF87ACF8428F2f9403AAA0256A7272d6549ECa8A8), + bytes(""), + pools + ); + + uint256 amountOut = uniswapV4Exposed.swap(amountIn, data); + assertEq( + RLUSD.balanceOf(eulerProxy), rlusdEulerBalanceBefore + amountIn + ); + assertTrue(USDT.balanceOf(ALICE) == amountOut); + } +} + contract TychoRouterForBalancerV3Test is TychoRouterTestSetup { function testSingleSwapUSV4CallbackPermit2() public { vm.startPrank(ALICE);