diff --git a/foundry/src/uniswap_x/UniswapXFiller.sol b/foundry/src/uniswap_x/UniswapXFiller.sol index de52edd..1156c31 100644 --- a/foundry/src/uniswap_x/UniswapXFiller.sol +++ b/foundry/src/uniswap_x/UniswapXFiller.sol @@ -11,6 +11,7 @@ import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/utils/Address.sol"; error UniswapXFiller__AddressZero(); +error UniswapXFiller__BatchExecutionNotSupported(); contract UniswapXFiller is AccessControl, IReactorCallback { using SafeERC20 for IERC20; @@ -34,7 +35,7 @@ contract UniswapXFiller is AccessControl, IReactorCallback { if (_reactor == address(0)) revert UniswapXFiller__AddressZero(); _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); - _grantRole(REACTOR_ROLE, address(reactor)); + _grantRole(REACTOR_ROLE, address(_reactor)); tychoRouter = _tychoRouter; reactor = IReactor(_reactor); } @@ -50,7 +51,34 @@ contract UniswapXFiller is AccessControl, IReactorCallback { ResolvedOrder[] calldata resolvedOrders, bytes calldata callbackData ) external onlyRole(REACTOR_ROLE) { - // TODO + require( + resolvedOrders.length == 1, + UniswapXFiller__BatchExecutionNotSupported() + ); + + ResolvedOrder memory order = resolvedOrders[0]; + + // 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); + + if (!success) { + revert( + string( + result.length > 0 + ? result + : abi.encodePacked("Execution failed") + ) + ); + } + + // 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); } /** @@ -103,6 +131,7 @@ contract UniswapXFiller is AccessControl, IReactorCallback { /** * @dev Allows this contract to receive native token with empty msg.data from contracts */ + // slither-disable-next-line locked-ether receive() external payable { require(msg.sender.code.length != 0); } diff --git a/foundry/test/uniswap_x/UniswapXFiller.t.sol b/foundry/test/uniswap_x/UniswapXFiller.t.sol index 1dc11df..8102e4f 100644 --- a/foundry/test/uniswap_x/UniswapXFiller.t.sol +++ b/foundry/test/uniswap_x/UniswapXFiller.t.sol @@ -17,6 +17,10 @@ contract UniswapXFillerTest is Test, TychoRouterTestSetup { address indexed token, uint256 amount, address indexed receiver ); + function getForkBlock() public pure override returns (uint256) { + return 22788691; + } + function fillerSetup() public { vm.startPrank(ADMIN); filler = new UniswapXFiller(tychoRouterAddr, REACTOR); @@ -35,6 +39,126 @@ contract UniswapXFillerTest is Test, TychoRouterTestSetup { filler = new UniswapXFiller(tychoRouterAddr, address(0)); } + function testCallback() public { + fillerSetup(); + uint256 amountIn = 10 ** 18; + uint256 amountOut = 1847751195973566072891; + bool zeroForOne = false; + bytes memory protocolData = abi.encodePacked( + WETH_ADDR, + WETH_DAI_POOL, + address(filler), + zeroForOne, + RestrictTransferFrom.TransferType.TransferFrom + ); + + bytes memory swap = + encodeSingleSwap(address(usv2Executor), protocolData); + + bytes memory callbackData = abi.encodeWithSelector( + tychoRouter.singleSwap.selector, + amountIn, + WETH_ADDR, + DAI_ADDR, + 2008817438608734439722, + false, + false, + address(filler), + true, + swap + ); + + deal(WETH_ADDR, address(filler), amountIn); + vm.startPrank(address(filler)); + IERC20(WETH_ADDR).approve(tychoRouterAddr, amountIn); + vm.stopPrank(); + ResolvedOrder[] memory orders = new ResolvedOrder[](1); + 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 + orders[0] = ResolvedOrder({ + info: OrderInfo({ + reactor: address(0), + swapper: address(0), + nonce: 0, + deadline: 0, + additionalValidationContract: address(0), + additionalValidationData: "" + }), + input: InputToken({token: address(WETH_ADDR), amount: 0, maxAmount: 0}), + outputs: outputs, + sig: "", + hash: "" + }); + + vm.startPrank(REACTOR); + filler.reactorCallback(orders, callbackData); + vm.stopPrank(); + + // Check that the funds are in the filler at the end of the function call + uint256 finalBalance = IERC20(DAI_ADDR).balanceOf(address(filler)); + assertGe(finalBalance, amountOut); + + // Check that the proper approval was set + vm.startPrank(REACTOR); + IERC20(DAI_ADDR).transferFrom(address(filler), BOB, amountOut); + vm.stopPrank(); + assertGe(IERC20(DAI_ADDR).balanceOf(BOB), amountOut); + } + + function testExecuteIntegration() public { + fillerSetup(); + // tx: 0x5b602b7d0a37e241bd032a907b9ddf314e9f2fc2104fd91cb55bdb3d8dfe4e9c + // 0.2 WBTC -> USDC + SignedOrder memory order = SignedOrder({ + order: hex"000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001000000000000000000000000004449cd34d1eb1fedcf02a1be3834ffde8e6a61800000000000000000000000002260fac5e5542a773aa44fbcfedf7c193bc2c5990000000000000000000000000000000000000000000000000000000001312d000000000000000000000000000000000000000000000000000000000001312d0000000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000042000000000000000000000000000000011f84b9aa48e5f8aa8b9897600006289be0000000000000000000000000d1100e55ef6c4e5800f4624b1e6121d798eb696046832163cef9c09382cf582bb878b37a42933ea2bdf33757942ab2747b3500100000000000000000000000000000000000000000000000000000000685d4150000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000004f921447c00000000000000000000000000000000000000000000000000000004f1464dea0000000000000000000000000d1100e55ef6c4e5800f4624b1e6121d798eb696000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000000330d86400000000000000000000000000000000000000000000000000000000032bce2600000000000000000000000027213e28d7fda5c57fe9e5dd923818dbccf71c4700000000000000000000000000000000000000000000000000000000685d407600000000000000000000000000000000000000000000000000000000685d40b2000000000000000000000000225a38bc71102999dd13478bfabd7c4d53f2dc170000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000004fb7f8815000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000417067afde0759ae3653dad5d412519f488b6e9ed8955b3e3b8606e85c0198a9d71075295d33fe84b5ccc9c2d38a7ea79d7fad68128a37cabc5557342756a4e8311b00000000000000000000000000000000000000000000000000000000000000", + sig: hex"41b7a696a04f897d1e4ccaf88136092169c2874242d55c3fe4c028125efe95340f5ce764b9dce9d2cae241d97ceb515d3f1739972ca884ed51b2870045438c3a1c" + }); + + uint256 amountIn = 0.2 * 10 ** 8; + bool zeroForOne = true; + bytes memory protocolData = abi.encodePacked( + WBTC_ADDR, + USDC_WBTC_POOL, + fillerAddr, + zeroForOne, + RestrictTransferFrom.TransferType.TransferFrom + ); + + bytes memory swap = + encodeSingleSwap(address(usv2Executor), protocolData); + + bytes memory callbackData = abi.encodeWithSelector( + tychoRouter.singleSwap.selector, + amountIn, + WBTC_ADDR, + USDC_ADDR, + 1, + false, + false, + fillerAddr, + true, + swap + ); + + vm.startPrank(address(filler)); + IERC20(WBTC_ADDR).approve(tychoRouterAddr, amountIn); + vm.stopPrank(); + + // This is a hack because the tx we are trying to replicate returns a looooot more USDC than what the uni v2 pool does at this point + // 5113180081 is the difference and 54068100 is the fee + deal(USDC_ADDR, address(filler), 5113180081 + 54068100); + + vm.startPrank(EXECUTOR); + filler.execute(order, callbackData); + vm.stopPrank(); + } + function testWithdrawNative() public { fillerSetup(); vm.startPrank(ADMIN);