From e243667f9ee8ec8e79fc5196da6960b19cd120b7 Mon Sep 17 00:00:00 2001 From: TAMARA LIPOWSKI Date: Tue, 8 Jul 2025 18:15:26 -0400 Subject: [PATCH 1/4] feat: (WIP) Handle approvals in UniswapXFiller Reasons for using regular approvals instead of permit2: - More simplicity, simpler setup - Unfortunately we won't be able to "expire" approvals, which is a risk we may be okay with taking - since one advantage of this is that we will be able save on gas by not approving the reactor every single time (is this our intention?) TODO: - Do we really need input tokens to pass through our router? - Handle native ETH (double check how this is transferred in the reactor) --- foundry/src/uniswap_x/UniswapXFiller.sol | 36 ++++++++++++++++++--- foundry/test/uniswap_x/UniswapXFiller.t.sol | 23 ++++++++++--- 2 files changed, 49 insertions(+), 10 deletions(-) diff --git a/foundry/src/uniswap_x/UniswapXFiller.sol b/foundry/src/uniswap_x/UniswapXFiller.sol index 1156c31..bc5a3f9 100644 --- a/foundry/src/uniswap_x/UniswapXFiller.sol +++ b/foundry/src/uniswap_x/UniswapXFiller.sol @@ -9,6 +9,7 @@ import { } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/utils/Address.sol"; +import {TychoRouter} from "../TychoRouter.sol"; error UniswapXFiller__AddressZero(); error UniswapXFiller__BatchExecutionNotSupported(); @@ -58,12 +59,34 @@ contract UniswapXFiller is AccessControl, IReactorCallback { ResolvedOrder memory order = resolvedOrders[0]; + ( + bool tokenInApprovalNeeded, + bool tokenOutApprovalNeeded, + bytes memory tychoCalldata + ) = abi.decode(callbackData, (bool, bool, bytes)); + + // The TychoRouter will take the input tokens from the filler + if (tokenInApprovalNeeded) { + // TODO only if ERC20 + IERC20(order.input.token).forceApprove( + tychoRouter, order.input.maxAmount + ); + } else { + // The filler will manually transfer into the TychoRouter + // Note: We are using the balance of our filler instead of the order + // input amount to avoid having to do a decay calculation in the filler. + IERC20 inputToken = IERC20(order.input.token); + inputToken.transfer( + tychoRouter, inputToken.balanceOf(address(this)) + ); + } + // TODO properly handle native in and out tokens uint256 ethValue = 0; // slither-disable-next-line low-level-calls (bool success, bytes memory result) = - tychoRouter.call{value: ethValue}(callbackData); + tychoRouter.call{value: ethValue}(tychoCalldata); if (!success) { revert( @@ -75,10 +98,13 @@ contract UniswapXFiller is AccessControl, IReactorCallback { ); } - // Multiple outputs are possible when taking fees - but token itself should - // not change. - IERC20 token = IERC20(order.outputs[0].token); - token.forceApprove(address(reactor), type(uint256).max); + if (tokenOutApprovalNeeded) { + // Multiple outputs are possible when taking fees - but token itself should + // not change. + // TODO only if ERC20 + IERC20 token = IERC20(order.outputs[0].token); + token.forceApprove(address(reactor), type(uint256).max); + } } /** diff --git a/foundry/test/uniswap_x/UniswapXFiller.t.sol b/foundry/test/uniswap_x/UniswapXFiller.t.sol index 8102e4f..49f9e90 100644 --- a/foundry/test/uniswap_x/UniswapXFiller.t.sol +++ b/foundry/test/uniswap_x/UniswapXFiller.t.sol @@ -55,7 +55,7 @@ contract UniswapXFillerTest is Test, TychoRouterTestSetup { bytes memory swap = encodeSingleSwap(address(usv2Executor), protocolData); - bytes memory callbackData = abi.encodeWithSelector( + bytes memory tychoRouterData = abi.encodeWithSelector( tychoRouter.singleSwap.selector, amountIn, WETH_ADDR, @@ -68,6 +68,8 @@ contract UniswapXFillerTest is Test, TychoRouterTestSetup { swap ); + bytes memory callbackData = abi.encode(true, true, tychoRouterData); + deal(WETH_ADDR, address(filler), amountIn); vm.startPrank(address(filler)); IERC20(WETH_ADDR).approve(tychoRouterAddr, amountIn); @@ -76,11 +78,11 @@ contract UniswapXFillerTest is Test, TychoRouterTestSetup { OutputToken[] memory outputs = new OutputToken[](1); outputs[0] = OutputToken({ token: address(DAI_ADDR), - // Irrelevant fields - we only need the token address for approval amount: 1847751195973566072891, recipient: BOB }); - // All irrelevant fields for this test - we only need the output token address + // Mostly irrelevant fields for this test - we only need token input and outputs + // info for the sake of testing. orders[0] = ResolvedOrder({ info: OrderInfo({ reactor: address(0), @@ -90,7 +92,12 @@ contract UniswapXFillerTest is Test, TychoRouterTestSetup { additionalValidationContract: address(0), additionalValidationData: "" }), - input: InputToken({token: address(WETH_ADDR), amount: 0, maxAmount: 0}), + input: InputToken({ + token: address(WETH_ADDR), + amount: amountIn, + // We need the proper maxAmount for our approval to work + maxAmount: amountIn + }), outputs: outputs, sig: "", hash: "" @@ -133,7 +140,7 @@ contract UniswapXFillerTest is Test, TychoRouterTestSetup { bytes memory swap = encodeSingleSwap(address(usv2Executor), protocolData); - bytes memory callbackData = abi.encodeWithSelector( + bytes memory tychoRouterData = abi.encodeWithSelector( tychoRouter.singleSwap.selector, amountIn, WBTC_ADDR, @@ -146,6 +153,12 @@ contract UniswapXFillerTest is Test, TychoRouterTestSetup { swap ); + bytes memory callbackData = abi.encode( + true, // tokenIn approval needed + true, // tokenOut approval needed + tychoRouterData + ); + vm.startPrank(address(filler)); IERC20(WBTC_ADDR).approve(tychoRouterAddr, amountIn); vm.stopPrank(); From 3752c155ea89d4d2d91be98f2c5fa79264a45999 Mon Sep 17 00:00:00 2001 From: TAMARA LIPOWSKI Date: Wed, 9 Jul 2025 12:06:15 -0400 Subject: [PATCH 2/4] feat: Handle native ETH outputs - In this case, the ETH needs to be manually transferred to the reactor. - Native ETH input is not supported for UniswapX Source: https://support.uniswap.org/hc/en-us/articles/17544708791821-Are-there-network-costs-for-UniswapX --- foundry/src/uniswap_x/UniswapXFiller.sol | 36 ++++++++++----------- foundry/test/uniswap_x/UniswapXFiller.t.sol | 6 ++-- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/foundry/src/uniswap_x/UniswapXFiller.sol b/foundry/src/uniswap_x/UniswapXFiller.sol index bc5a3f9..2daaf40 100644 --- a/foundry/src/uniswap_x/UniswapXFiller.sol +++ b/foundry/src/uniswap_x/UniswapXFiller.sol @@ -20,6 +20,7 @@ contract UniswapXFiller is AccessControl, IReactorCallback { // UniswapX V2DutchOrder Reactor IReactor public immutable reactor; address public immutable tychoRouter; + address public immutable nativeAddress; // keccak256("NAME_OF_ROLE") : save gas on deployment bytes32 public constant REACTOR_ROLE = @@ -31,7 +32,11 @@ contract UniswapXFiller is AccessControl, IReactorCallback { address indexed token, uint256 amount, address indexed receiver ); - constructor(address _tychoRouter, address _reactor) { + constructor( + address _tychoRouter, + address _reactor, + address _native_address + ) { if (_tychoRouter == address(0)) revert UniswapXFiller__AddressZero(); if (_reactor == address(0)) revert UniswapXFiller__AddressZero(); @@ -39,6 +44,7 @@ contract UniswapXFiller is AccessControl, IReactorCallback { _grantRole(REACTOR_ROLE, address(_reactor)); tychoRouter = _tychoRouter; reactor = IReactor(_reactor); + nativeAddress = _native_address; } function execute(SignedOrder calldata order, bytes calldata callbackData) @@ -67,26 +73,14 @@ contract UniswapXFiller is AccessControl, IReactorCallback { // The TychoRouter will take the input tokens from the filler if (tokenInApprovalNeeded) { - // TODO only if ERC20 + // Native ETH input is not supported by UniswapX IERC20(order.input.token).forceApprove( tychoRouter, order.input.maxAmount ); - } else { - // The filler will manually transfer into the TychoRouter - // Note: We are using the balance of our filler instead of the order - // input amount to avoid having to do a decay calculation in the filler. - IERC20 inputToken = IERC20(order.input.token); - inputToken.transfer( - tychoRouter, inputToken.balanceOf(address(this)) - ); } - // TODO properly handle native in and out tokens - uint256 ethValue = 0; - // slither-disable-next-line low-level-calls - (bool success, bytes memory result) = - tychoRouter.call{value: ethValue}(tychoCalldata); + (bool success, bytes memory result) = tychoRouter.call(tychoCalldata); if (!success) { revert( @@ -101,9 +95,15 @@ contract UniswapXFiller is AccessControl, IReactorCallback { if (tokenOutApprovalNeeded) { // Multiple outputs are possible when taking fees - but token itself should // not change. - // TODO only if ERC20 - IERC20 token = IERC20(order.outputs[0].token); - token.forceApprove(address(reactor), type(uint256).max); + OutputToken memory output = order.outputs[0]; + if (output.token != nativeAddress) { + IERC20 token = IERC20(output.token); + token.forceApprove(address(reactor), type(uint256).max); + } else { + // With native ETH - the filler is responsible for transferring back + // to the reactor. + Address.sendValue(payable(address(reactor)), output.amount); + } } } diff --git a/foundry/test/uniswap_x/UniswapXFiller.t.sol b/foundry/test/uniswap_x/UniswapXFiller.t.sol index 49f9e90..9ac004c 100644 --- a/foundry/test/uniswap_x/UniswapXFiller.t.sol +++ b/foundry/test/uniswap_x/UniswapXFiller.t.sol @@ -23,7 +23,7 @@ contract UniswapXFillerTest is Test, TychoRouterTestSetup { function fillerSetup() public { vm.startPrank(ADMIN); - filler = new UniswapXFiller(tychoRouterAddr, REACTOR); + filler = new UniswapXFiller(tychoRouterAddr, REACTOR, address(0)); fillerAddr = address(filler); filler.grantRole(keccak256("EXECUTOR_ROLE"), EXECUTOR); vm.stopPrank(); @@ -31,12 +31,12 @@ contract UniswapXFillerTest is Test, TychoRouterTestSetup { function testTychoAddressZeroTychoRouter() public { vm.expectRevert(UniswapXFiller__AddressZero.selector); - filler = new UniswapXFiller(address(0), REACTOR); + filler = new UniswapXFiller(address(0), REACTOR, address(0)); } function testTychoAddressZeroReactor() public { vm.expectRevert(UniswapXFiller__AddressZero.selector); - filler = new UniswapXFiller(tychoRouterAddr, address(0)); + filler = new UniswapXFiller(tychoRouterAddr, address(0), address(0)); } function testCallback() public { From 1b6a24fd75b91de7570f110e87d81ed081639bb7 Mon Sep 17 00:00:00 2001 From: TAMARA LIPOWSKI Date: Wed, 9 Jul 2025 12:19:19 -0400 Subject: [PATCH 3/4] fix: silence slither on native address setting - We don't need a zero address check, since we expect this to be the zero address for most cases. --- foundry/src/uniswap_x/UniswapXFiller.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/foundry/src/uniswap_x/UniswapXFiller.sol b/foundry/src/uniswap_x/UniswapXFiller.sol index 2daaf40..0904ef8 100644 --- a/foundry/src/uniswap_x/UniswapXFiller.sol +++ b/foundry/src/uniswap_x/UniswapXFiller.sol @@ -44,6 +44,8 @@ contract UniswapXFiller is AccessControl, IReactorCallback { _grantRole(REACTOR_ROLE, address(_reactor)); tychoRouter = _tychoRouter; reactor = IReactor(_reactor); + + // slither-disable-next-line missing-zero-check nativeAddress = _native_address; } From 9e2f228a470d163b71cac22a0f95116716490772 Mon Sep 17 00:00:00 2001 From: TAMARA LIPOWSKI Date: Wed, 9 Jul 2025 14:32:43 -0400 Subject: [PATCH 4/4] fix: use encodePacked to encode if approval needed - Also approve max amount for the tycho router to use (to save on gas) --- foundry/src/uniswap_x/UniswapXFiller.sol | 10 ++++------ foundry/test/uniswap_x/UniswapXFiller.t.sol | 5 +++-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/foundry/src/uniswap_x/UniswapXFiller.sol b/foundry/src/uniswap_x/UniswapXFiller.sol index 0904ef8..7826e3c 100644 --- a/foundry/src/uniswap_x/UniswapXFiller.sol +++ b/foundry/src/uniswap_x/UniswapXFiller.sol @@ -67,17 +67,15 @@ contract UniswapXFiller is AccessControl, IReactorCallback { ResolvedOrder memory order = resolvedOrders[0]; - ( - bool tokenInApprovalNeeded, - bool tokenOutApprovalNeeded, - bytes memory tychoCalldata - ) = abi.decode(callbackData, (bool, bool, bytes)); + bool tokenInApprovalNeeded = bool(uint8(callbackData[0]) == 1); + bool tokenOutApprovalNeeded = bool(uint8(callbackData[1]) == 1); + bytes calldata tychoCalldata = bytes(callbackData[2:]); // The TychoRouter will take the input tokens from the filler if (tokenInApprovalNeeded) { // Native ETH input is not supported by UniswapX IERC20(order.input.token).forceApprove( - tychoRouter, order.input.maxAmount + tychoRouter, type(uint256).max ); } diff --git a/foundry/test/uniswap_x/UniswapXFiller.t.sol b/foundry/test/uniswap_x/UniswapXFiller.t.sol index 9ac004c..5128339 100644 --- a/foundry/test/uniswap_x/UniswapXFiller.t.sol +++ b/foundry/test/uniswap_x/UniswapXFiller.t.sol @@ -68,7 +68,8 @@ contract UniswapXFillerTest is Test, TychoRouterTestSetup { swap ); - bytes memory callbackData = abi.encode(true, true, tychoRouterData); + bytes memory callbackData = + abi.encodePacked(true, true, tychoRouterData); deal(WETH_ADDR, address(filler), amountIn); vm.startPrank(address(filler)); @@ -153,7 +154,7 @@ contract UniswapXFillerTest is Test, TychoRouterTestSetup { swap ); - bytes memory callbackData = abi.encode( + bytes memory callbackData = abi.encodePacked( true, // tokenIn approval needed true, // tokenOut approval needed tychoRouterData