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/script/DeployLifiExecutor.s.sol b/script/DeployLifiExecutor.s.sol new file mode 100644 index 0000000..77f7c40 --- /dev/null +++ b/script/DeployLifiExecutor.s.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {DeployLifiExecutorBaseScript} from "./deploy/base/DeployLifiExecutorBase.s.sol"; + +// forge script script/DeployLifiExecutor.s.sol:DeployLifiExecutorScript --rpc-url=RPC --broadcast + +contract DeployLifiExecutorScript is DeployLifiExecutorBaseScript { + // Configurations - UPDATE THESE BEFORE DEPLOYMENT + + // LI.FI OIF InputSettlerEscrow this executor finalises orders through. + address public constant INPUT_SETTLER = 0x000025c3226C00B2Cdc200005a1600509f4e00C0; + // LI.FI OIF OutputSettler this executor fills and attests outputs through. + address public constant OUTPUT_SETTLER = 0x0000000000eC36B683C2E6AC89e9A75989C22a2e; + // Executor owner. Defaults to the sender when left zero. + address public constant ADMIN = 0x0000000000000000000000000000000000000000; + // Owner of the proxy's ProxyAdmin, authorized to upgrade. Defaults to the sender when left zero. + address public constant PROXY_ADMIN_OWNER = 0x0000000000000000000000000000000000000000; + // Initial caller allowed to invoke finalise and register with LI.FI. Defaults to the sender when left zero. + address public constant CALLER = 0x0000000000000000000000000000000000000000; + + function run() public returns (DeploymentData memory data) { + address owner = _scriptOwner(); + data = runBase( + DeployParams({ + inputSettler: INPUT_SETTLER, + outputSettler: OUTPUT_SETTLER, + admin: ADMIN == address(0) ? owner : ADMIN, + proxyAdminOwner: PROXY_ADMIN_OWNER == address(0) ? owner : PROXY_ADMIN_OWNER, + caller: CALLER == address(0) ? owner : CALLER + }) + ); + } +} diff --git a/script/deploy/base/DeployLifiExecutorBase.s.sol b/script/deploy/base/DeployLifiExecutorBase.s.sol new file mode 100644 index 0000000..9753883 --- /dev/null +++ b/script/deploy/base/DeployLifiExecutorBase.s.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {Script, console2} from "forge-std/Script.sol"; + +import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +import {LiquidLaneLifiExecutor} from "../../../src/lifi/LiquidLaneLifiExecutor.sol"; + +contract DeployLifiExecutorBaseScript is Script { + struct DeployParams { + address inputSettler; + address outputSettler; + address admin; + address proxyAdminOwner; + address caller; + } + + struct DeploymentData { + LiquidLaneLifiExecutor executor; + address implementation; + address inputSettler; + address outputSettler; + 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(); + LiquidLaneLifiExecutor implementation = new LiquidLaneLifiExecutor(params.inputSettler, params.outputSettler); + TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy( + address(implementation), + params.proxyAdminOwner, + abi.encodeCall(LiquidLaneLifiExecutor.initialize, (params.admin, callers)) + ); + _stopBroadcast(); + + data.executor = LiquidLaneLifiExecutor(address(proxy)); + data.implementation = address(implementation); + data.inputSettler = params.inputSettler; + data.outputSettler = params.outputSettler; + 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 _scriptOwner() internal view virtual returns (address owner_) { + (,, address origin) = vm.readCallers(); + owner_ = origin == address(0) ? msg.sender : origin; + } + + function _validateParams(DeployParams memory params) internal pure { + require(params.inputSettler != address(0), "invalid input settler"); + require(params.outputSettler != address(0), "invalid output settler"); + 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(data.executor.owner() == data.admin); + assert(data.executor.INPUT_SETTLER() == data.inputSettler); + assert(data.executor.OUTPUT_SETTLER() == data.outputSettler); + assert(data.executor.isCaller(data.caller)); + } + + function _logDeployment(DeploymentData memory data) internal view { + console2.log("Deployed LI.FI Executor"); + console2.log(" executor: ", address(data.executor)); + console2.log(" implementation: ", data.implementation); + console2.log(" inputSettler: ", data.inputSettler); + console2.log(" outputSettler: ", data.outputSettler); + console2.log(" admin: ", data.admin); + console2.log(" proxyAdminOwner: ", data.proxyAdminOwner); + console2.log(" caller: ", data.caller); + } +} diff --git a/src/lifi/LiquidLaneLifiExecutor.sol b/src/lifi/LiquidLaneLifiExecutor.sol index c1bea75..504039a 100644 --- a/src/lifi/LiquidLaneLifiExecutor.sol +++ b/src/lifi/LiquidLaneLifiExecutor.sol @@ -2,39 +2,31 @@ // Copyright (c) 2026 Symbiotic pragma solidity 0.8.28; -import {DISCOUNT_PRECISION, ILiquidLaneAdapter} from "../interfaces/ILiquidLaneAdapter.sol"; +import {ILiquidLaneAdapter} from "../interfaces/ILiquidLaneAdapter.sol"; import {IInputSettler} from "./interfaces/IInputSettler.sol"; import {ILiquidLaneLifiExecutor} from "./interfaces/ILiquidLaneLifiExecutor.sol"; -import {IOutputSettler, MandateOutput} from "./interfaces/IOutputSettler.sol"; +import {IOutputSettler} from "./interfaces/IOutputSettler.sol"; -import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {Math} from "@openzeppelin/contracts/utils/math/Math.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 {SignatureChecker} from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol"; - -interface ILiquidLaneRate { - function getAmountOut(address tokenToRedeem, uint256 amountIn) external view returns (uint256 amountOut); - function getMaxAssets(address tokenToRedeem) external returns (uint256 assets); - function minDiscount(address tokenToRedeem) external view returns (uint256 ppm); -} +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import {EIP712Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/cryptography/EIP712Upgradeable.sol"; /// @title LiquidLaneLifiExecutor /// @notice LI.FI same-chain solver that redeems released inputs and fills the order output atomically. -contract LiquidLaneLifiExecutor is Ownable, ReentrancyGuard, ILiquidLaneLifiExecutor { - using Address for address payable; - using Math for uint256; +/// @dev Order lifecycle, fill deadlines, auction pricing, exclusivity, and discount terms are delegated +/// to the input settler, the output settler, and the LiquidLane adapters, which enforce them +/// authoritatively; the executor only routes the received inputs and settles the generated output. +/// @dev Deployed behind a transparent proxy; the settler addresses are immutable in the implementation +/// while ownership, the caller list, and the EIP-712 domain live in proxy storage set by {initialize}. +contract LiquidLaneLifiExecutor is Initializable, OwnableUpgradeable, EIP712Upgradeable, ILiquidLaneLifiExecutor { using SafeERC20 for IERC20; - uint8 internal constant ORDER_STATUS_DEPOSITED = 1; - uint8 internal constant ORDER_STATUS_CLAIMED = 2; - uint8 internal constant OUTPUT_CONTEXT_SIMPLE = 0x00; - uint8 internal constant OUTPUT_CONTEXT_DUTCH = 0x01; - uint8 internal constant OUTPUT_CONTEXT_EXCLUSIVE = 0xe0; - uint8 internal constant OUTPUT_CONTEXT_EXCLUSIVE_DUTCH = 0xe1; + /// @notice EIP-712 typehash wrapping a LI.FI registration message so signatures are domain-bound. + bytes32 public constant LIFI_REGISTRATION_TYPEHASH = keccak256("LifiRegistration(bytes32 messageHash)"); /* IMMUTABLES */ @@ -50,13 +42,16 @@ contract LiquidLaneLifiExecutor is Ownable, ReentrancyGuard, ILiquidLaneLifiExec /* CONSTRUCTOR */ - constructor(address inputSettler, address outputSettler, address owner_, address[] memory initCallers) - Ownable(owner_) - { - if (inputSettler == address(0) || outputSettler == address(0) || owner_ == address(0)) revert ZeroAddress(); - + constructor(address inputSettler, address outputSettler) { INPUT_SETTLER = inputSettler; OUTPUT_SETTLER = outputSettler; + _disableInitializers(); + } + + /// @inheritdoc ILiquidLaneLifiExecutor + function initialize(address owner_, address[] calldata initCallers) external initializer { + __Ownable_init(owner_); + __EIP712_init("LiquidLaneLifiExecutor", "1"); callers = initCallers; } @@ -64,127 +59,113 @@ contract LiquidLaneLifiExecutor is Ownable, ReentrancyGuard, ILiquidLaneLifiExec /// @dev Reverts unless the caller is in the allowed caller list. modifier onlyCaller() { - if (!_isCaller(msg.sender)) { - revert NotCaller(); - } - + if (!_isCaller(msg.sender)) revert NotCaller(); _; } /* FINALISE WRAPPER */ - /// @inheritdoc ILiquidLaneLifiExecutor - function expectedOutput(ILiquidLaneLifiExecutor.FillCall calldata fillCall) - external - pure - returns (uint256 expectedAmountOut) - { - for (uint256 i; i < fillCall.routes.length; ++i) { - expectedAmountOut += fillCall.routes[i].expectedAmountOut; - } - } - /// @inheritdoc ILiquidLaneLifiExecutor function isCaller(address caller) external view returns (bool) { return _isCaller(caller); } /// @inheritdoc ILiquidLaneLifiExecutor - function finaliseWithCurrentTimestamp(IInputSettler.StandardOrder calldata order, bytes calldata call) + function finaliseWithCurrentTimestamp(IInputSettler.StandardOrder calldata order, FillRoute[] calldata routes) external onlyCaller { - ILiquidLaneLifiExecutor.FillCall memory fillCall = abi.decode(call, (ILiquidLaneLifiExecutor.FillCall)); - bytes32 orderId = _validateFinaliseCall(order, fillCall); - - uint8 orderState = IInputSettler(INPUT_SETTLER).orderStatus(orderId); - if (orderState != ORDER_STATUS_DEPOSITED) revert InvalidOrderStatus(orderState); - - bytes32 executorId = _addressIdentifier(address(this)); + bytes32 executorId = bytes32(uint256(uint160(address(this)))); IInputSettler.SolveParams[] memory solveParams = new IInputSettler.SolveParams[](1); solveParams[0] = IInputSettler.SolveParams({timestamp: uint32(block.timestamp), solver: executorId}); - - IInputSettler(INPUT_SETTLER).finalise(order, solveParams, executorId, call); + IInputSettler(INPUT_SETTLER) + .finalise( + order, + solveParams, + executorId, + abi.encode( + FillCall({ + orderId: IInputSettler(INPUT_SETTLER).orderIdentifier(order), + output: order.outputs[0], + fillDeadline: order.fillDeadline, + routes: routes + }) + ) + ); } /* IINPUTCALLBACK */ /// @notice Called by the LI.FI input settler during finalise, after inputs are transferred here. - function orderFinalised(uint256[2][] calldata inputs, bytes calldata executionData) external nonReentrant { - if (msg.sender != INPUT_SETTLER) revert NotInputSettler(); - if (inputs.length != 1) revert InvalidInputCount(); - - ILiquidLaneLifiExecutor.FillCall memory fillCall = abi.decode(executionData, (ILiquidLaneLifiExecutor.FillCall)); - _validateFillAfter(fillCall.fillAfter, fillCall.output.context); - - bytes32 solver = _addressIdentifier(address(this)); - uint256 resolvedAmountOut = _resolveOutputAmount(fillCall.output, solver); - _validateOutput(fillCall.output); - address outputToken = _outputToken(fillCall.output); - - uint8 status = IInputSettler(INPUT_SETTLER).orderStatus(fillCall.orderId); - if (status != ORDER_STATUS_CLAIMED) revert InvalidOrderStatus(status); + function orderFinalised(uint256[2][] calldata inputs, bytes calldata executionData) external { + if (INPUT_SETTLER != msg.sender) revert NotInputSettler(); - address tokenIn = _inputToken(inputs[0][0]); - uint256 amountIn = inputs[0][1]; - if (amountIn == 0) revert InvalidAmount(); + FillCall memory fillCall = abi.decode(executionData, (FillCall)); - (uint256 minAmountOut, uint256[] memory executableAmountOuts) = - _validateRoutes(fillCall.routes, tokenIn, amountIn); - if (resolvedAmountOut > minAmountOut) { - revert InsufficientMinimumOutput(minAmountOut, resolvedAmountOut); + // Adapters assume their input has already been transferred to them before the swap call. + // forge-lint: disable-next-line(unsafe-typecast) + address tokenIn = address(uint160(inputs[0][0])); + uint256 routesLength = fillCall.routes.length; + for (uint256 i; i < routesLength; ++i) { + IERC20(tokenIn).safeTransfer(fillCall.routes[i].adapter, fillCall.routes[i].amountIn); } - uint256 outputGained = _redeemInputs(fillCall, tokenIn, outputToken, executableAmountOuts); - uint256 surplus = outputGained - resolvedAmountOut; + for (uint256 i; i < routesLength; ++i) { + FillRoute memory route = fillCall.routes[i]; + if (route.discount.discountId == bytes32(0)) { + ILiquidLaneAdapter(route.adapter) + .swap( + ILiquidLaneAdapter.Swap({ + recipient: address(this), + tokenIn: tokenIn, + amountIn: route.amountIn, + amountOut: route.amountOut + }) + ); + } else { + ILiquidLaneAdapter(route.adapter) + .swap(route.discount.discountSwap, route.discount.protocolSignature, address(this), route.amountIn); + } + } - IERC20(outputToken).forceApprove(OUTPUT_SETTLER, resolvedAmountOut); + // The output settler resolves the context-dependent amount it is owed and pulls it, + // reverting on shortfall. + address outputToken = address(uint160(uint256(fillCall.output.token))); + if (IERC20(outputToken).allowance(address(this), OUTPUT_SETTLER) < type(uint256).max) { + IERC20(outputToken).forceApprove(OUTPUT_SETTLER, type(uint256).max); + } + bytes32 solver = bytes32(uint256(uint160(address(this)))); IOutputSettler(OUTPUT_SETTLER) .fill(fillCall.orderId, fillCall.output, fillCall.fillDeadline, abi.encode(solver)); IOutputSettler(OUTPUT_SETTLER) .setAttestation(fillCall.orderId, solver, uint32(block.timestamp), fillCall.output); - - emit OutputFilled( - fillCall.orderId, - solver, - outputToken, - _identifierAddress(fillCall.output.recipient), - resolvedAmountOut, - surplus - ); } /* OWNER */ /// @inheritdoc ILiquidLaneLifiExecutor - function setCallers(address[] calldata newCallers) public onlyOwner { + function setCallers(address[] calldata newCallers) external onlyOwner { callers = newCallers; - emit SetCallers(newCallers); } - /// @inheritdoc ILiquidLaneLifiExecutor - function sweepERC20(address token, address to, uint256 amount) external onlyOwner nonReentrant { - if (token == address(0) || to == address(0)) revert ZeroAddress(); - - IERC20(token).safeTransfer(to, amount); - emit SweepERC20(token, to, amount); - } + /* EIP-1271 */ /// @inheritdoc ILiquidLaneLifiExecutor - function sweepNative(address to, uint256 amount) external onlyOwner nonReentrant { - if (to == address(0)) revert ZeroAddress(); - - payable(to).sendValue(amount); - emit SweepNative(to, amount); + function lifiRegistrationDigest(bytes32 messageHash) public view returns (bytes32) { + return _hashTypedDataV4(keccak256(abi.encode(LIFI_REGISTRATION_TYPEHASH, messageHash))); } - /* EIP-1271 */ - /// @inheritdoc IERC1271 + /// @dev The LI.FI registration message is wrapped in this executor's EIP-712 domain and accepted + /// only if signed by an authorized caller, binding the signature to both the signer and this proxy. function isValidSignature(bytes32 hash, bytes calldata signature) external view returns (bytes4) { - if (SignatureChecker.isValidSignatureNow(owner(), hash, signature)) { - return IERC1271.isValidSignature.selector; + bytes32 digest = lifiRegistrationDigest(hash); + uint256 callersLength = callers.length; + for (uint256 i; i < callersLength; ++i) { + if (SignatureChecker.isValidSignatureNowCalldata(callers[i], digest, signature)) { + return IERC1271.isValidSignature.selector; + } } return 0xffffffff; } @@ -193,284 +174,10 @@ contract LiquidLaneLifiExecutor is Ownable, ReentrancyGuard, ILiquidLaneLifiExec /// @dev Returns whether `caller` can invoke the finalise entrypoint. function _isCaller(address caller) internal view returns (bool) { - for (uint256 i; i < callers.length; ++i) { - if (callers[i] == caller) { - return true; - } + uint256 callersLength = callers.length; + for (uint256 i; i < callersLength; ++i) { + if (callers[i] == caller) return true; } return false; } - - function _validateFinaliseCall( - IInputSettler.StandardOrder calldata order, - ILiquidLaneLifiExecutor.FillCall memory fillCall - ) internal view returns (bytes32 orderId) { - _validateFillAfter(fillCall.fillAfter, fillCall.output.context); - - orderId = IInputSettler(INPUT_SETTLER).orderIdentifier(order); - if (fillCall.orderId != orderId) revert InvalidOrderId(); - if (order.inputs.length != 1) revert InvalidInputCount(); - if (order.outputs.length != 1) revert InvalidOutputCount(); - if ( - fillCall.fillDeadline != order.fillDeadline || _outputHash(fillCall.output) != _outputHash(order.outputs[0]) - ) { - revert InvalidOrderOutput(); - } - - _validateOutput(fillCall.output); - } - - function _redeemInputs( - ILiquidLaneLifiExecutor.FillCall memory fillCall, - address tokenIn, - address outputToken, - uint256[] memory executableAmountOuts - ) internal returns (uint256 outputGained) { - for (uint256 i; i < fillCall.routes.length; ++i) { - IERC20(tokenIn).safeTransfer(fillCall.routes[i].adapter, fillCall.routes[i].amountIn); - } - - for (uint256 i; i < fillCall.routes.length; ++i) { - ILiquidLaneLifiExecutor.FillRoute memory route = fillCall.routes[i]; - uint256 outputBefore = IERC20(outputToken).balanceOf(address(this)); - if (route.discount.discountId == bytes32(0)) { - ILiquidLaneAdapter(route.adapter) - .swap( - ILiquidLaneAdapter.Swap({ - recipient: address(this), - tokenIn: tokenIn, - amountIn: route.amountIn, - amountOut: executableAmountOuts[i] - }) - ); - } else { - ILiquidLaneAdapter(route.adapter) - .swap(route.discount.discountSwap, route.discount.protocolSignature, address(this), route.amountIn); - } - - uint256 routeOutput = IERC20(outputToken).balanceOf(address(this)) - outputBefore; - if (routeOutput < route.minAmountOut) { - revert RouteOutputTooLow(route.adapter, route.minAmountOut, routeOutput); - } - outputGained += routeOutput; - - emit InputRedeemed( - fillCall.orderId, - route.adapter, - tokenIn, - outputToken, - route.amountIn, - routeOutput, - route.discount.discountId - ); - } - } - - function _validateOutput(MandateOutput memory output) internal view { - if (output.chainId != block.chainid) revert InvalidOutputChain(); - if (output.amount == 0) revert InvalidAmount(); - - bytes32 outputSettlerId = _addressIdentifier(OUTPUT_SETTLER); - if (output.settler != outputSettlerId) revert InvalidOutputSettler(); - if (output.oracle != outputSettlerId) revert InvalidOutputOracle(); - - _outputToken(output); - _identifierAddress(output.recipient); - } - - function _validateRoutes(ILiquidLaneLifiExecutor.FillRoute[] memory routes, address tokenIn, uint256 orderAmountIn) - internal - returns (uint256 minAmountOut, uint256[] memory executableAmountOuts) - { - if (routes.length == 0) revert EmptyRoutes(); - - executableAmountOuts = new uint256[](routes.length); - uint256 routedAmountIn; - for (uint256 i; i < routes.length; ++i) { - ILiquidLaneLifiExecutor.FillRoute memory route = routes[i]; - if (route.amountIn == 0) revert InvalidAmount(); - if (route.minAmountOut == 0 || route.minAmountOut > route.expectedAmountOut) { - revert InvalidRouteOutputBounds(route.expectedAmountOut, route.minAmountOut); - } - if (route.adapter == address(0)) revert ZeroAddress(); - - (uint256 currentAmountOut, uint256 maxAssets) = _routeState(route, tokenIn); - uint256 executableAmountOut = _executableAmountOut(route, currentAmountOut, maxAssets); - if (executableAmountOut < route.minAmountOut) { - revert RouteOutputTooLow(route.adapter, route.minAmountOut, executableAmountOut); - } - - routedAmountIn += route.amountIn; - minAmountOut += route.minAmountOut; - executableAmountOuts[i] = executableAmountOut; - } - if (routedAmountIn != orderAmountIn) revert RouteInputMismatch(routedAmountIn, orderAmountIn); - } - - function _executableAmountOut( - ILiquidLaneLifiExecutor.FillRoute memory route, - uint256 currentAmountOut, - uint256 maxAssets - ) internal pure returns (uint256) { - if (route.discount.discountId == bytes32(0)) { - return Math.min(route.expectedAmountOut, Math.min(currentAmountOut, maxAssets)); - } - if (currentAmountOut > maxAssets) { - revert PrivateRouteExceedsCapacity(route.adapter, currentAmountOut, maxAssets); - } - return currentAmountOut; - } - - function _routeState(ILiquidLaneLifiExecutor.FillRoute memory route, address tokenIn) - internal - returns (uint256 amountOut, uint256 maxAssets) - { - uint256 minimumDiscount = ILiquidLaneRate(route.adapter).minDiscount(tokenIn); - uint256 discount = minimumDiscount; - if (route.discount.discountId != bytes32(0)) { - ILiquidLaneAdapter.Discount memory terms = route.discount.discountSwap.discount; - if (terms.tokenToRedeem != tokenIn) revert DiscountTokenMismatch(tokenIn, terms.tokenToRedeem); - if (terms.deadline < block.timestamp || route.discount.discountSwap.protocolDeadline < block.timestamp) { - revert DiscountExpired(terms.deadline, route.discount.discountSwap.protocolDeadline, block.timestamp); - } - discount = terms.discount; - if (discount < minimumDiscount || discount > DISCOUNT_PRECISION) { - revert InvalidDiscount(discount, minimumDiscount); - } - } - - amountOut = ILiquidLaneRate(route.adapter).getAmountOut(tokenIn, route.amountIn) - .mulDiv(DISCOUNT_PRECISION - discount, DISCOUNT_PRECISION); - maxAssets = ILiquidLaneRate(route.adapter).getMaxAssets(tokenIn); - } - - function _validateFillAfter(uint32 fillAfter, bytes memory context) internal view { - if (fillAfter == 0) return; - - uint8 contextType = _outputContextType(context); - if (contextType != OUTPUT_CONTEXT_DUTCH && contextType != OUTPUT_CONTEXT_EXCLUSIVE_DUTCH) { - revert FillAfterWithoutAuction(); - } - - if (block.timestamp < fillAfter) { - revert FillTooEarly(fillAfter, uint32(block.timestamp)); - } - } - - function _resolveOutputAmount(MandateOutput memory output, bytes32 solver) internal view returns (uint256) { - bytes memory context = output.context; - uint8 contextType = _outputContextType(context); - if (contextType == OUTPUT_CONTEXT_SIMPLE) { - return output.amount; - } - if (contextType == OUTPUT_CONTEXT_DUTCH) { - return _dutchOutputAmount(output.amount, context, 1); - } - if (contextType == OUTPUT_CONTEXT_EXCLUSIVE) { - _validateExclusiveSolver(context, solver, 1, 33); - return output.amount; - } - - _validateExclusiveSolver(context, solver, 1, 33); - return _dutchOutputAmount(output.amount, context, 33); - } - - function _outputContextType(bytes memory context) internal pure returns (uint8 contextType) { - uint256 length = context.length; - if (length == 0) return OUTPUT_CONTEXT_SIMPLE; - - contextType = uint8(context[0]); - if (contextType == OUTPUT_CONTEXT_SIMPLE) { - if (length != 1) revert InvalidOutputContextLength(contextType, length); - } else if (contextType == OUTPUT_CONTEXT_DUTCH) { - if (length != 41) revert InvalidOutputContextLength(contextType, length); - } else if (contextType == OUTPUT_CONTEXT_EXCLUSIVE) { - if (length != 37) revert InvalidOutputContextLength(contextType, length); - } else if (contextType == OUTPUT_CONTEXT_EXCLUSIVE_DUTCH) { - if (length != 73) revert InvalidOutputContextLength(contextType, length); - } else { - revert UnknownOutputContext(context[0]); - } - } - - function _dutchOutputAmount(uint256 amount, bytes memory context, uint256 startTimeOffset) - internal - view - returns (uint256) - { - uint256 startTime = _readUint32(context, startTimeOffset); - uint256 stopTime = _readUint32(context, startTimeOffset + 4); - uint256 currentTime = block.timestamp > startTime ? block.timestamp : startTime; - if (stopTime < currentTime) return amount; - - return amount + _readUint256(context, startTimeOffset + 8) * (stopTime - currentTime); - } - - function _validateExclusiveSolver( - bytes memory context, - bytes32 solver, - uint256 exclusiveForOffset, - uint256 startTimeOffset - ) internal view { - bytes32 exclusiveFor = _readBytes32(context, exclusiveForOffset); - if (block.timestamp < _readUint32(context, startTimeOffset) && exclusiveFor != solver) { - revert ExclusiveForMismatch(exclusiveFor, solver); - } - } - - function _outputToken(MandateOutput memory output) internal pure returns (address) { - if (output.token == bytes32(0)) revert NativeOutputUnsupported(); - return _identifierAddress(output.token); - } - - function _outputHash(MandateOutput memory output) internal pure returns (bytes32) { - return keccak256( - abi.encode( - output.oracle, - output.settler, - output.chainId, - output.token, - output.amount, - output.recipient, - keccak256(output.callbackData), - keccak256(output.context) - ) - ); - } - - function _inputToken(uint256 tokenId) internal pure returns (address token) { - // High bits are rejected by the equality check below. - // forge-lint: disable-next-line(unsafe-typecast) - token = address(uint160(tokenId)); - if (token == address(0) || tokenId != uint256(uint160(token))) revert InvalidIdentifier(); - } - - function _readUint32(bytes memory data, uint256 offset) internal pure returns (uint32 value) { - bytes32 word = _readBytes32(data, offset); - value = uint32(uint256(word >> 224)); - } - - function _readUint256(bytes memory data, uint256 offset) internal pure returns (uint256 value) { - value = uint256(_readBytes32(data, offset)); - } - - function _readBytes32(bytes memory data, uint256 offset) internal pure returns (bytes32 value) { - assembly ("memory-safe") { - value := mload(add(add(data, 0x20), offset)) - } - } - - function _addressIdentifier(address addr) internal pure returns (bytes32 identifier) { - if (addr == address(0)) revert InvalidIdentifier(); - return bytes32(uint256(uint160(addr))); - } - - function _identifierAddress(bytes32 identifier) internal pure returns (address addr) { - addr = address(uint160(uint256(identifier))); - if (addr == address(0) || identifier != bytes32(uint256(uint160(addr)))) revert InvalidIdentifier(); - } - - /* RECEIVE */ - - receive() external payable {} } diff --git a/src/lifi/interfaces/IInputSettler.sol b/src/lifi/interfaces/IInputSettler.sol index 85509f7..591cad5 100644 --- a/src/lifi/interfaces/IInputSettler.sol +++ b/src/lifi/interfaces/IInputSettler.sol @@ -24,13 +24,6 @@ interface IInputSettler { bytes32 solver; } - /** - * @notice Returns the current order lifecycle status. - * @param orderId OIF order id. - * @return status Input settler order status. - */ - function orderStatus(bytes32 orderId) external view returns (uint8 status); - function orderIdentifier(StandardOrder calldata order) external view returns (bytes32 orderId); function finalise( diff --git a/src/lifi/interfaces/ILiquidLaneLifiExecutor.sol b/src/lifi/interfaces/ILiquidLaneLifiExecutor.sol index dfd065d..57cfd2b 100644 --- a/src/lifi/interfaces/ILiquidLaneLifiExecutor.sol +++ b/src/lifi/interfaces/ILiquidLaneLifiExecutor.sol @@ -16,34 +16,12 @@ import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol"; interface ILiquidLaneLifiExecutor is IInputCallback, IERC1271 { /* ERRORS */ - error DiscountExpired(uint48 deadline, uint48 protocolDeadline, uint256 currentTime); - error DiscountTokenMismatch(address expectedToken, address discountToken); - error EmptyRoutes(); - error ExclusiveForMismatch(bytes32 exclusiveFor, bytes32 solver); - error FillTooEarly(uint32 fillAfter, uint32 currentTime); - error FillAfterWithoutAuction(); - error InsufficientMinimumOutput(uint256 minimumAmountOut, uint256 resolvedAmountOut); - error InvalidAmount(); - error InvalidDiscount(uint256 discount, uint256 minimumDiscount); - error InvalidInputCount(); - error InvalidIdentifier(); - error InvalidOrderId(); - error InvalidOrderOutput(); - error InvalidOrderStatus(uint8 status); - error InvalidOutputCount(); - error InvalidOutputContextLength(uint8 contextType, uint256 length); - error InvalidOutputChain(); - error InvalidOutputOracle(); - error InvalidOutputSettler(); - error InvalidRouteOutputBounds(uint256 expectedAmountOut, uint256 minAmountOut); - error NativeOutputUnsupported(); error NotCaller(); error NotInputSettler(); - error PrivateRouteExceedsCapacity(address adapter, uint256 amountOut, uint256 maxAssets); - error RouteInputMismatch(uint256 routedAmountIn, uint256 orderAmountIn); - error RouteOutputTooLow(address adapter, uint256 minAmountOut, uint256 availableAmountOut); - error UnknownOutputContext(bytes1 contextType); - error ZeroAddress(); + + /* EVENTS */ + + event SetCallers(address[] newCallers); /* STRUCTS */ @@ -63,67 +41,40 @@ interface ILiquidLaneLifiExecutor is IInputCallback, IERC1271 { * @notice One atomic LiquidLane redemption leg. * @param adapter LiquidLane adapter selected by the solver. * @param amountIn Order-input amount routed to the adapter. - * @param expectedAmountOut Preferred output at the strategy's buffered quote. - * @param minAmountOut Hard economic floor after order output, gas, and minimum margin. + * @param amountOut Output amount requested from the adapter on the direct swap path; + * unused for discount routes, where the signed discount terms set the output. * @param discount Optional private-discount authorization; zero id means direct swap. */ struct FillRoute { address adapter; uint256 amountIn; - uint256 expectedAmountOut; - uint256 minAmountOut; + uint256 amountOut; FillDiscount discount; } /** - * @notice Callback payload built by the LI.FI solver and passed to InputSettler.finalise. + * @notice Callback payload constructed by `finaliseWithCurrentTimestamp` from the order itself. * @param orderId OIF order id. * @param output Single output to fill and attest. * @param fillDeadline Fill deadline carried by the order. - * @param fillAfter Earliest timestamp when the solver strategy allows filling. - * @param routes LiquidLane legs selected by the solver. Their input sum must equal the order input; - * each minimum output is checked against current rate/capacity and direct targets may be clamped. + * @param routes LiquidLane legs selected by the solver. */ struct FillCall { bytes32 orderId; MandateOutput output; uint32 fillDeadline; - uint32 fillAfter; FillRoute[] routes; } - /* EVENTS */ - - event InputRedeemed( - bytes32 indexed orderId, - address indexed adapter, - address indexed tokenIn, - address tokenOut, - uint256 amountIn, - uint256 amountOut, - bytes32 discountId - ); - event SetCallers(address[] newCallers); - event OutputFilled( - bytes32 indexed orderId, - bytes32 indexed solver, - address indexed token, - address recipient, - uint256 amount, - uint256 surplus - ); - event SweepERC20(address indexed token, address indexed to, uint256 amount); - event SweepNative(address indexed to, uint256 amount); - /* FUNCTIONS */ function INPUT_SETTLER() external view returns (address inputSettler); function OUTPUT_SETTLER() external view returns (address outputSettler); function callers(uint256 index) external view returns (address caller); - function expectedOutput(FillCall calldata fillCall) external pure returns (uint256 expectedAmountOut); - function finaliseWithCurrentTimestamp(IInputSettler.StandardOrder calldata order, bytes calldata call) external; + function initialize(address owner, address[] calldata initCallers) external; + function finaliseWithCurrentTimestamp(IInputSettler.StandardOrder calldata order, FillRoute[] calldata routes) + external; function isCaller(address caller) external view returns (bool allowed); + function lifiRegistrationDigest(bytes32 messageHash) external view returns (bytes32 digest); function setCallers(address[] calldata newCallers) external; - function sweepERC20(address token, address to, uint256 amount) external; - function sweepNative(address to, uint256 amount) external; } diff --git a/src/uniswapx/LiquidLaneUniswapXExecutor.sol b/src/uniswapx/LiquidLaneUniswapXExecutor.sol new file mode 100644 index 0000000..664a2c0 --- /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/lifi/LifiExecutorFork.t.sol b/test/lifi/LifiExecutorFork.t.sol index ec163c2..1d0d0bc 100644 --- a/test/lifi/LifiExecutorFork.t.sol +++ b/test/lifi/LifiExecutorFork.t.sol @@ -9,6 +9,7 @@ import {ILiquidLaneLifiExecutor} from "../../src/lifi/interfaces/ILiquidLaneLifi import {MandateOutput} from "../../src/lifi/interfaces/IOutputSettler.sol"; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; import {Test} from "forge-std/Test.sol"; interface IInputSettlerEscrowLike { @@ -24,6 +25,7 @@ contract LifiExecutorForkTest is Test { address internal user = makeAddr("user"); address internal owner = makeAddr("owner"); address internal recipient = makeAddr("recipient"); + address internal proxyAdminOwner = makeAddr("proxyAdminOwner"); ForkTestToken internal inputToken; ForkTestToken internal outputToken; @@ -43,16 +45,19 @@ contract LifiExecutorForkTest is Test { address[] memory callers = new address[](1); callers[0] = owner; - executor = new LiquidLaneLifiExecutor(INPUT_SETTLER, OUTPUT_SETTLER, owner, callers); + LiquidLaneLifiExecutor impl = new LiquidLaneLifiExecutor(INPUT_SETTLER, OUTPUT_SETTLER); + TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy( + address(impl), proxyAdminOwner, abi.encodeCall(LiquidLaneLifiExecutor.initialize, (owner, callers)) + ); + executor = LiquidLaneLifiExecutor(address(proxy)); } function testExecutorFinalisesOpenedOrderOnRealSettler() external { IInputSettler.StandardOrder memory order = _openOrder(10 ether, 9 ether, "executor"); bytes32 orderId = IInputSettlerEscrowLike(INPUT_SETTLER).orderIdentifier(order); - bytes memory call = _fillCall(order, orderId); vm.prank(owner); - executor.finaliseWithCurrentTimestamp(order, call); + executor.finaliseWithCurrentTimestamp(order, _routes(order)); assertEq(IInputSettlerEscrowLike(INPUT_SETTLER).orderStatus(orderId), 2, "claimed"); assertEq(outputToken.balanceOf(recipient), 9 ether, "recipient output"); @@ -107,13 +112,16 @@ contract LifiExecutorForkTest is Test { }); } - function _fillCall(IInputSettler.StandardOrder memory order, bytes32 orderId) internal view returns (bytes memory) { - ILiquidLaneLifiExecutor.FillRoute[] memory routes = new ILiquidLaneLifiExecutor.FillRoute[](1); + function _routes(IInputSettler.StandardOrder memory order) + internal + view + returns (ILiquidLaneLifiExecutor.FillRoute[] memory routes) + { + routes = new ILiquidLaneLifiExecutor.FillRoute[](1); routes[0] = ILiquidLaneLifiExecutor.FillRoute({ adapter: address(adapter), amountIn: order.inputs[0][1], - expectedAmountOut: 10 ether, - minAmountOut: 10 ether, + amountOut: 10 ether, discount: ILiquidLaneLifiExecutor.FillDiscount({ discountId: bytes32(0), discountSwap: ILiquidLaneAdapter.DiscountSwap({ @@ -131,15 +139,6 @@ contract LifiExecutorForkTest is Test { protocolSignature: "" }) }); - return abi.encode( - ILiquidLaneLifiExecutor.FillCall({ - orderId: orderId, - output: order.outputs[0], - fillDeadline: order.fillDeadline, - fillAfter: 0, - routes: routes - }) - ); } function _id(address addr) internal pure returns (bytes32) { @@ -154,18 +153,6 @@ contract ForkMintingAdapter is ILiquidLaneAdapter { outputToken = outputToken_; } - function getAmountOut(address, uint256 amountIn) external pure returns (uint256) { - return amountIn; - } - - function getMaxAssets(address) external pure returns (uint256) { - return type(uint256).max; - } - - function minDiscount(address) external pure returns (uint256) { - return 0; - } - function swap(Swap calldata swap_) external { require(ForkTestToken(swap_.tokenIn).balanceOf(address(this)) >= swap_.amountIn, "missing input"); outputToken.mint(swap_.recipient, swap_.amountOut); diff --git a/test/lifi/LiquidLaneLifiExecutor.t.sol b/test/lifi/LiquidLaneLifiExecutor.t.sol index e9abe63..07f9b9f 100644 --- a/test/lifi/LiquidLaneLifiExecutor.t.sol +++ b/test/lifi/LiquidLaneLifiExecutor.t.sol @@ -12,7 +12,10 @@ import {IOutputSettler, MandateOutput} from "../../src/lifi/interfaces/IOutputSe import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IERC20Errors} from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import {Test} from "forge-std/Test.sol"; interface IOutputCallbackLike { @@ -28,7 +31,7 @@ contract LiquidLaneLifiExecutorTest is Test { address internal owner; address internal recipient = makeAddr("recipient"); - address internal collector = makeAddr("collector"); + address internal proxyAdminOwner = makeAddr("proxyAdminOwner"); TestToken internal rwa; TestToken internal outputToken; @@ -44,711 +47,389 @@ contract LiquidLaneLifiExecutorTest is Test { adapter = new MockLifiAdapter(outputToken); inputSettler = new MockInputSettler(); outputSettler = new MockOutputSettler(); - inputSettler.setOrderStatus(ORDER_ID, ORDER_STATUS_DEPOSITED); - executor = new LiquidLaneLifiExecutor(address(inputSettler), address(outputSettler), owner, _callers(owner)); + executor = _deployExecutor(address(inputSettler), address(outputSettler), owner); outputToken.mint(address(adapter), 100 ether); } - function testFinaliseCallbackRedeemsInputThenFillsAndAttestsOutput() public { - rwa.mint(address(inputSettler), 10 ether); + function _deployExecutor(address inputSettler_, address outputSettler_, address owner_) + internal + returns (LiquidLaneLifiExecutor) + { + return _deployExecutor(inputSettler_, outputSettler_, owner_, _callers(owner_)); + } - vm.expectEmit(true, true, true, true, address(executor)); - emit ILiquidLaneLifiExecutor.InputRedeemed( - ORDER_ID, address(adapter), address(rwa), address(outputToken), 10 ether, 10 ether, bytes32(0) - ); - vm.expectEmit(true, true, true, true, address(executor)); - emit ILiquidLaneLifiExecutor.OutputFilled( - ORDER_ID, _id(address(executor)), address(outputToken), recipient, 9 ether, 1 ether + function _deployExecutor(address inputSettler_, address outputSettler_, address owner_, address[] memory callers_) + internal + returns (LiquidLaneLifiExecutor) + { + LiquidLaneLifiExecutor impl = new LiquidLaneLifiExecutor(inputSettler_, outputSettler_); + TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy( + address(impl), proxyAdminOwner, abi.encodeCall(LiquidLaneLifiExecutor.initialize, (owner_, callers_)) ); + return LiquidLaneLifiExecutor(address(proxy)); + } - inputSettler.finaliseCallback(address(executor), _inputs(10 ether), _fillCallData(9 ether)); - - assertEq(rwa.balanceOf(address(adapter)), 10 ether); - assertEq(outputToken.balanceOf(recipient), 9 ether); - assertEq(outputToken.balanceOf(address(executor)), 1 ether); - assertEq(inputSettler.orderStatus(ORDER_ID), ORDER_STATUS_CLAIMED); - assertEq(outputSettler.lastOrderId(), ORDER_ID); - assertEq(outputSettler.lastSolver(), _id(address(executor))); - assertTrue(outputSettler.attested()); + function _callers(address caller) internal pure returns (address[] memory callers_) { + callers_ = new address[](1); + callers_[0] = caller; } - function testFinaliseWithCurrentTimestampCallsFinaliseAsExecutor() public { - rwa.mint(address(inputSettler), 10 ether); + /* FINALISE HAPPY PATHS */ - vm.warp(1_717_171); + function testFinaliseWithCurrentTimestampFillsAndAttestsDirectRoute() public { IInputSettler.StandardOrder memory order = _order(10 ether, 9 ether); - bytes32 orderId = _orderId(order); - inputSettler.setOrderStatus(orderId, ORDER_STATUS_DEPOSITED); - bytes memory call = _fillCallData(orderId, 9 ether); + bytes32 orderId = _openOrder(order); - executor.finaliseWithCurrentTimestamp(order, call); + executor.finaliseWithCurrentTimestamp(order, _directRoutes(address(adapter), 10 ether, 10 ether)); + assertEq(rwa.balanceOf(address(adapter)), 10 ether); + assertEq(outputToken.balanceOf(recipient), 9 ether); + assertEq(outputToken.balanceOf(address(executor)), 1 ether); + assertEq(outputToken.allowance(address(executor), address(outputSettler)), type(uint256).max); + assertEq(inputSettler.orderStatus(orderId), ORDER_STATUS_CLAIMED); assertEq(inputSettler.lastTimestamp(), uint32(block.timestamp)); assertEq(inputSettler.lastSolver(), _id(address(executor))); assertEq(inputSettler.lastDestination(), _id(address(executor))); - assertEq(inputSettler.orderStatus(orderId), ORDER_STATUS_CLAIMED); - assertEq(outputToken.balanceOf(recipient), 9 ether); + assertEq(outputSettler.lastOrderId(), orderId); + assertEq(outputSettler.lastSolver(), _id(address(executor))); assertTrue(outputSettler.attested()); } - function testFinaliseWithCurrentTimestampRejectsAlreadyClaimedOrderBeforeFinalise() public { - IInputSettler.StandardOrder memory order = _order(10 ether, 9 ether); - bytes32 orderId = _orderId(order); - inputSettler.setOrderStatus(orderId, ORDER_STATUS_CLAIMED); - - vm.expectRevert( - abi.encodeWithSelector(ILiquidLaneLifiExecutor.InvalidOrderStatus.selector, ORDER_STATUS_CLAIMED) - ); - executor.finaliseWithCurrentTimestamp(order, _fillCallData(orderId, 9 ether)); - } - - function testFinaliseWithCurrentTimestampRejectsUnauthorizedCaller() public { - address caller = makeAddr("caller"); - IInputSettler.StandardOrder memory order = _order(10 ether, 9 ether); - - vm.expectRevert(ILiquidLaneLifiExecutor.NotCaller.selector); - vm.prank(caller); - executor.finaliseWithCurrentTimestamp(order, _fillCallData(_orderId(order), 9 ether)); - } - - function testSetCallersAllowsNonOwnerToFinaliseAndRevokesOldCaller() public { - address caller = makeAddr("caller"); - executor.setCallers(_callers(caller)); - - assertTrue(executor.isCaller(caller)); - assertFalse(executor.isCaller(owner)); - - rwa.mint(address(inputSettler), 20 ether); - IInputSettler.StandardOrder memory order = _order(10 ether, 9 ether); - bytes32 orderId = _orderId(order); - inputSettler.setOrderStatus(orderId, ORDER_STATUS_DEPOSITED); - - vm.prank(caller); - executor.finaliseWithCurrentTimestamp(order, _fillCallData(orderId, 9 ether)); - - IInputSettler.StandardOrder memory secondOrder = _order(10 ether, 8 ether); - vm.expectRevert(ILiquidLaneLifiExecutor.NotCaller.selector); - executor.finaliseWithCurrentTimestamp(secondOrder, _fillCallData(_orderId(secondOrder), 8 ether)); - } - - function testSetCallersRejectsNonOwner() public { - address caller = makeAddr("caller"); - - vm.expectRevert(abi.encodeWithSignature("OwnableUnauthorizedAccount(address)", caller)); - vm.prank(caller); - executor.setCallers(_callers(caller)); - } - - function testIsValidSignatureAcceptsOwner() public { - uint256 ownerKey = 0xA11CE; - LiquidLaneLifiExecutor ownedExecutor = new LiquidLaneLifiExecutor( - address(inputSettler), address(outputSettler), vm.addr(ownerKey), new address[](0) - ); - bytes32 digest = keccak256("lifi registration"); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerKey, digest); - - assertEq(ownedExecutor.isValidSignature(digest, abi.encodePacked(r, s, v)), IERC1271.isValidSignature.selector); - } - - function testIsValidSignatureRejectsOtherSigner() public { - LiquidLaneLifiExecutor ownedExecutor = new LiquidLaneLifiExecutor( - address(inputSettler), address(outputSettler), vm.addr(0xA11CE), new address[](0) - ); - bytes32 digest = keccak256("lifi registration"); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(0xB0B, digest); - - assertEq(ownedExecutor.isValidSignature(digest, abi.encodePacked(r, s, v)), bytes4(0xffffffff)); - } - - function testIsValidSignatureRejectsMalformedSignature() public { - LiquidLaneLifiExecutor ownedExecutor = new LiquidLaneLifiExecutor( - address(inputSettler), address(outputSettler), vm.addr(0xA11CE), new address[](0) - ); - - assertEq(ownedExecutor.isValidSignature(keccak256("lifi registration"), hex"deadbeef"), bytes4(0xffffffff)); - } - - function testFinaliseWithCurrentTimestampRejectsOrderIdMismatch() public { - IInputSettler.StandardOrder memory order = _order(10 ether, 9 ether); - ILiquidLaneLifiExecutor.FillCall memory fillCall = - _fillCallStruct(address(adapter), _orderId(order), _output(9 ether)); - fillCall.orderId = keccak256("wrong order"); - - vm.expectRevert(ILiquidLaneLifiExecutor.InvalidOrderId.selector); - executor.finaliseWithCurrentTimestamp(order, abi.encode(fillCall)); - } - - function testFinaliseWithCurrentTimestampRejectsOutputCountMismatch() public { - IInputSettler.StandardOrder memory order = _order(10 ether, 9 ether); - MandateOutput[] memory outputs = new MandateOutput[](2); - outputs[0] = _output(9 ether); - outputs[1] = _output(1 ether); - order.outputs = outputs; - bytes memory call = _fillCallData(_orderId(order), 9 ether); - - vm.expectRevert(ILiquidLaneLifiExecutor.InvalidOutputCount.selector); - executor.finaliseWithCurrentTimestamp(order, call); - } - - function testFinaliseWithCurrentTimestampRejectsOutputMismatch() public { - IInputSettler.StandardOrder memory order = _order(10 ether, 9 ether); - bytes memory call = _fillCallData(_orderId(order), 8 ether); - - vm.expectRevert(ILiquidLaneLifiExecutor.InvalidOrderOutput.selector); - executor.finaliseWithCurrentTimestamp(order, call); - } - - function testFinaliseWithCurrentTimestampRejectsInsufficientMinimumOutput() public { - IInputSettler.StandardOrder memory order = _order(10 ether, 9 ether); - _openOrder(order); - ILiquidLaneLifiExecutor.FillCall memory fillCall = - _fillCallStruct(address(adapter), _orderId(order), order.outputs[0]); - fillCall.routes[0].expectedAmountOut = 8 ether; - fillCall.routes[0].minAmountOut = 8 ether; - - vm.expectRevert( - abi.encodeWithSelector(ILiquidLaneLifiExecutor.InsufficientMinimumOutput.selector, 8 ether, 9 ether) - ); - executor.finaliseWithCurrentTimestamp(order, abi.encode(fillCall)); - } - - function testFinaliseWithCurrentTimestampRejectsInvalidRouteOutputBounds() public { + function testFinaliseWithCurrentTimestampExecutesMultipleRoutesAndKeepsSurplus() public { + MockLifiAdapter secondAdapter = new MockLifiAdapter(outputToken); + outputToken.mint(address(secondAdapter), 100 ether); IInputSettler.StandardOrder memory order = _order(10 ether, 9 ether); - _openOrder(order); - ILiquidLaneLifiExecutor.FillCall memory fillCall = - _fillCallStruct(address(adapter), _orderId(order), order.outputs[0]); - fillCall.routes[0].expectedAmountOut = 9 ether; - fillCall.routes[0].minAmountOut = 9.1 ether; - - vm.expectRevert( - abi.encodeWithSelector(ILiquidLaneLifiExecutor.InvalidRouteOutputBounds.selector, 9 ether, 9.1 ether) - ); - executor.finaliseWithCurrentTimestamp(order, abi.encode(fillCall)); - } + bytes32 orderId = _openOrder(order); - function testFinaliseWithCurrentTimestampClampsTargetToCurrentRate() public { - IInputSettler.StandardOrder memory order = _order(10 ether, 9 ether); - bytes32 orderId = _orderId(order); - inputSettler.setOrderStatus(orderId, ORDER_STATUS_DEPOSITED); - rwa.mint(address(inputSettler), 10 ether); - ILiquidLaneLifiExecutor.FillCall memory fillCall = _fillCallStruct(address(adapter), orderId, order.outputs[0]); - fillCall.routes[0].expectedAmountOut = 11 ether; + ILiquidLaneLifiExecutor.FillRoute[] memory routes = new ILiquidLaneLifiExecutor.FillRoute[](2); + routes[0] = _directRoute(address(adapter), 4 ether, 4 ether); + routes[1] = _directRoute(address(secondAdapter), 6 ether, 6 ether); - executor.finaliseWithCurrentTimestamp(order, abi.encode(fillCall)); + executor.finaliseWithCurrentTimestamp(order, routes); + assertEq(rwa.balanceOf(address(adapter)), 4 ether); + assertEq(rwa.balanceOf(address(secondAdapter)), 6 ether); assertEq(outputToken.balanceOf(recipient), 9 ether); assertEq(outputToken.balanceOf(address(executor)), 1 ether); } - function testFinaliseWithCurrentTimestampAcceptsPrivateDiscountRoute() public { + function testFinaliseWithCurrentTimestampExecutesPrivateDiscountRoute() public { vm.warp(1000); - adapter.setMinDiscount(100_000); - rwa.mint(address(inputSettler), 10 ether); - IInputSettler.StandardOrder memory order = _order(10 ether, 8 ether); - bytes32 orderId = _orderId(order); - inputSettler.setOrderStatus(orderId, ORDER_STATUS_DEPOSITED); - ILiquidLaneLifiExecutor.FillCall memory fillCall = _fillCallStruct(address(adapter), orderId, order.outputs[0]); - fillCall.routes[0] = _discountRoute(address(adapter), 10 ether, 9 ether, keccak256("discount"), 100_000); + bytes32 orderId = _openOrder(order); - executor.finaliseWithCurrentTimestamp(order, abi.encode(fillCall)); + ILiquidLaneLifiExecutor.FillRoute[] memory routes = new ILiquidLaneLifiExecutor.FillRoute[](1); + routes[0] = _discountRoute(address(adapter), 10 ether, keccak256("discount"), 100_000); + + executor.finaliseWithCurrentTimestamp(order, routes); + assertEq(rwa.balanceOf(address(adapter)), 10 ether); assertEq(outputToken.balanceOf(recipient), 8 ether); assertEq(outputToken.balanceOf(address(executor)), 1 ether); - assertEq(rwa.balanceOf(address(adapter)), 10 ether); } - function testFinaliseWithCurrentTimestampRejectsDiscountBelowAdapterMinimum() public { - vm.warp(1000); - adapter.setMinDiscount(100_000); - IInputSettler.StandardOrder memory order = _order(10 ether, 9 ether); + function testFinaliseWithCurrentTimestampSupportsSameInputAndOutputToken() public { + MockLifiAdapter sameTokenAdapter = new MockLifiAdapter(rwa); + rwa.mint(address(sameTokenAdapter), 100 ether); + rwa.mint(address(executor), 3 ether); + IInputSettler.StandardOrder memory order = _order(10 ether, 9.5 ether, address(rwa)); _openOrder(order); - ILiquidLaneLifiExecutor.FillCall memory fillCall = - _fillCallStruct(address(adapter), _orderId(order), order.outputs[0]); - fillCall.routes[0] = _discountRoute(address(adapter), 10 ether, 9.5 ether, keccak256("discount"), 50_000); - - vm.expectRevert(abi.encodeWithSelector(ILiquidLaneLifiExecutor.InvalidDiscount.selector, 50_000, 100_000)); - executor.finaliseWithCurrentTimestamp(order, abi.encode(fillCall)); - } - function testFinaliseWithCurrentTimestampRejectsExpiredPrivateDiscount() public { - vm.warp(1000); - IInputSettler.StandardOrder memory order = _order(10 ether, 9 ether); - _openOrder(order); - ILiquidLaneLifiExecutor.FillCall memory fillCall = - _fillCallStruct(address(adapter), _orderId(order), order.outputs[0]); - fillCall.routes[0] = _discountRoute(address(adapter), 10 ether, 10 ether, keccak256("discount"), 0); - fillCall.routes[0].discount.discountSwap.discount.deadline = 999; + executor.finaliseWithCurrentTimestamp(order, _directRoutes(address(sameTokenAdapter), 10 ether, 10 ether)); - vm.expectRevert( - abi.encodeWithSelector(ILiquidLaneLifiExecutor.DiscountExpired.selector, uint48(999), uint48(1100), 1000) - ); - executor.finaliseWithCurrentTimestamp(order, abi.encode(fillCall)); + assertEq(rwa.balanceOf(recipient), 9.5 ether); + // Pre-existing 3 ether plus 0.5 ether fill surplus stay with the executor. + assertEq(rwa.balanceOf(address(executor)), 3.5 ether); + assertEq(rwa.balanceOf(address(sameTokenAdapter)), 100 ether); } - function testFinaliseWithCurrentTimestampRejectsPrivateDiscountTokenMismatch() public { + function testFinaliseWithCurrentTimestampFillsDutchOutputAtResolvedAmount() public { vm.warp(1000); IInputSettler.StandardOrder memory order = _order(10 ether, 9 ether); + order.outputs[0].context = _dutchContext(900, 1100, 0.01 ether); _openOrder(order); - ILiquidLaneLifiExecutor.FillCall memory fillCall = - _fillCallStruct(address(adapter), _orderId(order), order.outputs[0]); - fillCall.routes[0] = _discountRoute(address(adapter), 10 ether, 10 ether, keccak256("discount"), 0); - fillCall.routes[0].discount.discountSwap.discount.tokenToRedeem = makeAddr("wrongToken"); - - vm.expectRevert( - abi.encodeWithSelector( - ILiquidLaneLifiExecutor.DiscountTokenMismatch.selector, - address(rwa), - fillCall.routes[0].discount.discountSwap.discount.tokenToRedeem - ) - ); - executor.finaliseWithCurrentTimestamp(order, abi.encode(fillCall)); - } - - function testFinaliseWithCurrentTimestampAppliesAdapterMinDiscount() public { - adapter.setMinDiscount(100_000); - IInputSettler.StandardOrder memory order = _order(10 ether, 9 ether); - _openOrder(order); - ILiquidLaneLifiExecutor.FillCall memory fillCall = - _fillCallStruct(address(adapter), _orderId(order), order.outputs[0]); - - vm.expectRevert( - abi.encodeWithSelector( - ILiquidLaneLifiExecutor.RouteOutputTooLow.selector, address(adapter), 10 ether, 9 ether - ) - ); - executor.finaliseWithCurrentTimestamp(order, abi.encode(fillCall)); - } - function testFinaliseWithCurrentTimestampRejectsRouteInputMismatch() public { - IInputSettler.StandardOrder memory order = _order(10 ether, 9 ether); - _openOrder(order); - ILiquidLaneLifiExecutor.FillCall memory fillCall = - _fillCallStruct(address(adapter), _orderId(order), order.outputs[0]); - fillCall.routes[0].amountIn = 9 ether; - fillCall.routes[0].expectedAmountOut = 9 ether; - fillCall.routes[0].minAmountOut = 9 ether; + executor.finaliseWithCurrentTimestamp(order, _directRoutes(address(adapter), 10 ether, 10.5 ether)); - vm.expectRevert(abi.encodeWithSelector(ILiquidLaneLifiExecutor.RouteInputMismatch.selector, 9 ether, 10 ether)); - executor.finaliseWithCurrentTimestamp(order, abi.encode(fillCall)); + assertEq(outputToken.balanceOf(recipient), 10 ether); + assertEq(outputSettler.lastOutputAmount(), 10 ether); + assertEq(outputToken.balanceOf(address(executor)), 0.5 ether); } - function testFinaliseWithCurrentTimestampRejectsEmptyRoutes() public { + function testFinaliseWithCurrentTimestampFillsExclusiveDutchOutputAtResolvedAmount() public { + vm.warp(1000); IInputSettler.StandardOrder memory order = _order(10 ether, 9 ether); + order.outputs[0].context = _exclusiveDutchContext(_id(makeAddr("otherSolver")), 900, 1100, 0.01 ether); _openOrder(order); - ILiquidLaneLifiExecutor.FillCall memory fillCall = - _fillCallStruct(address(adapter), _orderId(order), order.outputs[0]); - fillCall.routes = new ILiquidLaneLifiExecutor.FillRoute[](0); - - vm.expectRevert(ILiquidLaneLifiExecutor.EmptyRoutes.selector); - executor.finaliseWithCurrentTimestamp(order, abi.encode(fillCall)); - } - - function testFinaliseWithCurrentTimestampRejectsFillDeadlineMismatch() public { - IInputSettler.StandardOrder memory order = _order(10 ether, 9 ether); - ILiquidLaneLifiExecutor.FillCall memory fillCall = - _fillCallStruct(address(adapter), _orderId(order), _output(9 ether)); - fillCall.fillDeadline = order.fillDeadline + 1; - - vm.expectRevert(ILiquidLaneLifiExecutor.InvalidOrderOutput.selector); - executor.finaliseWithCurrentTimestamp(order, abi.encode(fillCall)); - } - function testFinaliseWithCurrentTimestampRejectsFillAfterWithoutAuction() public { - vm.warp(1000); - IInputSettler.StandardOrder memory order = _order(10 ether, 9 ether); - ILiquidLaneLifiExecutor.FillCall memory fillCall = - _fillCallStruct(address(adapter), _orderId(order), order.outputs[0]); - fillCall.fillAfter = uint32(block.timestamp); + executor.finaliseWithCurrentTimestamp(order, _directRoutes(address(adapter), 10 ether, 10.5 ether)); - vm.expectRevert(ILiquidLaneLifiExecutor.FillAfterWithoutAuction.selector); - executor.finaliseWithCurrentTimestamp(order, abi.encode(fillCall)); + assertEq(outputToken.balanceOf(recipient), 10 ether); + assertEq(outputSettler.lastOutputAmount(), 10 ether); } - function testFinaliseWithCurrentTimestampRejectsAuctionFillTooEarly() public { + function testFinaliseWithCurrentTimestampFillsExclusiveOutputAfterStartTime() public { vm.warp(1000); IInputSettler.StandardOrder memory order = _order(10 ether, 9 ether); - order.outputs[0].context = _dutchContext(900, 1100, 0.01 ether); - bytes32 orderId = _orderId(order); - ILiquidLaneLifiExecutor.FillCall memory fillCall = _fillCallStruct(address(adapter), orderId, order.outputs[0]); - fillCall.fillAfter = uint32(block.timestamp + 1); - - vm.expectRevert(abi.encodeWithSelector(ILiquidLaneLifiExecutor.FillTooEarly.selector, 1001, 1000)); - executor.finaliseWithCurrentTimestamp(order, abi.encode(fillCall)); - } - - function testMockFinaliseRejectsStaleOrFutureTimestamp() public { - IInputSettler.StandardOrder memory order = _order(10 ether, 9 ether); - IInputSettler.SolveParams[] memory solveParams = new IInputSettler.SolveParams[](1); - solveParams[0].solver = _id(address(executor)); - - vm.warp(100); - solveParams[0].timestamp = 99; - vm.expectRevert(MockInputSettler.TimestampPassed.selector); - inputSettler.finalise(order, solveParams, _id(address(executor)), _fillCallData(9 ether)); + order.outputs[0].context = _exclusiveContext(_id(makeAddr("otherSolver")), 1000); + _openOrder(order); - solveParams[0].timestamp = 101; - vm.expectRevert(MockInputSettler.TimestampNotPassed.selector); - inputSettler.finalise(order, solveParams, _id(address(executor)), _fillCallData(9 ether)); - } + executor.finaliseWithCurrentTimestamp(order, _directRoutes(address(adapter), 10 ether, 10 ether)); - function testOrderFinalisedRejectsNonInputSettler() public { - vm.expectRevert(ILiquidLaneLifiExecutor.NotInputSettler.selector); - executor.orderFinalised(_inputs(10 ether), _fillCallData(9 ether)); + assertEq(outputToken.balanceOf(recipient), 9 ether); } - function testOrderFinalisedRejectsDepositedOrderStatusInCallback() public { - rwa.mint(address(executor), 10 ether); - - vm.expectRevert( - abi.encodeWithSelector(ILiquidLaneLifiExecutor.InvalidOrderStatus.selector, ORDER_STATUS_DEPOSITED) - ); - vm.prank(address(inputSettler)); - executor.orderFinalised(_inputs(10 ether), _fillCallData(9 ether)); - - assertEq(outputToken.balanceOf(recipient), 0); - assertFalse(outputSettler.attested()); - } + function testFinaliseWithCurrentTimestampMatchesRoutesToReceivedAmountWhenSettlerTakesFee() public { + inputSettler.setInputFee(1 ether); + IInputSettler.StandardOrder memory order = _order(10 ether, 8.5 ether); + _openOrder(order); - function testOrderFinalisedRejectsMultipleInputs() public { - uint256[2][] memory inputs = new uint256[2][](2); - inputs[0][0] = uint256(uint160(address(rwa))); - inputs[0][1] = 10 ether; - inputs[1][0] = uint256(uint160(address(rwa))); - inputs[1][1] = 1 ether; + executor.finaliseWithCurrentTimestamp(order, _directRoutes(address(adapter), 9 ether, 9 ether)); - vm.expectRevert(ILiquidLaneLifiExecutor.InvalidInputCount.selector); - vm.prank(address(inputSettler)); - executor.orderFinalised(inputs, _fillCallData(9 ether)); + assertEq(rwa.balanceOf(address(adapter)), 9 ether); + assertEq(rwa.balanceOf(address(inputSettler)), 1 ether); + assertEq(outputToken.balanceOf(recipient), 8.5 ether); + assertEq(outputToken.balanceOf(address(executor)), 0.5 ether); } - function testOrderFinalisedExecutesArbitraryAdapter() public { - MockLifiAdapter otherAdapter = new MockLifiAdapter(outputToken); - outputToken.mint(address(otherAdapter), 10 ether); - inputSettler.setOrderStatus(ORDER_ID, ORDER_STATUS_CLAIMED); - rwa.mint(address(executor), 10 ether); + function testFinaliseWithCurrentTimestampPreservesExistingInputBalance() public { + rwa.mint(address(executor), 5 ether); + IInputSettler.StandardOrder memory order = _order(10 ether, 9 ether); + _openOrder(order); - vm.prank(address(inputSettler)); - executor.orderFinalised(_inputs(10 ether), _fillCallData(address(otherAdapter), 9 ether)); + executor.finaliseWithCurrentTimestamp(order, _directRoutes(address(adapter), 10 ether, 10 ether)); - assertEq(rwa.balanceOf(address(otherAdapter)), 10 ether); + assertEq(rwa.balanceOf(address(executor)), 5 ether); assertEq(outputToken.balanceOf(recipient), 9 ether); - assertEq(outputToken.balanceOf(address(executor)), 1 ether); } - function testOrderFinalisedKeepsOutputSeparateWhenExecutorIsRecipient() public { - inputSettler.setOrderStatus(ORDER_ID, ORDER_STATUS_CLAIMED); - rwa.mint(address(executor), 10 ether); - MandateOutput memory output = _output(9 ether); - output.recipient = _id(address(executor)); + function testFinaliseWithCurrentTimestampKeepsOutputWhenExecutorIsRecipient() public { + IInputSettler.StandardOrder memory order = _order(10 ether, 9 ether); + order.outputs[0].recipient = _id(address(executor)); + _openOrder(order); - vm.prank(address(inputSettler)); - executor.orderFinalised(_inputs(10 ether), _fillCallData(output)); + executor.finaliseWithCurrentTimestamp(order, _directRoutes(address(adapter), 10 ether, 10 ether)); assertEq(outputToken.balanceOf(address(executor)), 10 ether); } - function testOrderFinalisedKeepsCallbackRefundSeparateFromSurplusAccounting() public { + function testFinaliseWithCurrentTimestampKeepsCallbackRefundSeparateFromSurplus() public { RefundingOutputRecipient outputRecipient = new RefundingOutputRecipient(outputToken, address(executor), 1 ether); - inputSettler.setOrderStatus(ORDER_ID, ORDER_STATUS_CLAIMED); - rwa.mint(address(executor), 10 ether); - MandateOutput memory output = _output(9 ether); - output.recipient = _id(address(outputRecipient)); - output.callbackData = hex"01"; + IInputSettler.StandardOrder memory order = _order(10 ether, 9 ether); + order.outputs[0].recipient = _id(address(outputRecipient)); + order.outputs[0].callbackData = hex"01"; + _openOrder(order); - vm.prank(address(inputSettler)); - executor.orderFinalised(_inputs(10 ether), _fillCallData(output)); + executor.finaliseWithCurrentTimestamp(order, _directRoutes(address(adapter), 10 ether, 10 ether)); assertEq(outputToken.balanceOf(address(outputRecipient)), 8 ether); assertEq(outputToken.balanceOf(address(executor)), 2 ether); } - function testOrderFinalisedRejectsFillAfterWithoutAuction() public { - vm.warp(1000); - inputSettler.setOrderStatus(ORDER_ID, ORDER_STATUS_CLAIMED); - ILiquidLaneLifiExecutor.FillCall memory fillCall = _fillCallStruct(address(adapter), _output(9 ether)); - fillCall.fillAfter = uint32(block.timestamp); - - vm.expectRevert(ILiquidLaneLifiExecutor.FillAfterWithoutAuction.selector); - vm.prank(address(inputSettler)); - executor.orderFinalised(_inputs(10 ether), abi.encode(fillCall)); - } + /* FINALISE DELEGATED REVERTS */ - function testOrderFinalisedRejectsFillAfterForExclusiveLimitOutput() public { + function testFinaliseWithCurrentTimestampBubblesExclusivityBeforeStartTime() public { vm.warp(1000); - inputSettler.setOrderStatus(ORDER_ID, ORDER_STATUS_CLAIMED); - MandateOutput memory output = - _output(9 ether, _exclusiveContext(_id(address(executor)), uint32(block.timestamp))); - ILiquidLaneLifiExecutor.FillCall memory fillCall = _fillCallStruct(address(adapter), output); - fillCall.fillAfter = uint32(block.timestamp); + IInputSettler.StandardOrder memory order = _order(10 ether, 9 ether); + order.outputs[0].context = _exclusiveContext(_id(makeAddr("otherSolver")), 1001); + _openOrder(order); - vm.expectRevert(ILiquidLaneLifiExecutor.FillAfterWithoutAuction.selector); - vm.prank(address(inputSettler)); - executor.orderFinalised(_inputs(10 ether), abi.encode(fillCall)); + vm.expectRevert(bytes("exclusive")); + executor.finaliseWithCurrentTimestamp(order, _directRoutes(address(adapter), 10 ether, 10 ether)); } - function testOrderFinalisedRejectsAuctionFillTooEarly() public { + function testFinaliseWithCurrentTimestampBubblesInsufficientOutputForResolvedDutchAmount() public { vm.warp(1000); - inputSettler.setOrderStatus(ORDER_ID, ORDER_STATUS_CLAIMED); - ILiquidLaneLifiExecutor.FillCall memory fillCall = - _fillCallStruct(address(adapter), _output(9 ether, _dutchContext(900, 1100, 0.01 ether))); - fillCall.fillAfter = uint32(block.timestamp + 1); - - vm.expectRevert(abi.encodeWithSelector(ILiquidLaneLifiExecutor.FillTooEarly.selector, 1001, 1000)); - vm.prank(address(inputSettler)); - executor.orderFinalised(_inputs(10 ether), abi.encode(fillCall)); - } - - function testOrderFinalisedRejectsUnderDelivery() public { - inputSettler.setOrderStatus(ORDER_ID, ORDER_STATUS_CLAIMED); - adapter.setNextOutputAmount(8 ether); - rwa.mint(address(executor), 10 ether); + IInputSettler.StandardOrder memory order = _order(10 ether, 9 ether); + order.outputs[0].context = _dutchContext(900, 1100, 0.01 ether); + _openOrder(order); vm.expectRevert( abi.encodeWithSelector( - ILiquidLaneLifiExecutor.RouteOutputTooLow.selector, address(adapter), 10 ether, 8 ether + IERC20Errors.ERC20InsufficientBalance.selector, address(executor), 9.5 ether, 10 ether ) ); - vm.prank(address(inputSettler)); - executor.orderFinalised(_inputs(10 ether), _fillCallData(9 ether)); + executor.finaliseWithCurrentTimestamp(order, _directRoutes(address(adapter), 10 ether, 9.5 ether)); } - function testOrderFinalisedClampsDirectOutputToLiveCapacityAboveMinimum() public { - inputSettler.setOrderStatus(ORDER_ID, ORDER_STATUS_CLAIMED); - adapter.setMaxAssets(9.5 ether); - rwa.mint(address(executor), 10 ether); - - ILiquidLaneLifiExecutor.FillCall memory fillCall = _fillCallStruct(address(adapter), _output(9 ether)); - fillCall.routes[0].minAmountOut = 9.25 ether; - - vm.prank(address(inputSettler)); - executor.orderFinalised(_inputs(10 ether), abi.encode(fillCall)); + function testFinaliseWithCurrentTimestampBubblesAlreadyClaimedOrder() public { + IInputSettler.StandardOrder memory order = _order(10 ether, 9 ether); + inputSettler.setOrderStatus(_orderId(order), ORDER_STATUS_CLAIMED); - assertEq(outputToken.balanceOf(recipient), 9 ether); - assertEq(outputToken.balanceOf(address(executor)), 0.5 ether); + vm.expectRevert(MockInputSettler.InvalidOrderStatus.selector); + executor.finaliseWithCurrentTimestamp(order, _directRoutes(address(adapter), 10 ether, 10 ether)); } - function testOrderFinalisedRejectsPrivateOutputAboveLiveCapacity() public { - vm.warp(1000); - inputSettler.setOrderStatus(ORDER_ID, ORDER_STATUS_CLAIMED); - adapter.setMaxAssets(8.5 ether); - rwa.mint(address(executor), 10 ether); - - ILiquidLaneLifiExecutor.FillCall memory fillCall = _fillCallStruct(address(adapter), _output(8 ether)); - fillCall.routes[0] = _discountRoute(address(adapter), 10 ether, 9 ether, keccak256("discount"), 100_000); + function testFinaliseWithCurrentTimestampBubblesExpiredFillDeadline() public { + IInputSettler.StandardOrder memory order = _order(10 ether, 9 ether); + order.fillDeadline = uint32(block.timestamp - 1); + _openOrder(order); - vm.expectRevert( - abi.encodeWithSelector( - ILiquidLaneLifiExecutor.PrivateRouteExceedsCapacity.selector, address(adapter), 9 ether, 8.5 ether - ) - ); - vm.prank(address(inputSettler)); - executor.orderFinalised(_inputs(10 ether), abi.encode(fillCall)); + vm.expectRevert(bytes("deadline")); + executor.finaliseWithCurrentTimestamp(order, _directRoutes(address(adapter), 10 ether, 10 ether)); } - function testOrderFinalisedDutchOutputUsesResolvedAmount() public { - vm.warp(1000); - inputSettler.setOrderStatus(ORDER_ID, ORDER_STATUS_CLAIMED); - rwa.mint(address(executor), 10 ether); - - MandateOutput memory output = _output(9 ether, _dutchContext(900, 1100, 0.01 ether)); + /* CALLER AUTHORIZATION */ - vm.prank(address(inputSettler)); - executor.orderFinalised(_inputs(10 ether), _fillCallData(output)); + function testFinaliseWithCurrentTimestampRejectsUnauthorizedCaller() public { + address caller = makeAddr("caller"); + IInputSettler.StandardOrder memory order = _order(10 ether, 9 ether); - assertEq(outputToken.balanceOf(recipient), 10 ether); - assertEq(outputSettler.lastOutputAmount(), 10 ether); + vm.expectRevert(ILiquidLaneLifiExecutor.NotCaller.selector); + vm.prank(caller); + executor.finaliseWithCurrentTimestamp(order, _directRoutes(address(adapter), 10 ether, 10 ether)); } - function testOrderFinalisedExclusiveDutchOutputUsesResolvedAmount() public { - vm.warp(1000); - inputSettler.setOrderStatus(ORDER_ID, ORDER_STATUS_CLAIMED); - rwa.mint(address(executor), 10 ether); + function testSetCallersAllowsNonOwnerToFinaliseAndRevokesOldCaller() public { + address caller = makeAddr("caller"); + executor.setCallers(_callers(caller)); - MandateOutput memory output = - _output(9 ether, _exclusiveDutchContext(_id(makeAddr("otherSolver")), 900, 1100, 0.01 ether)); + assertTrue(executor.isCaller(caller)); + assertFalse(executor.isCaller(owner)); - vm.prank(address(inputSettler)); - executor.orderFinalised(_inputs(10 ether), _fillCallData(output)); + IInputSettler.StandardOrder memory order = _order(10 ether, 9 ether); + _openOrder(order); - assertEq(outputToken.balanceOf(recipient), 10 ether); - assertEq(outputSettler.lastOutputAmount(), 10 ether); - } + vm.prank(caller); + executor.finaliseWithCurrentTimestamp(order, _directRoutes(address(adapter), 10 ether, 10 ether)); + assertEq(outputToken.balanceOf(recipient), 9 ether); - function testOrderFinalisedRejectsDutchUnderDeliveryAgainstResolvedAmount() public { - vm.warp(1000); - inputSettler.setOrderStatus(ORDER_ID, ORDER_STATUS_CLAIMED); - adapter.setNextOutputAmount(9.5 ether); - rwa.mint(address(executor), 10 ether); + IInputSettler.StandardOrder memory secondOrder = _order(10 ether, 8 ether); + vm.expectRevert(ILiquidLaneLifiExecutor.NotCaller.selector); + executor.finaliseWithCurrentTimestamp(secondOrder, _directRoutes(address(adapter), 10 ether, 10 ether)); + } - MandateOutput memory output = _output(9 ether, _dutchContext(900, 1100, 0.01 ether)); + function testSetCallersRejectsNonOwner() public { + address caller = makeAddr("caller"); - vm.expectRevert( - abi.encodeWithSelector( - ILiquidLaneLifiExecutor.RouteOutputTooLow.selector, address(adapter), 10 ether, 9.5 ether - ) - ); - vm.prank(address(inputSettler)); - executor.orderFinalised(_inputs(10 ether), _fillCallData(output)); + vm.expectRevert(abi.encodeWithSignature("OwnableUnauthorizedAccount(address)", caller)); + vm.prank(caller); + executor.setCallers(_callers(caller)); } - function testOrderFinalisedRejectsExclusiveSolverMismatchBeforeStart() public { - vm.warp(1000); - inputSettler.setOrderStatus(ORDER_ID, ORDER_STATUS_CLAIMED); - - bytes32 exclusiveFor = _id(makeAddr("otherSolver")); - MandateOutput memory output = _output(9 ether, _exclusiveContext(exclusiveFor, 1001)); + /* CALLBACK AUTHENTICATION */ - vm.expectRevert( - abi.encodeWithSelector( - ILiquidLaneLifiExecutor.ExclusiveForMismatch.selector, exclusiveFor, _id(address(executor)) - ) - ); - vm.prank(address(inputSettler)); - executor.orderFinalised(_inputs(10 ether), _fillCallData(output)); + function testOrderFinalisedRejectsNonInputSettler() public { + vm.expectRevert(ILiquidLaneLifiExecutor.NotInputSettler.selector); + executor.orderFinalised(_inputs(10 ether), abi.encode(_unsolicitedFillCall())); } - function testOrderFinalisedAllowsExclusiveOutputAfterStart() public { - vm.warp(1000); - inputSettler.setOrderStatus(ORDER_ID, ORDER_STATUS_CLAIMED); - rwa.mint(address(executor), 10 ether); + /* EIP-1271 REGISTRATION */ - MandateOutput memory output = _output(9 ether, _exclusiveContext(_id(makeAddr("otherSolver")), 1000)); + function testIsValidSignatureAcceptsCallerWithRegistrationDomain() public { + uint256 callerKey = 0xA11CE; + LiquidLaneLifiExecutor callerExecutor = + _deployExecutor(address(inputSettler), address(outputSettler), owner, _callers(vm.addr(callerKey))); + bytes32 messageHash = keccak256("lifi registration"); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(callerKey, callerExecutor.lifiRegistrationDigest(messageHash)); - vm.prank(address(inputSettler)); - executor.orderFinalised(_inputs(10 ether), _fillCallData(output)); - - assertEq(outputToken.balanceOf(recipient), 9 ether); + assertEq( + callerExecutor.isValidSignature(messageHash, abi.encodePacked(r, s, v)), IERC1271.isValidSignature.selector + ); } - function testOrderFinalisedRejectsBadContextLength() public { - inputSettler.setOrderStatus(ORDER_ID, ORDER_STATUS_CLAIMED); - MandateOutput memory output = _output(9 ether, hex"0000"); + function testIsValidSignatureAcceptsAnyCaller() public { + uint256 firstCallerKey = 0xA11CE; + uint256 secondCallerKey = 0xB0B; + address[] memory allowedCallers = new address[](2); + allowedCallers[0] = vm.addr(firstCallerKey); + allowedCallers[1] = vm.addr(secondCallerKey); + LiquidLaneLifiExecutor callerExecutor = + _deployExecutor(address(inputSettler), address(outputSettler), owner, allowedCallers); + bytes32 messageHash = keccak256("lifi registration"); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(secondCallerKey, callerExecutor.lifiRegistrationDigest(messageHash)); - vm.expectRevert(abi.encodeWithSelector(ILiquidLaneLifiExecutor.InvalidOutputContextLength.selector, 0, 2)); - vm.prank(address(inputSettler)); - executor.orderFinalised(_inputs(10 ether), _fillCallData(output)); + assertEq( + callerExecutor.isValidSignature(messageHash, abi.encodePacked(r, s, v)), IERC1271.isValidSignature.selector + ); } - function testOrderFinalisedRejectsUnknownContextType() public { - inputSettler.setOrderStatus(ORDER_ID, ORDER_STATUS_CLAIMED); - MandateOutput memory output = _output(9 ether, hex"02"); + function testIsValidSignatureRejectsOwnerWhenNotCaller() public { + uint256 ownerKey = 0xA11CE; + LiquidLaneLifiExecutor callerExecutor = + _deployExecutor(address(inputSettler), address(outputSettler), vm.addr(ownerKey), _callers(vm.addr(0xB0B))); + bytes32 messageHash = keccak256("lifi registration"); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerKey, callerExecutor.lifiRegistrationDigest(messageHash)); - vm.expectRevert(abi.encodeWithSelector(ILiquidLaneLifiExecutor.UnknownOutputContext.selector, bytes1(0x02))); - vm.prank(address(inputSettler)); - executor.orderFinalised(_inputs(10 ether), _fillCallData(output)); + assertEq(callerExecutor.isValidSignature(messageHash, abi.encodePacked(r, s, v)), bytes4(0xffffffff)); } - function testOrderFinalisedRejectsZeroInputAmount() public { - inputSettler.setOrderStatus(ORDER_ID, ORDER_STATUS_CLAIMED); + function testIsValidSignatureRejectsRawMessageHashSignature() public { + uint256 callerKey = 0xA11CE; + LiquidLaneLifiExecutor callerExecutor = + _deployExecutor(address(inputSettler), address(outputSettler), owner, _callers(vm.addr(callerKey))); + bytes32 messageHash = keccak256("lifi registration"); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(callerKey, messageHash); - vm.expectRevert(ILiquidLaneLifiExecutor.InvalidAmount.selector); - vm.prank(address(inputSettler)); - executor.orderFinalised(_inputs(0), _fillCallData(9 ether)); + assertEq(callerExecutor.isValidSignature(messageHash, abi.encodePacked(r, s, v)), bytes4(0xffffffff)); } - function testOrderFinalisedRejectsWrongOutputSettlerIdentifier() public { - bytes memory call = _fillCallData(_output(9 ether, makeAddr("wrongSettler"), address(outputSettler))); + function testIsValidSignatureRejectsSignatureForAnotherExecutor() public { + uint256 callerKey = 0xA11CE; + address caller = vm.addr(callerKey); + LiquidLaneLifiExecutor firstExecutor = + _deployExecutor(address(inputSettler), address(outputSettler), owner, _callers(caller)); + LiquidLaneLifiExecutor secondExecutor = + _deployExecutor(address(inputSettler), address(outputSettler), owner, _callers(caller)); + bytes32 messageHash = keccak256("lifi registration"); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(callerKey, firstExecutor.lifiRegistrationDigest(messageHash)); - vm.expectRevert(ILiquidLaneLifiExecutor.InvalidOutputSettler.selector); - vm.prank(address(inputSettler)); - executor.orderFinalised(_inputs(10 ether), call); + assertEq(secondExecutor.isValidSignature(messageHash, abi.encodePacked(r, s, v)), bytes4(0xffffffff)); } - function testOrderFinalisedRejectsWrongOutputOracle() public { - bytes memory call = _fillCallData(_output(9 ether, address(outputSettler), makeAddr("wrongOracle"))); - - vm.expectRevert(ILiquidLaneLifiExecutor.InvalidOutputOracle.selector); - vm.prank(address(inputSettler)); - executor.orderFinalised(_inputs(10 ether), call); - } + function testIsValidSignatureRejectsRemovedCaller() public { + uint256 callerKey = 0xA11CE; + LiquidLaneLifiExecutor callerExecutor = + _deployExecutor(address(inputSettler), address(outputSettler), owner, _callers(vm.addr(callerKey))); + bytes32 messageHash = keccak256("lifi registration"); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(callerKey, callerExecutor.lifiRegistrationDigest(messageHash)); - function testOrderFinalisedRejectsWrongOutputChain() public { - MandateOutput memory output = _output(9 ether); - output.chainId = block.chainid + 1; + callerExecutor.setCallers(new address[](0)); - vm.expectRevert(ILiquidLaneLifiExecutor.InvalidOutputChain.selector); - vm.prank(address(inputSettler)); - executor.orderFinalised(_inputs(10 ether), _fillCallData(output)); + assertEq(callerExecutor.isValidSignature(messageHash, abi.encodePacked(r, s, v)), bytes4(0xffffffff)); } - function testOrderFinalisedRejectsNativeOutput() public { - MandateOutput memory output = _output(9 ether); - output.token = bytes32(0); - - vm.expectRevert(ILiquidLaneLifiExecutor.NativeOutputUnsupported.selector); - vm.prank(address(inputSettler)); - executor.orderFinalised(_inputs(10 ether), _fillCallData(output)); + function testIsValidSignatureRejectsMalformedSignature() public { + assertEq(executor.isValidSignature(keccak256("lifi registration"), hex"deadbeef"), bytes4(0xffffffff)); } - function testOrderFinalisedRejectsDirtyOutputIdentifier() public { - MandateOutput memory output = _output(9 ether); - output.token = bytes32(uint256(uint160(address(outputToken))) | (uint256(1) << 160)); + /* UPGRADEABILITY */ - vm.expectRevert(ILiquidLaneLifiExecutor.InvalidIdentifier.selector); - vm.prank(address(inputSettler)); - executor.orderFinalised(_inputs(10 ether), _fillCallData(output)); + function testInitializeSetsOwnerAndCallers() public view { + assertEq(executor.owner(), owner); + assertEq(executor.callers(0), owner); + assertTrue(executor.isCaller(owner)); } - function testOrderFinalisedSupportsSameInputAndOutputTokenAccounting() public { - inputSettler.setOrderStatus(ORDER_ID, ORDER_STATUS_CLAIMED); - MockLifiAdapter sameTokenAdapter = new MockLifiAdapter(rwa); - - rwa.mint(address(executor), 10 ether); - rwa.mint(address(sameTokenAdapter), 10 ether); - - vm.prank(address(inputSettler)); - executor.orderFinalised(_inputs(10 ether), _fillCallData(address(sameTokenAdapter), address(rwa), 10 ether)); - - assertEq(rwa.balanceOf(address(executor)), 0); - assertEq(rwa.balanceOf(address(sameTokenAdapter)), 10 ether); - assertEq(rwa.balanceOf(recipient), 10 ether); + function testInitializeCannotBeCalledTwice() public { + vm.expectRevert(Initializable.InvalidInitialization.selector); + executor.initialize(makeAddr("intruder"), _callers(makeAddr("intruder"))); } - function testOrderFinalisedExecutesMultipleRoutesAndKeepsSurplus() public { - MockLifiAdapter secondAdapter = new MockLifiAdapter(outputToken); - outputToken.mint(address(secondAdapter), 100 ether); + function testImplementationInitializerIsDisabled() public { + LiquidLaneLifiExecutor impl = new LiquidLaneLifiExecutor(address(inputSettler), address(outputSettler)); - inputSettler.setOrderStatus(ORDER_ID, ORDER_STATUS_CLAIMED); - rwa.mint(address(executor), 10 ether); - - ILiquidLaneLifiExecutor.FillCall memory fillCall = _fillCallStruct(address(adapter), _output(9 ether)); - fillCall.routes = new ILiquidLaneLifiExecutor.FillRoute[](2); - fillCall.routes[0] = _directRoute(address(adapter), 4 ether, 4 ether); - fillCall.routes[1] = _directRoute(address(secondAdapter), 6 ether, 6 ether); - - vm.prank(address(inputSettler)); - executor.orderFinalised(_inputs(10 ether), abi.encode(fillCall)); - - assertEq(rwa.balanceOf(address(adapter)), 4 ether); - assertEq(rwa.balanceOf(address(secondAdapter)), 6 ether); - assertEq(outputToken.balanceOf(recipient), 9 ether); - assertEq(outputToken.balanceOf(address(executor)), 1 ether); + vm.expectRevert(Initializable.InvalidInitialization.selector); + impl.initialize(makeAddr("intruder"), _callers(makeAddr("intruder"))); } - function testOrderFinalisedRejectsActualRouteUnderDeliveryEvenWhenAggregatePasses() public { - MockLifiAdapter secondAdapter = new MockLifiAdapter(outputToken); - outputToken.mint(address(secondAdapter), 100 ether); - adapter.setNextOutputAmount(3 ether); - secondAdapter.setBonus(1 ether); + /* MOCK SELF-TESTS */ - inputSettler.setOrderStatus(ORDER_ID, ORDER_STATUS_CLAIMED); - rwa.mint(address(executor), 10 ether); + function testMockFinaliseRejectsStaleOrFutureTimestamp() public { + IInputSettler.StandardOrder memory order = _order(10 ether, 9 ether); + IInputSettler.SolveParams[] memory solveParams = new IInputSettler.SolveParams[](1); + solveParams[0].solver = _id(address(executor)); - ILiquidLaneLifiExecutor.FillCall memory fillCall = _fillCallStruct(address(adapter), _output(9 ether)); - fillCall.routes = new ILiquidLaneLifiExecutor.FillRoute[](2); - fillCall.routes[0] = _directRoute(address(adapter), 4 ether, 4 ether); - fillCall.routes[1] = _directRoute(address(secondAdapter), 6 ether, 6 ether); + vm.warp(100); + solveParams[0].timestamp = 99; + vm.expectRevert(MockInputSettler.TimestampPassed.selector); + inputSettler.finalise(order, solveParams, _id(address(executor)), hex""); - vm.expectRevert( - abi.encodeWithSelector( - ILiquidLaneLifiExecutor.RouteOutputTooLow.selector, address(adapter), 4 ether, 3 ether - ) - ); - vm.prank(address(inputSettler)); - executor.orderFinalised(_inputs(10 ether), abi.encode(fillCall)); + solveParams[0].timestamp = 101; + vm.expectRevert(MockInputSettler.TimestampNotPassed.selector); + inputSettler.finalise(order, solveParams, _id(address(executor)), hex""); } function testMockOutputSettlerFillIsIdempotent() public { @@ -779,22 +460,19 @@ contract LiquidLaneLifiExecutorTest is Test { assertEq(outputRecipient.callbackData(), hex"1234"); } - function testOwnerSweepsFillSurplus() public { - inputSettler.setOrderStatus(ORDER_ID, ORDER_STATUS_CLAIMED); - rwa.mint(address(executor), 10 ether); - vm.prank(address(inputSettler)); - executor.orderFinalised(_inputs(10 ether), _fillCallData(9 ether)); - - vm.prank(owner); - executor.sweepERC20(address(outputToken), collector, 1 ether); + /* HELPERS */ - assertEq(outputToken.balanceOf(collector), 1 ether); - assertEq(outputToken.balanceOf(address(executor)), 0); + function _order(uint256 amountIn, uint256 amountOut) internal view returns (IInputSettler.StandardOrder memory) { + return _order(amountIn, amountOut, address(outputToken)); } - function _order(uint256 amountIn, uint256 amountOut) internal view returns (IInputSettler.StandardOrder memory) { + function _order(uint256 amountIn, uint256 amountOut, address tokenOut) + internal + view + returns (IInputSettler.StandardOrder memory) + { MandateOutput[] memory outputs = new MandateOutput[](1); - outputs[0] = _output(amountOut); + outputs[0] = _output(amountOut, tokenOut); return IInputSettler.StandardOrder({ user: address(0xA11CE), @@ -814,85 +492,40 @@ contract LiquidLaneLifiExecutorTest is Test { inputs[0][1] = amount; } - function _openOrder(IInputSettler.StandardOrder memory order) internal { - inputSettler.setOrderStatus(_orderId(order), ORDER_STATUS_DEPOSITED); + function _openOrder(IInputSettler.StandardOrder memory order) internal returns (bytes32 orderId) { + orderId = _orderId(order); + inputSettler.setOrderStatus(orderId, ORDER_STATUS_DEPOSITED); rwa.mint(address(inputSettler), order.inputs[0][1]); } - function _fillCallData(uint256 amountOut) internal view returns (bytes memory) { - return abi.encode(_fillCallStruct(address(adapter), _output(amountOut))); - } - - function _fillCallData(address fillAdapter, uint256 amountOut) internal view returns (bytes memory) { - return abi.encode(_fillCallStruct(fillAdapter, _output(amountOut))); - } - - function _fillCallData(address fillAdapter, address tokenOut, uint256 amountOut) - internal - view - returns (bytes memory) - { - return abi.encode(_fillCallStruct(fillAdapter, _output(amountOut, tokenOut))); - } - - function _fillCallData(MandateOutput memory output) internal view returns (bytes memory) { - return abi.encode(_fillCallStruct(address(adapter), output)); - } - - function _fillCallData(bytes32 orderId, uint256 amountOut) internal view returns (bytes memory) { - return abi.encode(_fillCallStruct(address(adapter), orderId, _output(amountOut))); - } - - function _fillCallStruct(address fillAdapter, MandateOutput memory output) + function _directRoutes(address fillAdapter, uint256 amountIn, uint256 amountOut) internal - view - returns (ILiquidLaneLifiExecutor.FillCall memory) + pure + returns (ILiquidLaneLifiExecutor.FillRoute[] memory routes) { - return _fillCallStruct(fillAdapter, ORDER_ID, output); + routes = new ILiquidLaneLifiExecutor.FillRoute[](1); + routes[0] = _directRoute(fillAdapter, amountIn, amountOut); } - function _fillCallStruct(address fillAdapter, bytes32 orderId, MandateOutput memory output) - internal - view - returns (ILiquidLaneLifiExecutor.FillCall memory) - { - ILiquidLaneLifiExecutor.FillRoute[] memory routes = new ILiquidLaneLifiExecutor.FillRoute[](1); - routes[0] = _directRoute(fillAdapter, 10 ether, 10 ether); - return ILiquidLaneLifiExecutor.FillCall({ - orderId: orderId, - output: output, - fillDeadline: uint32(block.timestamp + 1 hours), - fillAfter: 0, - routes: routes - }); - } - - function _directRoute(address fillAdapter, uint256 amountIn, uint256 expectedAmountOut) + function _directRoute(address fillAdapter, uint256 amountIn, uint256 amountOut) internal pure returns (ILiquidLaneLifiExecutor.FillRoute memory) { return ILiquidLaneLifiExecutor.FillRoute({ - adapter: fillAdapter, - amountIn: amountIn, - expectedAmountOut: expectedAmountOut, - minAmountOut: expectedAmountOut, - discount: _emptyDiscount() + adapter: fillAdapter, amountIn: amountIn, amountOut: amountOut, discount: _emptyDiscount() }); } - function _discountRoute( - address fillAdapter, - uint256 amountIn, - uint256 expectedAmountOut, - bytes32 discountId, - uint256 discount - ) internal view returns (ILiquidLaneLifiExecutor.FillRoute memory) { + function _discountRoute(address fillAdapter, uint256 amountIn, bytes32 discountId, uint256 discount) + internal + view + returns (ILiquidLaneLifiExecutor.FillRoute memory) + { return ILiquidLaneLifiExecutor.FillRoute({ adapter: fillAdapter, amountIn: amountIn, - expectedAmountOut: expectedAmountOut, - minAmountOut: expectedAmountOut, + amountOut: 0, discount: ILiquidLaneLifiExecutor.FillDiscount({ discountId: discountId, discountSwap: ILiquidLaneAdapter.DiscountSwap({ @@ -931,31 +564,23 @@ contract LiquidLaneLifiExecutorTest is Test { }); } + function _unsolicitedFillCall() internal view returns (ILiquidLaneLifiExecutor.FillCall memory) { + return ILiquidLaneLifiExecutor.FillCall({ + orderId: ORDER_ID, + output: _output(9 ether), + fillDeadline: uint32(block.timestamp + 1 hours), + routes: _directRoutes(address(adapter), 10 ether, 10 ether) + }); + } + function _output(uint256 amount) internal view returns (MandateOutput memory) { return _output(amount, address(outputToken)); } function _output(uint256 amount, address token) internal view returns (MandateOutput memory) { - return _output(amount, token, address(outputSettler), address(outputSettler)); - } - - function _output(uint256 amount, bytes memory context) internal view returns (MandateOutput memory output) { - output = _output(amount); - output.context = context; - } - - function _output(uint256 amount, address settler, address oracle) internal view returns (MandateOutput memory) { - return _output(amount, address(outputToken), settler, oracle); - } - - function _output(uint256 amount, address token, address settler, address oracle) - internal - view - returns (MandateOutput memory) - { return MandateOutput({ - oracle: _id(oracle), - settler: _id(settler), + oracle: _id(address(outputSettler)), + settler: _id(address(outputSettler)), chainId: block.chainid, token: _id(token), amount: amount, @@ -969,11 +594,6 @@ contract LiquidLaneLifiExecutorTest is Test { return bytes32(uint256(uint160(addr))); } - function _callers(address caller) internal pure returns (address[] memory result) { - result = new address[](1); - result[0] = caller; - } - function _dutchContext(uint32 startTime, uint32 stopTime, uint256 slope) internal pure returns (bytes memory) { return abi.encodePacked(bytes1(0x01), startTime, stopTime, slope); } @@ -1035,11 +655,13 @@ contract MockInputSettler is IInputSettler { uint8 internal constant ORDER_STATUS_DEPOSITED = 1; uint8 internal constant ORDER_STATUS_CLAIMED = 2; + error InvalidOrderStatus(); error InvalidTimestampLength(); error TimestampNotPassed(); error TimestampPassed(); mapping(bytes32 orderId => uint8 status) public orderStatus; + uint256 public inputFee; uint32 public lastTimestamp; bytes32 public lastSolver; bytes32 public lastDestination; @@ -1048,6 +670,10 @@ contract MockInputSettler is IInputSettler { orderStatus[orderId] = status; } + function setInputFee(uint256 fee) public { + inputFee = fee; + } + function orderIdentifier(StandardOrder calldata order) external pure returns (bytes32 orderId) { return _orderId(order); } @@ -1062,25 +688,21 @@ contract MockInputSettler is IInputSettler { if (solveParams[0].timestamp < block.timestamp) revert TimestampPassed(); if (solveParams[0].timestamp > block.timestamp) revert TimestampNotPassed(); + bytes32 orderId = _orderId(order); + if (orderStatus[orderId] != ORDER_STATUS_DEPOSITED) revert InvalidOrderStatus(); + lastTimestamp = solveParams[0].timestamp; lastSolver = solveParams[0].solver; lastDestination = destination; - _finalise(_orderId(order), address(uint160(uint256(destination))), order.inputs, call); - } - - function finaliseCallback(address destination, uint256[2][] memory inputs, bytes memory call) public { - ILiquidLaneLifiExecutor.FillCall memory fillCall = abi.decode(call, (ILiquidLaneLifiExecutor.FillCall)); - _finalise(fillCall.orderId, destination, inputs, call); - } - - function _finalise(bytes32 orderId, address destination, uint256[2][] memory inputs, bytes memory call) internal { - for (uint256 i; i < inputs.length; ++i) { - IERC20(address(uint160(inputs[i][0]))).safeTransfer(destination, inputs[i][1]); + address destinationAddress = address(uint160(uint256(destination))); + // The claimed input amounts are forwarded verbatim even when a fee is retained. + for (uint256 i; i < order.inputs.length; ++i) { + IERC20(address(uint160(order.inputs[i][0]))).safeTransfer(destinationAddress, order.inputs[i][1] - inputFee); } orderStatus[orderId] = ORDER_STATUS_CLAIMED; - IInputCallback(destination).orderFinalised(inputs, call); + IInputCallback(destinationAddress).orderFinalised(order.inputs, call); ILiquidLaneLifiExecutor.FillCall memory fillCall = abi.decode(call, (ILiquidLaneLifiExecutor.FillCall)); require(fillCall.orderId == orderId, "order mismatch"); @@ -1263,49 +885,14 @@ contract MockOutputSettler is IOutputSettler { contract MockLifiAdapter is ILiquidLaneAdapter { TestToken public immutable outputToken; - uint256 public bonus; - uint256 public discount; - uint256 public nextOutputAmount; - uint256 public maxAssets = type(uint256).max; constructor(TestToken outputToken_) { outputToken = outputToken_; } - function setBonus(uint256 bonus_) public { - bonus = bonus_; - } - - function setNextOutputAmount(uint256 amount) public { - nextOutputAmount = amount; - } - - function setMinDiscount(uint256 discount_) public { - discount = discount_; - } - - function setMaxAssets(uint256 maxAssets_) public { - maxAssets = maxAssets_; - } - - function getAmountOut(address, uint256 amountIn) external pure returns (uint256) { - return amountIn; - } - - function getMaxAssets(address) external returns (uint256) { - return maxAssets; - } - - function minDiscount(address) external view returns (uint256) { - return discount; - } - function swap(ILiquidLaneAdapter.Swap calldata swap_) public { require(IERC20(swap_.tokenIn).balanceOf(address(this)) >= swap_.amountIn, "missing input"); - - uint256 amount = nextOutputAmount == 0 ? swap_.amountOut + bonus : nextOutputAmount; - nextOutputAmount = 0; - outputToken.transfer(swap_.recipient, amount); + outputToken.transfer(swap_.recipient, swap_.amountOut); } function swap(ILiquidLaneAdapter.SignedSwap calldata, bytes calldata) public {} 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); + } +}