From 3dcbb6d1432c219627239a6ac0c16a0df0262a40 Mon Sep 17 00:00:00 2001 From: alrxy Date: Tue, 21 Jul 2026 14:14:36 +0700 Subject: [PATCH 1/5] feat(uniswapx): add liquidlane executor --- README.md | 2 + src/uniswapx/LiquidLaneUniswapXExecutor.sol | 163 +++++++ .../ILiquidLaneUniswapXExecutor.sol | 68 +++ src/uniswapx/interfaces/IUniswapXReactor.sol | 45 ++ .../uniswapx/LiquidLaneUniswapXExecutor.t.sol | 461 ++++++++++++++++++ 5 files changed, 739 insertions(+) create mode 100644 src/uniswapx/LiquidLaneUniswapXExecutor.sol create mode 100644 src/uniswapx/interfaces/ILiquidLaneUniswapXExecutor.sol create mode 100644 src/uniswapx/interfaces/IUniswapXReactor.sol create mode 100644 test/uniswapx/LiquidLaneUniswapXExecutor.t.sol diff --git a/README.md b/README.md index c474f32..efa5c1c 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ This directory contains the core RFQ settlement contracts used by the Symbiotic - `Reactor.sol` validates the signed order, pulls approved input from the swapper directly into factory-registered LiquidLane adapters, and enforces output delivery. - `Executor.sol` is an example role-gated execution surface that calls the Reactor, performs adapter swaps, runs any post-swap execution payload, and approves output transfers back to the Reactor. +- `LiquidLaneUniswapXExecutor.sol` fills ERC-20 UniswapX orders through its Reactor callback using owner-managed callers, matching the RFQ executor access model. The executor contract remains the Reactor-facing filler, while callers select LiquidLane routes. Routes may consume less than a Dutch order's resolved input; the positive difference remains in the executor as filler surplus. > [!NOTE] > @@ -15,6 +16,7 @@ This directory contains the core RFQ settlement contracts used by the Symbiotic - [Reactor.sol](src/Reactor.sol) - [Executor.sol](src/Executor.sol) +- [LiquidLaneUniswapXExecutor.sol](src/uniswapx/LiquidLaneUniswapXExecutor.sol) ## Flow diff --git a/src/uniswapx/LiquidLaneUniswapXExecutor.sol b/src/uniswapx/LiquidLaneUniswapXExecutor.sol new file mode 100644 index 0000000..1fe2e31 --- /dev/null +++ b/src/uniswapx/LiquidLaneUniswapXExecutor.sol @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: BUSL-1.1 +// Copyright (c) 2026 Symbiotic +pragma solidity 0.8.28; + +import {ILiquidLaneAdapter} from "../interfaces/ILiquidLaneAdapter.sol"; +import {ILiquidLaneUniswapXExecutor} from "./interfaces/ILiquidLaneUniswapXExecutor.sol"; +import {IUniswapXReactor, UniswapXResolvedOrder, UniswapXSignedOrder} from "./interfaces/IUniswapXReactor.sol"; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +/// @title LiquidLaneUniswapXExecutor +/// @notice UniswapX Reactor callback that atomically sources same-token outputs from LiquidLane adapters. +contract LiquidLaneUniswapXExecutor is Ownable, ReentrancyGuard, ILiquidLaneUniswapXExecutor { + using SafeERC20 for IERC20; + + address public immutable REACTOR; + address[] public callers; + + modifier onlyCaller() { + if (!_isCaller(msg.sender)) revert NotCaller(); + _; + } + + constructor(address reactor, address owner_, address[] memory initCallers) Ownable(owner_) { + if (reactor == address(0) || owner_ == address(0)) revert ZeroAddress(); + REACTOR = reactor; + callers = initCallers; + } + + function execute(UniswapXSignedOrder calldata order, FillCall calldata fillCall) external onlyCaller { + IUniswapXReactor(REACTOR).executeWithCallback(order, abi.encode(fillCall)); + } + + function isCaller(address caller) external view returns (bool) { + return _isCaller(caller); + } + + function reactorCallback(UniswapXResolvedOrder[] memory resolvedOrders, bytes memory callbackData) + external + nonReentrant + { + if (msg.sender != REACTOR) revert NotReactor(); + if (resolvedOrders.length != 1) revert InvalidOrderCount(); + + UniswapXResolvedOrder memory order = resolvedOrders[0]; + if (order.outputs.length == 0) revert InvalidOutputCount(); + address outputToken = order.outputs[0].token; + if (order.input.token == address(0) || outputToken == address(0)) revert ZeroAddress(); + if (order.input.amount == 0) revert InvalidAmount(); + if (order.input.token == outputToken) revert IdenticalTokens(); + uint256 requiredAmountOut; + for (uint256 i; i < order.outputs.length; ++i) { + if (order.outputs[i].token != outputToken) { + revert OutputTokenMismatch(outputToken, order.outputs[i].token); + } + if (order.outputs[i].amount == 0) revert InvalidAmount(); + requiredAmountOut += order.outputs[i].amount; + } + + FillCall memory fillCall = abi.decode(callbackData, (FillCall)); + (uint256 routedAmountIn, uint256 minimumAmountOut) = + _validateFillCall(fillCall, order.input.token, order.input.amount); + if (minimumAmountOut < requiredAmountOut) { + revert InsufficientMinimumOutput(minimumAmountOut, requiredAmountOut); + } + + IERC20 tokenIn = IERC20(order.input.token); + IERC20 tokenOut = IERC20(outputToken); + uint256 outputBefore = tokenOut.balanceOf(address(this)); + for (uint256 i; i < fillCall.routes.length; ++i) { + FillRoute memory route = fillCall.routes[i]; + uint256 routeOutputBefore = tokenOut.balanceOf(address(this)); + tokenIn.safeTransfer(route.adapter, route.amountIn); + ILiquidLaneAdapter(route.adapter) + .swap( + ILiquidLaneAdapter.Swap({ + recipient: address(this), + tokenIn: order.input.token, + amountIn: route.amountIn, + amountOut: route.amountOut + }) + ); + uint256 amountOut = tokenOut.balanceOf(address(this)) - routeOutputBefore; + if (amountOut < route.amountOut) { + revert RouteOutputTooLow(route.adapter, route.amountOut, amountOut); + } + emit InputRedeemed(order.hash, route.adapter, order.input.token, outputToken, route.amountIn, amountOut); + } + for (uint256 i; i < fillCall.discountRoutes.length; ++i) { + DiscountRoute memory route = fillCall.discountRoutes[i]; + uint256 routeOutputBefore = tokenOut.balanceOf(address(this)); + tokenIn.safeTransfer(route.adapter, route.amountIn); + ILiquidLaneAdapter(route.adapter) + .swap(route.discountSwap, route.protocolSignature, address(this), route.amountIn); + uint256 amountOut = tokenOut.balanceOf(address(this)) - routeOutputBefore; + if (amountOut < route.minAmountOut) { + revert RouteOutputTooLow(route.adapter, route.minAmountOut, amountOut); + } + emit InputRedeemed(order.hash, route.adapter, order.input.token, outputToken, route.amountIn, amountOut); + } + + uint256 outputGained = tokenOut.balanceOf(address(this)) - outputBefore; + if (outputGained < requiredAmountOut) revert InsufficientOutput(requiredAmountOut, outputGained); + tokenOut.forceApprove(REACTOR, requiredAmountOut); + + emit OrderFilled( + order.hash, + order.input.token, + outputToken, + order.input.amount, + order.input.amount - routedAmountIn, + requiredAmountOut, + outputGained - requiredAmountOut + ); + } + + function setCallers(address[] calldata newCallers) public onlyOwner { + callers = newCallers; + emit SetCallers(newCallers); + } + + function sweepERC20(address token, address to, uint256 amount) external onlyOwner { + if (token == address(0) || to == address(0)) revert ZeroAddress(); + IERC20(token).safeTransfer(to, amount); + } + + function _isCaller(address caller) internal view returns (bool) { + for (uint256 i; i < callers.length; ++i) { + if (callers[i] == caller) return true; + } + return false; + } + + function _validateFillCall(FillCall memory fillCall, address tokenIn, uint256 orderAmountIn) + internal + pure + returns (uint256 routedAmountIn, uint256 minimumAmountOut) + { + if (fillCall.routes.length == 0 && fillCall.discountRoutes.length == 0) revert EmptyRoutes(); + for (uint256 i; i < fillCall.routes.length; ++i) { + FillRoute memory route = fillCall.routes[i]; + if (route.adapter == address(0)) revert ZeroAddress(); + if (route.amountIn == 0 || route.amountOut == 0) revert InvalidAmount(); + routedAmountIn += route.amountIn; + minimumAmountOut += route.amountOut; + } + for (uint256 i; i < fillCall.discountRoutes.length; ++i) { + DiscountRoute memory route = fillCall.discountRoutes[i]; + if (route.adapter == address(0)) revert ZeroAddress(); + if (route.amountIn == 0 || route.minAmountOut == 0) revert InvalidAmount(); + if (route.discountSwap.discount.tokenToRedeem != tokenIn) { + revert DiscountTokenMismatch(tokenIn, route.discountSwap.discount.tokenToRedeem); + } + routedAmountIn += route.amountIn; + minimumAmountOut += route.minAmountOut; + } + // Exact-output Dutch input may increase after off-chain planning; retain that positive difference. + if (routedAmountIn > orderAmountIn) revert RouteInputExceedsOrder(routedAmountIn, orderAmountIn); + } +} diff --git a/src/uniswapx/interfaces/ILiquidLaneUniswapXExecutor.sol b/src/uniswapx/interfaces/ILiquidLaneUniswapXExecutor.sol new file mode 100644 index 0000000..8920005 --- /dev/null +++ b/src/uniswapx/interfaces/ILiquidLaneUniswapXExecutor.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: BUSL-1.1 +// Copyright (c) 2026 Symbiotic +pragma solidity ^0.8.0; + +import {ILiquidLaneAdapter} from "../../interfaces/ILiquidLaneAdapter.sol"; +import {IUniswapXReactorCallback, UniswapXSignedOrder} from "./IUniswapXReactor.sol"; + +interface ILiquidLaneUniswapXExecutor is IUniswapXReactorCallback { + error DiscountTokenMismatch(address expectedToken, address actualToken); + error EmptyRoutes(); + error InsufficientMinimumOutput(uint256 minimumAmountOut, uint256 requiredAmountOut); + error InsufficientOutput(uint256 requiredAmountOut, uint256 receivedAmountOut); + error IdenticalTokens(); + error InvalidAmount(); + error InvalidOrderCount(); + error InvalidOutputCount(); + error NotCaller(); + error NotReactor(); + error OutputTokenMismatch(address expectedToken, address actualToken); + error RouteInputExceedsOrder(uint256 routedAmountIn, uint256 orderAmountIn); + error RouteOutputTooLow(address adapter, uint256 minAmountOut, uint256 availableAmountOut); + error ZeroAddress(); + + struct FillRoute { + address adapter; + uint256 amountIn; + uint256 amountOut; + } + + struct FillCall { + FillRoute[] routes; + DiscountRoute[] discountRoutes; + } + + struct DiscountRoute { + address adapter; + uint256 amountIn; + uint256 minAmountOut; + ILiquidLaneAdapter.DiscountSwap discountSwap; + bytes protocolSignature; + } + + event SetCallers(address[] newCallers); + event InputRedeemed( + bytes32 indexed orderHash, + address indexed adapter, + address indexed tokenIn, + address tokenOut, + uint256 amountIn, + uint256 amountOut + ); + event OrderFilled( + bytes32 indexed orderHash, + address indexed tokenIn, + address indexed tokenOut, + uint256 amountIn, + uint256 inputSurplus, + uint256 amountOut, + uint256 outputSurplus + ); + + function REACTOR() external view returns (address reactor); + function callers(uint256 index) external view returns (address caller); + function execute(UniswapXSignedOrder calldata order, FillCall calldata fillCall) external; + function isCaller(address caller) external view returns (bool allowed); + function setCallers(address[] calldata newCallers) external; + function sweepERC20(address token, address to, uint256 amount) external; +} diff --git a/src/uniswapx/interfaces/IUniswapXReactor.sol b/src/uniswapx/interfaces/IUniswapXReactor.sol new file mode 100644 index 0000000..91994e5 --- /dev/null +++ b/src/uniswapx/interfaces/IUniswapXReactor.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: BUSL-1.1 +// Copyright (c) 2026 Symbiotic +pragma solidity ^0.8.0; + +struct UniswapXOrderInfo { + address reactor; + address swapper; + uint256 nonce; + uint256 deadline; + address additionalValidationContract; + bytes additionalValidationData; +} + +struct UniswapXInputToken { + address token; + uint256 amount; + uint256 maxAmount; +} + +struct UniswapXOutputToken { + address token; + uint256 amount; + address recipient; +} + +struct UniswapXResolvedOrder { + UniswapXOrderInfo info; + UniswapXInputToken input; + UniswapXOutputToken[] outputs; + bytes sig; + bytes32 hash; +} + +struct UniswapXSignedOrder { + bytes order; + bytes sig; +} + +interface IUniswapXReactor { + function executeWithCallback(UniswapXSignedOrder calldata order, bytes calldata callbackData) external payable; +} + +interface IUniswapXReactorCallback { + function reactorCallback(UniswapXResolvedOrder[] memory resolvedOrders, bytes memory callbackData) external; +} diff --git a/test/uniswapx/LiquidLaneUniswapXExecutor.t.sol b/test/uniswapx/LiquidLaneUniswapXExecutor.t.sol new file mode 100644 index 0000000..ad53b13 --- /dev/null +++ b/test/uniswapx/LiquidLaneUniswapXExecutor.t.sol @@ -0,0 +1,461 @@ +// SPDX-License-Identifier: BUSL-1.1 +// Copyright (c) 2026 Symbiotic +pragma solidity 0.8.28; + +import {ILiquidLaneAdapter} from "../../src/interfaces/ILiquidLaneAdapter.sol"; +import {LiquidLaneUniswapXExecutor} from "../../src/uniswapx/LiquidLaneUniswapXExecutor.sol"; +import {ILiquidLaneUniswapXExecutor} from "../../src/uniswapx/interfaces/ILiquidLaneUniswapXExecutor.sol"; +import { + IUniswapXReactor, + IUniswapXReactorCallback, + UniswapXInputToken, + UniswapXOrderInfo, + UniswapXOutputToken, + UniswapXResolvedOrder, + UniswapXSignedOrder +} from "../../src/uniswapx/interfaces/IUniswapXReactor.sol"; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {Test} from "forge-std/Test.sol"; + +contract LiquidLaneUniswapXExecutorTest is Test { + address internal caller = makeAddr("caller"); + address internal owner = makeAddr("owner"); + address internal recipient = makeAddr("recipient"); + address internal feeRecipient = makeAddr("feeRecipient"); + + TestToken internal inputToken; + TestToken internal outputToken; + MockUniswapXAdapter internal adapter; + MockUniswapXReactor internal reactor; + LiquidLaneUniswapXExecutor internal executor; + + function setUp() public { + inputToken = new TestToken("Input", "IN"); + outputToken = new TestToken("Output", "OUT"); + adapter = new MockUniswapXAdapter(outputToken); + reactor = new MockUniswapXReactor(); + + executor = new LiquidLaneUniswapXExecutor(address(reactor), owner, _callers(caller)); + + inputToken.mint(address(reactor), 10 ether); + outputToken.mint(address(adapter), 100 ether); + reactor.setOrder(_resolvedOrder(10 ether, 9 ether)); + } + + function testExecuteFillsThroughReactorCallback() public { + vm.prank(caller); + executor.execute(UniswapXSignedOrder({order: hex"01", sig: hex"02"}), _fillCall(10 ether, 9 ether)); + + assertEq(inputToken.balanceOf(address(adapter)), 10 ether); + assertEq(outputToken.balanceOf(recipient), 9 ether); + assertEq(outputToken.balanceOf(address(executor)), 0); + assertEq(outputToken.allowance(address(executor), address(reactor)), 0); + } + + function testExecuteKeepsDirectRouteSurplus() public { + vm.prank(caller); + executor.execute(UniswapXSignedOrder({order: hex"01", sig: hex"02"}), _fillCall(10 ether, 10 ether)); + + assertEq(outputToken.balanceOf(recipient), 9 ether); + assertEq(outputToken.balanceOf(address(executor)), 1 ether); + assertEq(outputToken.allowance(address(executor), address(reactor)), 0); + } + + function testExecuteFillsSignedDiscountRoute() public { + vm.prank(caller); + executor.execute(UniswapXSignedOrder({order: hex"01", sig: hex"02"}), _discountFillCall(10 ether, 9 ether)); + + assertEq(inputToken.balanceOf(address(adapter)), 10 ether); + assertEq(outputToken.balanceOf(recipient), 9 ether); + assertEq(outputToken.balanceOf(address(executor)), 1 ether); + assertEq(outputToken.allowance(address(executor), address(reactor)), 0); + } + + function testExecuteFillsSameTokenFeeOutputs() public { + reactor.setOrder(_resolvedMultiOutputOrder(10 ether, 8 ether, 1 ether)); + + vm.prank(caller); + executor.execute(UniswapXSignedOrder({order: hex"01", sig: hex"02"}), _fillCall(10 ether, 9 ether)); + + assertEq(outputToken.balanceOf(recipient), 8 ether); + assertEq(outputToken.balanceOf(feeRecipient), 1 ether); + assertEq(outputToken.balanceOf(address(executor)), 0); + assertEq(outputToken.allowance(address(executor), address(reactor)), 0); + } + + function testExecuteRejectsMixedOutputTokens() public { + TestToken otherOutput = new TestToken("Other Output", "OTHER"); + UniswapXResolvedOrder memory order = _resolvedMultiOutputOrder(10 ether, 8 ether, 1 ether); + order.outputs[1].token = address(otherOutput); + reactor.setOrder(order); + + vm.prank(caller); + vm.expectRevert( + abi.encodeWithSelector( + ILiquidLaneUniswapXExecutor.OutputTokenMismatch.selector, address(outputToken), address(otherOutput) + ) + ); + executor.execute(UniswapXSignedOrder({order: hex"01", sig: hex"02"}), _fillCall(10 ether, 9 ether)); + } + + function testExecuteRejectsDiscountAdapterThatOverreportsOutput() public { + adapter.setAmountOut(8 ether); + adapter.setReportedAmountOut(10 ether); + + vm.prank(caller); + vm.expectRevert( + abi.encodeWithSelector( + ILiquidLaneUniswapXExecutor.RouteOutputTooLow.selector, address(adapter), 9 ether, 8 ether + ) + ); + executor.execute(UniswapXSignedOrder({order: hex"01", sig: hex"02"}), _discountFillCall(10 ether, 9 ether)); + } + + function testExecuteRejectsNonCaller() public { + vm.expectRevert(ILiquidLaneUniswapXExecutor.NotCaller.selector); + vm.prank(makeAddr("relayer")); + executor.execute(UniswapXSignedOrder({order: hex"01", sig: hex"02"}), _fillCall(10 ether, 9 ether)); + } + + function testOwnerCanReplaceCallers() public { + address newCaller = makeAddr("newCaller"); + + vm.prank(owner); + executor.setCallers(_callers(newCaller)); + + assertEq(executor.callers(0), newCaller); + assertTrue(executor.isCaller(newCaller)); + assertFalse(executor.isCaller(caller)); + vm.prank(newCaller); + executor.execute(UniswapXSignedOrder({order: hex"01", sig: hex"02"}), _fillCall(10 ether, 9 ether)); + assertEq(outputToken.balanceOf(recipient), 9 ether); + } + + function testOnlyOwnerCanReplaceCallers() public { + address relayer = makeAddr("relayer"); + + vm.prank(relayer); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, relayer)); + executor.setCallers(_callers(relayer)); + } + + function testOnlyOwnerCanSweep() public { + address relayer = makeAddr("relayer"); + inputToken.mint(address(executor), 1 ether); + + vm.prank(relayer); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, relayer)); + executor.sweepERC20(address(inputToken), relayer, 1 ether); + } + + function testOwnerCanSweep() public { + inputToken.mint(address(executor), 1 ether); + + vm.prank(owner); + executor.sweepERC20(address(inputToken), owner, 1 ether); + + assertEq(inputToken.balanceOf(owner), 1 ether); + } + + function testCallbackRejectsNonReactor() public { + UniswapXResolvedOrder[] memory orders = new UniswapXResolvedOrder[](1); + orders[0] = _resolvedOrder(10 ether, 9 ether); + + vm.expectRevert(ILiquidLaneUniswapXExecutor.NotReactor.selector); + executor.reactorCallback(orders, abi.encode(_fillCall(10 ether, 9 ether))); + } + + function testCallbackRejectsZeroInputToken() public { + UniswapXResolvedOrder[] memory orders = new UniswapXResolvedOrder[](1); + orders[0] = _resolvedOrder(10 ether, 9 ether); + orders[0].input.token = address(0); + + vm.expectRevert(ILiquidLaneUniswapXExecutor.ZeroAddress.selector); + vm.prank(address(reactor)); + executor.reactorCallback(orders, abi.encode(_fillCall(10 ether, 9 ether))); + } + + function testCallbackRejectsZeroOutputToken() public { + UniswapXResolvedOrder[] memory orders = new UniswapXResolvedOrder[](1); + orders[0] = _resolvedOrder(10 ether, 9 ether); + orders[0].outputs[0].token = address(0); + + vm.expectRevert(ILiquidLaneUniswapXExecutor.ZeroAddress.selector); + vm.prank(address(reactor)); + executor.reactorCallback(orders, abi.encode(_fillCall(10 ether, 9 ether))); + } + + function testExecuteRejectsInsufficientRouteMinimum() public { + vm.prank(caller); + vm.expectRevert( + abi.encodeWithSelector(ILiquidLaneUniswapXExecutor.InsufficientMinimumOutput.selector, 8 ether, 9 ether) + ); + executor.execute(UniswapXSignedOrder({order: hex"01", sig: hex"02"}), _fillCall(10 ether, 8 ether)); + } + + function testExecuteRejectsRouteThatNoLongerMeetsMinimum() public { + adapter.setAmountOut(8 ether); + + vm.prank(caller); + vm.expectRevert( + abi.encodeWithSelector( + ILiquidLaneUniswapXExecutor.RouteOutputTooLow.selector, address(adapter), 9 ether, 8 ether + ) + ); + executor.execute(UniswapXSignedOrder({order: hex"01", sig: hex"02"}), _fillCall(10 ether, 9 ether)); + } + + function testExecuteAcceptsRoutePlannedBeforeExactOutputDecay() public { + vm.prank(caller); + executor.execute(UniswapXSignedOrder({order: hex"01", sig: hex"02"}), _fillCall(9 ether, 9 ether)); + + assertEq(inputToken.balanceOf(address(adapter)), 9 ether); + assertEq(inputToken.balanceOf(address(executor)), 1 ether); + assertEq(outputToken.balanceOf(recipient), 9 ether); + } + + function testExecuteAcceptsDiscountRoutePlannedBeforeExactOutputDecay() public { + vm.prank(caller); + executor.execute(UniswapXSignedOrder({order: hex"01", sig: hex"02"}), _discountFillCall(9 ether, 9 ether)); + + assertEq(inputToken.balanceOf(address(adapter)), 9 ether); + assertEq(inputToken.balanceOf(address(executor)), 1 ether); + assertEq(outputToken.balanceOf(recipient), 9 ether); + } + + function testExecuteRejectsInputAboveResolvedAmount() public { + vm.prank(caller); + vm.expectRevert( + abi.encodeWithSelector(ILiquidLaneUniswapXExecutor.RouteInputExceedsOrder.selector, 11 ether, 10 ether) + ); + executor.execute(UniswapXSignedOrder({order: hex"01", sig: hex"02"}), _fillCall(11 ether, 9 ether)); + } + + function testExecuteRejectsZeroAdapter() public { + ILiquidLaneUniswapXExecutor.FillCall memory fillCall = _fillCall(10 ether, 9 ether); + fillCall.routes[0].adapter = address(0); + + vm.prank(caller); + vm.expectRevert(ILiquidLaneUniswapXExecutor.ZeroAddress.selector); + executor.execute(UniswapXSignedOrder({order: hex"01", sig: hex"02"}), fillCall); + } + + function testExecuteRejectsZeroDiscountAdapter() public { + ILiquidLaneUniswapXExecutor.FillCall memory fillCall = _discountFillCall(10 ether, 9 ether); + fillCall.discountRoutes[0].adapter = address(0); + + vm.prank(caller); + vm.expectRevert(ILiquidLaneUniswapXExecutor.ZeroAddress.selector); + executor.execute(UniswapXSignedOrder({order: hex"01", sig: hex"02"}), fillCall); + } + + function testExecuteRejectsDiscountForAnotherInputToken() public { + ILiquidLaneUniswapXExecutor.FillCall memory fillCall = _discountFillCall(10 ether, 9 ether); + fillCall.discountRoutes[0].discountSwap.discount.tokenToRedeem = address(outputToken); + + vm.prank(caller); + vm.expectRevert( + abi.encodeWithSelector( + ILiquidLaneUniswapXExecutor.DiscountTokenMismatch.selector, address(inputToken), address(outputToken) + ) + ); + executor.execute(UniswapXSignedOrder({order: hex"01", sig: hex"02"}), fillCall); + } + + function _resolvedOrder(uint256 amountIn, uint256 amountOut) internal returns (UniswapXResolvedOrder memory order) { + UniswapXOutputToken[] memory outputs = new UniswapXOutputToken[](1); + outputs[0] = UniswapXOutputToken({token: address(outputToken), amount: amountOut, recipient: recipient}); + order = UniswapXResolvedOrder({ + info: UniswapXOrderInfo({ + reactor: address(reactor), + swapper: makeAddr("swapper"), + nonce: 1, + deadline: block.timestamp + 1 hours, + additionalValidationContract: address(0), + additionalValidationData: "" + }), + input: UniswapXInputToken({token: address(inputToken), amount: amountIn, maxAmount: amountIn}), + outputs: outputs, + sig: "", + hash: keccak256("order") + }); + } + + function _callers(address caller) internal pure returns (address[] memory callers_) { + callers_ = new address[](1); + callers_[0] = caller; + } + + function _resolvedMultiOutputOrder(uint256 amountIn, uint256 swapperAmountOut, uint256 feeAmountOut) + internal + returns (UniswapXResolvedOrder memory order) + { + UniswapXOutputToken[] memory outputs = new UniswapXOutputToken[](2); + outputs[0] = UniswapXOutputToken({token: address(outputToken), amount: swapperAmountOut, recipient: recipient}); + outputs[1] = UniswapXOutputToken({token: address(outputToken), amount: feeAmountOut, recipient: feeRecipient}); + order = UniswapXResolvedOrder({ + info: UniswapXOrderInfo({ + reactor: address(reactor), + swapper: makeAddr("swapper"), + nonce: 1, + deadline: block.timestamp + 1 hours, + additionalValidationContract: address(0), + additionalValidationData: "" + }), + input: UniswapXInputToken({token: address(inputToken), amount: amountIn, maxAmount: amountIn}), + outputs: outputs, + sig: "", + hash: keccak256("multi-output-order") + }); + } + + function _fillCall(uint256 amountIn, uint256 amountOut) + internal + view + returns (ILiquidLaneUniswapXExecutor.FillCall memory fillCall) + { + ILiquidLaneUniswapXExecutor.FillRoute[] memory routes = new ILiquidLaneUniswapXExecutor.FillRoute[](1); + routes[0] = ILiquidLaneUniswapXExecutor.FillRoute({ + adapter: address(adapter), amountIn: amountIn, amountOut: amountOut + }); + fillCall = ILiquidLaneUniswapXExecutor.FillCall({ + routes: routes, discountRoutes: new ILiquidLaneUniswapXExecutor.DiscountRoute[](0) + }); + } + + function _discountFillCall(uint256 amountIn, uint256 minAmountOut) + internal + view + returns (ILiquidLaneUniswapXExecutor.FillCall memory fillCall) + { + ILiquidLaneUniswapXExecutor.DiscountRoute[] memory routes = new ILiquidLaneUniswapXExecutor.DiscountRoute[](1); + routes[0] = ILiquidLaneUniswapXExecutor.DiscountRoute({ + adapter: address(adapter), + amountIn: amountIn, + minAmountOut: minAmountOut, + discountSwap: ILiquidLaneAdapter.DiscountSwap({ + discount: ILiquidLaneAdapter.Discount({ + tokenToRedeem: address(inputToken), + discount: 0, + signer: address(1), + protocol: address(2), + nonce: 1, + deadline: uint48(block.timestamp + 1 hours) + }), + signerSignature: hex"01", + protocolDeadline: uint48(block.timestamp + 1 hours) + }), + protocolSignature: hex"02" + }); + fillCall = ILiquidLaneUniswapXExecutor.FillCall({ + routes: new ILiquidLaneUniswapXExecutor.FillRoute[](0), discountRoutes: routes + }); + } +} + +contract MockUniswapXReactor is IUniswapXReactor { + using SafeERC20 for IERC20; + + address internal tokenIn; + uint256 internal amountIn; + UniswapXOutputToken[] internal storedOutputs; + + function setOrder(UniswapXResolvedOrder memory newOrder) external { + tokenIn = newOrder.input.token; + amountIn = newOrder.input.amount; + delete storedOutputs; + for (uint256 i; i < newOrder.outputs.length; ++i) { + storedOutputs.push(newOrder.outputs[i]); + } + } + + function executeWithCallback(UniswapXSignedOrder calldata, bytes calldata callbackData) external payable { + IERC20(tokenIn).safeTransfer(msg.sender, amountIn); + + UniswapXResolvedOrder[] memory orders = new UniswapXResolvedOrder[](1); + UniswapXOutputToken[] memory outputs = new UniswapXOutputToken[](storedOutputs.length); + for (uint256 i; i < storedOutputs.length; ++i) { + outputs[i] = storedOutputs[i]; + } + orders[0] = UniswapXResolvedOrder({ + info: UniswapXOrderInfo({ + reactor: address(this), + swapper: address(1), + nonce: 1, + deadline: block.timestamp + 1 hours, + additionalValidationContract: address(0), + additionalValidationData: "" + }), + input: UniswapXInputToken({token: tokenIn, amount: amountIn, maxAmount: amountIn}), + outputs: outputs, + sig: "", + hash: keccak256("order") + }); + IUniswapXReactorCallback(msg.sender).reactorCallback(orders, callbackData); + + for (uint256 i; i < outputs.length; ++i) { + IERC20(outputs[i].token).safeTransferFrom(msg.sender, outputs[i].recipient, outputs[i].amount); + } + } +} + +contract MockUniswapXAdapter is ILiquidLaneAdapter { + using SafeERC20 for IERC20; + + TestToken internal immutable outputToken; + uint256 internal amountOut = 10 ether; + uint256 internal reportedAmountOut = 10 ether; + + constructor(TestToken outputToken_) { + outputToken = outputToken_; + } + + function setAmountOut(uint256 newAmountOut) external { + amountOut = newAmountOut; + } + + function setReportedAmountOut(uint256 newAmountOut) external { + reportedAmountOut = newAmountOut; + } + + function getAmountOut(address, uint256) external view returns (uint256) { + return amountOut; + } + + function getMaxAssets(address) external view returns (uint256) { + return outputToken.balanceOf(address(this)); + } + + function minDiscount(address) external pure returns (uint256) { + return 0; + } + + function swap(Swap calldata swap_) external { + IERC20(address(outputToken)).safeTransfer(swap_.recipient, _min(swap_.amountOut, amountOut)); + } + + function swap(SignedSwap calldata, bytes calldata) external pure { + revert("unsupported"); + } + + function swap(DiscountSwap calldata, bytes calldata, address recipient_, uint256) external returns (uint256) { + IERC20(address(outputToken)).safeTransfer(recipient_, amountOut); + return reportedAmountOut; + } + + function _min(uint256 left, uint256 right) private pure returns (uint256) { + return left < right ? left : right; + } +} + +contract TestToken is ERC20 { + constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) {} + + function mint(address to, uint256 amount) external { + _mint(to, amount); + } +} From 945044fc23d5acd9c30ee12460c860666dffdba4 Mon Sep 17 00:00:00 2001 From: Andrey Date: Thu, 23 Jul 2026 13:46:27 +0400 Subject: [PATCH 2/5] refactor: uniswap executor --- README.md | 12 +- script/DeployUniswapXExecutor.s.sol | 19 + .../base/DeployUniswapXExecutorBase.s.sol | 81 +++ src/oev/SymbioticOevSolver.sol | 7 +- src/uniswapx/LiquidLaneUniswapXExecutor.sol | 145 ++--- .../ILiquidLaneUniswapXExecutor.sol | 34 +- test/deploy/DeployUniswapXExecutor.t.sol | 79 +++ .../uniswapx/LiquidLaneUniswapXExecutor.t.sol | 511 ++++++++++-------- 8 files changed, 512 insertions(+), 376 deletions(-) create mode 100644 script/DeployUniswapXExecutor.s.sol create mode 100644 script/deploy/base/DeployUniswapXExecutorBase.s.sol create mode 100644 test/deploy/DeployUniswapXExecutor.t.sol diff --git a/README.md b/README.md index efa5c1c..26d9f4a 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ This directory contains the core RFQ settlement contracts used by the Symbiotic - `Reactor.sol` validates the signed order, pulls approved input from the swapper directly into factory-registered LiquidLane adapters, and enforces output delivery. - `Executor.sol` is an example role-gated execution surface that calls the Reactor, performs adapter swaps, runs any post-swap execution payload, and approves output transfers back to the Reactor. -- `LiquidLaneUniswapXExecutor.sol` fills ERC-20 UniswapX orders through its Reactor callback using owner-managed callers, matching the RFQ executor access model. The executor contract remains the Reactor-facing filler, while callers select LiquidLane routes. Routes may consume less than a Dutch order's resolved input; the positive difference remains in the executor as filler surplus. +- `LiquidLaneUniswapXExecutor.sol` is a transparent-proxy, caller-gated UniswapX fill contract. It routes Reactor-supplied ERC-20 input through direct and discount LiquidLane adapters, grants the immutable Reactor output allowances, and forwards native output. The Reactor authoritatively resolves and settles orders; adapters enforce swap and discount terms. Direct and discount route arrays remain separate, and unspent input or output surplus remains on the executor. Because the Reactor has maximum ERC-20 allowances, retained balances can participate in subsequent Reactor settlement; there is no sweep entrypoint. > [!NOTE] > @@ -42,6 +42,7 @@ The repository includes helper scripts for deploying the Reactor and the example - `script/deploy/DeployExecutor.s.sol` - `script/deploy/DeployReactor.s.sol` +- `script/DeployUniswapXExecutor.s.sol` Example `Executor` deployment: @@ -55,6 +56,15 @@ forge script script/deploy/DeployExecutor.s.sol:DeployExecutorScript \ --broadcast ``` +UniswapX executor deployment expects `UNISWAPX_REACTOR` to be exported. `ADMIN`, `PROXY_ADMIN_OWNER`, and `CALLER` are optional and default to the broadcaster: + +```bash +forge script script/DeployUniswapXExecutor.s.sol:DeployUniswapXExecutorScript \ + --rpc-url "$RPC_URL" \ + --private-key "$PRIVATE_KEY" \ + --broadcast +``` + ## Files to know - `src/Reactor.sol` diff --git a/script/DeployUniswapXExecutor.s.sol b/script/DeployUniswapXExecutor.s.sol new file mode 100644 index 0000000..644de6e --- /dev/null +++ b/script/DeployUniswapXExecutor.s.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: BUSL-1.1 +// Copyright (c) 2026 Symbiotic +pragma solidity 0.8.28; + +import {DeployUniswapXExecutorBaseScript} from "./deploy/base/DeployUniswapXExecutorBase.s.sol"; + +contract DeployUniswapXExecutorScript is DeployUniswapXExecutorBaseScript { + function run() public returns (DeploymentData memory data) { + address owner = _scriptOwner(); + data = runBase( + DeployParams({ + reactor: vm.envAddress("UNISWAPX_REACTOR"), + admin: vm.envOr("ADMIN", owner), + proxyAdminOwner: vm.envOr("PROXY_ADMIN_OWNER", owner), + caller: vm.envOr("CALLER", owner) + }) + ); + } +} diff --git a/script/deploy/base/DeployUniswapXExecutorBase.s.sol b/script/deploy/base/DeployUniswapXExecutorBase.s.sol new file mode 100644 index 0000000..350ba53 --- /dev/null +++ b/script/deploy/base/DeployUniswapXExecutorBase.s.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: BUSL-1.1 +// Copyright (c) 2026 Symbiotic +pragma solidity 0.8.28; + +import {Script, console2} from "forge-std/Script.sol"; + +import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +import {LiquidLaneUniswapXExecutor} from "../../../src/uniswapx/LiquidLaneUniswapXExecutor.sol"; + +abstract contract DeployUniswapXExecutorBaseScript is Script { + struct DeployParams { + address reactor; + address admin; + address proxyAdminOwner; + address caller; + } + + struct DeploymentData { + LiquidLaneUniswapXExecutor executor; + address implementation; + address reactor; + address admin; + address proxyAdminOwner; + address caller; + } + + function runBase(DeployParams memory params) public virtual returns (DeploymentData memory data) { + _validateParams(params); + address[] memory callers = new address[](1); + callers[0] = params.caller; + + _startBroadcast(); + LiquidLaneUniswapXExecutor implementation = new LiquidLaneUniswapXExecutor(params.reactor); + TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy( + address(implementation), + params.proxyAdminOwner, + abi.encodeCall(LiquidLaneUniswapXExecutor.initialize, (params.admin, callers)) + ); + _stopBroadcast(); + + data.executor = LiquidLaneUniswapXExecutor(payable(address(proxy))); + data.implementation = address(implementation); + data.reactor = params.reactor; + data.admin = params.admin; + data.proxyAdminOwner = params.proxyAdminOwner; + data.caller = params.caller; + + assert(address(data.executor) != data.implementation); + assert(data.executor.owner() == data.admin); + assert(data.executor.callers(0) == data.caller); + + console2.log("Deployed UniswapX Executor"); + console2.log(" executor: ", address(data.executor)); + console2.log(" implementation: ", data.implementation); + console2.log(" reactor: ", data.reactor); + console2.log(" admin: ", data.admin); + console2.log(" proxyAdminOwner: ", data.proxyAdminOwner); + console2.log(" caller: ", data.caller); + } + + function _validateParams(DeployParams memory params) internal pure { + require(params.reactor != address(0), "invalid reactor"); + require(params.admin != address(0), "invalid admin"); + require(params.proxyAdminOwner != address(0), "invalid proxy admin owner"); + require(params.caller != address(0), "invalid caller"); + } + + function _startBroadcast() internal virtual { + vm.startBroadcast(); + } + + function _stopBroadcast() internal virtual { + vm.stopBroadcast(); + } + + function _scriptOwner() internal view virtual returns (address owner) { + (,, address origin) = vm.readCallers(); + owner = origin == address(0) ? msg.sender : origin; + } +} diff --git a/src/oev/SymbioticOevSolver.sol b/src/oev/SymbioticOevSolver.sol index ac0da9b..731b004 100644 --- a/src/oev/SymbioticOevSolver.sol +++ b/src/oev/SymbioticOevSolver.sol @@ -219,11 +219,8 @@ contract SymbioticOevSolver is IOperationCallback, IMorphoLiquidateCallback, Ree ILiquidLaneAdapter(LIQUID_LANE_ADAPTER) .swap( ILiquidLaneAdapter.Swap({ - recipient: address(this), - tokenIn: ctx.collateralToken, - amountIn: ctx.seizedAssets, - amountOut: amountOut - }) + recipient: address(this), tokenIn: ctx.collateralToken, amountIn: ctx.seizedAssets, amountOut: amountOut + }) ); uint256 gained = IERC20(ctx.loanToken).balanceOf(address(this)) - loanBefore; diff --git a/src/uniswapx/LiquidLaneUniswapXExecutor.sol b/src/uniswapx/LiquidLaneUniswapXExecutor.sol index 1fe2e31..9649ef7 100644 --- a/src/uniswapx/LiquidLaneUniswapXExecutor.sol +++ b/src/uniswapx/LiquidLaneUniswapXExecutor.sol @@ -6,115 +6,75 @@ import {ILiquidLaneAdapter} from "../interfaces/ILiquidLaneAdapter.sol"; import {ILiquidLaneUniswapXExecutor} from "./interfaces/ILiquidLaneUniswapXExecutor.sol"; import {IUniswapXReactor, UniswapXResolvedOrder, UniswapXSignedOrder} from "./interfaces/IUniswapXReactor.sol"; +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; -import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; /// @title LiquidLaneUniswapXExecutor -/// @notice UniswapX Reactor callback that atomically sources same-token outputs from LiquidLane adapters. -contract LiquidLaneUniswapXExecutor is Ownable, ReentrancyGuard, ILiquidLaneUniswapXExecutor { +/// @notice UniswapX Reactor callback that atomically sources outputs from LiquidLane adapters. +contract LiquidLaneUniswapXExecutor is Initializable, OwnableUpgradeable, ILiquidLaneUniswapXExecutor { + using Address for address payable; using SafeERC20 for IERC20; - address public immutable REACTOR; + address internal immutable REACTOR; address[] public callers; - modifier onlyCaller() { - if (!_isCaller(msg.sender)) revert NotCaller(); - _; + constructor(address reactor) { + REACTOR = reactor; + _disableInitializers(); } - constructor(address reactor, address owner_, address[] memory initCallers) Ownable(owner_) { - if (reactor == address(0) || owner_ == address(0)) revert ZeroAddress(); - REACTOR = reactor; + function initialize(address owner, address[] calldata initCallers) external initializer { + __Ownable_init(owner); callers = initCallers; } - function execute(UniswapXSignedOrder calldata order, FillCall calldata fillCall) external onlyCaller { - IUniswapXReactor(REACTOR).executeWithCallback(order, abi.encode(fillCall)); + modifier onlyCaller() { + if (!_isCaller(msg.sender)) revert NotCaller(); + _; } - function isCaller(address caller) external view returns (bool) { - return _isCaller(caller); + function execute(UniswapXSignedOrder calldata order, FillCall calldata fillCall) public onlyCaller { + IUniswapXReactor(REACTOR).executeWithCallback(order, abi.encode(fillCall)); } - function reactorCallback(UniswapXResolvedOrder[] memory resolvedOrders, bytes memory callbackData) - external - nonReentrant - { + function reactorCallback(UniswapXResolvedOrder[] memory resolvedOrders, bytes memory callbackData) external { if (msg.sender != REACTOR) revert NotReactor(); - if (resolvedOrders.length != 1) revert InvalidOrderCount(); - - UniswapXResolvedOrder memory order = resolvedOrders[0]; - if (order.outputs.length == 0) revert InvalidOutputCount(); - address outputToken = order.outputs[0].token; - if (order.input.token == address(0) || outputToken == address(0)) revert ZeroAddress(); - if (order.input.amount == 0) revert InvalidAmount(); - if (order.input.token == outputToken) revert IdenticalTokens(); - uint256 requiredAmountOut; - for (uint256 i; i < order.outputs.length; ++i) { - if (order.outputs[i].token != outputToken) { - revert OutputTokenMismatch(outputToken, order.outputs[i].token); - } - if (order.outputs[i].amount == 0) revert InvalidAmount(); - requiredAmountOut += order.outputs[i].amount; - } FillCall memory fillCall = abi.decode(callbackData, (FillCall)); - (uint256 routedAmountIn, uint256 minimumAmountOut) = - _validateFillCall(fillCall, order.input.token, order.input.amount); - if (minimumAmountOut < requiredAmountOut) { - revert InsufficientMinimumOutput(minimumAmountOut, requiredAmountOut); - } + address tokenIn = resolvedOrders[0].input.token; - IERC20 tokenIn = IERC20(order.input.token); - IERC20 tokenOut = IERC20(outputToken); - uint256 outputBefore = tokenOut.balanceOf(address(this)); - for (uint256 i; i < fillCall.routes.length; ++i) { + uint256 routesLength = fillCall.routes.length; + for (uint256 i; i < routesLength; ++i) { FillRoute memory route = fillCall.routes[i]; - uint256 routeOutputBefore = tokenOut.balanceOf(address(this)); - tokenIn.safeTransfer(route.adapter, route.amountIn); + IERC20(tokenIn).safeTransfer(route.adapter, route.amountIn); ILiquidLaneAdapter(route.adapter) .swap( ILiquidLaneAdapter.Swap({ - recipient: address(this), - tokenIn: order.input.token, - amountIn: route.amountIn, - amountOut: route.amountOut + recipient: address(this), tokenIn: tokenIn, amountIn: route.amountIn, amountOut: route.amountOut }) ); - uint256 amountOut = tokenOut.balanceOf(address(this)) - routeOutputBefore; - if (amountOut < route.amountOut) { - revert RouteOutputTooLow(route.adapter, route.amountOut, amountOut); - } - emit InputRedeemed(order.hash, route.adapter, order.input.token, outputToken, route.amountIn, amountOut); } - for (uint256 i; i < fillCall.discountRoutes.length; ++i) { + uint256 discountRoutesLength = fillCall.discountRoutes.length; + for (uint256 i; i < discountRoutesLength; ++i) { DiscountRoute memory route = fillCall.discountRoutes[i]; - uint256 routeOutputBefore = tokenOut.balanceOf(address(this)); - tokenIn.safeTransfer(route.adapter, route.amountIn); + IERC20(tokenIn).safeTransfer(route.adapter, route.amountIn); ILiquidLaneAdapter(route.adapter) .swap(route.discountSwap, route.protocolSignature, address(this), route.amountIn); - uint256 amountOut = tokenOut.balanceOf(address(this)) - routeOutputBefore; - if (amountOut < route.minAmountOut) { - revert RouteOutputTooLow(route.adapter, route.minAmountOut, amountOut); + } + + uint256 outputsLength = resolvedOrders[0].outputs.length; + for (uint256 i; i < outputsLength; ++i) { + address token = resolvedOrders[0].outputs[i].token; + if (IERC20(token).allowance(address(this), REACTOR) < type(uint256).max) { + IERC20(token).forceApprove(REACTOR, type(uint256).max); } - emit InputRedeemed(order.hash, route.adapter, order.input.token, outputToken, route.amountIn, amountOut); } - uint256 outputGained = tokenOut.balanceOf(address(this)) - outputBefore; - if (outputGained < requiredAmountOut) revert InsufficientOutput(requiredAmountOut, outputGained); - tokenOut.forceApprove(REACTOR, requiredAmountOut); - - emit OrderFilled( - order.hash, - order.input.token, - outputToken, - order.input.amount, - order.input.amount - routedAmountIn, - requiredAmountOut, - outputGained - requiredAmountOut - ); + uint256 balance = address(this).balance; + if (balance > 0) payable(REACTOR).sendValue(balance); } function setCallers(address[] calldata newCallers) public onlyOwner { @@ -122,42 +82,13 @@ contract LiquidLaneUniswapXExecutor is Ownable, ReentrancyGuard, ILiquidLaneUnis emit SetCallers(newCallers); } - function sweepERC20(address token, address to, uint256 amount) external onlyOwner { - if (token == address(0) || to == address(0)) revert ZeroAddress(); - IERC20(token).safeTransfer(to, amount); - } - function _isCaller(address caller) internal view returns (bool) { - for (uint256 i; i < callers.length; ++i) { + uint256 callersLength = callers.length; + for (uint256 i; i < callersLength; ++i) { if (callers[i] == caller) return true; } return false; } - function _validateFillCall(FillCall memory fillCall, address tokenIn, uint256 orderAmountIn) - internal - pure - returns (uint256 routedAmountIn, uint256 minimumAmountOut) - { - if (fillCall.routes.length == 0 && fillCall.discountRoutes.length == 0) revert EmptyRoutes(); - for (uint256 i; i < fillCall.routes.length; ++i) { - FillRoute memory route = fillCall.routes[i]; - if (route.adapter == address(0)) revert ZeroAddress(); - if (route.amountIn == 0 || route.amountOut == 0) revert InvalidAmount(); - routedAmountIn += route.amountIn; - minimumAmountOut += route.amountOut; - } - for (uint256 i; i < fillCall.discountRoutes.length; ++i) { - DiscountRoute memory route = fillCall.discountRoutes[i]; - if (route.adapter == address(0)) revert ZeroAddress(); - if (route.amountIn == 0 || route.minAmountOut == 0) revert InvalidAmount(); - if (route.discountSwap.discount.tokenToRedeem != tokenIn) { - revert DiscountTokenMismatch(tokenIn, route.discountSwap.discount.tokenToRedeem); - } - routedAmountIn += route.amountIn; - minimumAmountOut += route.minAmountOut; - } - // Exact-output Dutch input may increase after off-chain planning; retain that positive difference. - if (routedAmountIn > orderAmountIn) revert RouteInputExceedsOrder(routedAmountIn, orderAmountIn); - } + receive() external payable {} } diff --git a/src/uniswapx/interfaces/ILiquidLaneUniswapXExecutor.sol b/src/uniswapx/interfaces/ILiquidLaneUniswapXExecutor.sol index 8920005..38667e2 100644 --- a/src/uniswapx/interfaces/ILiquidLaneUniswapXExecutor.sol +++ b/src/uniswapx/interfaces/ILiquidLaneUniswapXExecutor.sol @@ -6,20 +6,8 @@ import {ILiquidLaneAdapter} from "../../interfaces/ILiquidLaneAdapter.sol"; import {IUniswapXReactorCallback, UniswapXSignedOrder} from "./IUniswapXReactor.sol"; interface ILiquidLaneUniswapXExecutor is IUniswapXReactorCallback { - error DiscountTokenMismatch(address expectedToken, address actualToken); - error EmptyRoutes(); - error InsufficientMinimumOutput(uint256 minimumAmountOut, uint256 requiredAmountOut); - error InsufficientOutput(uint256 requiredAmountOut, uint256 receivedAmountOut); - error IdenticalTokens(); - error InvalidAmount(); - error InvalidOrderCount(); - error InvalidOutputCount(); error NotCaller(); error NotReactor(); - error OutputTokenMismatch(address expectedToken, address actualToken); - error RouteInputExceedsOrder(uint256 routedAmountIn, uint256 orderAmountIn); - error RouteOutputTooLow(address adapter, uint256 minAmountOut, uint256 availableAmountOut); - error ZeroAddress(); struct FillRoute { address adapter; @@ -35,34 +23,14 @@ interface ILiquidLaneUniswapXExecutor is IUniswapXReactorCallback { struct DiscountRoute { address adapter; uint256 amountIn; - uint256 minAmountOut; ILiquidLaneAdapter.DiscountSwap discountSwap; bytes protocolSignature; } event SetCallers(address[] newCallers); - event InputRedeemed( - bytes32 indexed orderHash, - address indexed adapter, - address indexed tokenIn, - address tokenOut, - uint256 amountIn, - uint256 amountOut - ); - event OrderFilled( - bytes32 indexed orderHash, - address indexed tokenIn, - address indexed tokenOut, - uint256 amountIn, - uint256 inputSurplus, - uint256 amountOut, - uint256 outputSurplus - ); - function REACTOR() external view returns (address reactor); + function initialize(address owner, address[] calldata initCallers) external; function callers(uint256 index) external view returns (address caller); function execute(UniswapXSignedOrder calldata order, FillCall calldata fillCall) external; - function isCaller(address caller) external view returns (bool allowed); function setCallers(address[] calldata newCallers) external; - function sweepERC20(address token, address to, uint256 amount) external; } diff --git a/test/deploy/DeployUniswapXExecutor.t.sol b/test/deploy/DeployUniswapXExecutor.t.sol new file mode 100644 index 0000000..423122d --- /dev/null +++ b/test/deploy/DeployUniswapXExecutor.t.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: BUSL-1.1 +// Copyright (c) 2026 Symbiotic +pragma solidity 0.8.28; + +import {DeployUniswapXExecutorBaseScript} from "../../script/deploy/base/DeployUniswapXExecutorBase.s.sol"; +import {LiquidLaneUniswapXExecutor} from "../../src/uniswapx/LiquidLaneUniswapXExecutor.sol"; +import {ILiquidLaneUniswapXExecutor} from "../../src/uniswapx/interfaces/ILiquidLaneUniswapXExecutor.sol"; +import {IUniswapXReactor, UniswapXSignedOrder} from "../../src/uniswapx/interfaces/IUniswapXReactor.sol"; + +import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; +import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; +import {Test} from "forge-std/Test.sol"; + +contract DeployUniswapXExecutorTest is Test { + bytes32 internal constant ERC1967_ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; + bytes32 internal constant ERC1967_IMPLEMENTATION_SLOT = + 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + + RecordingUniswapXReactor internal reactor; + address internal admin = makeAddr("admin"); + address internal proxyAdminOwner = makeAddr("proxyAdminOwner"); + address internal caller = makeAddr("caller"); + + function setUp() public { + reactor = new RecordingUniswapXReactor(); + } + + function testDeploysProxyWithConfigurationAndInitializerLocks() public { + DeployUniswapXExecutorBaseHarness harness = new DeployUniswapXExecutorBaseHarness(); + DeployUniswapXExecutorBaseScript.DeploymentData memory data = harness.runBase( + DeployUniswapXExecutorBaseScript.DeployParams({ + reactor: address(reactor), admin: admin, proxyAdminOwner: proxyAdminOwner, caller: caller + }) + ); + + assertTrue(address(data.executor) != data.implementation); + assertEq(data.executor.owner(), admin); + assertEq(data.executor.callers(0), caller); + assertEq(data.reactor, address(reactor)); + + address implementation = address(uint160(uint256(vm.load(address(data.executor), ERC1967_IMPLEMENTATION_SLOT)))); + assertEq(implementation, data.implementation); + + address proxyAdmin = address(uint160(uint256(vm.load(address(data.executor), ERC1967_ADMIN_SLOT)))); + assertEq(ProxyAdmin(proxyAdmin).owner(), proxyAdminOwner); + + ILiquidLaneUniswapXExecutor.FillCall memory fillCall = ILiquidLaneUniswapXExecutor.FillCall({ + routes: new ILiquidLaneUniswapXExecutor.FillRoute[](0), + discountRoutes: new ILiquidLaneUniswapXExecutor.DiscountRoute[](0) + }); + vm.prank(caller); + data.executor.execute(UniswapXSignedOrder({order: bytes(""), sig: bytes("")}), fillCall); + assertTrue(reactor.executeWithCallbackCalled()); + assertEq(reactor.executor(), address(data.executor)); + + address[] memory callers = new address[](1); + callers[0] = caller; + vm.expectRevert(Initializable.InvalidInitialization.selector); + LiquidLaneUniswapXExecutor(payable(data.implementation)).initialize(admin, callers); + vm.expectRevert(Initializable.InvalidInitialization.selector); + data.executor.initialize(admin, callers); + } +} + +contract DeployUniswapXExecutorBaseHarness is DeployUniswapXExecutorBaseScript { + function _startBroadcast() internal override {} + + function _stopBroadcast() internal override {} +} + +contract RecordingUniswapXReactor is IUniswapXReactor { + bool public executeWithCallbackCalled; + address public executor; + + function executeWithCallback(UniswapXSignedOrder calldata, bytes calldata) external payable override { + executeWithCallbackCalled = true; + executor = msg.sender; + } +} diff --git a/test/uniswapx/LiquidLaneUniswapXExecutor.t.sol b/test/uniswapx/LiquidLaneUniswapXExecutor.t.sol index ad53b13..f6800d9 100644 --- a/test/uniswapx/LiquidLaneUniswapXExecutor.t.sol +++ b/test/uniswapx/LiquidLaneUniswapXExecutor.t.sol @@ -15,260 +15,259 @@ import { UniswapXSignedOrder } from "../../src/uniswapx/interfaces/IUniswapXReactor.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {IERC20Errors} from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol"; +import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; import {Test} from "forge-std/Test.sol"; contract LiquidLaneUniswapXExecutorTest is Test { + using Address for address payable; + address internal caller = makeAddr("caller"); address internal owner = makeAddr("owner"); + address internal proxyAdminOwner = makeAddr("proxyAdminOwner"); address internal recipient = makeAddr("recipient"); - address internal feeRecipient = makeAddr("feeRecipient"); TestToken internal inputToken; TestToken internal outputToken; MockUniswapXAdapter internal adapter; MockUniswapXReactor internal reactor; + LiquidLaneUniswapXExecutor internal implementation; LiquidLaneUniswapXExecutor internal executor; function setUp() public { inputToken = new TestToken("Input", "IN"); outputToken = new TestToken("Output", "OUT"); - adapter = new MockUniswapXAdapter(outputToken); + adapter = new MockUniswapXAdapter(address(outputToken)); reactor = new MockUniswapXReactor(); - executor = new LiquidLaneUniswapXExecutor(address(reactor), owner, _callers(caller)); + implementation = new LiquidLaneUniswapXExecutor(address(reactor)); + TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy( + address(implementation), + proxyAdminOwner, + abi.encodeCall(LiquidLaneUniswapXExecutor.initialize, (owner, _callers(caller))) + ); + executor = LiquidLaneUniswapXExecutor(payable(address(proxy))); inputToken.mint(address(reactor), 10 ether); outputToken.mint(address(adapter), 100 ether); - reactor.setOrder(_resolvedOrder(10 ether, 9 ether)); + reactor.setOrder(_resolvedOrder(10 ether, _erc20Outputs(address(outputToken), 9 ether, recipient))); } - function testExecuteFillsThroughReactorCallback() public { - vm.prank(caller); - executor.execute(UniswapXSignedOrder({order: hex"01", sig: hex"02"}), _fillCall(10 ether, 9 ether)); - - assertEq(inputToken.balanceOf(address(adapter)), 10 ether); - assertEq(outputToken.balanceOf(recipient), 9 ether); - assertEq(outputToken.balanceOf(address(executor)), 0); - assertEq(outputToken.allowance(address(executor), address(reactor)), 0); + function testInitializeSetsOwnerAndCallers() public view { + assertEq(executor.owner(), owner); + assertEq(executor.callers(0), caller); } - function testExecuteKeepsDirectRouteSurplus() public { - vm.prank(caller); - executor.execute(UniswapXSignedOrder({order: hex"01", sig: hex"02"}), _fillCall(10 ether, 10 ether)); - - assertEq(outputToken.balanceOf(recipient), 9 ether); - assertEq(outputToken.balanceOf(address(executor)), 1 ether); - assertEq(outputToken.allowance(address(executor), address(reactor)), 0); + function testInitializeCannotBeCalledTwice() public { + vm.expectRevert(Initializable.InvalidInitialization.selector); + executor.initialize(makeAddr("intruder"), _callers(makeAddr("intruderCaller"))); } - function testExecuteFillsSignedDiscountRoute() public { - vm.prank(caller); - executor.execute(UniswapXSignedOrder({order: hex"01", sig: hex"02"}), _discountFillCall(10 ether, 9 ether)); - - assertEq(inputToken.balanceOf(address(adapter)), 10 ether); - assertEq(outputToken.balanceOf(recipient), 9 ether); - assertEq(outputToken.balanceOf(address(executor)), 1 ether); - assertEq(outputToken.allowance(address(executor), address(reactor)), 0); - } - - function testExecuteFillsSameTokenFeeOutputs() public { - reactor.setOrder(_resolvedMultiOutputOrder(10 ether, 8 ether, 1 ether)); - - vm.prank(caller); - executor.execute(UniswapXSignedOrder({order: hex"01", sig: hex"02"}), _fillCall(10 ether, 9 ether)); - - assertEq(outputToken.balanceOf(recipient), 8 ether); - assertEq(outputToken.balanceOf(feeRecipient), 1 ether); - assertEq(outputToken.balanceOf(address(executor)), 0); - assertEq(outputToken.allowance(address(executor), address(reactor)), 0); + function testImplementationInitializerIsDisabled() public { + vm.expectRevert(Initializable.InvalidInitialization.selector); + implementation.initialize(owner, _callers(caller)); } - function testExecuteRejectsMixedOutputTokens() public { - TestToken otherOutput = new TestToken("Other Output", "OTHER"); - UniswapXResolvedOrder memory order = _resolvedMultiOutputOrder(10 ether, 8 ether, 1 ether); - order.outputs[1].token = address(otherOutput); - reactor.setOrder(order); - - vm.prank(caller); - vm.expectRevert( - abi.encodeWithSelector( - ILiquidLaneUniswapXExecutor.OutputTokenMismatch.selector, address(outputToken), address(otherOutput) - ) - ); - executor.execute(UniswapXSignedOrder({order: hex"01", sig: hex"02"}), _fillCall(10 ether, 9 ether)); - } - - function testExecuteRejectsDiscountAdapterThatOverreportsOutput() public { - adapter.setAmountOut(8 ether); - adapter.setReportedAmountOut(10 ether); - - vm.prank(caller); - vm.expectRevert( - abi.encodeWithSelector( - ILiquidLaneUniswapXExecutor.RouteOutputTooLow.selector, address(adapter), 9 ether, 8 ether - ) - ); - executor.execute(UniswapXSignedOrder({order: hex"01", sig: hex"02"}), _discountFillCall(10 ether, 9 ether)); - } - - function testExecuteRejectsNonCaller() public { + function testExecuteRejectsOwnerWhenOwnerIsNotCaller() public { + vm.prank(owner); vm.expectRevert(ILiquidLaneUniswapXExecutor.NotCaller.selector); - vm.prank(makeAddr("relayer")); - executor.execute(UniswapXSignedOrder({order: hex"01", sig: hex"02"}), _fillCall(10 ether, 9 ether)); + executor.execute(_signedOrder(), _emptyFillCall()); } - function testOwnerCanReplaceCallers() public { + function testSetCallersAllowsNewCallerAndRevokesOldCaller() public { address newCaller = makeAddr("newCaller"); - + reactor.setOrder(_resolvedOrder(0, _emptyOutputs())); vm.prank(owner); executor.setCallers(_callers(newCaller)); - assertEq(executor.callers(0), newCaller); - assertTrue(executor.isCaller(newCaller)); - assertFalse(executor.isCaller(caller)); - vm.prank(newCaller); - executor.execute(UniswapXSignedOrder({order: hex"01", sig: hex"02"}), _fillCall(10 ether, 9 ether)); - assertEq(outputToken.balanceOf(recipient), 9 ether); - } - - function testOnlyOwnerCanReplaceCallers() public { - address relayer = makeAddr("relayer"); - - vm.prank(relayer); - vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, relayer)); - executor.setCallers(_callers(relayer)); - } - - function testOnlyOwnerCanSweep() public { - address relayer = makeAddr("relayer"); - inputToken.mint(address(executor), 1 ether); + vm.prank(caller); + vm.expectRevert(ILiquidLaneUniswapXExecutor.NotCaller.selector); + executor.execute(_signedOrder(), _emptyFillCall()); - vm.prank(relayer); - vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, relayer)); - executor.sweepERC20(address(inputToken), relayer, 1 ether); + vm.prank(newCaller); + executor.execute(_signedOrder(), _emptyFillCall()); } - function testOwnerCanSweep() public { - inputToken.mint(address(executor), 1 ether); - - vm.prank(owner); - executor.sweepERC20(address(inputToken), owner, 1 ether); - - assertEq(inputToken.balanceOf(owner), 1 ether); + function testSetCallersRejectsNonOwner() public { + vm.prank(caller); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, caller)); + executor.setCallers(_callers(caller)); } - function testCallbackRejectsNonReactor() public { - UniswapXResolvedOrder[] memory orders = new UniswapXResolvedOrder[](1); - orders[0] = _resolvedOrder(10 ether, 9 ether); - + function testReactorCallbackRejectsNonReactor() public { + UniswapXResolvedOrder[] memory orders = + _resolvedOrders(_resolvedOrder(10 ether, _erc20Outputs(address(outputToken), 9 ether, recipient))); vm.expectRevert(ILiquidLaneUniswapXExecutor.NotReactor.selector); - executor.reactorCallback(orders, abi.encode(_fillCall(10 ether, 9 ether))); + executor.reactorCallback(orders, abi.encode(_emptyFillCall())); } - function testCallbackRejectsZeroInputToken() public { - UniswapXResolvedOrder[] memory orders = new UniswapXResolvedOrder[](1); - orders[0] = _resolvedOrder(10 ether, 9 ether); - orders[0].input.token = address(0); - - vm.expectRevert(ILiquidLaneUniswapXExecutor.ZeroAddress.selector); + function testReactorCallbackRejectsMalformedData() public { + UniswapXResolvedOrder[] memory orders = + _resolvedOrders(_resolvedOrder(10 ether, _erc20Outputs(address(outputToken), 9 ether, recipient))); vm.prank(address(reactor)); - executor.reactorCallback(orders, abi.encode(_fillCall(10 ether, 9 ether))); + vm.expectRevert(); + executor.reactorCallback(orders, hex"01"); } - function testCallbackRejectsZeroOutputToken() public { - UniswapXResolvedOrder[] memory orders = new UniswapXResolvedOrder[](1); - orders[0] = _resolvedOrder(10 ether, 9 ether); - orders[0].outputs[0].token = address(0); - - vm.expectRevert(ILiquidLaneUniswapXExecutor.ZeroAddress.selector); - vm.prank(address(reactor)); - executor.reactorCallback(orders, abi.encode(_fillCall(10 ether, 9 ether))); - } + function testExecuteRoutesDirectFillAndKeepsInputAndOutputSurplus() public { + adapter.setDirectOutput(10 ether); + ILiquidLaneUniswapXExecutor.FillRoute[] memory routes = new ILiquidLaneUniswapXExecutor.FillRoute[](1); + routes[0] = _directRoute(address(adapter), 9 ether, 10 ether); + reactor.setOrder(_resolvedOrder(10 ether, _erc20Outputs(address(outputToken), 9 ether, recipient))); - function testExecuteRejectsInsufficientRouteMinimum() public { vm.prank(caller); - vm.expectRevert( - abi.encodeWithSelector(ILiquidLaneUniswapXExecutor.InsufficientMinimumOutput.selector, 8 ether, 9 ether) - ); - executor.execute(UniswapXSignedOrder({order: hex"01", sig: hex"02"}), _fillCall(10 ether, 8 ether)); + executor.execute(_signedOrder(), _fillCall(routes, new ILiquidLaneUniswapXExecutor.DiscountRoute[](0))); + + assertEq(inputToken.balanceOf(address(executor)), 1 ether); + assertEq(outputToken.balanceOf(address(executor)), 1 ether); + assertEq(outputToken.allowance(address(executor), address(reactor)), type(uint256).max); } - function testExecuteRejectsRouteThatNoLongerMeetsMinimum() public { - adapter.setAmountOut(8 ether); + function testExecuteRoutesMultipleDirectFillsAndSettlesMixedErc20Outputs() public { + TestToken secondOutput = new TestToken("Second Output", "OUT2"); + MockUniswapXAdapter secondAdapter = new MockUniswapXAdapter(address(secondOutput)); + address secondRecipient = makeAddr("secondRecipient"); + adapter.setDirectOutput(4 ether); + secondAdapter.setDirectOutput(6 ether); + outputToken.mint(address(adapter), 4 ether); + secondOutput.mint(address(secondAdapter), 6 ether); + UniswapXOutputToken[] memory outputs = new UniswapXOutputToken[](2); + outputs[0] = UniswapXOutputToken({token: address(outputToken), amount: 4 ether, recipient: recipient}); + outputs[1] = UniswapXOutputToken({token: address(secondOutput), amount: 5 ether, recipient: secondRecipient}); + reactor.setOrder(_resolvedOrder(10 ether, outputs)); + + ILiquidLaneUniswapXExecutor.FillRoute[] memory routes = new ILiquidLaneUniswapXExecutor.FillRoute[](2); + routes[0] = _directRoute(address(adapter), 4 ether, 4 ether); + routes[1] = _directRoute(address(secondAdapter), 6 ether, 6 ether); vm.prank(caller); - vm.expectRevert( - abi.encodeWithSelector( - ILiquidLaneUniswapXExecutor.RouteOutputTooLow.selector, address(adapter), 9 ether, 8 ether - ) - ); - executor.execute(UniswapXSignedOrder({order: hex"01", sig: hex"02"}), _fillCall(10 ether, 9 ether)); + executor.execute(_signedOrder(), _fillCall(routes, new ILiquidLaneUniswapXExecutor.DiscountRoute[](0))); + + assertEq(outputToken.balanceOf(recipient), 4 ether); + assertEq(secondOutput.balanceOf(secondRecipient), 5 ether); + assertEq(outputToken.balanceOf(address(executor)), 0); + assertEq(secondOutput.balanceOf(address(executor)), 1 ether); + assertEq(outputToken.allowance(address(executor), address(reactor)), type(uint256).max); + assertEq(secondOutput.allowance(address(executor), address(reactor)), type(uint256).max); } - function testExecuteAcceptsRoutePlannedBeforeExactOutputDecay() public { + function testExecuteRoutesDiscountFill() public { + adapter.setDiscountOutput(9 ether); + ILiquidLaneUniswapXExecutor.DiscountRoute[] memory discountRoutes = + new ILiquidLaneUniswapXExecutor.DiscountRoute[](1); + discountRoutes[0] = _discountRoute(address(adapter), 10 ether); + vm.prank(caller); - executor.execute(UniswapXSignedOrder({order: hex"01", sig: hex"02"}), _fillCall(9 ether, 9 ether)); + executor.execute(_signedOrder(), _fillCall(new ILiquidLaneUniswapXExecutor.FillRoute[](0), discountRoutes)); - assertEq(inputToken.balanceOf(address(adapter)), 9 ether); - assertEq(inputToken.balanceOf(address(executor)), 1 ether); + assertEq(adapter.discountCalls(), 1); + assertEq(inputToken.balanceOf(address(adapter)), 10 ether); assertEq(outputToken.balanceOf(recipient), 9 ether); } - function testExecuteAcceptsDiscountRoutePlannedBeforeExactOutputDecay() public { + function testExecuteRoutesDirectAndDiscountFillsTogether() public { + MockUniswapXAdapter discountAdapter = new MockUniswapXAdapter(address(outputToken)); + adapter.setDirectOutput(4 ether); + discountAdapter.setDiscountOutput(6 ether); + outputToken.mint(address(discountAdapter), 6 ether); + reactor.setOrder(_resolvedOrder(10 ether, _erc20Outputs(address(outputToken), 10 ether, recipient))); + + ILiquidLaneUniswapXExecutor.FillRoute[] memory routes = new ILiquidLaneUniswapXExecutor.FillRoute[](1); + routes[0] = _directRoute(address(adapter), 4 ether, 4 ether); + ILiquidLaneUniswapXExecutor.DiscountRoute[] memory discountRoutes = + new ILiquidLaneUniswapXExecutor.DiscountRoute[](1); + discountRoutes[0] = _discountRoute(address(discountAdapter), 6 ether); vm.prank(caller); - executor.execute(UniswapXSignedOrder({order: hex"01", sig: hex"02"}), _discountFillCall(9 ether, 9 ether)); + executor.execute(_signedOrder(), _fillCall(routes, discountRoutes)); - assertEq(inputToken.balanceOf(address(adapter)), 9 ether); - assertEq(inputToken.balanceOf(address(executor)), 1 ether); - assertEq(outputToken.balanceOf(recipient), 9 ether); + assertEq(adapter.directCalls(), 1); + assertEq(discountAdapter.discountCalls(), 1); + assertEq(inputToken.balanceOf(address(adapter)), 4 ether); + assertEq(inputToken.balanceOf(address(discountAdapter)), 6 ether); } - function testExecuteRejectsInputAboveResolvedAmount() public { - vm.prank(caller); - vm.expectRevert( - abi.encodeWithSelector(ILiquidLaneUniswapXExecutor.RouteInputExceedsOrder.selector, 11 ether, 10 ether) - ); - executor.execute(UniswapXSignedOrder({order: hex"01", sig: hex"02"}), _fillCall(11 ether, 9 ether)); + function testExecuteKeepsMaxApprovalWithoutReapproving() public { + ApprovalCountingToken countingOutput = new ApprovalCountingToken("Counting Output", "COUNT"); + MockUniswapXAdapter countingAdapter = new MockUniswapXAdapter(address(countingOutput)); + countingAdapter.setDirectOutput(9 ether); + countingOutput.mint(address(countingAdapter), 18 ether); + inputToken.mint(address(reactor), 10 ether); + reactor.setOrder(_resolvedOrder(10 ether, _erc20Outputs(address(countingOutput), 9 ether, recipient))); + ILiquidLaneUniswapXExecutor.FillRoute[] memory routes = new ILiquidLaneUniswapXExecutor.FillRoute[](1); + routes[0] = _directRoute(address(countingAdapter), 10 ether, 9 ether); + + vm.startPrank(caller); + executor.execute(_signedOrder(), _fillCall(routes, new ILiquidLaneUniswapXExecutor.DiscountRoute[](0))); + executor.execute(_signedOrder(), _fillCall(routes, new ILiquidLaneUniswapXExecutor.DiscountRoute[](0))); + vm.stopPrank(); + + assertEq(countingOutput.allowance(address(executor), address(reactor)), type(uint256).max); + assertEq(countingOutput.approveCalls(), 1); } - function testExecuteRejectsZeroAdapter() public { - ILiquidLaneUniswapXExecutor.FillCall memory fillCall = _fillCall(10 ether, 9 ether); - fillCall.routes[0].adapter = address(0); + function testExecuteForwardsNativeOutputAndReceivesReactorRefund() public { + MockUniswapXAdapter nativeAdapter = new MockUniswapXAdapter(address(0)); + reactor.setOrder(_resolvedOrder(10 ether, _nativeOutputs(2 ether, recipient))); + ILiquidLaneUniswapXExecutor.FillRoute[] memory routes = new ILiquidLaneUniswapXExecutor.FillRoute[](1); + routes[0] = _directRoute(address(nativeAdapter), 10 ether, 0); + vm.deal(address(this), 3 ether); + payable(address(executor)).sendValue(3 ether); + uint256 recipientBalanceBefore = recipient.balance; vm.prank(caller); - vm.expectRevert(ILiquidLaneUniswapXExecutor.ZeroAddress.selector); - executor.execute(UniswapXSignedOrder({order: hex"01", sig: hex"02"}), fillCall); + executor.execute(_signedOrder(), _fillCall(routes, new ILiquidLaneUniswapXExecutor.DiscountRoute[](0))); + + assertEq(recipient.balance, recipientBalanceBefore + 2 ether); + assertEq(address(executor).balance, 1 ether); + assertEq(address(reactor).balance, 0); } - function testExecuteRejectsZeroDiscountAdapter() public { - ILiquidLaneUniswapXExecutor.FillCall memory fillCall = _discountFillCall(10 ether, 9 ether); - fillCall.discountRoutes[0].adapter = address(0); + function testExecuteBubblesAdapterRevertAndRollsBack() public { + adapter.setShouldRevert(true); + ILiquidLaneUniswapXExecutor.FillRoute[] memory routes = new ILiquidLaneUniswapXExecutor.FillRoute[](1); + routes[0] = _directRoute(address(adapter), 10 ether, 9 ether); vm.prank(caller); - vm.expectRevert(ILiquidLaneUniswapXExecutor.ZeroAddress.selector); - executor.execute(UniswapXSignedOrder({order: hex"01", sig: hex"02"}), fillCall); + vm.expectRevert(MockUniswapXAdapter.AdapterFailed.selector); + executor.execute(_signedOrder(), _fillCall(routes, new ILiquidLaneUniswapXExecutor.DiscountRoute[](0))); + + assertEq(inputToken.balanceOf(address(reactor)), 10 ether); + assertEq(inputToken.balanceOf(address(adapter)), 0); + assertEq(outputToken.balanceOf(recipient), 0); + assertEq(outputToken.allowance(address(executor), address(reactor)), 0); } - function testExecuteRejectsDiscountForAnotherInputToken() public { - ILiquidLaneUniswapXExecutor.FillCall memory fillCall = _discountFillCall(10 ether, 9 ether); - fillCall.discountRoutes[0].discountSwap.discount.tokenToRedeem = address(outputToken); + function testExecuteBubblesReactorOutputShortfallAndRollsBack() public { + adapter.setDirectOutput(8 ether); + ILiquidLaneUniswapXExecutor.FillRoute[] memory routes = new ILiquidLaneUniswapXExecutor.FillRoute[](1); + routes[0] = _directRoute(address(adapter), 10 ether, 9 ether); vm.prank(caller); vm.expectRevert( - abi.encodeWithSelector( - ILiquidLaneUniswapXExecutor.DiscountTokenMismatch.selector, address(inputToken), address(outputToken) - ) + abi.encodeWithSelector(IERC20Errors.ERC20InsufficientBalance.selector, address(executor), 8 ether, 9 ether) ); - executor.execute(UniswapXSignedOrder({order: hex"01", sig: hex"02"}), fillCall); + executor.execute(_signedOrder(), _fillCall(routes, new ILiquidLaneUniswapXExecutor.DiscountRoute[](0))); + + assertEq(inputToken.balanceOf(address(reactor)), 10 ether); + assertEq(inputToken.balanceOf(address(adapter)), 0); + assertEq(outputToken.balanceOf(recipient), 0); + assertEq(outputToken.allowance(address(executor), address(reactor)), 0); } - function _resolvedOrder(uint256 amountIn, uint256 amountOut) internal returns (UniswapXResolvedOrder memory order) { - UniswapXOutputToken[] memory outputs = new UniswapXOutputToken[](1); - outputs[0] = UniswapXOutputToken({token: address(outputToken), amount: amountOut, recipient: recipient}); + function _signedOrder() internal pure returns (UniswapXSignedOrder memory) { + return UniswapXSignedOrder({order: hex"01", sig: hex"02"}); + } + + function _resolvedOrder(uint256 amountIn, UniswapXOutputToken[] memory outputs) + internal + returns (UniswapXResolvedOrder memory order) + { order = UniswapXResolvedOrder({ info: UniswapXOrderInfo({ reactor: address(reactor), @@ -285,58 +284,71 @@ contract LiquidLaneUniswapXExecutorTest is Test { }); } - function _callers(address caller) internal pure returns (address[] memory callers_) { - callers_ = new address[](1); - callers_[0] = caller; + function _resolvedOrders(UniswapXResolvedOrder memory order) + internal + pure + returns (UniswapXResolvedOrder[] memory orders) + { + orders = new UniswapXResolvedOrder[](1); + orders[0] = order; } - function _resolvedMultiOutputOrder(uint256 amountIn, uint256 swapperAmountOut, uint256 feeAmountOut) + function _erc20Outputs(address token, uint256 amount, address outputRecipient) internal - returns (UniswapXResolvedOrder memory order) + pure + returns (UniswapXOutputToken[] memory outputs) { - UniswapXOutputToken[] memory outputs = new UniswapXOutputToken[](2); - outputs[0] = UniswapXOutputToken({token: address(outputToken), amount: swapperAmountOut, recipient: recipient}); - outputs[1] = UniswapXOutputToken({token: address(outputToken), amount: feeAmountOut, recipient: feeRecipient}); - order = UniswapXResolvedOrder({ - info: UniswapXOrderInfo({ - reactor: address(reactor), - swapper: makeAddr("swapper"), - nonce: 1, - deadline: block.timestamp + 1 hours, - additionalValidationContract: address(0), - additionalValidationData: "" - }), - input: UniswapXInputToken({token: address(inputToken), amount: amountIn, maxAmount: amountIn}), - outputs: outputs, - sig: "", - hash: keccak256("multi-output-order") - }); + outputs = new UniswapXOutputToken[](1); + outputs[0] = UniswapXOutputToken({token: token, amount: amount, recipient: outputRecipient}); } - function _fillCall(uint256 amountIn, uint256 amountOut) + function _nativeOutputs(uint256 amount, address outputRecipient) internal - view - returns (ILiquidLaneUniswapXExecutor.FillCall memory fillCall) + pure + returns (UniswapXOutputToken[] memory outputs) { - ILiquidLaneUniswapXExecutor.FillRoute[] memory routes = new ILiquidLaneUniswapXExecutor.FillRoute[](1); - routes[0] = ILiquidLaneUniswapXExecutor.FillRoute({ - adapter: address(adapter), amountIn: amountIn, amountOut: amountOut - }); - fillCall = ILiquidLaneUniswapXExecutor.FillCall({ - routes: routes, discountRoutes: new ILiquidLaneUniswapXExecutor.DiscountRoute[](0) - }); + outputs = new UniswapXOutputToken[](1); + outputs[0] = UniswapXOutputToken({token: address(0), amount: amount, recipient: outputRecipient}); + } + + function _emptyOutputs() internal pure returns (UniswapXOutputToken[] memory outputs) { + outputs = new UniswapXOutputToken[](0); + } + + function _callers(address caller_) internal pure returns (address[] memory callers_) { + callers_ = new address[](1); + callers_[0] = caller_; + } + + function _emptyFillCall() internal pure returns (ILiquidLaneUniswapXExecutor.FillCall memory fillCall) { + fillCall = _fillCall( + new ILiquidLaneUniswapXExecutor.FillRoute[](0), new ILiquidLaneUniswapXExecutor.DiscountRoute[](0) + ); + } + + function _fillCall( + ILiquidLaneUniswapXExecutor.FillRoute[] memory routes, + ILiquidLaneUniswapXExecutor.DiscountRoute[] memory discountRoutes + ) internal pure returns (ILiquidLaneUniswapXExecutor.FillCall memory fillCall) { + fillCall = ILiquidLaneUniswapXExecutor.FillCall({routes: routes, discountRoutes: discountRoutes}); + } + + function _directRoute(address routeAdapter, uint256 amountIn, uint256 amountOut) + internal + pure + returns (ILiquidLaneUniswapXExecutor.FillRoute memory) + { + return ILiquidLaneUniswapXExecutor.FillRoute({adapter: routeAdapter, amountIn: amountIn, amountOut: amountOut}); } - function _discountFillCall(uint256 amountIn, uint256 minAmountOut) + function _discountRoute(address routeAdapter, uint256 amountIn) internal view - returns (ILiquidLaneUniswapXExecutor.FillCall memory fillCall) + returns (ILiquidLaneUniswapXExecutor.DiscountRoute memory) { - ILiquidLaneUniswapXExecutor.DiscountRoute[] memory routes = new ILiquidLaneUniswapXExecutor.DiscountRoute[](1); - routes[0] = ILiquidLaneUniswapXExecutor.DiscountRoute({ - adapter: address(adapter), + return ILiquidLaneUniswapXExecutor.DiscountRoute({ + adapter: routeAdapter, amountIn: amountIn, - minAmountOut: minAmountOut, discountSwap: ILiquidLaneAdapter.DiscountSwap({ discount: ILiquidLaneAdapter.Discount({ tokenToRedeem: address(inputToken), @@ -351,19 +363,19 @@ contract LiquidLaneUniswapXExecutorTest is Test { }), protocolSignature: hex"02" }); - fillCall = ILiquidLaneUniswapXExecutor.FillCall({ - routes: new ILiquidLaneUniswapXExecutor.FillRoute[](0), discountRoutes: routes - }); } } contract MockUniswapXReactor is IUniswapXReactor { + using Address for address payable; using SafeERC20 for IERC20; address internal tokenIn; uint256 internal amountIn; UniswapXOutputToken[] internal storedOutputs; + receive() external payable {} + function setOrder(UniswapXResolvedOrder memory newOrder) external { tokenIn = newOrder.input.token; amountIn = newOrder.input.amount; @@ -396,38 +408,55 @@ contract MockUniswapXReactor is IUniswapXReactor { hash: keccak256("order") }); IUniswapXReactorCallback(msg.sender).reactorCallback(orders, callbackData); - for (uint256 i; i < outputs.length; ++i) { - IERC20(outputs[i].token).safeTransferFrom(msg.sender, outputs[i].recipient, outputs[i].amount); + if (outputs[i].token == address(0)) { + payable(outputs[i].recipient).sendValue(outputs[i].amount); + } else { + IERC20(outputs[i].token).safeTransferFrom(msg.sender, outputs[i].recipient, outputs[i].amount); + } } + uint256 nativeRefund = address(this).balance; + if (nativeRefund > 0) payable(msg.sender).sendValue(nativeRefund); } } contract MockUniswapXAdapter is ILiquidLaneAdapter { + using Address for address payable; using SafeERC20 for IERC20; - TestToken internal immutable outputToken; - uint256 internal amountOut = 10 ether; - uint256 internal reportedAmountOut = 10 ether; + error AdapterFailed(); - constructor(TestToken outputToken_) { + address internal immutable outputToken; + uint256 internal directOutput; + uint256 internal discountOutput; + uint256 public directCalls; + uint256 public discountCalls; + bool internal shouldRevert; + + constructor(address outputToken_) { outputToken = outputToken_; } - function setAmountOut(uint256 newAmountOut) external { - amountOut = newAmountOut; + receive() external payable {} + + function setDirectOutput(uint256 newDirectOutput) external { + directOutput = newDirectOutput; } - function setReportedAmountOut(uint256 newAmountOut) external { - reportedAmountOut = newAmountOut; + function setDiscountOutput(uint256 newDiscountOutput) external { + discountOutput = newDiscountOutput; + } + + function setShouldRevert(bool newShouldRevert) external { + shouldRevert = newShouldRevert; } function getAmountOut(address, uint256) external view returns (uint256) { - return amountOut; + return directOutput; } function getMaxAssets(address) external view returns (uint256) { - return outputToken.balanceOf(address(this)); + return outputToken == address(0) ? address(this).balance : IERC20(outputToken).balanceOf(address(this)); } function minDiscount(address) external pure returns (uint256) { @@ -435,20 +464,31 @@ contract MockUniswapXAdapter is ILiquidLaneAdapter { } function swap(Swap calldata swap_) external { - IERC20(address(outputToken)).safeTransfer(swap_.recipient, _min(swap_.amountOut, amountOut)); + ++directCalls; + if (shouldRevert) revert AdapterFailed(); + _transferOutput(swap_.recipient, directOutput); } function swap(SignedSwap calldata, bytes calldata) external pure { revert("unsupported"); } - function swap(DiscountSwap calldata, bytes calldata, address recipient_, uint256) external returns (uint256) { - IERC20(address(outputToken)).safeTransfer(recipient_, amountOut); - return reportedAmountOut; + function swap(DiscountSwap calldata, bytes calldata, address recipient_, uint256) + external + returns (uint256 amountOut) + { + ++discountCalls; + if (shouldRevert) revert AdapterFailed(); + amountOut = discountOutput; + _transferOutput(recipient_, amountOut); } - function _min(uint256 left, uint256 right) private pure returns (uint256) { - return left < right ? left : right; + function _transferOutput(address recipient_, uint256 amount) internal { + if (outputToken == address(0)) { + payable(recipient_).sendValue(amount); + } else { + IERC20(outputToken).safeTransfer(recipient_, amount); + } } } @@ -459,3 +499,14 @@ contract TestToken is ERC20 { _mint(to, amount); } } + +contract ApprovalCountingToken is TestToken { + uint256 public approveCalls; + + constructor(string memory name_, string memory symbol_) TestToken(name_, symbol_) {} + + function _approve(address owner_, address spender, uint256 value, bool emitEvent) internal override { + if (emitEvent) ++approveCalls; + super._approve(owner_, spender, value, emitEvent); + } +} From 877371f9e12e8fbecf593601262f79917078bbad Mon Sep 17 00:00:00 2001 From: Andrey Date: Fri, 24 Jul 2026 13:53:12 +0400 Subject: [PATCH 3/5] fix(uniswapx): skip Reactor approval for native output reactorCallback looped over every resolved output and called IERC20(token).allowance/forceApprove unconditionally. For a native output the token is address(0), so the allowance staticcall reverted with "call to non-contract address", reverting the whole fill. Guard the approval on token != address(0), mirroring Executor.sol's `token != NATIVE` check. Native output is still forwarded to the Reactor via the existing sendValue path. Co-Authored-By: Claude Opus 4.8 --- src/uniswapx/LiquidLaneUniswapXExecutor.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/uniswapx/LiquidLaneUniswapXExecutor.sol b/src/uniswapx/LiquidLaneUniswapXExecutor.sol index 9649ef7..722c193 100644 --- a/src/uniswapx/LiquidLaneUniswapXExecutor.sol +++ b/src/uniswapx/LiquidLaneUniswapXExecutor.sol @@ -68,7 +68,8 @@ contract LiquidLaneUniswapXExecutor is Initializable, OwnableUpgradeable, ILiqui uint256 outputsLength = resolvedOrders[0].outputs.length; for (uint256 i; i < outputsLength; ++i) { address token = resolvedOrders[0].outputs[i].token; - if (IERC20(token).allowance(address(this), REACTOR) < type(uint256).max) { + // Native output (address(0)) is forwarded below; only ERC-20 outputs need a Reactor allowance. + if (token != address(0) && IERC20(token).allowance(address(this), REACTOR) < type(uint256).max) { IERC20(token).forceApprove(REACTOR, type(uint256).max); } } From 485905be2e533462a175df980d9e0ef61914ae2d Mon Sep 17 00:00:00 2001 From: Andrey Date: Fri, 24 Jul 2026 13:59:03 +0400 Subject: [PATCH 4/5] refactor(uniswapx): use constant deploy config, drop owner fallbacks Align DeployUniswapXExecutorScript with DeployExecutor: declare REACTOR, ADMIN, PROXY_ADMIN_OWNER, and CALLER as `public constant` config fields edited in-file instead of reading them from the environment. Remove the msg.sender/origin owner fallbacks (env defaults and the _scriptOwner helper); the constants are passed straight through, and _validateParams reverts if any is left unset. Factor the base's inline asserts and logging into _validateDeployment/_logDeployment to mirror DeployExecutorBase. Co-Authored-By: Claude Opus 4.8 --- script/DeployUniswapXExecutor.s.sol | 24 +++++++---- .../base/DeployUniswapXExecutorBase.s.sol | 41 ++++++++++--------- 2 files changed, 37 insertions(+), 28 deletions(-) diff --git a/script/DeployUniswapXExecutor.s.sol b/script/DeployUniswapXExecutor.s.sol index 644de6e..18b9a2a 100644 --- a/script/DeployUniswapXExecutor.s.sol +++ b/script/DeployUniswapXExecutor.s.sol @@ -4,16 +4,22 @@ pragma solidity 0.8.28; import {DeployUniswapXExecutorBaseScript} from "./deploy/base/DeployUniswapXExecutorBase.s.sol"; +// forge script script/DeployUniswapXExecutor.s.sol:DeployUniswapXExecutorScript --rpc-url=RPC --broadcast + contract DeployUniswapXExecutorScript is DeployUniswapXExecutorBaseScript { + // Configurations - UPDATE THESE BEFORE DEPLOYMENT + + // Deployed UniswapX Reactor address this Executor forwards fills to. + address public constant REACTOR = 0x0000000000000000000000000000000000000000; + // Executor owner. + address public constant ADMIN = 0x0000000000000000000000000000000000000000; + // Proxy admin owner allowed to upgrade the executor proxy. + address public constant PROXY_ADMIN_OWNER = 0x0000000000000000000000000000000000000000; + // Initial caller allowed to invoke fill entrypoints. + address public constant CALLER = 0x0000000000000000000000000000000000000000; + function run() public returns (DeploymentData memory data) { - address owner = _scriptOwner(); - data = runBase( - DeployParams({ - reactor: vm.envAddress("UNISWAPX_REACTOR"), - admin: vm.envOr("ADMIN", owner), - proxyAdminOwner: vm.envOr("PROXY_ADMIN_OWNER", owner), - caller: vm.envOr("CALLER", owner) - }) - ); + data = + runBase(DeployParams({reactor: REACTOR, admin: ADMIN, proxyAdminOwner: PROXY_ADMIN_OWNER, caller: CALLER})); } } diff --git a/script/deploy/base/DeployUniswapXExecutorBase.s.sol b/script/deploy/base/DeployUniswapXExecutorBase.s.sol index 350ba53..1bd0439 100644 --- a/script/deploy/base/DeployUniswapXExecutorBase.s.sol +++ b/script/deploy/base/DeployUniswapXExecutorBase.s.sol @@ -27,6 +27,7 @@ abstract contract DeployUniswapXExecutorBaseScript is Script { function runBase(DeployParams memory params) public virtual returns (DeploymentData memory data) { _validateParams(params); + address[] memory callers = new address[](1); callers[0] = params.caller; @@ -46,17 +47,16 @@ abstract contract DeployUniswapXExecutorBaseScript is Script { data.proxyAdminOwner = params.proxyAdminOwner; data.caller = params.caller; - assert(address(data.executor) != data.implementation); - assert(data.executor.owner() == data.admin); - assert(data.executor.callers(0) == data.caller); + _validateDeployment(data); + _logDeployment(data); + } - console2.log("Deployed UniswapX Executor"); - console2.log(" executor: ", address(data.executor)); - console2.log(" implementation: ", data.implementation); - console2.log(" reactor: ", data.reactor); - console2.log(" admin: ", data.admin); - console2.log(" proxyAdminOwner: ", data.proxyAdminOwner); - console2.log(" caller: ", data.caller); + function _startBroadcast() internal virtual { + vm.startBroadcast(); + } + + function _stopBroadcast() internal virtual { + vm.stopBroadcast(); } function _validateParams(DeployParams memory params) internal pure { @@ -66,16 +66,19 @@ abstract contract DeployUniswapXExecutorBaseScript is Script { require(params.caller != address(0), "invalid caller"); } - function _startBroadcast() internal virtual { - vm.startBroadcast(); - } - - function _stopBroadcast() internal virtual { - vm.stopBroadcast(); + function _validateDeployment(DeploymentData memory data) internal view { + assert(address(data.executor) != data.implementation); + assert(data.executor.owner() == data.admin); + assert(data.executor.callers(0) == data.caller); } - function _scriptOwner() internal view virtual returns (address owner) { - (,, address origin) = vm.readCallers(); - owner = origin == address(0) ? msg.sender : origin; + function _logDeployment(DeploymentData memory data) internal view { + console2.log("Deployed UniswapX Executor"); + console2.log(" executor: ", address(data.executor)); + console2.log(" implementation: ", data.implementation); + console2.log(" reactor: ", data.reactor); + console2.log(" admin: ", data.admin); + console2.log(" proxyAdminOwner: ", data.proxyAdminOwner); + console2.log(" caller: ", data.caller); } } From 7fa8163a93d70076e0736ff50defdfcde982e32b Mon Sep 17 00:00:00 2001 From: Andrey Date: Fri, 24 Jul 2026 14:45:27 +0400 Subject: [PATCH 5/5] chore: update script --- script/DeployUniswapXExecutor.s.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/DeployUniswapXExecutor.s.sol b/script/DeployUniswapXExecutor.s.sol index 18b9a2a..739fd67 100644 --- a/script/DeployUniswapXExecutor.s.sol +++ b/script/DeployUniswapXExecutor.s.sol @@ -10,7 +10,7 @@ contract DeployUniswapXExecutorScript is DeployUniswapXExecutorBaseScript { // Configurations - UPDATE THESE BEFORE DEPLOYMENT // Deployed UniswapX Reactor address this Executor forwards fills to. - address public constant REACTOR = 0x0000000000000000000000000000000000000000; + address public constant REACTOR = 0x00000011F84B9aa48e5f8aA8B9897600006289Be; // Executor owner. address public constant ADMIN = 0x0000000000000000000000000000000000000000; // Proxy admin owner allowed to upgrade the executor proxy.