Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` 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]
>
Expand All @@ -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

Expand Down
25 changes: 25 additions & 0 deletions script/DeployUniswapXExecutor.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// SPDX-License-Identifier: BUSL-1.1
// Copyright (c) 2026 Symbiotic
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 = 0x00000011F84B9aa48e5f8aA8B9897600006289Be;
// 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) {
data =
runBase(DeployParams({reactor: REACTOR, admin: ADMIN, proxyAdminOwner: PROXY_ADMIN_OWNER, caller: CALLER}));
}
}
84 changes: 84 additions & 0 deletions script/deploy/base/DeployUniswapXExecutorBase.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// 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;

_validateDeployment(data);
_logDeployment(data);
}

function _startBroadcast() internal virtual {
vm.startBroadcast();
}

function _stopBroadcast() internal virtual {
vm.stopBroadcast();
}

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 _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 _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);
}
}
95 changes: 95 additions & 0 deletions src/uniswapx/LiquidLaneUniswapXExecutor.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// 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 {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 {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 outputs from LiquidLane adapters.
contract LiquidLaneUniswapXExecutor is Initializable, OwnableUpgradeable, ILiquidLaneUniswapXExecutor {
using Address for address payable;
using SafeERC20 for IERC20;

address internal immutable REACTOR;
address[] public callers;

constructor(address reactor) {
REACTOR = reactor;
_disableInitializers();
}

function initialize(address owner, address[] calldata initCallers) external initializer {
__Ownable_init(owner);
callers = initCallers;
}

modifier onlyCaller() {
if (!_isCaller(msg.sender)) revert NotCaller();
_;
}

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 {
if (msg.sender != REACTOR) revert NotReactor();

FillCall memory fillCall = abi.decode(callbackData, (FillCall));
address tokenIn = resolvedOrders[0].input.token;

uint256 routesLength = fillCall.routes.length;
for (uint256 i; i < routesLength; ++i) {
FillRoute memory route = fillCall.routes[i];
IERC20(tokenIn).safeTransfer(route.adapter, route.amountIn);
ILiquidLaneAdapter(route.adapter)
.swap(
ILiquidLaneAdapter.Swap({
recipient: address(this), tokenIn: tokenIn, amountIn: route.amountIn, amountOut: route.amountOut
})
);
}
uint256 discountRoutesLength = fillCall.discountRoutes.length;
for (uint256 i; i < discountRoutesLength; ++i) {
DiscountRoute memory route = fillCall.discountRoutes[i];
IERC20(tokenIn).safeTransfer(route.adapter, route.amountIn);
ILiquidLaneAdapter(route.adapter)
.swap(route.discountSwap, route.protocolSignature, address(this), route.amountIn);
}

uint256 outputsLength = resolvedOrders[0].outputs.length;
for (uint256 i; i < outputsLength; ++i) {
address token = resolvedOrders[0].outputs[i].token;
// 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);
}
}

uint256 balance = address(this).balance;
if (balance > 0) payable(REACTOR).sendValue(balance);
}

function setCallers(address[] calldata newCallers) public onlyOwner {
callers = newCallers;
emit SetCallers(newCallers);
}

function _isCaller(address caller) internal view returns (bool) {
uint256 callersLength = callers.length;
for (uint256 i; i < callersLength; ++i) {
if (callers[i] == caller) return true;
}
return false;
}

receive() external payable {}
}
36 changes: 36 additions & 0 deletions src/uniswapx/interfaces/ILiquidLaneUniswapXExecutor.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// 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 NotCaller();
error NotReactor();

struct FillRoute {
address adapter;
uint256 amountIn;
uint256 amountOut;
}

struct FillCall {
FillRoute[] routes;
DiscountRoute[] discountRoutes;
}

struct DiscountRoute {
address adapter;
uint256 amountIn;
ILiquidLaneAdapter.DiscountSwap discountSwap;
bytes protocolSignature;
}

event SetCallers(address[] newCallers);

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 setCallers(address[] calldata newCallers) external;
}
45 changes: 45 additions & 0 deletions src/uniswapx/interfaces/IUniswapXReactor.sol
Original file line number Diff line number Diff line change
@@ -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;
}
79 changes: 79 additions & 0 deletions test/deploy/DeployUniswapXExecutor.t.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading
Loading