diff --git a/foundry/src/uniswap_x/UniswapXFiller.sol b/foundry/src/uniswap_x/UniswapXFiller.sol index 1156c31..7826e3c 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(); @@ -19,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 = @@ -30,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(); @@ -38,6 +44,9 @@ 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; } function execute(SignedOrder calldata order, bytes calldata callbackData) @@ -58,12 +67,20 @@ contract UniswapXFiller is AccessControl, IReactorCallback { ResolvedOrder memory order = resolvedOrders[0]; - // TODO properly handle native in and out tokens - uint256 ethValue = 0; + 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, type(uint256).max + ); + } // slither-disable-next-line low-level-calls - (bool success, bytes memory result) = - tychoRouter.call{value: ethValue}(callbackData); + (bool success, bytes memory result) = tychoRouter.call(tychoCalldata); if (!success) { revert( @@ -75,10 +92,19 @@ 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. + 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 8102e4f..5128339 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 { @@ -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,9 @@ contract UniswapXFillerTest is Test, TychoRouterTestSetup { swap ); + bytes memory callbackData = + abi.encodePacked(true, true, tychoRouterData); + deal(WETH_ADDR, address(filler), amountIn); vm.startPrank(address(filler)); IERC20(WETH_ADDR).approve(tychoRouterAddr, amountIn); @@ -76,11 +79,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 +93,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 +141,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 +154,12 @@ contract UniswapXFillerTest is Test, TychoRouterTestSetup { swap ); + bytes memory callbackData = abi.encodePacked( + true, // tokenIn approval needed + true, // tokenOut approval needed + tychoRouterData + ); + vm.startPrank(address(filler)); IERC20(WBTC_ADDR).approve(tychoRouterAddr, amountIn); vm.stopPrank();