From 1154bc8ec20331edb6189ece00964ae60b77c8aa Mon Sep 17 00:00:00 2001 From: alrxy Date: Thu, 9 Jul 2026 11:17:59 +0700 Subject: [PATCH 1/6] feat(lifi): add executor callback --- src/lifi/LiquidLaneLifiExecutor.sol | 327 ++++++ src/lifi/interfaces/IInputCallback.sol | 15 + src/lifi/interfaces/IInputSettler.sol | 43 + .../interfaces/ILiquidLaneLifiExecutor.sol | 95 ++ src/lifi/interfaces/IOutputSettler.sol | 44 + test/lifi/LifiSignatureRequirementFork.t.sol | 187 ++++ test/lifi/LiquidLaneLifiExecutor.t.sol | 943 ++++++++++++++++++ 7 files changed, 1654 insertions(+) create mode 100644 src/lifi/LiquidLaneLifiExecutor.sol create mode 100644 src/lifi/interfaces/IInputCallback.sol create mode 100644 src/lifi/interfaces/IInputSettler.sol create mode 100644 src/lifi/interfaces/ILiquidLaneLifiExecutor.sol create mode 100644 src/lifi/interfaces/IOutputSettler.sol create mode 100644 test/lifi/LifiSignatureRequirementFork.t.sol create mode 100644 test/lifi/LiquidLaneLifiExecutor.t.sol diff --git a/src/lifi/LiquidLaneLifiExecutor.sol b/src/lifi/LiquidLaneLifiExecutor.sol new file mode 100644 index 0000000..fb0c369 --- /dev/null +++ b/src/lifi/LiquidLaneLifiExecutor.sol @@ -0,0 +1,327 @@ +// SPDX-License-Identifier: BUSL-1.1 +// Copyright (c) 2026 Symbiotic +pragma solidity 0.8.28; + +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 {Address} from "@openzeppelin/contracts/utils/Address.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 LiquidLaneLifiExecutor +/// @notice LI.FI same-chain callback that redeems released inputs and fills the order output atomically. +contract LiquidLaneLifiExecutor is Ownable, ReentrancyGuard, ILiquidLaneLifiExecutor { + using Address for address payable; + 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; + + /* IMMUTABLES */ + + /// @inheritdoc ILiquidLaneLifiExecutor + address public immutable INPUT_SETTLER; + /// @inheritdoc ILiquidLaneLifiExecutor + address public immutable OUTPUT_SETTLER; + + /* STATE */ + + /// @dev LiquidLane adapters allowed for LI.FI input redemptions. + address[] public adapters; + /// @inheritdoc ILiquidLaneLifiExecutor + mapping(address adapter => bool allowed) public isAdapterAllowed; + + /* CONSTRUCTOR */ + + constructor(address inputSettler, address outputSettler, address owner_, address[] memory initAdapters) + Ownable(owner_) + { + if (inputSettler == address(0) || outputSettler == address(0) || owner_ == address(0)) revert ZeroAddress(); + + INPUT_SETTLER = inputSettler; + OUTPUT_SETTLER = outputSettler; + _setAdapters(initAdapters); + } + + /* FINALISE WRAPPER */ + + /// @inheritdoc ILiquidLaneLifiExecutor + function finaliseWithCurrentTimestamp( + address inputSettler, + IInputSettler.StandardOrder calldata order, + address solver, + address destination, + bytes calldata call, + bytes calldata orderOwnerSignature + ) external { + if (inputSettler != INPUT_SETTLER) revert InvalidInputSettler(); + if (destination != address(this)) revert InvalidDestination(); + + bytes32 solverId = _addressIdentifier(solver); + ILiquidLaneLifiExecutor.FillCall memory fillCall = abi.decode(call, (ILiquidLaneLifiExecutor.FillCall)); + _validateFillAfter(fillCall.fillAfter, fillCall.output.context); + if (_cleanIdentifier(fillCall.solver) != solverId) revert SolverMismatch(); + + bytes32 orderId = IInputSettler(INPUT_SETTLER).orderIdentifier(order); + if (fillCall.orderId != orderId) revert InvalidOrderId(); + if (order.outputs.length != 1) revert InvalidOutputCount(); + if ( + fillCall.fillDeadline != order.fillDeadline || _outputHash(fillCall.output) != _outputHash(order.outputs[0]) + ) { + revert InvalidOrderOutput(); + } + + uint8 status = IInputSettler(INPUT_SETTLER).orderStatus(orderId); + if (status != ORDER_STATUS_DEPOSITED) revert InvalidOrderStatus(status); + + IInputSettler.SolveParams[] memory solveParams = new IInputSettler.SolveParams[](1); + solveParams[0] = IInputSettler.SolveParams({timestamp: uint32(block.timestamp), solver: solverId}); + + IInputSettler(INPUT_SETTLER) + .finaliseWithSignature(order, solveParams, _addressIdentifier(destination), call, orderOwnerSignature); + } + + /* 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); + if (!isAdapterAllowed[fillCall.adapter]) revert AdapterNotAllowed(); + + bytes32 solver = _cleanIdentifier(fillCall.solver); + uint256 resolvedAmount = _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); + + address tokenIn = _inputToken(inputs[0][0]); + uint256 amountIn = inputs[0][1]; + if (amountIn == 0) revert InvalidAmount(); + + IERC20(tokenIn).safeTransfer(fillCall.adapter, amountIn); + uint256 outputBefore = IERC20(outputToken).balanceOf(address(this)); + + ILiquidLaneAdapter(fillCall.adapter) + .swap( + ILiquidLaneAdapter.Swap({ + recipient: address(this), tokenIn: tokenIn, amountIn: amountIn, amountOut: resolvedAmount + }) + ); + + uint256 outputGained = IERC20(outputToken).balanceOf(address(this)) - outputBefore; + if (outputGained < resolvedAmount) revert InsufficientOutput(); + + IERC20(outputToken).forceApprove(OUTPUT_SETTLER, resolvedAmount); + 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 InputRedeemed(fillCall.orderId, fillCall.adapter, tokenIn, outputToken, amountIn, outputGained); + emit OutputFilled( + fillCall.orderId, solver, outputToken, _identifierAddress(fillCall.output.recipient), resolvedAmount + ); + } + + /* OWNER */ + + /// @inheritdoc ILiquidLaneLifiExecutor + function setAdapters(address[] calldata newAdapters) external onlyOwner { + _setAdapters(newAdapters); + } + + /// @inheritdoc ILiquidLaneLifiExecutor + function sweepERC20(address token, address to, uint256 amount) external onlyOwner { + if (token == address(0) || to == address(0)) revert ZeroAddress(); + + IERC20(token).safeTransfer(to, amount); + emit SweepERC20(token, to, amount); + } + + /// @inheritdoc ILiquidLaneLifiExecutor + function sweepNative(address to, uint256 amount) external onlyOwner { + if (to == address(0)) revert ZeroAddress(); + + payable(to).sendValue(amount); + emit SweepNative(to, amount); + } + + /* INTERNAL */ + + function _setAdapters(address[] memory newAdapters) internal { + for (uint256 i; i < adapters.length; ++i) { + isAdapterAllowed[adapters[i]] = false; + } + delete adapters; + + for (uint256 i; i < newAdapters.length; ++i) { + address adapter = newAdapters[i]; + if (adapter == address(0)) revert ZeroAddress(); + if (isAdapterAllowed[adapter]) revert DuplicateAdapter(); + + isAdapterAllowed[adapter] = true; + adapters.push(adapter); + } + + emit SetAdapters(newAdapters); + } + + 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 _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) { + 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 _cleanIdentifier(bytes32 identifier) internal pure returns (bytes32 clean) { + clean = _addressIdentifier(_identifierAddress(identifier)); + } + + 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/IInputCallback.sol b/src/lifi/interfaces/IInputCallback.sol new file mode 100644 index 0000000..186a8fa --- /dev/null +++ b/src/lifi/interfaces/IInputCallback.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/** + * @title IInputCallback + * @notice OIF callback invoked after an input settler transfers order inputs to the destination. + */ +interface IInputCallback { + /** + * @notice Handles order inputs delivered to the callback destination. + * @param inputs Order input token ids and amounts. + * @param executionData Callback-specific execution data. + */ + function orderFinalised(uint256[2][] calldata inputs, bytes calldata executionData) external; +} diff --git a/src/lifi/interfaces/IInputSettler.sol b/src/lifi/interfaces/IInputSettler.sol new file mode 100644 index 0000000..34625ab --- /dev/null +++ b/src/lifi/interfaces/IInputSettler.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {MandateOutput} from "./IOutputSettler.sol"; + +/** + * @title IInputSettler + * @notice Minimal OIF InputSettler surface used by the LI.FI executor. + */ +interface IInputSettler { + struct StandardOrder { + address user; + uint256 nonce; + uint256 originChainId; + uint32 expires; + uint32 fillDeadline; + address inputOracle; + uint256[2][] inputs; + MandateOutput[] outputs; + } + + struct SolveParams { + uint32 timestamp; + 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 finaliseWithSignature( + StandardOrder calldata order, + SolveParams[] calldata solveParams, + bytes32 destination, + bytes calldata call, + bytes calldata orderOwnerSignature + ) external; +} diff --git a/src/lifi/interfaces/ILiquidLaneLifiExecutor.sol b/src/lifi/interfaces/ILiquidLaneLifiExecutor.sol new file mode 100644 index 0000000..15aeaff --- /dev/null +++ b/src/lifi/interfaces/ILiquidLaneLifiExecutor.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: BUSL-1.1 +// Copyright (c) 2026 Symbiotic +pragma solidity ^0.8.0; + +import {IInputCallback} from "./IInputCallback.sol"; +import {IInputSettler} from "./IInputSettler.sol"; +import {MandateOutput} from "./IOutputSettler.sol"; + +/** + * @title ILiquidLaneLifiExecutor + * @notice LI.FI same-chain executor callback for on-chain orders. + */ +interface ILiquidLaneLifiExecutor is IInputCallback { + /* ERRORS */ + + error AdapterNotAllowed(); + error DuplicateAdapter(); + error ExclusiveForMismatch(bytes32 exclusiveFor, bytes32 solver); + error FillTooEarly(uint32 fillAfter, uint32 currentTime); + error FillAfterWithoutAuction(); + error InsufficientOutput(); + error InvalidAmount(); + error InvalidDestination(); + error InvalidInputCount(); + error InvalidInputSettler(); + 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 NativeOutputUnsupported(); + error NotInputSettler(); + error SolverMismatch(); + error UnknownOutputContext(bytes1 contextType); + error ZeroAddress(); + + /* STRUCTS */ + + /** + * @notice Callback payload built by the LI.FI solver and passed to InputSettler.finalise. + * @param adapter LiquidLane adapter to redeem inputs through. + * @param orderId OIF order id. + * @param output Single output to fill and attest. + * @param fillDeadline Fill deadline carried by the order. + * @param solver Solver identifier written into filler data and attestation. + * @param fillAfter Earliest timestamp when the solver strategy allows filling. + */ + struct FillCall { + address adapter; + bytes32 orderId; + MandateOutput output; + uint32 fillDeadline; + bytes32 solver; + uint32 fillAfter; + } + + /* EVENTS */ + + event InputRedeemed( + bytes32 indexed orderId, + address indexed adapter, + address indexed tokenIn, + address tokenOut, + uint256 amountIn, + uint256 amountOut + ); + event OutputFilled( + bytes32 indexed orderId, bytes32 indexed solver, address indexed token, address recipient, uint256 amount + ); + event SetAdapters(address[] adapters); + 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 adapters(uint256 index) external view returns (address adapter); + function finaliseWithCurrentTimestamp( + address inputSettler, + IInputSettler.StandardOrder calldata order, + address solver, + address destination, + bytes calldata call, + bytes calldata orderOwnerSignature + ) external; + function isAdapterAllowed(address adapter) external view returns (bool allowed); + function setAdapters(address[] calldata newAdapters) external; + function sweepERC20(address token, address to, uint256 amount) external; + function sweepNative(address to, uint256 amount) external; +} diff --git a/src/lifi/interfaces/IOutputSettler.sol b/src/lifi/interfaces/IOutputSettler.sol new file mode 100644 index 0000000..5aa21d0 --- /dev/null +++ b/src/lifi/interfaces/IOutputSettler.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/** + * @notice OIF output description used by LI.FI same-chain intents. + */ +struct MandateOutput { + bytes32 oracle; + bytes32 settler; + uint256 chainId; + bytes32 token; + uint256 amount; + bytes32 recipient; + bytes callbackData; + bytes context; +} + +/** + * @title IOutputSettler + * @notice Minimal OIF OutputSettler surface used by the LI.FI callback. + */ +interface IOutputSettler { + /** + * @notice Fills one output and transfers the output token from the caller. + * @param orderId OIF order id. + * @param output Output to satisfy. + * @param fillDeadline Fill deadline carried by the order. + * @param fillerData Solver identifier. + * @return fillRecordHash Output settler fill record hash. + */ + function fill(bytes32 orderId, MandateOutput calldata output, uint48 fillDeadline, bytes calldata fillerData) + external + payable + returns (bytes32 fillRecordHash); + + /** + * @notice Stores a same-chain attestation for a filled output. + * @param orderId OIF order id. + * @param solver Solver identifier. + * @param timestamp Fill timestamp. + * @param output Filled output. + */ + function setAttestation(bytes32 orderId, bytes32 solver, uint32 timestamp, MandateOutput calldata output) external; +} diff --git a/test/lifi/LifiSignatureRequirementFork.t.sol b/test/lifi/LifiSignatureRequirementFork.t.sol new file mode 100644 index 0000000..76d8867 --- /dev/null +++ b/test/lifi/LifiSignatureRequirementFork.t.sol @@ -0,0 +1,187 @@ +// SPDX-License-Identifier: BUSL-1.1 +// Copyright (c) 2026 Symbiotic +pragma solidity 0.8.28; + +import {ILiquidLaneAdapter} from "../../src/interfaces/ILiquidLaneAdapter.sol"; +import {LiquidLaneLifiExecutor} from "../../src/lifi/LiquidLaneLifiExecutor.sol"; +import {IInputSettler} from "../../src/lifi/interfaces/IInputSettler.sol"; +import {ILiquidLaneLifiExecutor} from "../../src/lifi/interfaces/ILiquidLaneLifiExecutor.sol"; +import {MandateOutput} from "../../src/lifi/interfaces/IOutputSettler.sol"; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {Test} from "forge-std/Test.sol"; + +interface IInputSettlerEscrowLike { + function DOMAIN_SEPARATOR() external view returns (bytes32); + function open(IInputSettler.StandardOrder calldata order) external; + function orderIdentifier(IInputSettler.StandardOrder calldata order) external view returns (bytes32); + function orderStatus(bytes32 orderId) external view returns (uint8); +} + +contract LifiSignatureRequirementForkTest is Test { + address internal constant INPUT_SETTLER = 0x000025c3226C00B2Cdc200005a1600509f4e00C0; + address internal constant OUTPUT_SETTLER = 0x0000000000eC36B683C2E6AC89e9A75989C22a2e; + + address internal user = makeAddr("user"); + address internal solver; + uint256 internal solverKey; + address internal owner = makeAddr("owner"); + address internal recipient = makeAddr("recipient"); + + ForkTestToken internal inputToken; + ForkTestToken internal outputToken; + ForkMintingAdapter internal adapter; + LiquidLaneLifiExecutor internal executor; + + function setUp() external { + string memory rpcUrl = vm.envOr("ETH_RPC_URL_SEPOLIA", string("")); + if (bytes(rpcUrl).length == 0) { + vm.skip(true, "ETH_RPC_URL_SEPOLIA not set"); + } + + vm.createSelectFork(rpcUrl); + (solver, solverKey) = makeAddrAndKey("solver"); + + inputToken = new ForkTestToken("Fork RWA", "FRWA"); + outputToken = new ForkTestToken("Fork USD", "FUSD"); + adapter = new ForkMintingAdapter(outputToken); + + address[] memory adapters = new address[](1); + adapters[0] = address(adapter); + executor = new LiquidLaneLifiExecutor(INPUT_SETTLER, OUTPUT_SETTLER, owner, adapters); + } + + function testEmptyOrderOwnerSignatureRevertsOnRealSettler() external { + IInputSettler.StandardOrder memory order = _openOrder(10 ether, 9 ether, "empty"); + bytes32 orderId = IInputSettlerEscrowLike(INPUT_SETTLER).orderIdentifier(order); + bytes memory call = _fillCall(order, orderId); + + vm.expectRevert(); + executor.finaliseWithCurrentTimestamp(INPUT_SETTLER, order, solver, address(executor), call, ""); + } + + function testSignedAllowOpenSettlesOnRealSettler() external { + IInputSettler.StandardOrder memory order = _openOrder(10 ether, 9 ether, "signed"); + bytes32 orderId = IInputSettlerEscrowLike(INPUT_SETTLER).orderIdentifier(order); + bytes memory call = _fillCall(order, orderId); + + executor.finaliseWithCurrentTimestamp( + INPUT_SETTLER, order, solver, address(executor), call, _allowOpenSignature(orderId, address(executor), call) + ); + + assertEq(IInputSettlerEscrowLike(INPUT_SETTLER).orderStatus(orderId), 2, "claimed"); + assertEq(outputToken.balanceOf(recipient), 9 ether, "recipient output"); + } + + function _openOrder(uint256 amountIn, uint256 amountOut, string memory salt) + internal + returns (IInputSettler.StandardOrder memory order) + { + order = _order(amountIn, amountOut, salt); + + inputToken.mint(user, amountIn); + vm.startPrank(user); + inputToken.approve(INPUT_SETTLER, amountIn); + IInputSettlerEscrowLike(INPUT_SETTLER).open(order); + vm.stopPrank(); + + bytes32 orderId = IInputSettlerEscrowLike(INPUT_SETTLER).orderIdentifier(order); + assertEq(IInputSettlerEscrowLike(INPUT_SETTLER).orderStatus(orderId), 1, "deposited"); + } + + function _order(uint256 amountIn, uint256 amountOut, string memory salt) + internal + view + returns (IInputSettler.StandardOrder memory order) + { + uint256[2][] memory inputs = new uint256[2][](1); + inputs[0] = [uint256(uint160(address(inputToken))), amountIn]; + + MandateOutput[] memory outputs = new MandateOutput[](1); + outputs[0] = MandateOutput({ + oracle: _id(OUTPUT_SETTLER), + settler: _id(OUTPUT_SETTLER), + chainId: block.chainid, + token: _id(address(outputToken)), + amount: amountOut, + recipient: _id(recipient), + callbackData: hex"", + context: hex"" + }); + + order = IInputSettler.StandardOrder({ + user: user, + nonce: uint256(keccak256(abi.encodePacked("signature-requirement", salt))), + originChainId: block.chainid, + expires: uint32(block.timestamp + 1 hours), + fillDeadline: uint32(block.timestamp + 30 minutes), + inputOracle: OUTPUT_SETTLER, + inputs: inputs, + outputs: outputs + }); + } + + function _fillCall(IInputSettler.StandardOrder memory order, bytes32 orderId) internal view returns (bytes memory) { + return abi.encode( + ILiquidLaneLifiExecutor.FillCall({ + adapter: address(adapter), + orderId: orderId, + output: order.outputs[0], + fillDeadline: order.fillDeadline, + solver: _id(solver), + fillAfter: 0 + }) + ); + } + + function _allowOpenSignature(bytes32 orderId, address destination, bytes memory call) + internal + view + returns (bytes memory) + { + bytes32 structHash = keccak256( + abi.encode( + keccak256("AllowOpen(bytes32 orderId,bytes32 destination,bytes call)"), + orderId, + _id(destination), + keccak256(call) + ) + ); + bytes32 digest = keccak256( + abi.encodePacked("\x19\x01", IInputSettlerEscrowLike(INPUT_SETTLER).DOMAIN_SEPARATOR(), structHash) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(solverKey, digest); + return abi.encodePacked(r, s, v); + } + + function _id(address addr) internal pure returns (bytes32) { + return bytes32(uint256(uint160(addr))); + } +} + +contract ForkMintingAdapter is ILiquidLaneAdapter { + ForkTestToken public immutable outputToken; + + constructor(ForkTestToken outputToken_) { + outputToken = outputToken_; + } + + function swap(Swap calldata swap_) external { + require(ForkTestToken(swap_.tokenIn).balanceOf(address(this)) >= swap_.amountIn, "missing input"); + outputToken.mint(swap_.recipient, swap_.amountOut); + } + + function swap(SignedSwap calldata, bytes calldata) external {} + + function swap(DiscountSwap calldata, bytes calldata, address, uint256) external pure returns (uint256) { + return 0; + } +} + +contract ForkTestToken is ERC20 { + constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) {} + + function mint(address to, uint256 amount) external { + _mint(to, amount); + } +} diff --git a/test/lifi/LiquidLaneLifiExecutor.t.sol b/test/lifi/LiquidLaneLifiExecutor.t.sol new file mode 100644 index 0000000..8bc72fa --- /dev/null +++ b/test/lifi/LiquidLaneLifiExecutor.t.sol @@ -0,0 +1,943 @@ +// SPDX-License-Identifier: BUSL-1.1 +// Copyright (c) 2026 Symbiotic +pragma solidity 0.8.28; + +import {ILiquidLaneAdapter} from "../../src/interfaces/ILiquidLaneAdapter.sol"; +import {IInputCallback} from "../../src/lifi/interfaces/IInputCallback.sol"; +import {IInputSettler} from "../../src/lifi/interfaces/IInputSettler.sol"; +import {LiquidLaneLifiExecutor} from "../../src/lifi/LiquidLaneLifiExecutor.sol"; +import {ILiquidLaneLifiExecutor} from "../../src/lifi/interfaces/ILiquidLaneLifiExecutor.sol"; +import {IOutputSettler, MandateOutput} from "../../src/lifi/interfaces/IOutputSettler.sol"; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {Test} from "forge-std/Test.sol"; + +contract LiquidLaneLifiExecutorTest is Test { + bytes32 internal constant ORDER_ID = keccak256("order"); + address internal constant SOLVER_ADDR = address(0x515011); + bytes32 internal constant SOLVER = bytes32(uint256(uint160(SOLVER_ADDR))); + uint8 internal constant ORDER_STATUS_DEPOSITED = 1; + uint8 internal constant ORDER_STATUS_CLAIMED = 2; + + address internal owner = makeAddr("owner"); + address internal recipient = makeAddr("recipient"); + address internal collector = makeAddr("collector"); + + TestToken internal rwa; + TestToken internal outputToken; + MockLifiAdapter internal adapter; + MockInputSettler internal inputSettler; + MockOutputSettler internal outputSettler; + LiquidLaneLifiExecutor internal executor; + + function setUp() public { + rwa = new TestToken("RWA", "RWA"); + outputToken = new TestToken("USD", "USD"); + adapter = new MockLifiAdapter(outputToken); + inputSettler = new MockInputSettler(); + outputSettler = new MockOutputSettler(); + inputSettler.setOrderStatus(ORDER_ID, ORDER_STATUS_DEPOSITED); + + address[] memory adapters = new address[](1); + adapters[0] = address(adapter); + executor = new LiquidLaneLifiExecutor(address(inputSettler), address(outputSettler), owner, adapters); + + outputToken.mint(address(adapter), 100 ether); + } + + function testFinaliseCallbackRedeemsInputThenFillsAndAttestsOutput() public { + adapter.setBonus(1 ether); + rwa.mint(address(inputSettler), 10 ether); + + vm.expectEmit(true, true, true, true, address(executor)); + emit ILiquidLaneLifiExecutor.InputRedeemed( + ORDER_ID, address(adapter), address(rwa), address(outputToken), 10 ether, 10 ether + ); + vm.expectEmit(true, true, true, true, address(executor)); + emit ILiquidLaneLifiExecutor.OutputFilled(ORDER_ID, SOLVER, address(outputToken), recipient, 9 ether); + + inputSettler.finalise(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(), SOLVER); + assertTrue(outputSettler.attested()); + } + + function testFinaliseWithCurrentTimestampCallsSignaturePathAndCallback() public { + adapter.setBonus(1 ether); + rwa.mint(address(inputSettler), 10 ether); + + vm.warp(1_717_171); + 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); + + executor.finaliseWithCurrentTimestamp( + address(inputSettler), order, SOLVER_ADDR, address(executor), call, hex"1234" + ); + + assertEq(inputSettler.lastTimestamp(), uint32(block.timestamp)); + assertEq(inputSettler.lastSolver(), SOLVER); + assertEq(inputSettler.lastDestination(), _id(address(executor))); + assertEq(inputSettler.lastOrderOwnerSignature(), hex"1234"); + assertEq(inputSettler.orderStatus(orderId), ORDER_STATUS_CLAIMED); + assertEq(outputToken.balanceOf(recipient), 9 ether); + 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( + address(inputSettler), order, SOLVER_ADDR, address(executor), _fillCallData(orderId, 9 ether), "" + ); + } + + function testFinaliseWithCurrentTimestampRejectsSolverMismatch() public { + IInputSettler.StandardOrder memory order = _order(10 ether, 9 ether); + + vm.expectRevert(ILiquidLaneLifiExecutor.SolverMismatch.selector); + executor.finaliseWithCurrentTimestamp( + address(inputSettler), + order, + makeAddr("wrongSolver"), + address(executor), + _fillCallData(_orderId(order), 9 ether), + "" + ); + } + + 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( + address(inputSettler), order, SOLVER_ADDR, address(executor), 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(address(inputSettler), order, SOLVER_ADDR, address(executor), 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(address(inputSettler), order, SOLVER_ADDR, address(executor), call, ""); + } + + 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( + address(inputSettler), order, SOLVER_ADDR, address(executor), 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); + + vm.expectRevert(ILiquidLaneLifiExecutor.FillAfterWithoutAuction.selector); + executor.finaliseWithCurrentTimestamp( + address(inputSettler), order, SOLVER_ADDR, address(executor), abi.encode(fillCall), "" + ); + } + + function testFinaliseWithCurrentTimestampRejectsAuctionFillTooEarly() 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( + address(inputSettler), order, SOLVER_ADDR, address(executor), abi.encode(fillCall), "" + ); + } + + function testFinaliseWithCurrentTimestampRejectsWrongInputSettler() public { + IInputSettler.StandardOrder memory order = _order(10 ether, 9 ether); + + vm.expectRevert(ILiquidLaneLifiExecutor.InvalidInputSettler.selector); + executor.finaliseWithCurrentTimestamp( + makeAddr("wrongSettler"), order, SOLVER_ADDR, address(executor), _fillCallData(_orderId(order), 9 ether), "" + ); + } + + function testFinaliseWithCurrentTimestampRejectsWrongDestination() public { + IInputSettler.StandardOrder memory order = _order(10 ether, 9 ether); + + vm.expectRevert(ILiquidLaneLifiExecutor.InvalidDestination.selector); + executor.finaliseWithCurrentTimestamp( + address(inputSettler), + order, + SOLVER_ADDR, + makeAddr("wrongDestination"), + _fillCallData(_orderId(order), 9 ether), + "" + ); + } + + function testMockFinaliseWithSignatureRejectsStaleOrFutureTimestamp() public { + IInputSettler.StandardOrder memory order = _order(10 ether, 9 ether); + IInputSettler.SolveParams[] memory solveParams = new IInputSettler.SolveParams[](1); + solveParams[0].solver = SOLVER; + + vm.warp(100); + solveParams[0].timestamp = 99; + vm.expectRevert(MockInputSettler.TimestampPassed.selector); + inputSettler.finaliseWithSignature(order, solveParams, _id(address(executor)), _fillCallData(9 ether), ""); + + solveParams[0].timestamp = 101; + vm.expectRevert(MockInputSettler.TimestampNotPassed.selector); + inputSettler.finaliseWithSignature(order, solveParams, _id(address(executor)), _fillCallData(9 ether), ""); + } + + function testOrderFinalisedRejectsNonInputSettler() public { + vm.expectRevert(ILiquidLaneLifiExecutor.NotInputSettler.selector); + executor.orderFinalised(_inputs(10 ether), _fillCallData(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 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; + + vm.expectRevert(ILiquidLaneLifiExecutor.InvalidInputCount.selector); + vm.prank(address(inputSettler)); + executor.orderFinalised(inputs, _fillCallData(9 ether)); + } + + function testOrderFinalisedRejectsUnallowedAdapter() public { + MockLifiAdapter otherAdapter = new MockLifiAdapter(outputToken); + rwa.mint(address(executor), 10 ether); + + vm.expectRevert(ILiquidLaneLifiExecutor.AdapterNotAllowed.selector); + vm.prank(address(inputSettler)); + executor.orderFinalised(_inputs(10 ether), _fillCallData(address(otherAdapter), 9 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)); + } + + function testOrderFinalisedRejectsFillAfterForExclusiveLimitOutput() public { + vm.warp(1000); + inputSettler.setOrderStatus(ORDER_ID, ORDER_STATUS_CLAIMED); + MandateOutput memory output = _output(9 ether, _exclusiveContext(SOLVER, uint32(block.timestamp))); + ILiquidLaneLifiExecutor.FillCall memory fillCall = _fillCallStruct(address(adapter), output); + fillCall.fillAfter = uint32(block.timestamp); + + vm.expectRevert(ILiquidLaneLifiExecutor.FillAfterWithoutAuction.selector); + vm.prank(address(inputSettler)); + executor.orderFinalised(_inputs(10 ether), abi.encode(fillCall)); + } + + function testOrderFinalisedRejectsAuctionFillTooEarly() 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); + + vm.expectRevert(ILiquidLaneLifiExecutor.InsufficientOutput.selector); + vm.prank(address(inputSettler)); + executor.orderFinalised(_inputs(10 ether), _fillCallData(9 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)); + + vm.prank(address(inputSettler)); + executor.orderFinalised(_inputs(10 ether), _fillCallData(output)); + + assertEq(outputToken.balanceOf(recipient), 10 ether); + assertEq(outputSettler.lastOutputAmount(), 10 ether); + } + + function testOrderFinalisedExclusiveDutchOutputUsesResolvedAmount() public { + vm.warp(1000); + inputSettler.setOrderStatus(ORDER_ID, ORDER_STATUS_CLAIMED); + rwa.mint(address(executor), 10 ether); + + MandateOutput memory output = + _output(9 ether, _exclusiveDutchContext(_id(makeAddr("otherSolver")), 900, 1100, 0.01 ether)); + + vm.prank(address(inputSettler)); + executor.orderFinalised(_inputs(10 ether), _fillCallData(output)); + + assertEq(outputToken.balanceOf(recipient), 10 ether); + assertEq(outputSettler.lastOutputAmount(), 10 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); + + MandateOutput memory output = _output(9 ether, _dutchContext(900, 1100, 0.01 ether)); + + vm.expectRevert(ILiquidLaneLifiExecutor.InsufficientOutput.selector); + vm.prank(address(inputSettler)); + executor.orderFinalised(_inputs(10 ether), _fillCallData(output)); + } + + 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)); + + vm.expectRevert( + abi.encodeWithSelector(ILiquidLaneLifiExecutor.ExclusiveForMismatch.selector, exclusiveFor, SOLVER) + ); + vm.prank(address(inputSettler)); + executor.orderFinalised(_inputs(10 ether), _fillCallData(output)); + } + + function testOrderFinalisedAllowsExclusiveOutputAfterStart() public { + vm.warp(1000); + inputSettler.setOrderStatus(ORDER_ID, ORDER_STATUS_CLAIMED); + rwa.mint(address(executor), 10 ether); + + MandateOutput memory output = _output(9 ether, _exclusiveContext(_id(makeAddr("otherSolver")), 1000)); + + vm.prank(address(inputSettler)); + executor.orderFinalised(_inputs(10 ether), _fillCallData(output)); + + assertEq(outputToken.balanceOf(recipient), 9 ether); + } + + function testOrderFinalisedRejectsBadContextLength() public { + inputSettler.setOrderStatus(ORDER_ID, ORDER_STATUS_CLAIMED); + MandateOutput memory output = _output(9 ether, hex"0000"); + + vm.expectRevert(abi.encodeWithSelector(ILiquidLaneLifiExecutor.InvalidOutputContextLength.selector, 0, 2)); + vm.prank(address(inputSettler)); + executor.orderFinalised(_inputs(10 ether), _fillCallData(output)); + } + + function testOrderFinalisedRejectsUnknownContextType() public { + inputSettler.setOrderStatus(ORDER_ID, ORDER_STATUS_CLAIMED); + MandateOutput memory output = _output(9 ether, hex"02"); + + vm.expectRevert(abi.encodeWithSelector(ILiquidLaneLifiExecutor.UnknownOutputContext.selector, bytes1(0x02))); + vm.prank(address(inputSettler)); + executor.orderFinalised(_inputs(10 ether), _fillCallData(output)); + } + + function testOrderFinalisedRejectsZeroInputAmount() public { + inputSettler.setOrderStatus(ORDER_ID, ORDER_STATUS_CLAIMED); + + vm.expectRevert(ILiquidLaneLifiExecutor.InvalidAmount.selector); + vm.prank(address(inputSettler)); + executor.orderFinalised(_inputs(0), _fillCallData(9 ether)); + } + + function testOrderFinalisedRejectsWrongOutputSettlerIdentifier() public { + bytes memory call = _fillCallData(_output(9 ether, makeAddr("wrongSettler"), address(outputSettler))); + + vm.expectRevert(ILiquidLaneLifiExecutor.InvalidOutputSettler.selector); + vm.prank(address(inputSettler)); + executor.orderFinalised(_inputs(10 ether), call); + } + + 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 testOrderFinalisedRejectsWrongOutputChain() public { + MandateOutput memory output = _output(9 ether); + output.chainId = block.chainid + 1; + + vm.expectRevert(ILiquidLaneLifiExecutor.InvalidOutputChain.selector); + vm.prank(address(inputSettler)); + executor.orderFinalised(_inputs(10 ether), _fillCallData(output)); + } + + 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 testOrderFinalisedRejectsDirtyOutputIdentifier() public { + MandateOutput memory output = _output(9 ether); + output.token = bytes32(uint256(uint160(address(outputToken))) | (uint256(1) << 160)); + + vm.expectRevert(ILiquidLaneLifiExecutor.InvalidIdentifier.selector); + vm.prank(address(inputSettler)); + executor.orderFinalised(_inputs(10 ether), _fillCallData(output)); + } + + function testOrderFinalisedRejectsDirtySolverIdentifier() public { + ILiquidLaneLifiExecutor.FillCall memory fillCall = _fillCallStruct(address(adapter), _output(9 ether)); + fillCall.solver = bytes32(uint256(SOLVER) | (uint256(1) << 160)); + + vm.expectRevert(ILiquidLaneLifiExecutor.InvalidIdentifier.selector); + vm.prank(address(inputSettler)); + executor.orderFinalised(_inputs(10 ether), abi.encode(fillCall)); + } + + function testOrderFinalisedSupportsSameInputAndOutputTokenAccounting() public { + inputSettler.setOrderStatus(ORDER_ID, ORDER_STATUS_CLAIMED); + MockLifiAdapter sameTokenAdapter = new MockLifiAdapter(rwa); + address[] memory adapters = new address[](1); + adapters[0] = address(sameTokenAdapter); + + vm.prank(owner); + executor.setAdapters(adapters); + + 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 testSetAdaptersReplacesAllowlistAndOwnerSweepsSurplus() public { + MockLifiAdapter nextAdapter = new MockLifiAdapter(outputToken); + address[] memory adapters = new address[](1); + adapters[0] = address(nextAdapter); + + vm.prank(owner); + executor.setAdapters(adapters); + + assertFalse(executor.isAdapterAllowed(address(adapter))); + assertTrue(executor.isAdapterAllowed(address(nextAdapter))); + assertEq(executor.adapters(0), address(nextAdapter)); + + outputToken.mint(address(executor), 2 ether); + vm.prank(owner); + executor.sweepERC20(address(outputToken), collector, 2 ether); + + assertEq(outputToken.balanceOf(collector), 2 ether); + } + + function testSetAdaptersRejectsDuplicates() public { + address[] memory adapters = new address[](2); + adapters[0] = address(adapter); + adapters[1] = address(adapter); + + vm.expectRevert(ILiquidLaneLifiExecutor.DuplicateAdapter.selector); + vm.prank(owner); + executor.setAdapters(adapters); + } + + function _order(uint256 amountIn, uint256 amountOut) internal view returns (IInputSettler.StandardOrder memory) { + MandateOutput[] memory outputs = new MandateOutput[](1); + outputs[0] = _output(amountOut); + + return IInputSettler.StandardOrder({ + user: address(0xA11CE), + nonce: uint256(ORDER_ID), + originChainId: block.chainid, + expires: uint32(block.timestamp + 1 hours), + fillDeadline: uint32(block.timestamp + 1 hours), + inputOracle: address(outputSettler), + inputs: _inputs(amountIn), + outputs: outputs + }); + } + + function _inputs(uint256 amount) internal view returns (uint256[2][] memory inputs) { + inputs = new uint256[2][](1); + inputs[0][0] = uint256(uint160(address(rwa))); + inputs[0][1] = amount; + } + + 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) + internal + view + returns (ILiquidLaneLifiExecutor.FillCall memory) + { + return _fillCallStruct(fillAdapter, ORDER_ID, output); + } + + function _fillCallStruct(address fillAdapter, bytes32 orderId, MandateOutput memory output) + internal + view + returns (ILiquidLaneLifiExecutor.FillCall memory) + { + return ILiquidLaneLifiExecutor.FillCall({ + adapter: fillAdapter, + orderId: orderId, + output: output, + fillDeadline: uint32(block.timestamp + 1 hours), + solver: SOLVER, + fillAfter: 0 + }); + } + + 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), + chainId: block.chainid, + token: _id(token), + amount: amount, + recipient: _id(recipient), + callbackData: bytes(""), + context: bytes("") + }); + } + + function _id(address addr) internal pure returns (bytes32) { + return bytes32(uint256(uint160(addr))); + } + + function _dutchContext(uint32 startTime, uint32 stopTime, uint256 slope) internal pure returns (bytes memory) { + return abi.encodePacked(bytes1(0x01), startTime, stopTime, slope); + } + + function _exclusiveContext(bytes32 exclusiveFor, uint32 startTime) internal pure returns (bytes memory) { + return abi.encodePacked(bytes1(0xe0), exclusiveFor, startTime); + } + + function _exclusiveDutchContext(bytes32 exclusiveFor, uint32 startTime, uint32 stopTime, uint256 slope) + internal + pure + returns (bytes memory) + { + return abi.encodePacked(bytes1(0xe1), exclusiveFor, startTime, stopTime, slope); + } + + function _orderId(IInputSettler.StandardOrder memory order) internal pure returns (bytes32) { + return keccak256( + abi.encode( + order.user, + order.nonce, + order.originChainId, + order.expires, + order.fillDeadline, + order.inputOracle, + keccak256(abi.encode(order.inputs)), + _outputsHash(order.outputs) + ) + ); + } + + function _outputsHash(MandateOutput[] memory outputs) internal pure returns (bytes32) { + bytes32[] memory outputHashes = new bytes32[](outputs.length); + for (uint256 i; i < outputs.length; ++i) { + outputHashes[i] = _outputHash(outputs[i]); + } + return keccak256(abi.encode(outputHashes)); + } + + 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) + ) + ); + } +} + +contract MockInputSettler is IInputSettler { + using SafeERC20 for IERC20; + + uint8 internal constant ORDER_STATUS_DEPOSITED = 1; + uint8 internal constant ORDER_STATUS_CLAIMED = 2; + + error InvalidTimestampLength(); + error TimestampNotPassed(); + error TimestampPassed(); + + mapping(bytes32 orderId => uint8 status) public orderStatus; + uint32 public lastTimestamp; + bytes32 public lastSolver; + bytes32 public lastDestination; + bytes public lastOrderOwnerSignature; + + function setOrderStatus(bytes32 orderId, uint8 status) public { + orderStatus[orderId] = status; + } + + function orderIdentifier(StandardOrder calldata order) external pure returns (bytes32 orderId) { + return _orderId(order); + } + + function finaliseWithSignature( + StandardOrder calldata order, + SolveParams[] calldata solveParams, + bytes32 destination, + bytes calldata call, + bytes calldata orderOwnerSignature + ) external { + if (solveParams.length != 1) revert InvalidTimestampLength(); + if (solveParams[0].timestamp < block.timestamp) revert TimestampPassed(); + if (solveParams[0].timestamp > block.timestamp) revert TimestampNotPassed(); + + lastTimestamp = solveParams[0].timestamp; + lastSolver = solveParams[0].solver; + lastDestination = destination; + lastOrderOwnerSignature = orderOwnerSignature; + + _finalise(_orderId(order), address(uint160(uint256(destination))), order.inputs, call); + } + + function finalise(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]); + } + + orderStatus[orderId] = ORDER_STATUS_CLAIMED; + IInputCallback(destination).orderFinalised(inputs, call); + + ILiquidLaneLifiExecutor.FillCall memory fillCall = abi.decode(call, (ILiquidLaneLifiExecutor.FillCall)); + require(fillCall.orderId == orderId, "order mismatch"); + require(MockOutputSettler(address(uint160(uint256(fillCall.output.settler)))).attested(), "not attested"); + } + + function _orderId(StandardOrder calldata order) internal pure returns (bytes32) { + return keccak256( + abi.encode( + order.user, + order.nonce, + order.originChainId, + order.expires, + order.fillDeadline, + order.inputOracle, + keccak256(abi.encode(order.inputs)), + _outputsHash(order.outputs) + ) + ); + } + + function _outputsHash(MandateOutput[] calldata outputs) internal pure returns (bytes32) { + bytes32[] memory outputHashes = new bytes32[](outputs.length); + for (uint256 i; i < outputs.length; ++i) { + outputHashes[i] = _outputHash(outputs[i]); + } + return keccak256(abi.encode(outputHashes)); + } + + function _outputHash(MandateOutput calldata 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) + ) + ); + } +} + +contract MockOutputSettler is IOutputSettler { + using SafeERC20 for IERC20; + + bool public attested; + bytes32 public lastOrderId; + bytes32 public lastSolver; + uint256 public lastOutputAmount; + mapping(bytes32 orderId => mapping(bytes32 outputHash => bytes32 fillRecord)) public fillRecords; + + function fill(bytes32 orderId, MandateOutput calldata output, uint48 fillDeadline, bytes calldata fillerData) + external + payable + returns (bytes32 fillRecordHash) + { + require(fillDeadline >= block.timestamp, "deadline"); + bytes32 solver = abi.decode(fillerData, (bytes32)); + address token = _identifierAddress(output.token); + address recipient = _identifierAddress(output.recipient); + require(output.chainId == block.chainid, "chain"); + require(output.settler == _id(address(this)), "settler"); + require(output.oracle == _id(address(this)), "oracle"); + + uint256 resolvedAmount = _resolveOutputAmount(output, solver); + IERC20(token).safeTransferFrom(msg.sender, recipient, resolvedAmount); + + fillRecordHash = keccak256(abi.encodePacked(solver, uint32(block.timestamp))); + fillRecords[orderId][_outputHash(output)] = fillRecordHash; + lastOrderId = orderId; + lastSolver = solver; + lastOutputAmount = resolvedAmount; + } + + function setAttestation(bytes32 orderId, bytes32 solver, uint32 timestamp, MandateOutput calldata output) external { + bytes32 expected = keccak256(abi.encodePacked(solver, timestamp)); + require(fillRecords[orderId][_outputHash(output)] == expected, "invalid attestation"); + attested = true; + } + + function _outputHash(MandateOutput calldata 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 _identifierAddress(bytes32 identifier) internal pure returns (address addr) { + addr = address(uint160(uint256(identifier))); + require(addr != address(0) && identifier == _id(addr), "invalid identifier"); + } + + function _resolveOutputAmount(MandateOutput calldata output, bytes32 solver) internal view returns (uint256) { + bytes calldata context = output.context; + if (context.length == 0) return output.amount; + + uint8 contextType = uint8(context[0]); + if (contextType == 0x00) { + require(context.length == 1, "bad length"); + return output.amount; + } + if (contextType == 0x01) { + require(context.length == 41, "bad length"); + return _dutchOutputAmount(output.amount, context, 1); + } + if (contextType == 0xe0) { + require(context.length == 37, "bad length"); + _validateExclusiveSolver(context, solver, 1, 33); + return output.amount; + } + if (contextType == 0xe1) { + require(context.length == 73, "bad length"); + _validateExclusiveSolver(context, solver, 1, 33); + return _dutchOutputAmount(output.amount, context, 33); + } + + revert("unknown context"); + } + + function _dutchOutputAmount(uint256 amount, bytes calldata 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 calldata context, + bytes32 solver, + uint256 exclusiveForOffset, + uint256 startTimeOffset + ) internal view { + bytes32 exclusiveFor = _readBytes32(context, exclusiveForOffset); + require(block.timestamp >= _readUint32(context, startTimeOffset) || exclusiveFor == solver, "exclusive"); + } + + function _readUint32(bytes calldata data, uint256 offset) internal pure returns (uint32 value) { + bytes32 word = _readBytes32(data, offset); + value = uint32(uint256(word >> 224)); + } + + function _readUint256(bytes calldata data, uint256 offset) internal pure returns (uint256 value) { + value = uint256(_readBytes32(data, offset)); + } + + function _readBytes32(bytes calldata data, uint256 offset) internal pure returns (bytes32 value) { + assembly ("memory-safe") { + value := calldataload(add(data.offset, offset)) + } + } + + function _id(address addr) internal pure returns (bytes32) { + return bytes32(uint256(uint160(addr))); + } +} + +contract MockLifiAdapter is ILiquidLaneAdapter { + TestToken public immutable outputToken; + uint256 public bonus; + uint256 public nextOutputAmount; + + constructor(TestToken outputToken_) { + outputToken = outputToken_; + } + + function setBonus(uint256 bonus_) public { + bonus = bonus_; + } + + function setNextOutputAmount(uint256 amount) public { + nextOutputAmount = amount; + } + + 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); + } + + function swap(ILiquidLaneAdapter.SignedSwap calldata, bytes calldata) public {} + + function swap(ILiquidLaneAdapter.DiscountSwap calldata, bytes calldata, address, uint256) + public + pure + returns (uint256) + { + return 0; + } +} + +contract TestToken is ERC20 { + constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) {} + + function mint(address to, uint256 amount) public { + _mint(to, amount); + } +} From 4c3c887ccbb6ba109b5b13e6d6ea074deed6a0b5 Mon Sep 17 00:00:00 2001 From: alrxy Date: Fri, 10 Jul 2026 11:40:05 +0700 Subject: [PATCH 2/6] feat(lifi): harden atomic executor fills --- src/lifi/LiquidLaneLifiExecutor.sol | 186 ++++++++-- .../interfaces/ILiquidLaneLifiExecutor.sol | 56 ++- test/lifi/LifiSignatureRequirementFork.t.sol | 39 +- test/lifi/LiquidLaneLifiExecutor.t.sol | 339 +++++++++++++++++- 4 files changed, 573 insertions(+), 47 deletions(-) diff --git a/src/lifi/LiquidLaneLifiExecutor.sol b/src/lifi/LiquidLaneLifiExecutor.sol index fb0c369..29f1f7d 100644 --- a/src/lifi/LiquidLaneLifiExecutor.sol +++ b/src/lifi/LiquidLaneLifiExecutor.sol @@ -2,21 +2,29 @@ // Copyright (c) 2026 Symbiotic pragma solidity 0.8.28; -import {ILiquidLaneAdapter} from "../interfaces/ILiquidLaneAdapter.sol"; +import {DISCOUNT_PRECISION, 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 {Address} from "@openzeppelin/contracts/utils/Address.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"; +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); +} + /// @title LiquidLaneLifiExecutor /// @notice LI.FI same-chain callback 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; using SafeERC20 for IERC20; uint8 internal constant ORDER_STATUS_DEPOSITED = 1; @@ -54,6 +62,17 @@ contract LiquidLaneLifiExecutor is Ownable, ReentrancyGuard, ILiquidLaneLifiExec /* 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 finaliseWithCurrentTimestamp( address inputSettler, @@ -68,20 +87,10 @@ contract LiquidLaneLifiExecutor is Ownable, ReentrancyGuard, ILiquidLaneLifiExec bytes32 solverId = _addressIdentifier(solver); ILiquidLaneLifiExecutor.FillCall memory fillCall = abi.decode(call, (ILiquidLaneLifiExecutor.FillCall)); - _validateFillAfter(fillCall.fillAfter, fillCall.output.context); - if (_cleanIdentifier(fillCall.solver) != solverId) revert SolverMismatch(); + bytes32 orderId = _validateFinaliseCall(order, fillCall, solverId); - bytes32 orderId = IInputSettler(INPUT_SETTLER).orderIdentifier(order); - if (fillCall.orderId != orderId) revert InvalidOrderId(); - if (order.outputs.length != 1) revert InvalidOutputCount(); - if ( - fillCall.fillDeadline != order.fillDeadline || _outputHash(fillCall.output) != _outputHash(order.outputs[0]) - ) { - revert InvalidOrderOutput(); - } - - uint8 status = IInputSettler(INPUT_SETTLER).orderStatus(orderId); - if (status != ORDER_STATUS_DEPOSITED) revert InvalidOrderStatus(status); + uint8 orderState = IInputSettler(INPUT_SETTLER).orderStatus(orderId); + if (orderState != ORDER_STATUS_DEPOSITED) revert InvalidOrderStatus(orderState); IInputSettler.SolveParams[] memory solveParams = new IInputSettler.SolveParams[](1); solveParams[0] = IInputSettler.SolveParams({timestamp: uint32(block.timestamp), solver: solverId}); @@ -99,10 +108,9 @@ contract LiquidLaneLifiExecutor is Ownable, ReentrancyGuard, ILiquidLaneLifiExec ILiquidLaneLifiExecutor.FillCall memory fillCall = abi.decode(executionData, (ILiquidLaneLifiExecutor.FillCall)); _validateFillAfter(fillCall.fillAfter, fillCall.output.context); - if (!isAdapterAllowed[fillCall.adapter]) revert AdapterNotAllowed(); bytes32 solver = _cleanIdentifier(fillCall.solver); - uint256 resolvedAmount = _resolveOutputAmount(fillCall.output, solver); + uint256 resolvedAmountOut = _resolveOutputAmount(fillCall.output, solver); _validateOutput(fillCall.output); address outputToken = _outputToken(fillCall.output); @@ -113,28 +121,62 @@ contract LiquidLaneLifiExecutor is Ownable, ReentrancyGuard, ILiquidLaneLifiExec uint256 amountIn = inputs[0][1]; if (amountIn == 0) revert InvalidAmount(); - IERC20(tokenIn).safeTransfer(fillCall.adapter, amountIn); + (uint256 minAmountOut, uint256[] memory executableAmountOuts) = + _validateRoutes(fillCall.routes, tokenIn, amountIn); + if (resolvedAmountOut > minAmountOut) { + revert InsufficientMinimumOutput(minAmountOut, resolvedAmountOut); + } + + for (uint256 i; i < fillCall.routes.length; ++i) { + IERC20(tokenIn).safeTransfer(fillCall.routes[i].adapter, fillCall.routes[i].amountIn); + } uint256 outputBefore = IERC20(outputToken).balanceOf(address(this)); - ILiquidLaneAdapter(fillCall.adapter) - .swap( - ILiquidLaneAdapter.Swap({ - recipient: address(this), tokenIn: tokenIn, amountIn: amountIn, amountOut: resolvedAmount - }) + for (uint256 i; i < fillCall.routes.length; ++i) { + ILiquidLaneLifiExecutor.FillRoute memory route = fillCall.routes[i]; + uint256 redeemedAmountOut; + if (route.discount.discountId == bytes32(0)) { + redeemedAmountOut = executableAmountOuts[i]; + ILiquidLaneAdapter(route.adapter) + .swap( + ILiquidLaneAdapter.Swap({ + recipient: address(this), + tokenIn: tokenIn, + amountIn: route.amountIn, + amountOut: redeemedAmountOut + }) + ); + } else { + redeemedAmountOut = ILiquidLaneAdapter(route.adapter) + .swap(route.discount.discountSwap, route.discount.protocolSignature, address(this), route.amountIn); + } + emit InputRedeemed( + fillCall.orderId, + route.adapter, + tokenIn, + outputToken, + route.amountIn, + redeemedAmountOut, + route.discount.discountId ); + } uint256 outputGained = IERC20(outputToken).balanceOf(address(this)) - outputBefore; - if (outputGained < resolvedAmount) revert InsufficientOutput(); + if (outputGained < minAmountOut) revert InsufficientOutput(minAmountOut, outputGained); - IERC20(outputToken).forceApprove(OUTPUT_SETTLER, resolvedAmount); + IERC20(outputToken).forceApprove(OUTPUT_SETTLER, resolvedAmountOut); 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 InputRedeemed(fillCall.orderId, fillCall.adapter, tokenIn, outputToken, amountIn, outputGained); emit OutputFilled( - fillCall.orderId, solver, outputToken, _identifierAddress(fillCall.output.recipient), resolvedAmount + fillCall.orderId, + solver, + outputToken, + _identifierAddress(fillCall.output.recipient), + resolvedAmountOut, + outputGained - resolvedAmountOut ); } @@ -163,6 +205,32 @@ contract LiquidLaneLifiExecutor is Ownable, ReentrancyGuard, ILiquidLaneLifiExec /* INTERNAL */ + function _validateFinaliseCall( + IInputSettler.StandardOrder calldata order, + ILiquidLaneLifiExecutor.FillCall memory fillCall, + bytes32 solver + ) internal returns (bytes32 orderId) { + _validateFillAfter(fillCall.fillAfter, fillCall.output.context); + if (_cleanIdentifier(fillCall.solver) != solver) revert SolverMismatch(); + + 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); + (uint256 minAmountOut,) = _validateRoutes(fillCall.routes, _inputToken(order.inputs[0][0]), order.inputs[0][1]); + uint256 resolvedAmountOut = _resolveOutputAmount(fillCall.output, solver); + if (resolvedAmountOut > minAmountOut) { + revert InsufficientMinimumOutput(minAmountOut, resolvedAmountOut); + } + } + function _setAdapters(address[] memory newAdapters) internal { for (uint256 i; i < adapters.length; ++i) { isAdapterAllowed[adapters[i]] = false; @@ -193,6 +261,72 @@ contract LiquidLaneLifiExecutor is Ownable, ReentrancyGuard, ILiquidLaneLifiExec _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 (!isAdapterAllowed[route.adapter]) revert AdapterNotAllowed(); + + (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; diff --git a/src/lifi/interfaces/ILiquidLaneLifiExecutor.sol b/src/lifi/interfaces/ILiquidLaneLifiExecutor.sol index 15aeaff..0716277 100644 --- a/src/lifi/interfaces/ILiquidLaneLifiExecutor.sol +++ b/src/lifi/interfaces/ILiquidLaneLifiExecutor.sol @@ -5,6 +5,7 @@ pragma solidity ^0.8.0; import {IInputCallback} from "./IInputCallback.sol"; import {IInputSettler} from "./IInputSettler.sol"; import {MandateOutput} from "./IOutputSettler.sol"; +import {ILiquidLaneAdapter} from "../../interfaces/ILiquidLaneAdapter.sol"; /** * @title ILiquidLaneLifiExecutor @@ -15,12 +16,17 @@ interface ILiquidLaneLifiExecutor is IInputCallback { error AdapterNotAllowed(); error DuplicateAdapter(); + 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 InsufficientOutput(); + error InsufficientMinimumOutput(uint256 minimumAmountOut, uint256 resolvedAmountOut); + error InsufficientOutput(uint256 minimumAmountOut, uint256 receivedAmountOut); error InvalidAmount(); error InvalidDestination(); + error InvalidDiscount(uint256 discount, uint256 minimumDiscount); error InvalidInputCount(); error InvalidInputSettler(); error InvalidIdentifier(); @@ -32,30 +38,63 @@ interface ILiquidLaneLifiExecutor is IInputCallback { error InvalidOutputChain(); error InvalidOutputOracle(); error InvalidOutputSettler(); + error InvalidRouteOutputBounds(uint256 expectedAmountOut, uint256 minAmountOut); error NativeOutputUnsupported(); 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 SolverMismatch(); error UnknownOutputContext(bytes1 contextType); error ZeroAddress(); /* STRUCTS */ + /** + * @notice Optional private-discount authorization for one route. + * @param discountId Backend discount identifier; zero selects the direct swap path. + * @param discountSwap Reusable signer policy plus the fresh protocol deadline. + * @param protocolSignature Fresh protocol cosign verified by the LiquidLane adapter. + */ + struct FillDiscount { + bytes32 discountId; + ILiquidLaneAdapter.DiscountSwap discountSwap; + bytes protocolSignature; + } + + /** + * @notice One atomic LiquidLane redemption leg. + * @param adapter Allowed LiquidLane adapter. + * @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 discount Optional private-discount authorization; zero id means direct swap. + */ + struct FillRoute { + address adapter; + uint256 amountIn; + uint256 expectedAmountOut; + uint256 minAmountOut; + FillDiscount discount; + } + /** * @notice Callback payload built by the LI.FI solver and passed to InputSettler.finalise. - * @param adapter LiquidLane adapter to redeem inputs through. * @param orderId OIF order id. * @param output Single output to fill and attest. * @param fillDeadline Fill deadline carried by the order. * @param solver Solver identifier written into filler data and attestation. * @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. */ struct FillCall { - address adapter; bytes32 orderId; MandateOutput output; uint32 fillDeadline; bytes32 solver; uint32 fillAfter; + FillRoute[] routes; } /* EVENTS */ @@ -66,10 +105,16 @@ interface ILiquidLaneLifiExecutor is IInputCallback { address indexed tokenIn, address tokenOut, uint256 amountIn, - uint256 amountOut + uint256 amountOut, + bytes32 discountId ); event OutputFilled( - bytes32 indexed orderId, bytes32 indexed solver, address indexed token, address recipient, uint256 amount + bytes32 indexed orderId, + bytes32 indexed solver, + address indexed token, + address recipient, + uint256 amount, + uint256 surplus ); event SetAdapters(address[] adapters); event SweepERC20(address indexed token, address indexed to, uint256 amount); @@ -80,6 +125,7 @@ interface ILiquidLaneLifiExecutor is IInputCallback { function INPUT_SETTLER() external view returns (address inputSettler); function OUTPUT_SETTLER() external view returns (address outputSettler); function adapters(uint256 index) external view returns (address adapter); + function expectedOutput(FillCall calldata fillCall) external pure returns (uint256 expectedAmountOut); function finaliseWithCurrentTimestamp( address inputSettler, IInputSettler.StandardOrder calldata order, diff --git a/test/lifi/LifiSignatureRequirementFork.t.sol b/test/lifi/LifiSignatureRequirementFork.t.sol index 76d8867..7e8324a 100644 --- a/test/lifi/LifiSignatureRequirementFork.t.sol +++ b/test/lifi/LifiSignatureRequirementFork.t.sol @@ -122,14 +122,37 @@ contract LifiSignatureRequirementForkTest is Test { } function _fillCall(IInputSettler.StandardOrder memory order, bytes32 orderId) internal view returns (bytes memory) { + ILiquidLaneLifiExecutor.FillRoute[] memory routes = new ILiquidLaneLifiExecutor.FillRoute[](1); + routes[0] = ILiquidLaneLifiExecutor.FillRoute({ + adapter: address(adapter), + amountIn: order.inputs[0][1], + expectedAmountOut: 10 ether, + minAmountOut: 10 ether, + discount: ILiquidLaneLifiExecutor.FillDiscount({ + discountId: bytes32(0), + discountSwap: ILiquidLaneAdapter.DiscountSwap({ + discount: ILiquidLaneAdapter.Discount({ + tokenToRedeem: address(0), + discount: 0, + signer: address(0), + protocol: address(0), + nonce: 0, + deadline: 0 + }), + signerSignature: "", + protocolDeadline: 0 + }), + protocolSignature: "" + }) + }); return abi.encode( ILiquidLaneLifiExecutor.FillCall({ - adapter: address(adapter), orderId: orderId, output: order.outputs[0], fillDeadline: order.fillDeadline, solver: _id(solver), - fillAfter: 0 + fillAfter: 0, + routes: routes }) ); } @@ -166,6 +189,18 @@ 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 8bc72fa..0806f1f 100644 --- a/test/lifi/LiquidLaneLifiExecutor.t.sol +++ b/test/lifi/LiquidLaneLifiExecutor.t.sol @@ -48,15 +48,14 @@ contract LiquidLaneLifiExecutorTest is Test { } function testFinaliseCallbackRedeemsInputThenFillsAndAttestsOutput() public { - adapter.setBonus(1 ether); rwa.mint(address(inputSettler), 10 ether); vm.expectEmit(true, true, true, true, address(executor)); emit ILiquidLaneLifiExecutor.InputRedeemed( - ORDER_ID, address(adapter), address(rwa), address(outputToken), 10 ether, 10 ether + 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, SOLVER, address(outputToken), recipient, 9 ether); + emit ILiquidLaneLifiExecutor.OutputFilled(ORDER_ID, SOLVER, address(outputToken), recipient, 9 ether, 1 ether); inputSettler.finalise(address(executor), _inputs(10 ether), _fillCallData(9 ether)); @@ -70,7 +69,6 @@ contract LiquidLaneLifiExecutorTest is Test { } function testFinaliseWithCurrentTimestampCallsSignaturePathAndCallback() public { - adapter.setBonus(1 ether); rwa.mint(address(inputSettler), 10 ether); vm.warp(1_717_171); @@ -151,6 +149,164 @@ contract LiquidLaneLifiExecutorTest is Test { executor.finaliseWithCurrentTimestamp(address(inputSettler), order, SOLVER_ADDR, address(executor), call, ""); } + function testFinaliseWithCurrentTimestampRejectsInsufficientMinimumOutput() public { + IInputSettler.StandardOrder memory order = _order(10 ether, 9 ether); + 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( + address(inputSettler), order, SOLVER_ADDR, address(executor), abi.encode(fillCall), "" + ); + } + + function testFinaliseWithCurrentTimestampRejectsInvalidRouteOutputBounds() public { + IInputSettler.StandardOrder memory order = _order(10 ether, 9 ether); + 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( + address(inputSettler), order, SOLVER_ADDR, address(executor), abi.encode(fillCall), "" + ); + } + + 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; + + executor.finaliseWithCurrentTimestamp( + address(inputSettler), order, SOLVER_ADDR, address(executor), abi.encode(fillCall), "" + ); + + assertEq(outputToken.balanceOf(recipient), 9 ether); + assertEq(outputToken.balanceOf(address(executor)), 1 ether); + } + + function testFinaliseWithCurrentTimestampAcceptsPrivateDiscountRoute() 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); + + executor.finaliseWithCurrentTimestamp( + address(inputSettler), order, SOLVER_ADDR, address(executor), abi.encode(fillCall), hex"1234" + ); + + 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); + 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( + address(inputSettler), order, SOLVER_ADDR, address(executor), abi.encode(fillCall), "" + ); + } + + function testFinaliseWithCurrentTimestampRejectsExpiredPrivateDiscount() 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.routes[0] = _discountRoute(address(adapter), 10 ether, 10 ether, keccak256("discount"), 0); + fillCall.routes[0].discount.discountSwap.discount.deadline = 999; + + vm.expectRevert( + abi.encodeWithSelector(ILiquidLaneLifiExecutor.DiscountExpired.selector, uint48(999), uint48(1100), 1000) + ); + executor.finaliseWithCurrentTimestamp( + address(inputSettler), order, SOLVER_ADDR, address(executor), abi.encode(fillCall), "" + ); + } + + function testFinaliseWithCurrentTimestampRejectsPrivateDiscountTokenMismatch() 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.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( + address(inputSettler), order, SOLVER_ADDR, address(executor), abi.encode(fillCall), "" + ); + } + + function testFinaliseWithCurrentTimestampAppliesAdapterMinDiscount() public { + adapter.setMinDiscount(100_000); + IInputSettler.StandardOrder memory order = _order(10 ether, 9 ether); + 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( + address(inputSettler), order, SOLVER_ADDR, address(executor), abi.encode(fillCall), "" + ); + } + + function testFinaliseWithCurrentTimestampRejectsRouteInputMismatch() public { + IInputSettler.StandardOrder memory order = _order(10 ether, 9 ether); + 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; + + vm.expectRevert(abi.encodeWithSelector(ILiquidLaneLifiExecutor.RouteInputMismatch.selector, 9 ether, 10 ether)); + executor.finaliseWithCurrentTimestamp( + address(inputSettler), order, SOLVER_ADDR, address(executor), abi.encode(fillCall), "" + ); + } + + function testFinaliseWithCurrentTimestampRejectsEmptyRoutes() public { + IInputSettler.StandardOrder memory order = _order(10 ether, 9 ether); + 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( + address(inputSettler), order, SOLVER_ADDR, address(executor), abi.encode(fillCall), "" + ); + } + function testFinaliseWithCurrentTimestampRejectsFillDeadlineMismatch() public { IInputSettler.StandardOrder memory order = _order(10 ether, 9 ether); ILiquidLaneLifiExecutor.FillCall memory fillCall = @@ -260,6 +416,7 @@ contract LiquidLaneLifiExecutorTest is Test { function testOrderFinalisedRejectsUnallowedAdapter() public { MockLifiAdapter otherAdapter = new MockLifiAdapter(outputToken); + inputSettler.setOrderStatus(ORDER_ID, ORDER_STATUS_CLAIMED); rwa.mint(address(executor), 10 ether); vm.expectRevert(ILiquidLaneLifiExecutor.AdapterNotAllowed.selector); @@ -307,11 +464,44 @@ contract LiquidLaneLifiExecutorTest is Test { adapter.setNextOutputAmount(8 ether); rwa.mint(address(executor), 10 ether); - vm.expectRevert(ILiquidLaneLifiExecutor.InsufficientOutput.selector); + vm.expectRevert(abi.encodeWithSelector(ILiquidLaneLifiExecutor.InsufficientOutput.selector, 10 ether, 8 ether)); vm.prank(address(inputSettler)); executor.orderFinalised(_inputs(10 ether), _fillCallData(9 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)); + + assertEq(outputToken.balanceOf(recipient), 9 ether); + assertEq(outputToken.balanceOf(address(executor)), 0.5 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); + + 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)); + } + function testOrderFinalisedDutchOutputUsesResolvedAmount() public { vm.warp(1000); inputSettler.setOrderStatus(ORDER_ID, ORDER_STATUS_CLAIMED); @@ -349,7 +539,9 @@ contract LiquidLaneLifiExecutorTest is Test { MandateOutput memory output = _output(9 ether, _dutchContext(900, 1100, 0.01 ether)); - vm.expectRevert(ILiquidLaneLifiExecutor.InsufficientOutput.selector); + vm.expectRevert( + abi.encodeWithSelector(ILiquidLaneLifiExecutor.InsufficientOutput.selector, 10 ether, 9.5 ether) + ); vm.prank(address(inputSettler)); executor.orderFinalised(_inputs(10 ether), _fillCallData(output)); } @@ -479,6 +671,33 @@ contract LiquidLaneLifiExecutorTest is Test { assertEq(rwa.balanceOf(recipient), 10 ether); } + function testOrderFinalisedExecutesMultipleRoutesAndKeepsSurplus() public { + MockLifiAdapter secondAdapter = new MockLifiAdapter(outputToken); + outputToken.mint(address(secondAdapter), 100 ether); + address[] memory adapters = new address[](2); + adapters[0] = address(adapter); + adapters[1] = address(secondAdapter); + + vm.prank(owner); + executor.setAdapters(adapters); + + 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); + } + function testSetAdaptersReplacesAllowlistAndOwnerSweepsSurplus() public { MockLifiAdapter nextAdapter = new MockLifiAdapter(outputToken); address[] memory adapters = new address[](1); @@ -567,13 +786,79 @@ contract LiquidLaneLifiExecutorTest is Test { view returns (ILiquidLaneLifiExecutor.FillCall memory) { + ILiquidLaneLifiExecutor.FillRoute[] memory routes = new ILiquidLaneLifiExecutor.FillRoute[](1); + routes[0] = _directRoute(fillAdapter, 10 ether, 10 ether); return ILiquidLaneLifiExecutor.FillCall({ - adapter: fillAdapter, orderId: orderId, output: output, fillDeadline: uint32(block.timestamp + 1 hours), solver: SOLVER, - fillAfter: 0 + fillAfter: 0, + routes: routes + }); + } + + function _directRoute(address fillAdapter, uint256 amountIn, uint256 expectedAmountOut) + internal + pure + returns (ILiquidLaneLifiExecutor.FillRoute memory) + { + return ILiquidLaneLifiExecutor.FillRoute({ + adapter: fillAdapter, + amountIn: amountIn, + expectedAmountOut: expectedAmountOut, + minAmountOut: expectedAmountOut, + discount: _emptyDiscount() + }); + } + + function _discountRoute( + address fillAdapter, + uint256 amountIn, + uint256 expectedAmountOut, + bytes32 discountId, + uint256 discount + ) internal view returns (ILiquidLaneLifiExecutor.FillRoute memory) { + return ILiquidLaneLifiExecutor.FillRoute({ + adapter: fillAdapter, + amountIn: amountIn, + expectedAmountOut: expectedAmountOut, + minAmountOut: expectedAmountOut, + discount: ILiquidLaneLifiExecutor.FillDiscount({ + discountId: discountId, + discountSwap: ILiquidLaneAdapter.DiscountSwap({ + discount: ILiquidLaneAdapter.Discount({ + tokenToRedeem: address(rwa), + discount: discount, + signer: SOLVER_ADDR, + protocol: address(0xBEEF), + nonce: 1, + deadline: uint48(block.timestamp + 100) + }), + signerSignature: hex"1234", + protocolDeadline: uint48(block.timestamp + 100) + }), + protocolSignature: hex"5678" + }) + }); + } + + function _emptyDiscount() internal pure returns (ILiquidLaneLifiExecutor.FillDiscount memory) { + return ILiquidLaneLifiExecutor.FillDiscount({ + discountId: bytes32(0), + discountSwap: ILiquidLaneAdapter.DiscountSwap({ + discount: ILiquidLaneAdapter.Discount({ + tokenToRedeem: address(0), + discount: 0, + signer: address(0), + protocol: address(0), + nonce: 0, + deadline: 0 + }), + signerSignature: "", + protocolDeadline: 0 + }), + protocolSignature: "" }); } @@ -901,7 +1186,9 @@ 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_; @@ -915,6 +1202,26 @@ contract MockLifiAdapter is ILiquidLaneAdapter { 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"); @@ -925,12 +1232,16 @@ contract MockLifiAdapter is ILiquidLaneAdapter { function swap(ILiquidLaneAdapter.SignedSwap calldata, bytes calldata) public {} - function swap(ILiquidLaneAdapter.DiscountSwap calldata, bytes calldata, address, uint256) - public - pure - returns (uint256) - { - return 0; + function swap( + ILiquidLaneAdapter.DiscountSwap calldata discountSwap, + bytes calldata, + address recipient, + uint256 amountIn + ) public returns (uint256) { + require(IERC20(discountSwap.discount.tokenToRedeem).balanceOf(address(this)) >= amountIn, "missing input"); + uint256 amountOut = amountIn * (1_000_000 - discountSwap.discount.discount) / 1_000_000; + outputToken.transfer(recipient, amountOut); + return amountOut; } } From 6817848a805a6f854b153265409f264ff2cdbea7 Mon Sep 17 00:00:00 2001 From: alrxy Date: Mon, 20 Jul 2026 17:44:28 +0700 Subject: [PATCH 3/6] feat(lifi): make executor the solver --- src/lifi/LiquidLaneLifiExecutor.sol | 172 ++++---- src/lifi/interfaces/IInputSettler.sol | 5 +- .../interfaces/ILiquidLaneLifiExecutor.sol | 29 +- ...ementFork.t.sol => LifiExecutorFork.t.sol} | 53 +-- test/lifi/LiquidLaneLifiExecutor.t.sol | 380 +++++++++++------- 5 files changed, 315 insertions(+), 324 deletions(-) rename test/lifi/{LifiSignatureRequirementFork.t.sol => LifiExecutorFork.t.sol} (77%) diff --git a/src/lifi/LiquidLaneLifiExecutor.sol b/src/lifi/LiquidLaneLifiExecutor.sol index 29f1f7d..72351cd 100644 --- a/src/lifi/LiquidLaneLifiExecutor.sol +++ b/src/lifi/LiquidLaneLifiExecutor.sol @@ -8,11 +8,13 @@ import {ILiquidLaneLifiExecutor} from "./interfaces/ILiquidLaneLifiExecutor.sol" import {IOutputSettler, MandateOutput} 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); @@ -21,7 +23,7 @@ interface ILiquidLaneRate { } /// @title LiquidLaneLifiExecutor -/// @notice LI.FI same-chain callback that redeems released inputs and fills the order output atomically. +/// @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; @@ -41,23 +43,13 @@ contract LiquidLaneLifiExecutor is Ownable, ReentrancyGuard, ILiquidLaneLifiExec /// @inheritdoc ILiquidLaneLifiExecutor address public immutable OUTPUT_SETTLER; - /* STATE */ - - /// @dev LiquidLane adapters allowed for LI.FI input redemptions. - address[] public adapters; - /// @inheritdoc ILiquidLaneLifiExecutor - mapping(address adapter => bool allowed) public isAdapterAllowed; - /* CONSTRUCTOR */ - constructor(address inputSettler, address outputSettler, address owner_, address[] memory initAdapters) - Ownable(owner_) - { + constructor(address inputSettler, address outputSettler, address owner_) Ownable(owner_) { if (inputSettler == address(0) || outputSettler == address(0) || owner_ == address(0)) revert ZeroAddress(); INPUT_SETTLER = inputSettler; OUTPUT_SETTLER = outputSettler; - _setAdapters(initAdapters); } /* FINALISE WRAPPER */ @@ -74,29 +66,21 @@ contract LiquidLaneLifiExecutor is Ownable, ReentrancyGuard, ILiquidLaneLifiExec } /// @inheritdoc ILiquidLaneLifiExecutor - function finaliseWithCurrentTimestamp( - address inputSettler, - IInputSettler.StandardOrder calldata order, - address solver, - address destination, - bytes calldata call, - bytes calldata orderOwnerSignature - ) external { - if (inputSettler != INPUT_SETTLER) revert InvalidInputSettler(); - if (destination != address(this)) revert InvalidDestination(); - - bytes32 solverId = _addressIdentifier(solver); + function finaliseWithCurrentTimestamp(IInputSettler.StandardOrder calldata order, bytes calldata call) + external + onlyOwner + { ILiquidLaneLifiExecutor.FillCall memory fillCall = abi.decode(call, (ILiquidLaneLifiExecutor.FillCall)); - bytes32 orderId = _validateFinaliseCall(order, fillCall, solverId); + 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)); IInputSettler.SolveParams[] memory solveParams = new IInputSettler.SolveParams[](1); - solveParams[0] = IInputSettler.SolveParams({timestamp: uint32(block.timestamp), solver: solverId}); + solveParams[0] = IInputSettler.SolveParams({timestamp: uint32(block.timestamp), solver: executorId}); - IInputSettler(INPUT_SETTLER) - .finaliseWithSignature(order, solveParams, _addressIdentifier(destination), call, orderOwnerSignature); + IInputSettler(INPUT_SETTLER).finalise(order, solveParams, executorId, call); } /* IINPUTCALLBACK */ @@ -109,7 +93,7 @@ contract LiquidLaneLifiExecutor is Ownable, ReentrancyGuard, ILiquidLaneLifiExec ILiquidLaneLifiExecutor.FillCall memory fillCall = abi.decode(executionData, (ILiquidLaneLifiExecutor.FillCall)); _validateFillAfter(fillCall.fillAfter, fillCall.output.context); - bytes32 solver = _cleanIdentifier(fillCall.solver); + bytes32 solver = _addressIdentifier(address(this)); uint256 resolvedAmountOut = _resolveOutputAmount(fillCall.output, solver); _validateOutput(fillCall.output); address outputToken = _outputToken(fillCall.output); @@ -127,42 +111,8 @@ contract LiquidLaneLifiExecutor is Ownable, ReentrancyGuard, ILiquidLaneLifiExec revert InsufficientMinimumOutput(minAmountOut, resolvedAmountOut); } - for (uint256 i; i < fillCall.routes.length; ++i) { - IERC20(tokenIn).safeTransfer(fillCall.routes[i].adapter, fillCall.routes[i].amountIn); - } - uint256 outputBefore = IERC20(outputToken).balanceOf(address(this)); - - for (uint256 i; i < fillCall.routes.length; ++i) { - ILiquidLaneLifiExecutor.FillRoute memory route = fillCall.routes[i]; - uint256 redeemedAmountOut; - if (route.discount.discountId == bytes32(0)) { - redeemedAmountOut = executableAmountOuts[i]; - ILiquidLaneAdapter(route.adapter) - .swap( - ILiquidLaneAdapter.Swap({ - recipient: address(this), - tokenIn: tokenIn, - amountIn: route.amountIn, - amountOut: redeemedAmountOut - }) - ); - } else { - redeemedAmountOut = ILiquidLaneAdapter(route.adapter) - .swap(route.discount.discountSwap, route.discount.protocolSignature, address(this), route.amountIn); - } - emit InputRedeemed( - fillCall.orderId, - route.adapter, - tokenIn, - outputToken, - route.amountIn, - redeemedAmountOut, - route.discount.discountId - ); - } - - uint256 outputGained = IERC20(outputToken).balanceOf(address(this)) - outputBefore; - if (outputGained < minAmountOut) revert InsufficientOutput(minAmountOut, outputGained); + uint256 outputGained = _redeemInputs(fillCall, tokenIn, outputToken, executableAmountOuts); + uint256 surplus = outputGained - resolvedAmountOut; IERC20(outputToken).forceApprove(OUTPUT_SETTLER, resolvedAmountOut); IOutputSettler(OUTPUT_SETTLER) @@ -176,19 +126,14 @@ contract LiquidLaneLifiExecutor is Ownable, ReentrancyGuard, ILiquidLaneLifiExec outputToken, _identifierAddress(fillCall.output.recipient), resolvedAmountOut, - outputGained - resolvedAmountOut + surplus ); } /* OWNER */ /// @inheritdoc ILiquidLaneLifiExecutor - function setAdapters(address[] calldata newAdapters) external onlyOwner { - _setAdapters(newAdapters); - } - - /// @inheritdoc ILiquidLaneLifiExecutor - function sweepERC20(address token, address to, uint256 amount) external onlyOwner { + 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); @@ -196,22 +141,30 @@ contract LiquidLaneLifiExecutor is Ownable, ReentrancyGuard, ILiquidLaneLifiExec } /// @inheritdoc ILiquidLaneLifiExecutor - function sweepNative(address to, uint256 amount) external onlyOwner { + function sweepNative(address to, uint256 amount) external onlyOwner nonReentrant { if (to == address(0)) revert ZeroAddress(); payable(to).sendValue(amount); emit SweepNative(to, amount); } + /* EIP-1271 */ + + /// @inheritdoc IERC1271 + function isValidSignature(bytes32 hash, bytes calldata signature) external view returns (bytes4) { + if (SignatureChecker.isValidSignatureNow(owner(), hash, signature)) { + return IERC1271.isValidSignature.selector; + } + return 0xffffffff; + } + /* INTERNAL */ function _validateFinaliseCall( IInputSettler.StandardOrder calldata order, - ILiquidLaneLifiExecutor.FillCall memory fillCall, - bytes32 solver - ) internal returns (bytes32 orderId) { + ILiquidLaneLifiExecutor.FillCall memory fillCall + ) internal view returns (bytes32 orderId) { _validateFillAfter(fillCall.fillAfter, fillCall.output.context); - if (_cleanIdentifier(fillCall.solver) != solver) revert SolverMismatch(); orderId = IInputSettler(INPUT_SETTLER).orderIdentifier(order); if (fillCall.orderId != orderId) revert InvalidOrderId(); @@ -224,29 +177,52 @@ contract LiquidLaneLifiExecutor is Ownable, ReentrancyGuard, ILiquidLaneLifiExec } _validateOutput(fillCall.output); - (uint256 minAmountOut,) = _validateRoutes(fillCall.routes, _inputToken(order.inputs[0][0]), order.inputs[0][1]); - uint256 resolvedAmountOut = _resolveOutputAmount(fillCall.output, solver); - if (resolvedAmountOut > minAmountOut) { - revert InsufficientMinimumOutput(minAmountOut, resolvedAmountOut); - } } - function _setAdapters(address[] memory newAdapters) internal { - for (uint256 i; i < adapters.length; ++i) { - isAdapterAllowed[adapters[i]] = false; + 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); } - delete adapters; - for (uint256 i; i < newAdapters.length; ++i) { - address adapter = newAdapters[i]; - if (adapter == address(0)) revert ZeroAddress(); - if (isAdapterAllowed[adapter]) revert DuplicateAdapter(); + 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); + } - isAdapterAllowed[adapter] = true; - adapters.push(adapter); - } + uint256 routeOutput = IERC20(outputToken).balanceOf(address(this)) - outputBefore; + if (routeOutput < route.minAmountOut) { + revert RouteOutputTooLow(route.adapter, route.minAmountOut, routeOutput); + } + outputGained += routeOutput; - emit SetAdapters(newAdapters); + emit InputRedeemed( + fillCall.orderId, + route.adapter, + tokenIn, + outputToken, + route.amountIn, + routeOutput, + route.discount.discountId + ); + } } function _validateOutput(MandateOutput memory output) internal view { @@ -275,7 +251,7 @@ contract LiquidLaneLifiExecutor is Ownable, ReentrancyGuard, ILiquidLaneLifiExec if (route.minAmountOut == 0 || route.minAmountOut > route.expectedAmountOut) { revert InvalidRouteOutputBounds(route.expectedAmountOut, route.minAmountOut); } - if (!isAdapterAllowed[route.adapter]) revert AdapterNotAllowed(); + if (route.adapter == address(0)) revert ZeroAddress(); (uint256 currentAmountOut, uint256 maxAssets) = _routeState(route, tokenIn); uint256 executableAmountOut = _executableAmountOut(route, currentAmountOut, maxAssets); @@ -422,6 +398,8 @@ contract LiquidLaneLifiExecutor is Ownable, ReentrancyGuard, ILiquidLaneLifiExec } 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(); } @@ -441,10 +419,6 @@ contract LiquidLaneLifiExecutor is Ownable, ReentrancyGuard, ILiquidLaneLifiExec } } - function _cleanIdentifier(bytes32 identifier) internal pure returns (bytes32 clean) { - clean = _addressIdentifier(_identifierAddress(identifier)); - } - function _addressIdentifier(address addr) internal pure returns (bytes32 identifier) { if (addr == address(0)) revert InvalidIdentifier(); return bytes32(uint256(uint160(addr))); diff --git a/src/lifi/interfaces/IInputSettler.sol b/src/lifi/interfaces/IInputSettler.sol index 34625ab..85509f7 100644 --- a/src/lifi/interfaces/IInputSettler.sol +++ b/src/lifi/interfaces/IInputSettler.sol @@ -33,11 +33,10 @@ interface IInputSettler { function orderIdentifier(StandardOrder calldata order) external view returns (bytes32 orderId); - function finaliseWithSignature( + function finalise( StandardOrder calldata order, SolveParams[] calldata solveParams, bytes32 destination, - bytes calldata call, - bytes calldata orderOwnerSignature + bytes calldata call ) external; } diff --git a/src/lifi/interfaces/ILiquidLaneLifiExecutor.sol b/src/lifi/interfaces/ILiquidLaneLifiExecutor.sol index 0716277..2936e62 100644 --- a/src/lifi/interfaces/ILiquidLaneLifiExecutor.sol +++ b/src/lifi/interfaces/ILiquidLaneLifiExecutor.sol @@ -7,15 +7,15 @@ import {IInputSettler} from "./IInputSettler.sol"; import {MandateOutput} from "./IOutputSettler.sol"; import {ILiquidLaneAdapter} from "../../interfaces/ILiquidLaneAdapter.sol"; +import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol"; + /** * @title ILiquidLaneLifiExecutor - * @notice LI.FI same-chain executor callback for on-chain orders. + * @notice LI.FI same-chain solver and executor callback for on-chain orders. */ -interface ILiquidLaneLifiExecutor is IInputCallback { +interface ILiquidLaneLifiExecutor is IInputCallback, IERC1271 { /* ERRORS */ - error AdapterNotAllowed(); - error DuplicateAdapter(); error DiscountExpired(uint48 deadline, uint48 protocolDeadline, uint256 currentTime); error DiscountTokenMismatch(address expectedToken, address discountToken); error EmptyRoutes(); @@ -23,12 +23,9 @@ interface ILiquidLaneLifiExecutor is IInputCallback { error FillTooEarly(uint32 fillAfter, uint32 currentTime); error FillAfterWithoutAuction(); error InsufficientMinimumOutput(uint256 minimumAmountOut, uint256 resolvedAmountOut); - error InsufficientOutput(uint256 minimumAmountOut, uint256 receivedAmountOut); error InvalidAmount(); - error InvalidDestination(); error InvalidDiscount(uint256 discount, uint256 minimumDiscount); error InvalidInputCount(); - error InvalidInputSettler(); error InvalidIdentifier(); error InvalidOrderId(); error InvalidOrderOutput(); @@ -44,7 +41,6 @@ interface ILiquidLaneLifiExecutor is IInputCallback { error PrivateRouteExceedsCapacity(address adapter, uint256 amountOut, uint256 maxAssets); error RouteInputMismatch(uint256 routedAmountIn, uint256 orderAmountIn); error RouteOutputTooLow(address adapter, uint256 minAmountOut, uint256 availableAmountOut); - error SolverMismatch(); error UnknownOutputContext(bytes1 contextType); error ZeroAddress(); @@ -64,7 +60,7 @@ interface ILiquidLaneLifiExecutor is IInputCallback { /** * @notice One atomic LiquidLane redemption leg. - * @param adapter Allowed LiquidLane adapter. + * @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. @@ -83,7 +79,6 @@ interface ILiquidLaneLifiExecutor is IInputCallback { * @param orderId OIF order id. * @param output Single output to fill and attest. * @param fillDeadline Fill deadline carried by the order. - * @param solver Solver identifier written into filler data and attestation. * @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. @@ -92,7 +87,6 @@ interface ILiquidLaneLifiExecutor is IInputCallback { bytes32 orderId; MandateOutput output; uint32 fillDeadline; - bytes32 solver; uint32 fillAfter; FillRoute[] routes; } @@ -116,7 +110,6 @@ interface ILiquidLaneLifiExecutor is IInputCallback { uint256 amount, uint256 surplus ); - event SetAdapters(address[] adapters); event SweepERC20(address indexed token, address indexed to, uint256 amount); event SweepNative(address indexed to, uint256 amount); @@ -124,18 +117,8 @@ interface ILiquidLaneLifiExecutor is IInputCallback { function INPUT_SETTLER() external view returns (address inputSettler); function OUTPUT_SETTLER() external view returns (address outputSettler); - function adapters(uint256 index) external view returns (address adapter); function expectedOutput(FillCall calldata fillCall) external pure returns (uint256 expectedAmountOut); - function finaliseWithCurrentTimestamp( - address inputSettler, - IInputSettler.StandardOrder calldata order, - address solver, - address destination, - bytes calldata call, - bytes calldata orderOwnerSignature - ) external; - function isAdapterAllowed(address adapter) external view returns (bool allowed); - function setAdapters(address[] calldata newAdapters) external; + function finaliseWithCurrentTimestamp(IInputSettler.StandardOrder calldata order, bytes calldata call) external; function sweepERC20(address token, address to, uint256 amount) external; function sweepNative(address to, uint256 amount) external; } diff --git a/test/lifi/LifiSignatureRequirementFork.t.sol b/test/lifi/LifiExecutorFork.t.sol similarity index 77% rename from test/lifi/LifiSignatureRequirementFork.t.sol rename to test/lifi/LifiExecutorFork.t.sol index 7e8324a..5595173 100644 --- a/test/lifi/LifiSignatureRequirementFork.t.sol +++ b/test/lifi/LifiExecutorFork.t.sol @@ -12,19 +12,16 @@ import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {Test} from "forge-std/Test.sol"; interface IInputSettlerEscrowLike { - function DOMAIN_SEPARATOR() external view returns (bytes32); function open(IInputSettler.StandardOrder calldata order) external; function orderIdentifier(IInputSettler.StandardOrder calldata order) external view returns (bytes32); function orderStatus(bytes32 orderId) external view returns (uint8); } -contract LifiSignatureRequirementForkTest is Test { +contract LifiExecutorForkTest is Test { address internal constant INPUT_SETTLER = 0x000025c3226C00B2Cdc200005a1600509f4e00C0; address internal constant OUTPUT_SETTLER = 0x0000000000eC36B683C2E6AC89e9A75989C22a2e; address internal user = makeAddr("user"); - address internal solver; - uint256 internal solverKey; address internal owner = makeAddr("owner"); address internal recipient = makeAddr("recipient"); @@ -40,37 +37,24 @@ contract LifiSignatureRequirementForkTest is Test { } vm.createSelectFork(rpcUrl); - (solver, solverKey) = makeAddrAndKey("solver"); - inputToken = new ForkTestToken("Fork RWA", "FRWA"); outputToken = new ForkTestToken("Fork USD", "FUSD"); adapter = new ForkMintingAdapter(outputToken); - address[] memory adapters = new address[](1); - adapters[0] = address(adapter); - executor = new LiquidLaneLifiExecutor(INPUT_SETTLER, OUTPUT_SETTLER, owner, adapters); + executor = new LiquidLaneLifiExecutor(INPUT_SETTLER, OUTPUT_SETTLER, owner); } - function testEmptyOrderOwnerSignatureRevertsOnRealSettler() external { - IInputSettler.StandardOrder memory order = _openOrder(10 ether, 9 ether, "empty"); + 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.expectRevert(); - executor.finaliseWithCurrentTimestamp(INPUT_SETTLER, order, solver, address(executor), call, ""); - } - - function testSignedAllowOpenSettlesOnRealSettler() external { - IInputSettler.StandardOrder memory order = _openOrder(10 ether, 9 ether, "signed"); - bytes32 orderId = IInputSettlerEscrowLike(INPUT_SETTLER).orderIdentifier(order); - bytes memory call = _fillCall(order, orderId); - - executor.finaliseWithCurrentTimestamp( - INPUT_SETTLER, order, solver, address(executor), call, _allowOpenSignature(orderId, address(executor), call) - ); + vm.prank(owner); + executor.finaliseWithCurrentTimestamp(order, call); assertEq(IInputSettlerEscrowLike(INPUT_SETTLER).orderStatus(orderId), 2, "claimed"); assertEq(outputToken.balanceOf(recipient), 9 ether, "recipient output"); + assertEq(outputToken.balanceOf(address(executor)), 1 ether, "executor surplus"); } function _openOrder(uint256 amountIn, uint256 amountOut, string memory salt) @@ -111,7 +95,7 @@ contract LifiSignatureRequirementForkTest is Test { order = IInputSettler.StandardOrder({ user: user, - nonce: uint256(keccak256(abi.encodePacked("signature-requirement", salt))), + nonce: uint256(keccak256(abi.encodePacked("lifi-executor", salt))), originChainId: block.chainid, expires: uint32(block.timestamp + 1 hours), fillDeadline: uint32(block.timestamp + 30 minutes), @@ -150,33 +134,12 @@ contract LifiSignatureRequirementForkTest is Test { orderId: orderId, output: order.outputs[0], fillDeadline: order.fillDeadline, - solver: _id(solver), fillAfter: 0, routes: routes }) ); } - function _allowOpenSignature(bytes32 orderId, address destination, bytes memory call) - internal - view - returns (bytes memory) - { - bytes32 structHash = keccak256( - abi.encode( - keccak256("AllowOpen(bytes32 orderId,bytes32 destination,bytes call)"), - orderId, - _id(destination), - keccak256(call) - ) - ); - bytes32 digest = keccak256( - abi.encodePacked("\x19\x01", IInputSettlerEscrowLike(INPUT_SETTLER).DOMAIN_SEPARATOR(), structHash) - ); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(solverKey, digest); - return abi.encodePacked(r, s, v); - } - function _id(address addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(addr))); } diff --git a/test/lifi/LiquidLaneLifiExecutor.t.sol b/test/lifi/LiquidLaneLifiExecutor.t.sol index 0806f1f..8f59d84 100644 --- a/test/lifi/LiquidLaneLifiExecutor.t.sol +++ b/test/lifi/LiquidLaneLifiExecutor.t.sol @@ -10,18 +10,23 @@ import {ILiquidLaneLifiExecutor} from "../../src/lifi/interfaces/ILiquidLaneLifi import {IOutputSettler, MandateOutput} from "../../src/lifi/interfaces/IOutputSettler.sol"; 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 {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {Test} from "forge-std/Test.sol"; +interface IOutputCallbackLike { + function outputFilled(bytes32 token, uint256 amount, bytes calldata callbackData) external; +} + contract LiquidLaneLifiExecutorTest is Test { bytes32 internal constant ORDER_ID = keccak256("order"); - address internal constant SOLVER_ADDR = address(0x515011); - bytes32 internal constant SOLVER = bytes32(uint256(uint160(SOLVER_ADDR))); + address internal constant DISCOUNT_SIGNER = address(0x515011); + bytes32 internal constant MOCK_SOLVER = bytes32(uint256(uint160(address(0x515012)))); uint8 internal constant ORDER_STATUS_DEPOSITED = 1; uint8 internal constant ORDER_STATUS_CLAIMED = 2; - address internal owner = makeAddr("owner"); + address internal owner; address internal recipient = makeAddr("recipient"); address internal collector = makeAddr("collector"); @@ -33,6 +38,7 @@ contract LiquidLaneLifiExecutorTest is Test { LiquidLaneLifiExecutor internal executor; function setUp() public { + owner = address(this); rwa = new TestToken("RWA", "RWA"); outputToken = new TestToken("USD", "USD"); adapter = new MockLifiAdapter(outputToken); @@ -40,9 +46,7 @@ contract LiquidLaneLifiExecutorTest is Test { outputSettler = new MockOutputSettler(); inputSettler.setOrderStatus(ORDER_ID, ORDER_STATUS_DEPOSITED); - address[] memory adapters = new address[](1); - adapters[0] = address(adapter); - executor = new LiquidLaneLifiExecutor(address(inputSettler), address(outputSettler), owner, adapters); + executor = new LiquidLaneLifiExecutor(address(inputSettler), address(outputSettler), owner); outputToken.mint(address(adapter), 100 ether); } @@ -55,20 +59,22 @@ contract LiquidLaneLifiExecutorTest is Test { 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, SOLVER, address(outputToken), recipient, 9 ether, 1 ether); + emit ILiquidLaneLifiExecutor.OutputFilled( + ORDER_ID, _id(address(executor)), address(outputToken), recipient, 9 ether, 1 ether + ); - inputSettler.finalise(address(executor), _inputs(10 ether), _fillCallData(9 ether)); + 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(), SOLVER); + assertEq(outputSettler.lastSolver(), _id(address(executor))); assertTrue(outputSettler.attested()); } - function testFinaliseWithCurrentTimestampCallsSignaturePathAndCallback() public { + function testFinaliseWithCurrentTimestampCallsFinaliseAsExecutor() public { rwa.mint(address(inputSettler), 10 ether); vm.warp(1_717_171); @@ -77,14 +83,11 @@ contract LiquidLaneLifiExecutorTest is Test { inputSettler.setOrderStatus(orderId, ORDER_STATUS_DEPOSITED); bytes memory call = _fillCallData(orderId, 9 ether); - executor.finaliseWithCurrentTimestamp( - address(inputSettler), order, SOLVER_ADDR, address(executor), call, hex"1234" - ); + executor.finaliseWithCurrentTimestamp(order, call); assertEq(inputSettler.lastTimestamp(), uint32(block.timestamp)); - assertEq(inputSettler.lastSolver(), SOLVER); + assertEq(inputSettler.lastSolver(), _id(address(executor))); assertEq(inputSettler.lastDestination(), _id(address(executor))); - assertEq(inputSettler.lastOrderOwnerSignature(), hex"1234"); assertEq(inputSettler.orderStatus(orderId), ORDER_STATUS_CLAIMED); assertEq(outputToken.balanceOf(recipient), 9 ether); assertTrue(outputSettler.attested()); @@ -98,23 +101,42 @@ contract LiquidLaneLifiExecutorTest is Test { vm.expectRevert( abi.encodeWithSelector(ILiquidLaneLifiExecutor.InvalidOrderStatus.selector, ORDER_STATUS_CLAIMED) ); - executor.finaliseWithCurrentTimestamp( - address(inputSettler), order, SOLVER_ADDR, address(executor), _fillCallData(orderId, 9 ether), "" - ); + executor.finaliseWithCurrentTimestamp(order, _fillCallData(orderId, 9 ether)); } - function testFinaliseWithCurrentTimestampRejectsSolverMismatch() public { + function testFinaliseWithCurrentTimestampRejectsNonOwner() public { + address caller = makeAddr("caller"); IInputSettler.StandardOrder memory order = _order(10 ether, 9 ether); - vm.expectRevert(ILiquidLaneLifiExecutor.SolverMismatch.selector); - executor.finaliseWithCurrentTimestamp( - address(inputSettler), - order, - makeAddr("wrongSolver"), - address(executor), - _fillCallData(_orderId(order), 9 ether), - "" - ); + vm.expectRevert(abi.encodeWithSignature("OwnableUnauthorizedAccount(address)", caller)); + vm.prank(caller); + executor.finaliseWithCurrentTimestamp(order, _fillCallData(_orderId(order), 9 ether)); + } + + function testIsValidSignatureAcceptsOwner() public { + uint256 ownerKey = 0xA11CE; + LiquidLaneLifiExecutor ownedExecutor = + new LiquidLaneLifiExecutor(address(inputSettler), address(outputSettler), vm.addr(ownerKey)); + 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)); + 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)); + + assertEq(ownedExecutor.isValidSignature(keccak256("lifi registration"), hex"deadbeef"), bytes4(0xffffffff)); } function testFinaliseWithCurrentTimestampRejectsOrderIdMismatch() public { @@ -124,9 +146,7 @@ contract LiquidLaneLifiExecutorTest is Test { fillCall.orderId = keccak256("wrong order"); vm.expectRevert(ILiquidLaneLifiExecutor.InvalidOrderId.selector); - executor.finaliseWithCurrentTimestamp( - address(inputSettler), order, SOLVER_ADDR, address(executor), abi.encode(fillCall), "" - ); + executor.finaliseWithCurrentTimestamp(order, abi.encode(fillCall)); } function testFinaliseWithCurrentTimestampRejectsOutputCountMismatch() public { @@ -138,7 +158,7 @@ contract LiquidLaneLifiExecutorTest is Test { bytes memory call = _fillCallData(_orderId(order), 9 ether); vm.expectRevert(ILiquidLaneLifiExecutor.InvalidOutputCount.selector); - executor.finaliseWithCurrentTimestamp(address(inputSettler), order, SOLVER_ADDR, address(executor), call, ""); + executor.finaliseWithCurrentTimestamp(order, call); } function testFinaliseWithCurrentTimestampRejectsOutputMismatch() public { @@ -146,11 +166,12 @@ contract LiquidLaneLifiExecutorTest is Test { bytes memory call = _fillCallData(_orderId(order), 8 ether); vm.expectRevert(ILiquidLaneLifiExecutor.InvalidOrderOutput.selector); - executor.finaliseWithCurrentTimestamp(address(inputSettler), order, SOLVER_ADDR, address(executor), call, ""); + 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; @@ -159,13 +180,12 @@ contract LiquidLaneLifiExecutorTest is Test { vm.expectRevert( abi.encodeWithSelector(ILiquidLaneLifiExecutor.InsufficientMinimumOutput.selector, 8 ether, 9 ether) ); - executor.finaliseWithCurrentTimestamp( - address(inputSettler), order, SOLVER_ADDR, address(executor), abi.encode(fillCall), "" - ); + executor.finaliseWithCurrentTimestamp(order, abi.encode(fillCall)); } function testFinaliseWithCurrentTimestampRejectsInvalidRouteOutputBounds() 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 = 9 ether; @@ -174,9 +194,7 @@ contract LiquidLaneLifiExecutorTest is Test { vm.expectRevert( abi.encodeWithSelector(ILiquidLaneLifiExecutor.InvalidRouteOutputBounds.selector, 9 ether, 9.1 ether) ); - executor.finaliseWithCurrentTimestamp( - address(inputSettler), order, SOLVER_ADDR, address(executor), abi.encode(fillCall), "" - ); + executor.finaliseWithCurrentTimestamp(order, abi.encode(fillCall)); } function testFinaliseWithCurrentTimestampClampsTargetToCurrentRate() public { @@ -187,9 +205,7 @@ contract LiquidLaneLifiExecutorTest is Test { ILiquidLaneLifiExecutor.FillCall memory fillCall = _fillCallStruct(address(adapter), orderId, order.outputs[0]); fillCall.routes[0].expectedAmountOut = 11 ether; - executor.finaliseWithCurrentTimestamp( - address(inputSettler), order, SOLVER_ADDR, address(executor), abi.encode(fillCall), "" - ); + executor.finaliseWithCurrentTimestamp(order, abi.encode(fillCall)); assertEq(outputToken.balanceOf(recipient), 9 ether); assertEq(outputToken.balanceOf(address(executor)), 1 ether); @@ -206,9 +222,7 @@ contract LiquidLaneLifiExecutorTest is Test { 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); - executor.finaliseWithCurrentTimestamp( - address(inputSettler), order, SOLVER_ADDR, address(executor), abi.encode(fillCall), hex"1234" - ); + executor.finaliseWithCurrentTimestamp(order, abi.encode(fillCall)); assertEq(outputToken.balanceOf(recipient), 8 ether); assertEq(outputToken.balanceOf(address(executor)), 1 ether); @@ -219,19 +233,19 @@ contract LiquidLaneLifiExecutorTest is Test { vm.warp(1000); 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]); 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( - address(inputSettler), order, SOLVER_ADDR, address(executor), abi.encode(fillCall), "" - ); + 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); @@ -240,14 +254,13 @@ contract LiquidLaneLifiExecutorTest is Test { vm.expectRevert( abi.encodeWithSelector(ILiquidLaneLifiExecutor.DiscountExpired.selector, uint48(999), uint48(1100), 1000) ); - executor.finaliseWithCurrentTimestamp( - address(inputSettler), order, SOLVER_ADDR, address(executor), abi.encode(fillCall), "" - ); + executor.finaliseWithCurrentTimestamp(order, abi.encode(fillCall)); } function testFinaliseWithCurrentTimestampRejectsPrivateDiscountTokenMismatch() 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); @@ -260,14 +273,13 @@ contract LiquidLaneLifiExecutorTest is Test { fillCall.routes[0].discount.discountSwap.discount.tokenToRedeem ) ); - executor.finaliseWithCurrentTimestamp( - address(inputSettler), order, SOLVER_ADDR, address(executor), abi.encode(fillCall), "" - ); + 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]); @@ -276,13 +288,12 @@ contract LiquidLaneLifiExecutorTest is Test { ILiquidLaneLifiExecutor.RouteOutputTooLow.selector, address(adapter), 10 ether, 9 ether ) ); - executor.finaliseWithCurrentTimestamp( - address(inputSettler), order, SOLVER_ADDR, address(executor), abi.encode(fillCall), "" - ); + 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; @@ -290,21 +301,18 @@ contract LiquidLaneLifiExecutorTest is Test { fillCall.routes[0].minAmountOut = 9 ether; vm.expectRevert(abi.encodeWithSelector(ILiquidLaneLifiExecutor.RouteInputMismatch.selector, 9 ether, 10 ether)); - executor.finaliseWithCurrentTimestamp( - address(inputSettler), order, SOLVER_ADDR, address(executor), abi.encode(fillCall), "" - ); + executor.finaliseWithCurrentTimestamp(order, abi.encode(fillCall)); } function testFinaliseWithCurrentTimestampRejectsEmptyRoutes() 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 = new ILiquidLaneLifiExecutor.FillRoute[](0); vm.expectRevert(ILiquidLaneLifiExecutor.EmptyRoutes.selector); - executor.finaliseWithCurrentTimestamp( - address(inputSettler), order, SOLVER_ADDR, address(executor), abi.encode(fillCall), "" - ); + executor.finaliseWithCurrentTimestamp(order, abi.encode(fillCall)); } function testFinaliseWithCurrentTimestampRejectsFillDeadlineMismatch() public { @@ -314,9 +322,7 @@ contract LiquidLaneLifiExecutorTest is Test { fillCall.fillDeadline = order.fillDeadline + 1; vm.expectRevert(ILiquidLaneLifiExecutor.InvalidOrderOutput.selector); - executor.finaliseWithCurrentTimestamp( - address(inputSettler), order, SOLVER_ADDR, address(executor), abi.encode(fillCall), "" - ); + executor.finaliseWithCurrentTimestamp(order, abi.encode(fillCall)); } function testFinaliseWithCurrentTimestampRejectsFillAfterWithoutAuction() public { @@ -327,9 +333,7 @@ contract LiquidLaneLifiExecutorTest is Test { fillCall.fillAfter = uint32(block.timestamp); vm.expectRevert(ILiquidLaneLifiExecutor.FillAfterWithoutAuction.selector); - executor.finaliseWithCurrentTimestamp( - address(inputSettler), order, SOLVER_ADDR, address(executor), abi.encode(fillCall), "" - ); + executor.finaliseWithCurrentTimestamp(order, abi.encode(fillCall)); } function testFinaliseWithCurrentTimestampRejectsAuctionFillTooEarly() public { @@ -341,47 +345,22 @@ contract LiquidLaneLifiExecutorTest is Test { fillCall.fillAfter = uint32(block.timestamp + 1); vm.expectRevert(abi.encodeWithSelector(ILiquidLaneLifiExecutor.FillTooEarly.selector, 1001, 1000)); - executor.finaliseWithCurrentTimestamp( - address(inputSettler), order, SOLVER_ADDR, address(executor), abi.encode(fillCall), "" - ); + executor.finaliseWithCurrentTimestamp(order, abi.encode(fillCall)); } - function testFinaliseWithCurrentTimestampRejectsWrongInputSettler() public { - IInputSettler.StandardOrder memory order = _order(10 ether, 9 ether); - - vm.expectRevert(ILiquidLaneLifiExecutor.InvalidInputSettler.selector); - executor.finaliseWithCurrentTimestamp( - makeAddr("wrongSettler"), order, SOLVER_ADDR, address(executor), _fillCallData(_orderId(order), 9 ether), "" - ); - } - - function testFinaliseWithCurrentTimestampRejectsWrongDestination() public { - IInputSettler.StandardOrder memory order = _order(10 ether, 9 ether); - - vm.expectRevert(ILiquidLaneLifiExecutor.InvalidDestination.selector); - executor.finaliseWithCurrentTimestamp( - address(inputSettler), - order, - SOLVER_ADDR, - makeAddr("wrongDestination"), - _fillCallData(_orderId(order), 9 ether), - "" - ); - } - - function testMockFinaliseWithSignatureRejectsStaleOrFutureTimestamp() public { + function testMockFinaliseRejectsStaleOrFutureTimestamp() public { IInputSettler.StandardOrder memory order = _order(10 ether, 9 ether); IInputSettler.SolveParams[] memory solveParams = new IInputSettler.SolveParams[](1); - solveParams[0].solver = SOLVER; + solveParams[0].solver = _id(address(executor)); vm.warp(100); solveParams[0].timestamp = 99; vm.expectRevert(MockInputSettler.TimestampPassed.selector); - inputSettler.finaliseWithSignature(order, solveParams, _id(address(executor)), _fillCallData(9 ether), ""); + inputSettler.finalise(order, solveParams, _id(address(executor)), _fillCallData(9 ether)); solveParams[0].timestamp = 101; vm.expectRevert(MockInputSettler.TimestampNotPassed.selector); - inputSettler.finaliseWithSignature(order, solveParams, _id(address(executor)), _fillCallData(9 ether), ""); + inputSettler.finalise(order, solveParams, _id(address(executor)), _fillCallData(9 ether)); } function testOrderFinalisedRejectsNonInputSettler() public { @@ -414,14 +393,45 @@ contract LiquidLaneLifiExecutorTest is Test { executor.orderFinalised(inputs, _fillCallData(9 ether)); } - function testOrderFinalisedRejectsUnallowedAdapter() public { + 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); - vm.expectRevert(ILiquidLaneLifiExecutor.AdapterNotAllowed.selector); vm.prank(address(inputSettler)); executor.orderFinalised(_inputs(10 ether), _fillCallData(address(otherAdapter), 9 ether)); + + assertEq(rwa.balanceOf(address(otherAdapter)), 10 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)); + + vm.prank(address(inputSettler)); + executor.orderFinalised(_inputs(10 ether), _fillCallData(output)); + + assertEq(outputToken.balanceOf(address(executor)), 10 ether); + } + + function testOrderFinalisedKeepsCallbackRefundSeparateFromSurplusAccounting() 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"; + + vm.prank(address(inputSettler)); + executor.orderFinalised(_inputs(10 ether), _fillCallData(output)); + + assertEq(outputToken.balanceOf(address(outputRecipient)), 8 ether); + assertEq(outputToken.balanceOf(address(executor)), 2 ether); } function testOrderFinalisedRejectsFillAfterWithoutAuction() public { @@ -438,7 +448,8 @@ contract LiquidLaneLifiExecutorTest is Test { function testOrderFinalisedRejectsFillAfterForExclusiveLimitOutput() public { vm.warp(1000); inputSettler.setOrderStatus(ORDER_ID, ORDER_STATUS_CLAIMED); - MandateOutput memory output = _output(9 ether, _exclusiveContext(SOLVER, uint32(block.timestamp))); + 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); @@ -464,7 +475,11 @@ contract LiquidLaneLifiExecutorTest is Test { adapter.setNextOutputAmount(8 ether); rwa.mint(address(executor), 10 ether); - vm.expectRevert(abi.encodeWithSelector(ILiquidLaneLifiExecutor.InsufficientOutput.selector, 10 ether, 8 ether)); + vm.expectRevert( + abi.encodeWithSelector( + ILiquidLaneLifiExecutor.RouteOutputTooLow.selector, address(adapter), 10 ether, 8 ether + ) + ); vm.prank(address(inputSettler)); executor.orderFinalised(_inputs(10 ether), _fillCallData(9 ether)); } @@ -540,7 +555,9 @@ contract LiquidLaneLifiExecutorTest is Test { MandateOutput memory output = _output(9 ether, _dutchContext(900, 1100, 0.01 ether)); vm.expectRevert( - abi.encodeWithSelector(ILiquidLaneLifiExecutor.InsufficientOutput.selector, 10 ether, 9.5 ether) + abi.encodeWithSelector( + ILiquidLaneLifiExecutor.RouteOutputTooLow.selector, address(adapter), 10 ether, 9.5 ether + ) ); vm.prank(address(inputSettler)); executor.orderFinalised(_inputs(10 ether), _fillCallData(output)); @@ -554,7 +571,9 @@ contract LiquidLaneLifiExecutorTest is Test { MandateOutput memory output = _output(9 ether, _exclusiveContext(exclusiveFor, 1001)); vm.expectRevert( - abi.encodeWithSelector(ILiquidLaneLifiExecutor.ExclusiveForMismatch.selector, exclusiveFor, SOLVER) + abi.encodeWithSelector( + ILiquidLaneLifiExecutor.ExclusiveForMismatch.selector, exclusiveFor, _id(address(executor)) + ) ); vm.prank(address(inputSettler)); executor.orderFinalised(_inputs(10 ether), _fillCallData(output)); @@ -642,23 +661,9 @@ contract LiquidLaneLifiExecutorTest is Test { executor.orderFinalised(_inputs(10 ether), _fillCallData(output)); } - function testOrderFinalisedRejectsDirtySolverIdentifier() public { - ILiquidLaneLifiExecutor.FillCall memory fillCall = _fillCallStruct(address(adapter), _output(9 ether)); - fillCall.solver = bytes32(uint256(SOLVER) | (uint256(1) << 160)); - - vm.expectRevert(ILiquidLaneLifiExecutor.InvalidIdentifier.selector); - vm.prank(address(inputSettler)); - executor.orderFinalised(_inputs(10 ether), abi.encode(fillCall)); - } - function testOrderFinalisedSupportsSameInputAndOutputTokenAccounting() public { inputSettler.setOrderStatus(ORDER_ID, ORDER_STATUS_CLAIMED); MockLifiAdapter sameTokenAdapter = new MockLifiAdapter(rwa); - address[] memory adapters = new address[](1); - adapters[0] = address(sameTokenAdapter); - - vm.prank(owner); - executor.setAdapters(adapters); rwa.mint(address(executor), 10 ether); rwa.mint(address(sameTokenAdapter), 10 ether); @@ -674,12 +679,6 @@ contract LiquidLaneLifiExecutorTest is Test { function testOrderFinalisedExecutesMultipleRoutesAndKeepsSurplus() public { MockLifiAdapter secondAdapter = new MockLifiAdapter(outputToken); outputToken.mint(address(secondAdapter), 100 ether); - address[] memory adapters = new address[](2); - adapters[0] = address(adapter); - adapters[1] = address(secondAdapter); - - vm.prank(owner); - executor.setAdapters(adapters); inputSettler.setOrderStatus(ORDER_ID, ORDER_STATUS_CLAIMED); rwa.mint(address(executor), 10 ether); @@ -698,33 +697,68 @@ contract LiquidLaneLifiExecutorTest is Test { assertEq(outputToken.balanceOf(address(executor)), 1 ether); } - function testSetAdaptersReplacesAllowlistAndOwnerSweepsSurplus() public { - MockLifiAdapter nextAdapter = new MockLifiAdapter(outputToken); - address[] memory adapters = new address[](1); - adapters[0] = address(nextAdapter); + function testOrderFinalisedRejectsActualRouteUnderDeliveryEvenWhenAggregatePasses() public { + MockLifiAdapter secondAdapter = new MockLifiAdapter(outputToken); + outputToken.mint(address(secondAdapter), 100 ether); + adapter.setNextOutputAmount(3 ether); + secondAdapter.setBonus(1 ether); - vm.prank(owner); - executor.setAdapters(adapters); + inputSettler.setOrderStatus(ORDER_ID, ORDER_STATUS_CLAIMED); + rwa.mint(address(executor), 10 ether); - assertFalse(executor.isAdapterAllowed(address(adapter))); - assertTrue(executor.isAdapterAllowed(address(nextAdapter))); - assertEq(executor.adapters(0), address(nextAdapter)); + 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); - outputToken.mint(address(executor), 2 ether); - vm.prank(owner); - executor.sweepERC20(address(outputToken), collector, 2 ether); + 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)); + } - assertEq(outputToken.balanceOf(collector), 2 ether); + function testMockOutputSettlerFillIsIdempotent() public { + MandateOutput memory output = _output(9 ether); + outputToken.mint(address(this), 18 ether); + outputToken.approve(address(outputSettler), 18 ether); + + bytes32 firstRecord = outputSettler.fill(ORDER_ID, output, uint48(block.timestamp), abi.encode(MOCK_SOLVER)); + bytes32 secondRecord = outputSettler.fill(ORDER_ID, output, uint48(block.timestamp), abi.encode(MOCK_SOLVER)); + + assertEq(secondRecord, firstRecord); + assertEq(outputToken.balanceOf(recipient), 9 ether); + assertEq(outputToken.balanceOf(address(this)), 9 ether); + } + + function testMockOutputSettlerInvokesOutputCallback() public { + MockOutputRecipient outputRecipient = new MockOutputRecipient(); + MandateOutput memory output = _output(9 ether); + output.recipient = _id(address(outputRecipient)); + output.callbackData = hex"1234"; + outputToken.mint(address(this), 9 ether); + outputToken.approve(address(outputSettler), 9 ether); + + outputSettler.fill(ORDER_ID, output, uint48(block.timestamp), abi.encode(MOCK_SOLVER)); + + assertEq(outputRecipient.token(), output.token); + assertEq(outputRecipient.amount(), 9 ether); + assertEq(outputRecipient.callbackData(), hex"1234"); } - function testSetAdaptersRejectsDuplicates() public { - address[] memory adapters = new address[](2); - adapters[0] = address(adapter); - adapters[1] = address(adapter); + 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.expectRevert(ILiquidLaneLifiExecutor.DuplicateAdapter.selector); vm.prank(owner); - executor.setAdapters(adapters); + executor.sweepERC20(address(outputToken), collector, 1 ether); + + assertEq(outputToken.balanceOf(collector), 1 ether); + assertEq(outputToken.balanceOf(address(executor)), 0); } function _order(uint256 amountIn, uint256 amountOut) internal view returns (IInputSettler.StandardOrder memory) { @@ -749,6 +783,11 @@ contract LiquidLaneLifiExecutorTest is Test { inputs[0][1] = amount; } + function _openOrder(IInputSettler.StandardOrder memory order) internal { + inputSettler.setOrderStatus(_orderId(order), 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))); } @@ -792,7 +831,6 @@ contract LiquidLaneLifiExecutorTest is Test { orderId: orderId, output: output, fillDeadline: uint32(block.timestamp + 1 hours), - solver: SOLVER, fillAfter: 0, routes: routes }); @@ -830,7 +868,7 @@ contract LiquidLaneLifiExecutorTest is Test { discount: ILiquidLaneAdapter.Discount({ tokenToRedeem: address(rwa), discount: discount, - signer: SOLVER_ADDR, + signer: DISCOUNT_SIGNER, protocol: address(0xBEEF), nonce: 1, deadline: uint48(block.timestamp + 100) @@ -969,7 +1007,6 @@ contract MockInputSettler is IInputSettler { uint32 public lastTimestamp; bytes32 public lastSolver; bytes32 public lastDestination; - bytes public lastOrderOwnerSignature; function setOrderStatus(bytes32 orderId, uint8 status) public { orderStatus[orderId] = status; @@ -979,12 +1016,11 @@ contract MockInputSettler is IInputSettler { return _orderId(order); } - function finaliseWithSignature( + function finalise( StandardOrder calldata order, SolveParams[] calldata solveParams, bytes32 destination, - bytes calldata call, - bytes calldata orderOwnerSignature + bytes calldata call ) external { if (solveParams.length != 1) revert InvalidTimestampLength(); if (solveParams[0].timestamp < block.timestamp) revert TimestampPassed(); @@ -993,12 +1029,11 @@ contract MockInputSettler is IInputSettler { lastTimestamp = solveParams[0].timestamp; lastSolver = solveParams[0].solver; lastDestination = destination; - lastOrderOwnerSignature = orderOwnerSignature; _finalise(_orderId(order), address(uint160(uint256(destination))), order.inputs, call); } - function finalise(address destination, uint256[2][] memory inputs, bytes memory call) public { + 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); } @@ -1078,13 +1113,20 @@ contract MockOutputSettler is IOutputSettler { require(output.oracle == _id(address(this)), "oracle"); uint256 resolvedAmount = _resolveOutputAmount(output, solver); - IERC20(token).safeTransferFrom(msg.sender, recipient, resolvedAmount); + bytes32 outputHash = _outputHash(output); + fillRecordHash = fillRecords[orderId][outputHash]; + if (fillRecordHash != bytes32(0)) return fillRecordHash; fillRecordHash = keccak256(abi.encodePacked(solver, uint32(block.timestamp))); - fillRecords[orderId][_outputHash(output)] = fillRecordHash; + fillRecords[orderId][outputHash] = fillRecordHash; lastOrderId = orderId; lastSolver = solver; lastOutputAmount = resolvedAmount; + + IERC20(token).safeTransferFrom(msg.sender, recipient, resolvedAmount); + if (output.callbackData.length != 0) { + IOutputCallbackLike(recipient).outputFilled(output.token, resolvedAmount, output.callbackData); + } } function setAttestation(bytes32 orderId, bytes32 solver, uint32 timestamp, MandateOutput calldata output) external { @@ -1245,6 +1287,36 @@ contract MockLifiAdapter is ILiquidLaneAdapter { } } +contract MockOutputRecipient is IOutputCallbackLike { + bytes32 public token; + uint256 public amount; + bytes public callbackData; + + function outputFilled(bytes32 token_, uint256 amount_, bytes calldata callbackData_) external { + token = token_; + amount = amount_; + callbackData = callbackData_; + } +} + +contract RefundingOutputRecipient is IOutputCallbackLike { + using SafeERC20 for IERC20; + + IERC20 public immutable token; + address public immutable recipient; + uint256 public immutable refundAmount; + + constructor(IERC20 token_, address recipient_, uint256 refundAmount_) { + token = token_; + recipient = recipient_; + refundAmount = refundAmount_; + } + + function outputFilled(bytes32, uint256, bytes calldata) external { + token.safeTransfer(recipient, refundAmount); + } +} + contract TestToken is ERC20 { constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) {} From 2141c6ba032b425b9613b28bd61dc0ca71deedf1 Mon Sep 17 00:00:00 2001 From: Andrey Date: Tue, 21 Jul 2026 14:25:42 +0400 Subject: [PATCH 4/6] refactor: lifi --- script/deploy/DeployExecutor.s.sol | 16 +- script/deploy/DeployLifiExecutor.s.sol | 31 + src/Executor.sol | 15 +- src/interfaces/IExecutor.sol | 7 + src/lifi/LiquidLaneLifiExecutor.sol | 429 ++------- src/lifi/interfaces/IInputSettler.sol | 7 - .../interfaces/ILiquidLaneLifiExecutor.sol | 69 +- src/oev/SymbioticOevSolver.sol | 7 +- test/CoreMirrorIntegration.t.sol | 10 +- test/Reactor.t.sol | 39 +- test/ReactorFork.t.sol | 10 +- test/lifi/LifiExecutorFork.t.sol | 43 +- test/lifi/LiquidLaneLifiExecutor.t.sol | 909 +++++------------- 13 files changed, 422 insertions(+), 1170 deletions(-) create mode 100644 script/deploy/DeployLifiExecutor.s.sol diff --git a/script/deploy/DeployExecutor.s.sol b/script/deploy/DeployExecutor.s.sol index d6c3b0e..d4ffe0b 100644 --- a/script/deploy/DeployExecutor.s.sol +++ b/script/deploy/DeployExecutor.s.sol @@ -3,21 +3,31 @@ pragma solidity ^0.8.28; import {Script, console2} from "forge-std/Script.sol"; +import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + import {Executor} from "../../src/Executor.sol"; // forge script rfq/reactor/script/deploy/DeployExecutor.s.sol:DeployExecutorScript --rpc-url=RPC --private-key PRIVATE_KEY --broadcast contract DeployExecutorScript is Script { - function run() public returns (Executor executor) { + function run() public returns (Executor executor, address implementation) { address reactor = vm.envAddress("REACTOR"); address admin = vm.envAddress("ADMIN"); + address proxyAdminOwner = vm.envAddress("PROXY_ADMIN_OWNER"); address caller = vm.envAddress("CALLER"); address[] memory callers = new address[](1); callers[0] = caller; vm.startBroadcast(); - executor = new Executor(reactor, admin, callers); + Executor impl = new Executor(reactor); + TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy( + address(impl), proxyAdminOwner, abi.encodeCall(Executor.initialize, (admin, callers)) + ); vm.stopBroadcast(); - console2.log("Deployed Executor:", address(executor)); + implementation = address(impl); + executor = Executor(payable(address(proxy))); + + console2.log("Deployed Executor implementation:", implementation); + console2.log("Deployed Executor proxy:", address(executor)); } } diff --git a/script/deploy/DeployLifiExecutor.s.sol b/script/deploy/DeployLifiExecutor.s.sol new file mode 100644 index 0000000..c8d5cf8 --- /dev/null +++ b/script/deploy/DeployLifiExecutor.s.sol @@ -0,0 +1,31 @@ +// 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"; + +// forge script rfq/reactor/script/deploy/DeployLifiExecutor.s.sol:DeployLifiExecutorScript --rpc-url=RPC --private-key PRIVATE_KEY --broadcast +contract DeployLifiExecutorScript is Script { + function run() public returns (LiquidLaneLifiExecutor executor, address implementation) { + address inputSettler = vm.envAddress("INPUT_SETTLER"); + address outputSettler = vm.envAddress("OUTPUT_SETTLER"); + address admin = vm.envAddress("ADMIN"); + address proxyAdminOwner = vm.envAddress("PROXY_ADMIN_OWNER"); + + vm.startBroadcast(); + LiquidLaneLifiExecutor impl = new LiquidLaneLifiExecutor(inputSettler, outputSettler); + TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy( + address(impl), proxyAdminOwner, abi.encodeCall(LiquidLaneLifiExecutor.initialize, (admin)) + ); + vm.stopBroadcast(); + + implementation = address(impl); + executor = LiquidLaneLifiExecutor(address(proxy)); + + console2.log("Deployed LiquidLaneLifiExecutor implementation:", implementation); + console2.log("Deployed LiquidLaneLifiExecutor proxy:", address(executor)); + } +} diff --git a/src/Executor.sol b/src/Executor.sol index 0dd2d02..21028a9 100644 --- a/src/Executor.sol +++ b/src/Executor.sol @@ -8,12 +8,15 @@ import {IReactor, NATIVE} from "./interfaces/IReactor.sol"; import {Address} from "@openzeppelin/contracts/utils/Address.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 {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; /// @title Executor /// @notice Caller-gated executor that forwards fills into Reactor and handles execution callbacks. -contract Executor is Ownable, IExecutor { +/// @dev Deployed behind a transparent proxy; the Reactor address is immutable in the implementation +/// while ownership and the caller list live in proxy storage set by {initialize}. +contract Executor is Initializable, OwnableUpgradeable, IExecutor { using Address for address payable; using SafeERC20 for IERC20; using Address for address; @@ -30,8 +33,14 @@ contract Executor is Ownable, IExecutor { /* CONSTRUCTOR */ - constructor(address reactor, address owner, address[] memory initCallers) Ownable(owner) { + constructor(address reactor) { REACTOR = reactor; + _disableInitializers(); + } + + /// @inheritdoc IExecutor + function initialize(address owner, address[] calldata initCallers) external initializer { + __Ownable_init(owner); callers = initCallers; } diff --git a/src/interfaces/IExecutor.sol b/src/interfaces/IExecutor.sol index 31ad6e4..0a0a234 100644 --- a/src/interfaces/IExecutor.sol +++ b/src/interfaces/IExecutor.sol @@ -45,6 +45,13 @@ interface IExecutor { /* FUNCTIONS */ + /** + * @notice Initializes the proxy with its owner and allowed caller list. + * @param owner Owner authorized to manage the caller list. + * @param initCallers Initial addresses allowed to call the fill entrypoints. + */ + function initialize(address owner, address[] calldata initCallers) external; + /** * @notice Returns an allowed caller by index. * @param index Caller index. diff --git a/src/lifi/LiquidLaneLifiExecutor.sol b/src/lifi/LiquidLaneLifiExecutor.sol index 72351cd..b8a2736 100644 --- a/src/lifi/LiquidLaneLifiExecutor.sol +++ b/src/lifi/LiquidLaneLifiExecutor.sol @@ -2,40 +2,28 @@ // 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"; /// @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 lives in proxy storage set by {initialize}. +contract LiquidLaneLifiExecutor is Initializable, OwnableUpgradeable, 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; - /* IMMUTABLES */ /// @inheritdoc ILiquidLaneLifiExecutor @@ -45,391 +33,94 @@ contract LiquidLaneLifiExecutor is Ownable, ReentrancyGuard, ILiquidLaneLifiExec /* CONSTRUCTOR */ - constructor(address inputSettler, address outputSettler, address owner_) 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(); } - /* 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; - } + function initialize(address owner_) external initializer { + __Ownable_init(owner_); } + /* FINALISE WRAPPER */ + /// @inheritdoc ILiquidLaneLifiExecutor - function finaliseWithCurrentTimestamp(IInputSettler.StandardOrder calldata order, bytes calldata call) + function finaliseWithCurrentTimestamp(IInputSettler.StandardOrder calldata order, FillRoute[] calldata routes) external onlyOwner { - 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); + function orderFinalised(uint256[2][] calldata inputs, bytes calldata executionData) external { + if (INPUT_SETTLER != msg.sender) revert NotInputSettler(); - uint8 status = IInputSettler(INPUT_SETTLER).orderStatus(fillCall.orderId); - if (status != ORDER_STATUS_CLAIMED) revert InvalidOrderStatus(status); - - address tokenIn = _inputToken(inputs[0][0]); - uint256 amountIn = inputs[0][1]; - if (amountIn == 0) revert InvalidAmount(); - - (uint256 minAmountOut, uint256[] memory executableAmountOuts) = - _validateRoutes(fillCall.routes, tokenIn, amountIn); - if (resolvedAmountOut > minAmountOut) { - revert InsufficientMinimumOutput(minAmountOut, resolvedAmountOut); - } - - uint256 outputGained = _redeemInputs(fillCall, tokenIn, outputToken, executableAmountOuts); - uint256 surplus = outputGained - resolvedAmountOut; - - IERC20(outputToken).forceApprove(OUTPUT_SETTLER, resolvedAmountOut); - 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 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); - } + FillCall memory fillCall = abi.decode(executionData, (FillCall)); - /// @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); - } - - /* EIP-1271 */ - - /// @inheritdoc IERC1271 - function isValidSignature(bytes32 hash, bytes calldata signature) external view returns (bytes4) { - if (SignatureChecker.isValidSignatureNow(owner(), hash, signature)) { - return IERC1271.isValidSignature.selector; - } - return 0xffffffff; - } - - /* INTERNAL */ - - 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) { + // 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); } - for (uint256 i; i < fillCall.routes.length; ++i) { - ILiquidLaneLifiExecutor.FillRoute memory route = fillCall.routes[i]; - uint256 outputBefore = IERC20(outputToken).balanceOf(address(this)); + 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: executableAmountOuts[i] - }) + 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); } - - 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); + // 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); } - 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)); - } + /* EIP-1271 */ - function _readBytes32(bytes memory data, uint256 offset) internal pure returns (bytes32 value) { - assembly ("memory-safe") { - value := mload(add(add(data, 0x20), offset)) + /// @inheritdoc IERC1271 + function isValidSignature(bytes32 hash, bytes calldata signature) external view returns (bytes4) { + if (SignatureChecker.isValidSignatureNow(owner(), hash, signature)) { + return IERC1271.isValidSignature.selector; } + return 0xffffffff; } - - 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 2936e62..025ec26 100644 --- a/src/lifi/interfaces/ILiquidLaneLifiExecutor.sol +++ b/src/lifi/interfaces/ILiquidLaneLifiExecutor.sol @@ -16,33 +16,7 @@ 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 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(); /* STRUCTS */ @@ -62,63 +36,36 @@ 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 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 expectedOutput(FillCall calldata fillCall) external pure returns (uint256 expectedAmountOut); - function finaliseWithCurrentTimestamp(IInputSettler.StandardOrder calldata order, bytes calldata call) external; - function sweepERC20(address token, address to, uint256 amount) external; - function sweepNative(address to, uint256 amount) external; + function initialize(address owner) external; + function finaliseWithCurrentTimestamp(IInputSettler.StandardOrder calldata order, FillRoute[] calldata routes) + external; } diff --git a/src/oev/SymbioticOevSolver.sol b/src/oev/SymbioticOevSolver.sol index ac0da9b..731b004 100644 --- a/src/oev/SymbioticOevSolver.sol +++ b/src/oev/SymbioticOevSolver.sol @@ -219,11 +219,8 @@ contract SymbioticOevSolver is IOperationCallback, IMorphoLiquidateCallback, Ree ILiquidLaneAdapter(LIQUID_LANE_ADAPTER) .swap( ILiquidLaneAdapter.Swap({ - recipient: address(this), - tokenIn: ctx.collateralToken, - amountIn: ctx.seizedAssets, - amountOut: amountOut - }) + recipient: address(this), tokenIn: ctx.collateralToken, amountIn: ctx.seizedAssets, amountOut: amountOut + }) ); uint256 gained = IERC20(ctx.loanToken).balanceOf(address(this)) - loanBefore; diff --git a/test/CoreMirrorIntegration.t.sol b/test/CoreMirrorIntegration.t.sol index 34a9f31..b832777 100644 --- a/test/CoreMirrorIntegration.t.sol +++ b/test/CoreMirrorIntegration.t.sol @@ -16,6 +16,7 @@ import {IReactor, ORDER_TYPEHASH, OUTPUT_TYPEHASH, REQUEST_TYPEHASH} from "../sr import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.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"; @@ -46,7 +47,14 @@ contract LiquidLaneIntegrationTest is Test { adapterFactory.setEntity(address(adapter), true); adapterFactory.setEntity(address(secondaryAdapter), true); reactor = new Reactor(address(adapterFactory)); - executor = new Executor(address(reactor), address(this), _callers(filler)); + Executor executorImpl = new Executor(address(reactor)); + executor = Executor( + payable(new TransparentUpgradeableProxy( + address(executorImpl), + makeAddr("proxyAdminOwner"), + abi.encodeCall(Executor.initialize, (address(this), _callers(filler))) + )) + ); rwa = new IntegrationERC20("RWA", "RWA"); outputToken = new IntegrationERC20("USD", "USD"); diff --git a/test/Reactor.t.sol b/test/Reactor.t.sol index 15cafdd..b95e6f0 100644 --- a/test/Reactor.t.sol +++ b/test/Reactor.t.sol @@ -11,6 +11,8 @@ import {IReactor, NATIVE, ORDER_TYPEHASH, OUTPUT_TYPEHASH, REQUEST_TYPEHASH} fro import {IERC20Errors} from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol"; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {ReentrancyGuardTransient} from "@openzeppelin/contracts/utils/ReentrancyGuardTransient.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"; @@ -30,6 +32,7 @@ contract ReactorTest is Test { address internal vault1 = makeAddr("vault1"); address internal vault0Account = makeAddr("vault0Account"); address internal vault1Account = makeAddr("vault1Account"); + address internal proxyAdminOwner = makeAddr("proxyAdminOwner"); MockAdapter internal adapter; MockAdapter internal secondaryAdapter; @@ -48,7 +51,7 @@ contract ReactorTest is Test { adapterFactory.setEntity(address(secondaryAdapter), true); callTarget = new MockCallTarget(); reactor = new Reactor(address(adapterFactory)); - executor = new Executor(address(reactor), address(this), _callers(filler)); + executor = _deployExecutor(address(reactor), address(this), _callers(filler)); rwa = new MockERC20("RWA", "RWA"); outputToken = new MockERC20("USD", "USD"); @@ -246,7 +249,7 @@ contract ReactorTest is Test { function testExecutorRequiresCaller() public { Reactor customReactor = new Reactor(address(adapterFactory)); - Executor lockedExecutor = new Executor(address(customReactor), address(this), new address[](0)); + Executor lockedExecutor = _deployExecutor(address(customReactor), address(this), new address[](0)); outputToken.mint(address(lockedExecutor), 5 ether); @@ -276,8 +279,25 @@ contract ReactorTest is Test { assertEq(adapter.signedSwapCount(), 0); } + function testExecutorInitializeSetsOwnerAndCallers() public view { + assertEq(executor.owner(), address(this)); + assertEq(executor.callers(0), filler); + } + + function testExecutorInitializeCannotBeCalledTwice() public { + vm.expectRevert(Initializable.InvalidInitialization.selector); + executor.initialize(makeAddr("intruder"), _callers(filler)); + } + + function testExecutorImplementationInitializerIsDisabled() public { + Executor impl = new Executor(address(reactor)); + + vm.expectRevert(Initializable.InvalidInitialization.selector); + impl.initialize(makeAddr("intruder"), _callers(filler)); + } + function testFillRevertsIfExecutorDoesNotMatchOrderFiller() public { - Executor otherExecutor = new Executor(address(reactor), address(this), _callers(filler)); + Executor otherExecutor = _deployExecutor(address(reactor), address(this), _callers(filler)); IReactor.Output[] memory outputs = new IReactor.Output[](1); outputs[0] = IReactor.Output({token: address(outputToken), amount: 5 ether, recipient: swapper}); @@ -887,6 +907,17 @@ contract ReactorTest is Test { callers_[0] = caller; } + function _deployExecutor(address reactor_, address owner_, address[] memory initCallers) + internal + returns (Executor) + { + Executor impl = new Executor(reactor_); + TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy( + address(impl), proxyAdminOwner, abi.encodeCall(Executor.initialize, (owner_, initCallers)) + ); + return Executor(payable(address(proxy))); + } + function _order(IReactor.Output[] memory outputs) internal view returns (IReactor.Order memory) { return _order(outputs, 10 ether, address(executor)); } @@ -1228,6 +1259,8 @@ contract ReentrantNativeRecipient is IExecutor { bytes calldata ) external {} + function initialize(address, address[] calldata) external {} + function setCallers(address[] calldata) external {} function callers(uint256) external pure returns (address) { diff --git a/test/ReactorFork.t.sol b/test/ReactorFork.t.sol index 506dd0d..7fd8d44 100644 --- a/test/ReactorFork.t.sol +++ b/test/ReactorFork.t.sol @@ -9,6 +9,7 @@ import {ILiquidLaneAdapter} from "../src/interfaces/ILiquidLaneAdapter.sol"; import {IReactor, ORDER_TYPEHASH, OUTPUT_TYPEHASH, REQUEST_TYPEHASH} from "../src/interfaces/IReactor.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; import {Test} from "forge-std/Test.sol"; @@ -42,7 +43,14 @@ contract ReactorMainnetForkTest is Test { adapterFactory = new ForkAdapterFactory(); adapterFactory.setEntity(address(adapter), true); reactor = new Reactor(address(adapterFactory)); - executor = new Executor(address(reactor), address(this), _callers(filler)); + Executor executorImpl = new Executor(address(reactor)); + executor = Executor( + payable(new TransparentUpgradeableProxy( + address(executorImpl), + makeAddr("proxyAdminOwner"), + abi.encodeCall(Executor.initialize, (address(this), _callers(filler))) + )) + ); adapter.setAccount(vault, DAI, vaultAccount); diff --git a/test/lifi/LifiExecutorFork.t.sol b/test/lifi/LifiExecutorFork.t.sol index 5595173..e3a2a96 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; @@ -41,16 +43,19 @@ contract LifiExecutorForkTest is Test { outputToken = new ForkTestToken("Fork USD", "FUSD"); adapter = new ForkMintingAdapter(outputToken); - executor = new LiquidLaneLifiExecutor(INPUT_SETTLER, OUTPUT_SETTLER, owner); + LiquidLaneLifiExecutor impl = new LiquidLaneLifiExecutor(INPUT_SETTLER, OUTPUT_SETTLER); + TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy( + address(impl), proxyAdminOwner, abi.encodeCall(LiquidLaneLifiExecutor.initialize, (owner)) + ); + 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"); @@ -105,13 +110,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({ @@ -129,15 +137,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) { @@ -152,18 +151,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 8f59d84..d2195d1 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,680 +47,297 @@ 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); + executor = _deployExecutor(address(inputSettler), address(outputSettler), owner); outputToken.mint(address(adapter), 100 ether); } - function testFinaliseCallbackRedeemsInputThenFillsAndAttestsOutput() public { - rwa.mint(address(inputSettler), 10 ether); - - 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_) + internal + returns (LiquidLaneLifiExecutor) + { + LiquidLaneLifiExecutor impl = new LiquidLaneLifiExecutor(inputSettler_, outputSettler_); + TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy( + address(impl), proxyAdminOwner, abi.encodeCall(LiquidLaneLifiExecutor.initialize, (owner_)) ); - - 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()); + return LiquidLaneLifiExecutor(address(proxy)); } - 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 testFinaliseWithCurrentTimestampRejectsNonOwner() public { - address caller = makeAddr("caller"); - IInputSettler.StandardOrder memory order = _order(10 ether, 9 ether); - - vm.expectRevert(abi.encodeWithSignature("OwnableUnauthorizedAccount(address)", caller)); - vm.prank(caller); - executor.finaliseWithCurrentTimestamp(order, _fillCallData(_orderId(order), 9 ether)); - } - - function testIsValidSignatureAcceptsOwner() public { - uint256 ownerKey = 0xA11CE; - LiquidLaneLifiExecutor ownedExecutor = - new LiquidLaneLifiExecutor(address(inputSettler), address(outputSettler), vm.addr(ownerKey)); - 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)); - 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)); - - 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; + bytes32 orderId = _openOrder(order); - vm.expectRevert( - abi.encodeWithSelector(ILiquidLaneLifiExecutor.InvalidRouteOutputBounds.selector, 9 ether, 9.1 ether) - ); - executor.finaliseWithCurrentTimestamp(order, abi.encode(fillCall)); - } + 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); - 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; - - 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); + + ILiquidLaneLifiExecutor.FillRoute[] memory routes = new ILiquidLaneLifiExecutor.FillRoute[](1); + routes[0] = _discountRoute(address(adapter), 10 ether, keccak256("discount"), 100_000); - executor.finaliseWithCurrentTimestamp(order, abi.encode(fillCall)); + 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]); + executor.finaliseWithCurrentTimestamp(order, _directRoutes(address(adapter), 10 ether, 10.5 ether)); - 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; - - 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)); - } + executor.finaliseWithCurrentTimestamp(order, _directRoutes(address(adapter), 10 ether, 10.5 ether)); - 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); - - 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)); + order.outputs[0].context = _exclusiveContext(_id(makeAddr("otherSolver")), 1000); + _openOrder(order); - vm.warp(100); - solveParams[0].timestamp = 99; - vm.expectRevert(MockInputSettler.TimestampPassed.selector); - inputSettler.finalise(order, solveParams, _id(address(executor)), _fillCallData(9 ether)); + executor.finaliseWithCurrentTimestamp(order, _directRoutes(address(adapter), 10 ether, 10 ether)); - solveParams[0].timestamp = 101; - vm.expectRevert(MockInputSettler.TimestampNotPassed.selector); - inputSettler.finalise(order, solveParams, _id(address(executor)), _fillCallData(9 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); - - vm.expectRevert(ILiquidLaneLifiExecutor.FillAfterWithoutAuction.selector); - vm.prank(address(inputSettler)); - executor.orderFinalised(_inputs(10 ether), abi.encode(fillCall)); - } - - function testOrderFinalisedRejectsAuctionFillTooEarly() 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); - - vm.expectRevert( - abi.encodeWithSelector( - ILiquidLaneLifiExecutor.RouteOutputTooLow.selector, address(adapter), 10 ether, 8 ether - ) - ); - vm.prank(address(inputSettler)); - executor.orderFinalised(_inputs(10 ether), _fillCallData(9 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)); + IInputSettler.StandardOrder memory order = _order(10 ether, 9 ether); + order.outputs[0].context = _exclusiveContext(_id(makeAddr("otherSolver")), 1001); + _openOrder(order); - assertEq(outputToken.balanceOf(recipient), 9 ether); - assertEq(outputToken.balanceOf(address(executor)), 0.5 ether); + vm.expectRevert(bytes("exclusive")); + executor.finaliseWithCurrentTimestamp(order, _directRoutes(address(adapter), 10 ether, 10 ether)); } - function testOrderFinalisedRejectsPrivateOutputAboveLiveCapacity() public { + function testFinaliseWithCurrentTimestampBubblesInsufficientOutputForResolvedDutchAmount() 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); + 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.PrivateRouteExceedsCapacity.selector, address(adapter), 9 ether, 8.5 ether + IERC20Errors.ERC20InsufficientBalance.selector, address(executor), 9.5 ether, 10 ether ) ); - vm.prank(address(inputSettler)); - executor.orderFinalised(_inputs(10 ether), abi.encode(fillCall)); - } - - 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)); - - vm.prank(address(inputSettler)); - executor.orderFinalised(_inputs(10 ether), _fillCallData(output)); - - assertEq(outputToken.balanceOf(recipient), 10 ether); - assertEq(outputSettler.lastOutputAmount(), 10 ether); + executor.finaliseWithCurrentTimestamp(order, _directRoutes(address(adapter), 10 ether, 9.5 ether)); } - function testOrderFinalisedExclusiveDutchOutputUsesResolvedAmount() public { - vm.warp(1000); - inputSettler.setOrderStatus(ORDER_ID, ORDER_STATUS_CLAIMED); - rwa.mint(address(executor), 10 ether); - - MandateOutput memory output = - _output(9 ether, _exclusiveDutchContext(_id(makeAddr("otherSolver")), 900, 1100, 0.01 ether)); - - vm.prank(address(inputSettler)); - executor.orderFinalised(_inputs(10 ether), _fillCallData(output)); + function testFinaliseWithCurrentTimestampBubblesAlreadyClaimedOrder() public { + IInputSettler.StandardOrder memory order = _order(10 ether, 9 ether); + inputSettler.setOrderStatus(_orderId(order), ORDER_STATUS_CLAIMED); - assertEq(outputToken.balanceOf(recipient), 10 ether); - assertEq(outputSettler.lastOutputAmount(), 10 ether); + vm.expectRevert(MockInputSettler.InvalidOrderStatus.selector); + executor.finaliseWithCurrentTimestamp(order, _directRoutes(address(adapter), 10 ether, 10 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); - - MandateOutput memory output = _output(9 ether, _dutchContext(900, 1100, 0.01 ether)); - - 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)); - } - - 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)); - - vm.expectRevert( - abi.encodeWithSelector( - ILiquidLaneLifiExecutor.ExclusiveForMismatch.selector, exclusiveFor, _id(address(executor)) - ) - ); - vm.prank(address(inputSettler)); - executor.orderFinalised(_inputs(10 ether), _fillCallData(output)); - } - - function testOrderFinalisedAllowsExclusiveOutputAfterStart() public { - vm.warp(1000); - inputSettler.setOrderStatus(ORDER_ID, ORDER_STATUS_CLAIMED); - rwa.mint(address(executor), 10 ether); - - MandateOutput memory output = _output(9 ether, _exclusiveContext(_id(makeAddr("otherSolver")), 1000)); - - vm.prank(address(inputSettler)); - executor.orderFinalised(_inputs(10 ether), _fillCallData(output)); + function testFinaliseWithCurrentTimestampBubblesExpiredFillDeadline() public { + IInputSettler.StandardOrder memory order = _order(10 ether, 9 ether); + order.fillDeadline = uint32(block.timestamp - 1); + _openOrder(order); - assertEq(outputToken.balanceOf(recipient), 9 ether); + vm.expectRevert(bytes("deadline")); + executor.finaliseWithCurrentTimestamp(order, _directRoutes(address(adapter), 10 ether, 10 ether)); } - function testOrderFinalisedRejectsBadContextLength() public { - inputSettler.setOrderStatus(ORDER_ID, ORDER_STATUS_CLAIMED); - MandateOutput memory output = _output(9 ether, hex"0000"); - - vm.expectRevert(abi.encodeWithSelector(ILiquidLaneLifiExecutor.InvalidOutputContextLength.selector, 0, 2)); - vm.prank(address(inputSettler)); - executor.orderFinalised(_inputs(10 ether), _fillCallData(output)); - } + /* FINALISE LOCAL VALIDATION */ - function testOrderFinalisedRejectsUnknownContextType() public { - inputSettler.setOrderStatus(ORDER_ID, ORDER_STATUS_CLAIMED); - MandateOutput memory output = _output(9 ether, hex"02"); + function testFinaliseWithCurrentTimestampRejectsNonOwner() public { + address caller = makeAddr("caller"); + IInputSettler.StandardOrder memory order = _order(10 ether, 9 ether); - vm.expectRevert(abi.encodeWithSelector(ILiquidLaneLifiExecutor.UnknownOutputContext.selector, bytes1(0x02))); - vm.prank(address(inputSettler)); - executor.orderFinalised(_inputs(10 ether), _fillCallData(output)); + vm.expectRevert(abi.encodeWithSignature("OwnableUnauthorizedAccount(address)", caller)); + vm.prank(caller); + executor.finaliseWithCurrentTimestamp(order, _directRoutes(address(adapter), 10 ether, 10 ether)); } - function testOrderFinalisedRejectsZeroInputAmount() public { - inputSettler.setOrderStatus(ORDER_ID, ORDER_STATUS_CLAIMED); + /* CALLBACK AUTHENTICATION */ - vm.expectRevert(ILiquidLaneLifiExecutor.InvalidAmount.selector); - vm.prank(address(inputSettler)); - executor.orderFinalised(_inputs(0), _fillCallData(9 ether)); + function testOrderFinalisedRejectsNonInputSettler() public { + vm.expectRevert(ILiquidLaneLifiExecutor.NotInputSettler.selector); + executor.orderFinalised(_inputs(10 ether), abi.encode(_unsolicitedFillCall())); } - function testOrderFinalisedRejectsWrongOutputSettlerIdentifier() public { - bytes memory call = _fillCallData(_output(9 ether, makeAddr("wrongSettler"), address(outputSettler))); - - vm.expectRevert(ILiquidLaneLifiExecutor.InvalidOutputSettler.selector); - vm.prank(address(inputSettler)); - executor.orderFinalised(_inputs(10 ether), call); - } + /* EIP-1271 */ - function testOrderFinalisedRejectsWrongOutputOracle() public { - bytes memory call = _fillCallData(_output(9 ether, address(outputSettler), makeAddr("wrongOracle"))); + function testIsValidSignatureAcceptsOwner() public { + uint256 ownerKey = 0xA11CE; + LiquidLaneLifiExecutor ownedExecutor = + _deployExecutor(address(inputSettler), address(outputSettler), vm.addr(ownerKey)); + bytes32 digest = keccak256("lifi registration"); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerKey, digest); - vm.expectRevert(ILiquidLaneLifiExecutor.InvalidOutputOracle.selector); - vm.prank(address(inputSettler)); - executor.orderFinalised(_inputs(10 ether), call); + assertEq(ownedExecutor.isValidSignature(digest, abi.encodePacked(r, s, v)), IERC1271.isValidSignature.selector); } - function testOrderFinalisedRejectsWrongOutputChain() public { - MandateOutput memory output = _output(9 ether); - output.chainId = block.chainid + 1; + function testIsValidSignatureRejectsOtherSigner() public { + LiquidLaneLifiExecutor ownedExecutor = + _deployExecutor(address(inputSettler), address(outputSettler), vm.addr(0xA11CE)); + bytes32 digest = keccak256("lifi registration"); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(0xB0B, digest); - vm.expectRevert(ILiquidLaneLifiExecutor.InvalidOutputChain.selector); - vm.prank(address(inputSettler)); - executor.orderFinalised(_inputs(10 ether), _fillCallData(output)); + assertEq(ownedExecutor.isValidSignature(digest, abi.encodePacked(r, s, v)), bytes4(0xffffffff)); } - function testOrderFinalisedRejectsNativeOutput() public { - MandateOutput memory output = _output(9 ether); - output.token = bytes32(0); + function testIsValidSignatureRejectsMalformedSignature() public { + LiquidLaneLifiExecutor ownedExecutor = + _deployExecutor(address(inputSettler), address(outputSettler), vm.addr(0xA11CE)); - vm.expectRevert(ILiquidLaneLifiExecutor.NativeOutputUnsupported.selector); - vm.prank(address(inputSettler)); - executor.orderFinalised(_inputs(10 ether), _fillCallData(output)); + assertEq(ownedExecutor.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 testInitializeSetsOwner() public view { + assertEq(executor.owner(), 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")); } - 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")); } - 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 { @@ -748,22 +368,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), @@ -783,85 +400,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) + function _directRoutes(address fillAdapter, uint256 amountIn, 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) - 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({ @@ -900,31 +472,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, @@ -999,11 +563,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; @@ -1012,6 +578,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); } @@ -1026,25 +596,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"); @@ -1227,49 +793,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 {} From 53bf165d8398e3bdf38fb401bf5969f0ccb1139d Mon Sep 17 00:00:00 2001 From: Andrey Date: Tue, 21 Jul 2026 17:16:30 +0400 Subject: [PATCH 5/6] refactor: callers for lifi --- lib/core | 2 +- script/DeployLifiExecutor.s.sol | 5 +- .../deploy/base/DeployLifiExecutorBase.s.sol | 11 +- src/lifi/LiquidLaneLifiExecutor.sol | 66 ++++++++- .../interfaces/ILiquidLaneLifiExecutor.sol | 11 +- test/deploy/DeployExecutor.t.sol | 11 +- test/lifi/LifiExecutorFork.t.sol | 4 +- test/lifi/LiquidLaneLifiExecutor.t.sol | 140 +++++++++++++++--- 8 files changed, 213 insertions(+), 37 deletions(-) diff --git a/lib/core b/lib/core index 3f0e820..8e5c033 160000 --- a/lib/core +++ b/lib/core @@ -1 +1 @@ -Subproject commit 3f0e820c1b5a529724b3d4c378db60dcf7e3e2f7 +Subproject commit 8e5c03316c8bf7420b71abb3abb4a78c9dd9b6bd diff --git a/script/DeployLifiExecutor.s.sol b/script/DeployLifiExecutor.s.sol index e056a8e..77f7c40 100644 --- a/script/DeployLifiExecutor.s.sol +++ b/script/DeployLifiExecutor.s.sol @@ -16,6 +16,8 @@ contract DeployLifiExecutorScript is DeployLifiExecutorBaseScript { 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(); @@ -24,7 +26,8 @@ contract DeployLifiExecutorScript is DeployLifiExecutorBaseScript { inputSettler: INPUT_SETTLER, outputSettler: OUTPUT_SETTLER, admin: ADMIN == address(0) ? owner : ADMIN, - proxyAdminOwner: PROXY_ADMIN_OWNER == address(0) ? owner : PROXY_ADMIN_OWNER + 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 index a1b1052..9753883 100644 --- a/script/deploy/base/DeployLifiExecutorBase.s.sol +++ b/script/deploy/base/DeployLifiExecutorBase.s.sol @@ -13,6 +13,7 @@ contract DeployLifiExecutorBaseScript is Script { address outputSettler; address admin; address proxyAdminOwner; + address caller; } struct DeploymentData { @@ -22,17 +23,21 @@ contract DeployLifiExecutorBaseScript is Script { 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)) + abi.encodeCall(LiquidLaneLifiExecutor.initialize, (params.admin, callers)) ); _stopBroadcast(); @@ -42,6 +47,7 @@ contract DeployLifiExecutorBaseScript is Script { data.outputSettler = params.outputSettler; data.admin = params.admin; data.proxyAdminOwner = params.proxyAdminOwner; + data.caller = params.caller; _validateDeployment(data); _logDeployment(data); @@ -65,12 +71,14 @@ contract DeployLifiExecutorBaseScript is Script { 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 { @@ -81,5 +89,6 @@ contract DeployLifiExecutorBaseScript is Script { 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 b8a2736..346ed6b 100644 --- a/src/lifi/LiquidLaneLifiExecutor.sol +++ b/src/lifi/LiquidLaneLifiExecutor.sol @@ -13,6 +13,7 @@ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol import {SignatureChecker} from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol"; 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. @@ -20,10 +21,13 @@ import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Own /// 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 lives in proxy storage set by {initialize}. -contract LiquidLaneLifiExecutor is Initializable, OwnableUpgradeable, ILiquidLaneLifiExecutor { +/// 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; + /// @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 */ /// @inheritdoc ILiquidLaneLifiExecutor @@ -31,6 +35,11 @@ contract LiquidLaneLifiExecutor is Initializable, OwnableUpgradeable, ILiquidLan /// @inheritdoc ILiquidLaneLifiExecutor address public immutable OUTPUT_SETTLER; + /* STATE VARIABLES */ + + /// @inheritdoc ILiquidLaneLifiExecutor + address[] public callers; + /* CONSTRUCTOR */ constructor(address inputSettler, address outputSettler) { @@ -40,16 +49,31 @@ contract LiquidLaneLifiExecutor is Initializable, OwnableUpgradeable, ILiquidLan } /// @inheritdoc ILiquidLaneLifiExecutor - function initialize(address owner_) external initializer { + function initialize(address owner_, address[] calldata initCallers) external initializer { __Ownable_init(owner_); + __EIP712_init("LiquidLaneLifiExecutor", "1"); + callers = initCallers; + } + + /* MODIFIERS */ + + /// @dev Reverts unless the caller is in the allowed caller list. + modifier onlyCaller() { + if (!_isCaller(msg.sender)) revert NotCaller(); + _; } /* FINALISE WRAPPER */ + /// @inheritdoc ILiquidLaneLifiExecutor + function isCaller(address caller) external view returns (bool) { + return _isCaller(caller); + } + /// @inheritdoc ILiquidLaneLifiExecutor function finaliseWithCurrentTimestamp(IInputSettler.StandardOrder calldata order, FillRoute[] calldata routes) external - onlyOwner + onlyCaller { bytes32 executorId = bytes32(uint256(uint160(address(this)))); IInputSettler.SolveParams[] memory solveParams = new IInputSettler.SolveParams[](1); @@ -114,13 +138,43 @@ contract LiquidLaneLifiExecutor is Initializable, OwnableUpgradeable, ILiquidLan .setAttestation(fillCall.orderId, solver, uint32(block.timestamp), fillCall.output); } + /* OWNER */ + + /// @inheritdoc ILiquidLaneLifiExecutor + function setCallers(address[] calldata newCallers) external onlyOwner { + callers = newCallers; + emit SetCallers(newCallers); + } + /* EIP-1271 */ + /// @inheritdoc ILiquidLaneLifiExecutor + function lifiRegistrationDigest(bytes32 messageHash) public view returns (bytes32) { + return _hashTypedDataV4(keccak256(abi.encode(LIFI_REGISTRATION_TYPEHASH, messageHash))); + } + /// @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; } + + /* INTERNAL */ + + /// @dev Returns whether `caller` can invoke the finalise entrypoint. + function _isCaller(address caller) internal view returns (bool) { + uint256 callersLength = callers.length; + for (uint256 i; i < callersLength; ++i) { + if (callers[i] == caller) return true; + } + return false; + } } diff --git a/src/lifi/interfaces/ILiquidLaneLifiExecutor.sol b/src/lifi/interfaces/ILiquidLaneLifiExecutor.sol index 025ec26..57cfd2b 100644 --- a/src/lifi/interfaces/ILiquidLaneLifiExecutor.sol +++ b/src/lifi/interfaces/ILiquidLaneLifiExecutor.sol @@ -16,8 +16,13 @@ import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol"; interface ILiquidLaneLifiExecutor is IInputCallback, IERC1271 { /* ERRORS */ + error NotCaller(); error NotInputSettler(); + /* EVENTS */ + + event SetCallers(address[] newCallers); + /* STRUCTS */ /** @@ -65,7 +70,11 @@ interface ILiquidLaneLifiExecutor is IInputCallback, IERC1271 { function INPUT_SETTLER() external view returns (address inputSettler); function OUTPUT_SETTLER() external view returns (address outputSettler); - function initialize(address owner) external; + function callers(uint256 index) external view returns (address caller); + 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; } diff --git a/test/deploy/DeployExecutor.t.sol b/test/deploy/DeployExecutor.t.sol index b92aa9d..b926a4c 100644 --- a/test/deploy/DeployExecutor.t.sol +++ b/test/deploy/DeployExecutor.t.sol @@ -40,7 +40,11 @@ contract DeployExecutorTest is Test { DeployLifiExecutorBaseHarness harness = new DeployLifiExecutorBaseHarness(); DeployLifiExecutorBaseScript.DeploymentData memory data = harness.runBase( DeployLifiExecutorBaseScript.DeployParams({ - inputSettler: inputSettler, outputSettler: outputSettler, admin: admin, proxyAdminOwner: proxyAdminOwner + inputSettler: inputSettler, + outputSettler: outputSettler, + admin: admin, + proxyAdminOwner: proxyAdminOwner, + caller: caller }) ); @@ -48,9 +52,12 @@ contract DeployExecutorTest is Test { assertEq(data.executor.owner(), admin); assertEq(data.executor.INPUT_SETTLER(), inputSettler); assertEq(data.executor.OUTPUT_SETTLER(), outputSettler); + assertTrue(data.executor.isCaller(caller)); + address[] memory callers = new address[](1); + callers[0] = caller; vm.expectRevert(Initializable.InvalidInitialization.selector); - LiquidLaneLifiExecutor(data.implementation).initialize(admin); + LiquidLaneLifiExecutor(data.implementation).initialize(admin, callers); } } diff --git a/test/lifi/LifiExecutorFork.t.sol b/test/lifi/LifiExecutorFork.t.sol index e3a2a96..1d0d0bc 100644 --- a/test/lifi/LifiExecutorFork.t.sol +++ b/test/lifi/LifiExecutorFork.t.sol @@ -43,9 +43,11 @@ contract LifiExecutorForkTest is Test { outputToken = new ForkTestToken("Fork USD", "FUSD"); adapter = new ForkMintingAdapter(outputToken); + address[] memory callers = new address[](1); + callers[0] = owner; LiquidLaneLifiExecutor impl = new LiquidLaneLifiExecutor(INPUT_SETTLER, OUTPUT_SETTLER); TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy( - address(impl), proxyAdminOwner, abi.encodeCall(LiquidLaneLifiExecutor.initialize, (owner)) + address(impl), proxyAdminOwner, abi.encodeCall(LiquidLaneLifiExecutor.initialize, (owner, callers)) ); executor = LiquidLaneLifiExecutor(address(proxy)); } diff --git a/test/lifi/LiquidLaneLifiExecutor.t.sol b/test/lifi/LiquidLaneLifiExecutor.t.sol index d2195d1..07f9b9f 100644 --- a/test/lifi/LiquidLaneLifiExecutor.t.sol +++ b/test/lifi/LiquidLaneLifiExecutor.t.sol @@ -56,14 +56,26 @@ contract LiquidLaneLifiExecutorTest is Test { function _deployExecutor(address inputSettler_, address outputSettler_, address owner_) internal returns (LiquidLaneLifiExecutor) + { + return _deployExecutor(inputSettler_, outputSettler_, owner_, _callers(owner_)); + } + + 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_)) + address(impl), proxyAdminOwner, abi.encodeCall(LiquidLaneLifiExecutor.initialize, (owner_, callers_)) ); return LiquidLaneLifiExecutor(address(proxy)); } + function _callers(address caller) internal pure returns (address[] memory callers_) { + callers_ = new address[](1); + callers_[0] = caller; + } + /* FINALISE HAPPY PATHS */ function testFinaliseWithCurrentTimestampFillsAndAttestsDirectRoute() public { @@ -259,17 +271,44 @@ contract LiquidLaneLifiExecutorTest is Test { executor.finaliseWithCurrentTimestamp(order, _directRoutes(address(adapter), 10 ether, 10 ether)); } - /* FINALISE LOCAL VALIDATION */ + /* CALLER AUTHORIZATION */ - function testFinaliseWithCurrentTimestampRejectsNonOwner() public { + function testFinaliseWithCurrentTimestampRejectsUnauthorizedCaller() public { address caller = makeAddr("caller"); IInputSettler.StandardOrder memory order = _order(10 ether, 9 ether); - vm.expectRevert(abi.encodeWithSignature("OwnableUnauthorizedAccount(address)", caller)); + vm.expectRevert(ILiquidLaneLifiExecutor.NotCaller.selector); vm.prank(caller); executor.finaliseWithCurrentTimestamp(order, _directRoutes(address(adapter), 10 ether, 10 ether)); } + function testSetCallersAllowsNonOwnerToFinaliseAndRevokesOldCaller() public { + address caller = makeAddr("caller"); + executor.setCallers(_callers(caller)); + + assertTrue(executor.isCaller(caller)); + assertFalse(executor.isCaller(owner)); + + IInputSettler.StandardOrder memory order = _order(10 ether, 9 ether); + _openOrder(order); + + vm.prank(caller); + executor.finaliseWithCurrentTimestamp(order, _directRoutes(address(adapter), 10 ether, 10 ether)); + assertEq(outputToken.balanceOf(recipient), 9 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)); + } + + function testSetCallersRejectsNonOwner() public { + address caller = makeAddr("caller"); + + vm.expectRevert(abi.encodeWithSignature("OwnableUnauthorizedAccount(address)", caller)); + vm.prank(caller); + executor.setCallers(_callers(caller)); + } + /* CALLBACK AUTHENTICATION */ function testOrderFinalisedRejectsNonInputSettler() public { @@ -277,50 +316,103 @@ contract LiquidLaneLifiExecutorTest is Test { executor.orderFinalised(_inputs(10 ether), abi.encode(_unsolicitedFillCall())); } - /* EIP-1271 */ + /* EIP-1271 REGISTRATION */ + + 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)); - function testIsValidSignatureAcceptsOwner() public { + assertEq( + callerExecutor.isValidSignature(messageHash, abi.encodePacked(r, s, v)), IERC1271.isValidSignature.selector + ); + } + + 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)); + + assertEq( + callerExecutor.isValidSignature(messageHash, abi.encodePacked(r, s, v)), IERC1271.isValidSignature.selector + ); + } + + function testIsValidSignatureRejectsOwnerWhenNotCaller() public { uint256 ownerKey = 0xA11CE; - LiquidLaneLifiExecutor ownedExecutor = - _deployExecutor(address(inputSettler), address(outputSettler), vm.addr(ownerKey)); - bytes32 digest = keccak256("lifi registration"); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerKey, digest); + 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)); - assertEq(ownedExecutor.isValidSignature(digest, abi.encodePacked(r, s, v)), IERC1271.isValidSignature.selector); + assertEq(callerExecutor.isValidSignature(messageHash, abi.encodePacked(r, s, v)), bytes4(0xffffffff)); } - function testIsValidSignatureRejectsOtherSigner() public { - LiquidLaneLifiExecutor ownedExecutor = - _deployExecutor(address(inputSettler), address(outputSettler), vm.addr(0xA11CE)); - bytes32 digest = keccak256("lifi registration"); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(0xB0B, digest); + 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); - assertEq(ownedExecutor.isValidSignature(digest, abi.encodePacked(r, s, v)), bytes4(0xffffffff)); + assertEq(callerExecutor.isValidSignature(messageHash, abi.encodePacked(r, s, v)), bytes4(0xffffffff)); } - function testIsValidSignatureRejectsMalformedSignature() public { - LiquidLaneLifiExecutor ownedExecutor = - _deployExecutor(address(inputSettler), address(outputSettler), vm.addr(0xA11CE)); + 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)); + + assertEq(secondExecutor.isValidSignature(messageHash, abi.encodePacked(r, s, v)), bytes4(0xffffffff)); + } - assertEq(ownedExecutor.isValidSignature(keccak256("lifi registration"), hex"deadbeef"), bytes4(0xffffffff)); + 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)); + + callerExecutor.setCallers(new address[](0)); + + assertEq(callerExecutor.isValidSignature(messageHash, abi.encodePacked(r, s, v)), bytes4(0xffffffff)); + } + + function testIsValidSignatureRejectsMalformedSignature() public { + assertEq(executor.isValidSignature(keccak256("lifi registration"), hex"deadbeef"), bytes4(0xffffffff)); } /* UPGRADEABILITY */ - function testInitializeSetsOwner() public view { + function testInitializeSetsOwnerAndCallers() public view { assertEq(executor.owner(), owner); + assertEq(executor.callers(0), owner); + assertTrue(executor.isCaller(owner)); } function testInitializeCannotBeCalledTwice() public { vm.expectRevert(Initializable.InvalidInitialization.selector); - executor.initialize(makeAddr("intruder")); + executor.initialize(makeAddr("intruder"), _callers(makeAddr("intruder"))); } function testImplementationInitializerIsDisabled() public { LiquidLaneLifiExecutor impl = new LiquidLaneLifiExecutor(address(inputSettler), address(outputSettler)); vm.expectRevert(Initializable.InvalidInitialization.selector); - impl.initialize(makeAddr("intruder")); + impl.initialize(makeAddr("intruder"), _callers(makeAddr("intruder"))); } /* MOCK SELF-TESTS */ From 9d9ae87284e747e832e854a3da866a81f5a7b4b4 Mon Sep 17 00:00:00 2001 From: Andrey Date: Thu, 23 Jul 2026 20:13:19 +0400 Subject: [PATCH 6/6] refactor(lifi): split direct and discount routes Replace the single FillRoute array with embedded optional FillDiscount by separate FillRoute[] routes and DiscountRoute[] discountRoutes, matching the RFQ executor (swapInputs/discountSwapInputs) and the UniswapX executor (routes/discountRoutes). Co-Authored-By: Claude Opus 4.8 --- src/lifi/LiquidLaneLifiExecutor.sol | 41 ++--- .../interfaces/ILiquidLaneLifiExecutor.sol | 40 +++-- test/lifi/LifiExecutorFork.t.sol | 22 +-- test/lifi/LiquidLaneLifiExecutor.t.sol | 154 +++++++++++------- 4 files changed, 138 insertions(+), 119 deletions(-) diff --git a/src/lifi/LiquidLaneLifiExecutor.sol b/src/lifi/LiquidLaneLifiExecutor.sol index 346ed6b..5027ee5 100644 --- a/src/lifi/LiquidLaneLifiExecutor.sol +++ b/src/lifi/LiquidLaneLifiExecutor.sol @@ -71,10 +71,11 @@ contract LiquidLaneLifiExecutor is Initializable, OwnableUpgradeable, EIP712Upgr } /// @inheritdoc ILiquidLaneLifiExecutor - function finaliseWithCurrentTimestamp(IInputSettler.StandardOrder calldata order, FillRoute[] calldata routes) - external - onlyCaller - { + function finaliseWithCurrentTimestamp( + IInputSettler.StandardOrder calldata order, + FillRoute[] calldata routes, + DiscountRoute[] calldata discountRoutes + ) external onlyCaller { 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}); @@ -88,7 +89,8 @@ contract LiquidLaneLifiExecutor is Initializable, OwnableUpgradeable, EIP712Upgr orderId: IInputSettler(INPUT_SETTLER).orderIdentifier(order), output: order.outputs[0], fillDeadline: order.fillDeadline, - routes: routes + routes: routes, + discountRoutes: discountRoutes }) ) ); @@ -106,23 +108,22 @@ contract LiquidLaneLifiExecutor is Initializable, OwnableUpgradeable, EIP712Upgr // 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); - } - 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(tokenIn).safeTransfer(route.adapter, route.amountIn); + ILiquidLaneAdapter(route.adapter) + .swap( + ILiquidLaneAdapter.Swap({ + recipient: address(this), tokenIn: tokenIn, amountIn: route.amountIn, amountOut: route.amountOut + }) + ); + } + uint256 discountRoutesLength = fillCall.discountRoutes.length; + for (uint256 i; i < discountRoutesLength; ++i) { + DiscountRoute memory route = fillCall.discountRoutes[i]; + IERC20(tokenIn).safeTransfer(route.adapter, route.amountIn); + ILiquidLaneAdapter(route.adapter) + .swap(route.discountSwap, route.protocolSignature, address(this), route.amountIn); } // The output settler resolves the context-dependent amount it is owed and pulls it, diff --git a/src/lifi/interfaces/ILiquidLaneLifiExecutor.sol b/src/lifi/interfaces/ILiquidLaneLifiExecutor.sol index 57cfd2b..97b32ea 100644 --- a/src/lifi/interfaces/ILiquidLaneLifiExecutor.sol +++ b/src/lifi/interfaces/ILiquidLaneLifiExecutor.sol @@ -26,30 +26,29 @@ interface ILiquidLaneLifiExecutor is IInputCallback, IERC1271 { /* STRUCTS */ /** - * @notice Optional private-discount authorization for one route. - * @param discountId Backend discount identifier; zero selects the direct swap path. - * @param discountSwap Reusable signer policy plus the fresh protocol deadline. - * @param protocolSignature Fresh protocol cosign verified by the LiquidLane adapter. + * @notice One atomic direct-swap LiquidLane redemption leg. + * @param adapter LiquidLane adapter selected by the solver. + * @param amountIn Order-input amount routed to the adapter. + * @param amountOut Output amount requested from the adapter. */ - struct FillDiscount { - bytes32 discountId; - ILiquidLaneAdapter.DiscountSwap discountSwap; - bytes protocolSignature; + struct FillRoute { + address adapter; + uint256 amountIn; + uint256 amountOut; } /** - * @notice One atomic LiquidLane redemption leg. + * @notice One atomic discount-backed LiquidLane redemption leg. * @param adapter LiquidLane adapter selected by the solver. * @param amountIn Order-input amount routed to the adapter. - * @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. + * @param discountSwap Reusable signer policy plus the fresh protocol deadline. + * @param protocolSignature Fresh protocol cosign verified by the LiquidLane adapter. */ - struct FillRoute { + struct DiscountRoute { address adapter; uint256 amountIn; - uint256 amountOut; - FillDiscount discount; + ILiquidLaneAdapter.DiscountSwap discountSwap; + bytes protocolSignature; } /** @@ -57,13 +56,15 @@ interface ILiquidLaneLifiExecutor is IInputCallback, IERC1271 { * @param orderId OIF order id. * @param output Single output to fill and attest. * @param fillDeadline Fill deadline carried by the order. - * @param routes LiquidLane legs selected by the solver. + * @param routes Direct-swap LiquidLane legs selected by the solver. + * @param discountRoutes Discount-backed LiquidLane legs selected by the solver. */ struct FillCall { bytes32 orderId; MandateOutput output; uint32 fillDeadline; FillRoute[] routes; + DiscountRoute[] discountRoutes; } /* FUNCTIONS */ @@ -72,8 +73,11 @@ interface ILiquidLaneLifiExecutor is IInputCallback, IERC1271 { function OUTPUT_SETTLER() external view returns (address outputSettler); function callers(uint256 index) external view returns (address caller); function initialize(address owner, address[] calldata initCallers) external; - function finaliseWithCurrentTimestamp(IInputSettler.StandardOrder calldata order, FillRoute[] calldata routes) - external; + function finaliseWithCurrentTimestamp( + IInputSettler.StandardOrder calldata order, + FillRoute[] calldata routes, + DiscountRoute[] calldata discountRoutes + ) 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; diff --git a/test/lifi/LifiExecutorFork.t.sol b/test/lifi/LifiExecutorFork.t.sol index 1d0d0bc..82677ba 100644 --- a/test/lifi/LifiExecutorFork.t.sol +++ b/test/lifi/LifiExecutorFork.t.sol @@ -57,7 +57,7 @@ contract LifiExecutorForkTest is Test { bytes32 orderId = IInputSettlerEscrowLike(INPUT_SETTLER).orderIdentifier(order); vm.prank(owner); - executor.finaliseWithCurrentTimestamp(order, _routes(order)); + executor.finaliseWithCurrentTimestamp(order, _routes(order), new ILiquidLaneLifiExecutor.DiscountRoute[](0)); assertEq(IInputSettlerEscrowLike(INPUT_SETTLER).orderStatus(orderId), 2, "claimed"); assertEq(outputToken.balanceOf(recipient), 9 ether, "recipient output"); @@ -119,25 +119,7 @@ contract LifiExecutorForkTest is Test { { routes = new ILiquidLaneLifiExecutor.FillRoute[](1); routes[0] = ILiquidLaneLifiExecutor.FillRoute({ - adapter: address(adapter), - amountIn: order.inputs[0][1], - amountOut: 10 ether, - discount: ILiquidLaneLifiExecutor.FillDiscount({ - discountId: bytes32(0), - discountSwap: ILiquidLaneAdapter.DiscountSwap({ - discount: ILiquidLaneAdapter.Discount({ - tokenToRedeem: address(0), - discount: 0, - signer: address(0), - protocol: address(0), - nonce: 0, - deadline: 0 - }), - signerSignature: "", - protocolDeadline: 0 - }), - protocolSignature: "" - }) + adapter: address(adapter), amountIn: order.inputs[0][1], amountOut: 10 ether }); } diff --git a/test/lifi/LiquidLaneLifiExecutor.t.sol b/test/lifi/LiquidLaneLifiExecutor.t.sol index 07f9b9f..6d09c8d 100644 --- a/test/lifi/LiquidLaneLifiExecutor.t.sol +++ b/test/lifi/LiquidLaneLifiExecutor.t.sol @@ -82,7 +82,9 @@ contract LiquidLaneLifiExecutorTest is Test { IInputSettler.StandardOrder memory order = _order(10 ether, 9 ether); bytes32 orderId = _openOrder(order); - executor.finaliseWithCurrentTimestamp(order, _directRoutes(address(adapter), 10 ether, 10 ether)); + executor.finaliseWithCurrentTimestamp( + order, _directRoutes(address(adapter), 10 ether, 10 ether), _noDiscountRoutes() + ); assertEq(rwa.balanceOf(address(adapter)), 10 ether); assertEq(outputToken.balanceOf(recipient), 9 ether); @@ -107,7 +109,7 @@ contract LiquidLaneLifiExecutorTest is Test { routes[0] = _directRoute(address(adapter), 4 ether, 4 ether); routes[1] = _directRoute(address(secondAdapter), 6 ether, 6 ether); - executor.finaliseWithCurrentTimestamp(order, routes); + executor.finaliseWithCurrentTimestamp(order, routes, _noDiscountRoutes()); assertEq(rwa.balanceOf(address(adapter)), 4 ether); assertEq(rwa.balanceOf(address(secondAdapter)), 6 ether); @@ -115,21 +117,37 @@ contract LiquidLaneLifiExecutorTest is Test { assertEq(outputToken.balanceOf(address(executor)), 1 ether); } - function testFinaliseWithCurrentTimestampExecutesPrivateDiscountRoute() public { + function testFinaliseWithCurrentTimestampExecutesDiscountRoute() public { vm.warp(1000); IInputSettler.StandardOrder memory order = _order(10 ether, 8 ether); - bytes32 orderId = _openOrder(order); + _openOrder(order); - ILiquidLaneLifiExecutor.FillRoute[] memory routes = new ILiquidLaneLifiExecutor.FillRoute[](1); - routes[0] = _discountRoute(address(adapter), 10 ether, keccak256("discount"), 100_000); + ILiquidLaneLifiExecutor.DiscountRoute[] memory discountRoutes = new ILiquidLaneLifiExecutor.DiscountRoute[](1); + discountRoutes[0] = _discountRoute(address(adapter), 10 ether, 100_000); - executor.finaliseWithCurrentTimestamp(order, routes); + executor.finaliseWithCurrentTimestamp(order, _noRoutes(), discountRoutes); assertEq(rwa.balanceOf(address(adapter)), 10 ether); assertEq(outputToken.balanceOf(recipient), 8 ether); assertEq(outputToken.balanceOf(address(executor)), 1 ether); } + function testFinaliseWithCurrentTimestampExecutesDirectAndDiscountRoutes() public { + vm.warp(1000); + IInputSettler.StandardOrder memory order = _order(10 ether, 8 ether); + _openOrder(order); + + ILiquidLaneLifiExecutor.DiscountRoute[] memory discountRoutes = new ILiquidLaneLifiExecutor.DiscountRoute[](1); + discountRoutes[0] = _discountRoute(address(adapter), 6 ether, 100_000); + + executor.finaliseWithCurrentTimestamp(order, _directRoutes(address(adapter), 4 ether, 4 ether), discountRoutes); + + assertEq(rwa.balanceOf(address(adapter)), 10 ether); + // 4 ether direct output plus 5.4 ether discounted output; 8 ether fill leaves 1.4 ether surplus. + assertEq(outputToken.balanceOf(recipient), 8 ether); + assertEq(outputToken.balanceOf(address(executor)), 1.4 ether); + } + function testFinaliseWithCurrentTimestampSupportsSameInputAndOutputToken() public { MockLifiAdapter sameTokenAdapter = new MockLifiAdapter(rwa); rwa.mint(address(sameTokenAdapter), 100 ether); @@ -137,7 +155,9 @@ contract LiquidLaneLifiExecutorTest is Test { IInputSettler.StandardOrder memory order = _order(10 ether, 9.5 ether, address(rwa)); _openOrder(order); - executor.finaliseWithCurrentTimestamp(order, _directRoutes(address(sameTokenAdapter), 10 ether, 10 ether)); + executor.finaliseWithCurrentTimestamp( + order, _directRoutes(address(sameTokenAdapter), 10 ether, 10 ether), _noDiscountRoutes() + ); assertEq(rwa.balanceOf(recipient), 9.5 ether); // Pre-existing 3 ether plus 0.5 ether fill surplus stay with the executor. @@ -151,7 +171,9 @@ contract LiquidLaneLifiExecutorTest is Test { order.outputs[0].context = _dutchContext(900, 1100, 0.01 ether); _openOrder(order); - executor.finaliseWithCurrentTimestamp(order, _directRoutes(address(adapter), 10 ether, 10.5 ether)); + executor.finaliseWithCurrentTimestamp( + order, _directRoutes(address(adapter), 10 ether, 10.5 ether), _noDiscountRoutes() + ); assertEq(outputToken.balanceOf(recipient), 10 ether); assertEq(outputSettler.lastOutputAmount(), 10 ether); @@ -164,7 +186,9 @@ contract LiquidLaneLifiExecutorTest is Test { order.outputs[0].context = _exclusiveDutchContext(_id(makeAddr("otherSolver")), 900, 1100, 0.01 ether); _openOrder(order); - executor.finaliseWithCurrentTimestamp(order, _directRoutes(address(adapter), 10 ether, 10.5 ether)); + executor.finaliseWithCurrentTimestamp( + order, _directRoutes(address(adapter), 10 ether, 10.5 ether), _noDiscountRoutes() + ); assertEq(outputToken.balanceOf(recipient), 10 ether); assertEq(outputSettler.lastOutputAmount(), 10 ether); @@ -176,7 +200,9 @@ contract LiquidLaneLifiExecutorTest is Test { order.outputs[0].context = _exclusiveContext(_id(makeAddr("otherSolver")), 1000); _openOrder(order); - executor.finaliseWithCurrentTimestamp(order, _directRoutes(address(adapter), 10 ether, 10 ether)); + executor.finaliseWithCurrentTimestamp( + order, _directRoutes(address(adapter), 10 ether, 10 ether), _noDiscountRoutes() + ); assertEq(outputToken.balanceOf(recipient), 9 ether); } @@ -186,7 +212,9 @@ contract LiquidLaneLifiExecutorTest is Test { IInputSettler.StandardOrder memory order = _order(10 ether, 8.5 ether); _openOrder(order); - executor.finaliseWithCurrentTimestamp(order, _directRoutes(address(adapter), 9 ether, 9 ether)); + executor.finaliseWithCurrentTimestamp( + order, _directRoutes(address(adapter), 9 ether, 9 ether), _noDiscountRoutes() + ); assertEq(rwa.balanceOf(address(adapter)), 9 ether); assertEq(rwa.balanceOf(address(inputSettler)), 1 ether); @@ -199,7 +227,9 @@ contract LiquidLaneLifiExecutorTest is Test { IInputSettler.StandardOrder memory order = _order(10 ether, 9 ether); _openOrder(order); - executor.finaliseWithCurrentTimestamp(order, _directRoutes(address(adapter), 10 ether, 10 ether)); + executor.finaliseWithCurrentTimestamp( + order, _directRoutes(address(adapter), 10 ether, 10 ether), _noDiscountRoutes() + ); assertEq(rwa.balanceOf(address(executor)), 5 ether); assertEq(outputToken.balanceOf(recipient), 9 ether); @@ -210,7 +240,9 @@ contract LiquidLaneLifiExecutorTest is Test { order.outputs[0].recipient = _id(address(executor)); _openOrder(order); - executor.finaliseWithCurrentTimestamp(order, _directRoutes(address(adapter), 10 ether, 10 ether)); + executor.finaliseWithCurrentTimestamp( + order, _directRoutes(address(adapter), 10 ether, 10 ether), _noDiscountRoutes() + ); assertEq(outputToken.balanceOf(address(executor)), 10 ether); } @@ -222,7 +254,9 @@ contract LiquidLaneLifiExecutorTest is Test { order.outputs[0].callbackData = hex"01"; _openOrder(order); - executor.finaliseWithCurrentTimestamp(order, _directRoutes(address(adapter), 10 ether, 10 ether)); + executor.finaliseWithCurrentTimestamp( + order, _directRoutes(address(adapter), 10 ether, 10 ether), _noDiscountRoutes() + ); assertEq(outputToken.balanceOf(address(outputRecipient)), 8 ether); assertEq(outputToken.balanceOf(address(executor)), 2 ether); @@ -237,7 +271,9 @@ contract LiquidLaneLifiExecutorTest is Test { _openOrder(order); vm.expectRevert(bytes("exclusive")); - executor.finaliseWithCurrentTimestamp(order, _directRoutes(address(adapter), 10 ether, 10 ether)); + executor.finaliseWithCurrentTimestamp( + order, _directRoutes(address(adapter), 10 ether, 10 ether), _noDiscountRoutes() + ); } function testFinaliseWithCurrentTimestampBubblesInsufficientOutputForResolvedDutchAmount() public { @@ -251,7 +287,9 @@ contract LiquidLaneLifiExecutorTest is Test { IERC20Errors.ERC20InsufficientBalance.selector, address(executor), 9.5 ether, 10 ether ) ); - executor.finaliseWithCurrentTimestamp(order, _directRoutes(address(adapter), 10 ether, 9.5 ether)); + executor.finaliseWithCurrentTimestamp( + order, _directRoutes(address(adapter), 10 ether, 9.5 ether), _noDiscountRoutes() + ); } function testFinaliseWithCurrentTimestampBubblesAlreadyClaimedOrder() public { @@ -259,7 +297,9 @@ contract LiquidLaneLifiExecutorTest is Test { inputSettler.setOrderStatus(_orderId(order), ORDER_STATUS_CLAIMED); vm.expectRevert(MockInputSettler.InvalidOrderStatus.selector); - executor.finaliseWithCurrentTimestamp(order, _directRoutes(address(adapter), 10 ether, 10 ether)); + executor.finaliseWithCurrentTimestamp( + order, _directRoutes(address(adapter), 10 ether, 10 ether), _noDiscountRoutes() + ); } function testFinaliseWithCurrentTimestampBubblesExpiredFillDeadline() public { @@ -268,7 +308,9 @@ contract LiquidLaneLifiExecutorTest is Test { _openOrder(order); vm.expectRevert(bytes("deadline")); - executor.finaliseWithCurrentTimestamp(order, _directRoutes(address(adapter), 10 ether, 10 ether)); + executor.finaliseWithCurrentTimestamp( + order, _directRoutes(address(adapter), 10 ether, 10 ether), _noDiscountRoutes() + ); } /* CALLER AUTHORIZATION */ @@ -279,7 +321,9 @@ contract LiquidLaneLifiExecutorTest is Test { vm.expectRevert(ILiquidLaneLifiExecutor.NotCaller.selector); vm.prank(caller); - executor.finaliseWithCurrentTimestamp(order, _directRoutes(address(adapter), 10 ether, 10 ether)); + executor.finaliseWithCurrentTimestamp( + order, _directRoutes(address(adapter), 10 ether, 10 ether), _noDiscountRoutes() + ); } function testSetCallersAllowsNonOwnerToFinaliseAndRevokesOldCaller() public { @@ -293,12 +337,16 @@ contract LiquidLaneLifiExecutorTest is Test { _openOrder(order); vm.prank(caller); - executor.finaliseWithCurrentTimestamp(order, _directRoutes(address(adapter), 10 ether, 10 ether)); + executor.finaliseWithCurrentTimestamp( + order, _directRoutes(address(adapter), 10 ether, 10 ether), _noDiscountRoutes() + ); assertEq(outputToken.balanceOf(recipient), 9 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)); + executor.finaliseWithCurrentTimestamp( + secondOrder, _directRoutes(address(adapter), 10 ether, 10 ether), _noDiscountRoutes() + ); } function testSetCallersRejectsNonOwner() public { @@ -512,64 +560,48 @@ contract LiquidLaneLifiExecutorTest is Test { pure returns (ILiquidLaneLifiExecutor.FillRoute memory) { - return ILiquidLaneLifiExecutor.FillRoute({ - adapter: fillAdapter, amountIn: amountIn, amountOut: amountOut, discount: _emptyDiscount() - }); + return ILiquidLaneLifiExecutor.FillRoute({adapter: fillAdapter, amountIn: amountIn, amountOut: amountOut}); } - function _discountRoute(address fillAdapter, uint256 amountIn, bytes32 discountId, uint256 discount) + function _discountRoute(address fillAdapter, uint256 amountIn, uint256 discount) internal view - returns (ILiquidLaneLifiExecutor.FillRoute memory) + returns (ILiquidLaneLifiExecutor.DiscountRoute memory) { - return ILiquidLaneLifiExecutor.FillRoute({ + return ILiquidLaneLifiExecutor.DiscountRoute({ adapter: fillAdapter, amountIn: amountIn, - amountOut: 0, - discount: ILiquidLaneLifiExecutor.FillDiscount({ - discountId: discountId, - discountSwap: ILiquidLaneAdapter.DiscountSwap({ - discount: ILiquidLaneAdapter.Discount({ - tokenToRedeem: address(rwa), - discount: discount, - signer: DISCOUNT_SIGNER, - protocol: address(0xBEEF), - nonce: 1, - deadline: uint48(block.timestamp + 100) - }), - signerSignature: hex"1234", - protocolDeadline: uint48(block.timestamp + 100) - }), - protocolSignature: hex"5678" - }) - }); - } - - function _emptyDiscount() internal pure returns (ILiquidLaneLifiExecutor.FillDiscount memory) { - return ILiquidLaneLifiExecutor.FillDiscount({ - discountId: bytes32(0), discountSwap: ILiquidLaneAdapter.DiscountSwap({ discount: ILiquidLaneAdapter.Discount({ - tokenToRedeem: address(0), - discount: 0, - signer: address(0), - protocol: address(0), - nonce: 0, - deadline: 0 + tokenToRedeem: address(rwa), + discount: discount, + signer: DISCOUNT_SIGNER, + protocol: address(0xBEEF), + nonce: 1, + deadline: uint48(block.timestamp + 100) }), - signerSignature: "", - protocolDeadline: 0 + signerSignature: hex"1234", + protocolDeadline: uint48(block.timestamp + 100) }), - protocolSignature: "" + protocolSignature: hex"5678" }); } + function _noRoutes() internal pure returns (ILiquidLaneLifiExecutor.FillRoute[] memory routes) {} + + function _noDiscountRoutes() + internal + pure + returns (ILiquidLaneLifiExecutor.DiscountRoute[] memory discountRoutes) + {} + 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) + routes: _directRoutes(address(adapter), 10 ether, 10 ether), + discountRoutes: _noDiscountRoutes() }); }