diff --git a/contracts/solve/src/_optimized/SolverNetExecutor.sol b/contracts/solve/src/_optimized/SolverNetExecutor.sol new file mode 100644 index 000000000..a8db43390 --- /dev/null +++ b/contracts/solve/src/_optimized/SolverNetExecutor.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity =0.8.24; + +import { SafeTransferLib } from "solady/src/utils/SafeTransferLib.sol"; +import { ISolverNetExecutor } from "./interfaces/ISolverNetExecutor.sol"; +import { AddrUtils } from "../lib/AddrUtils.sol"; + +contract SolverNetExecutor is ISolverNetExecutor { + using SafeTransferLib for address; + using AddrUtils for bytes32; + + /** + * @notice Address of the outbox. + */ + address public immutable outbox; + + /** + * @notice Modifier to provide access control to the outbox. + * @dev This was used as it is more efficient than using Ownable. Only the outbox will call these functions. + */ + modifier onlyOutbox() { + if (msg.sender != outbox) revert NotOutbox(); + _; + } + + constructor(address _outbox) { + outbox = _outbox; + } + + /** + * @notice Approves a spender (usually call target) to spend a token held by the executor. + * @dev Called prior to `execute` in order to ensure tokens can be spent and after to purge excess approvals. + */ + function approve(address token, address spender, uint256 amount) external onlyOutbox { + token.safeApprove(spender, amount); + } + + /** + * @notice Executes a call. + * @param target Address of the contract to call. + * @param value Value to send with the call. + * @param data Data to send with the call. + */ + function execute(address target, uint256 value, bytes calldata data) external payable onlyOutbox { + (bool success,) = payable(target).call{ value: value }(data); + if (!success) revert CallFailed(); + } + + /** + * @notice Transfers a token to a recipient. + * @dev Called after `execute` in order to refund any excess or returned tokens. + */ + function transfer(address token, address to, uint256 amount) external onlyOutbox { + token.safeTransfer(to, amount); + } + + /** + * @notice Transfers native currency to a recipient. + * @dev Called after `execute` in order to refund any native currency sent back to the executor. + */ + function transferNative(address to, uint256 amount) external onlyOutbox { + to.safeTransferETH(amount); + } + + /** + * @dev Allows target contracts to arbitrarily return native tokens to the executor. + */ + receive() external payable { } + + /** + * @dev Allows target contracts to arbitrarily return native tokens to the executor. + */ + fallback() external payable { } +} diff --git a/contracts/solve/src/_optimized/SolverNetInbox.sol b/contracts/solve/src/_optimized/SolverNetInbox.sol new file mode 100644 index 000000000..2fee87937 --- /dev/null +++ b/contracts/solve/src/_optimized/SolverNetInbox.sol @@ -0,0 +1,530 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity =0.8.24; + +import { OwnableRoles } from "solady/src/auth/OwnableRoles.sol"; +import { ReentrancyGuard } from "solady/src/utils/ReentrancyGuard.sol"; +import { Initializable } from "solady/src/utils/Initializable.sol"; +import { DeployedAt } from "../util/DeployedAt.sol"; +import { XAppBase } from "core/src/pkg/XAppBase.sol"; +import { IERC7683 } from "../erc7683/IERC7683.sol"; +import { ISolverNetInbox } from "./interfaces/ISolverNetInbox.sol"; +import { SafeTransferLib } from "solady/src/utils/SafeTransferLib.sol"; +import { SolverNet } from "./lib/SolverNet.sol"; +import { AddrUtils } from "../lib/AddrUtils.sol"; + +/** + * @title SolverNetInbox + * @notice Entrypoint and alt-mempool for user solve orders. + */ +contract SolverNetInbox is OwnableRoles, ReentrancyGuard, Initializable, DeployedAt, XAppBase, ISolverNetInbox { + using SafeTransferLib for address; + using AddrUtils for address; + + /** + * @notice Role for solvers. + * @dev _ROLE_0 evaluates to '1'. + */ + uint256 internal constant SOLVER = _ROLE_0; + + /** + * @notice Typehash for the OrderData struct. + */ + bytes32 internal constant ORDERDATA_TYPEHASH = keccak256( + "OrderData(Header header,Deposit deposit,Call[] calls,Expense[] expenses)Header(address owner,uint64 destChainId,uint32 fillDeadline)Call(address target,bytes4 selector,uint256 value,bytes params)Deposit(address token,uint96 amount)Expense(address spender,address token,uint96 amount)" + ); + + /** + * @dev Counter for generating unique order IDs. Incremented each time a new order is created. + */ + uint256 internal _lastId; + + /** + * @notice Addresses of the outbox contracts. + */ + mapping(uint64 chainId => address outbox) internal _outboxes; + + /** + * @notice Map order ID to header parameters. + * @dev (owner, destChainId, fillDeadline) + */ + mapping(bytes32 id => SolverNet.Header) internal _orderHeader; + + /** + * @notice Map order ID to deposit parameters. + * @dev (token, amount) + */ + mapping(bytes32 id => SolverNet.Deposit) internal _orderDeposit; + + /** + * @notice Map order ID to call parameters. + * @dev (target, selector, value, params) + */ + mapping(bytes32 id => SolverNet.Call[]) internal _orderCalls; + + /** + * @notice Map order ID to expense parameters. + * @dev (spender, token, amount) + */ + mapping(bytes32 id => SolverNet.Expense[]) internal _orderExpenses; + + /** + * @notice Map order ID to order parameters. + */ + mapping(bytes32 id => OrderState) internal _orderState; + + /** + * @notice Map status to latest order ID. + */ + mapping(Status => bytes32 id) internal _latestOrderIdByStatus; + + constructor() { + _disableInitializers(); + } + + /** + * @notice Initialize the contract's owner and solver. + * @dev Used instead of constructor as we want to use the transparent upgradeable proxy pattern. + * @param owner_ Address of the owner. + * @param solver_ Address of the solver. + * @param omni_ Address of the OmniPortal. + */ + function initialize(address owner_, address solver_, address omni_) external initializer { + _initializeOwner(owner_); + _grantRoles(solver_, SOLVER); + _setOmniPortal(omni_); + } + + /** + * @notice Set the outbox addresses for the given chain IDs. + * @param chainIds IDs of the chains. + * @param outboxes Addresses of the outboxes. + */ + function setOutboxes(uint64[] calldata chainIds, address[] calldata outboxes) external onlyOwner { + for (uint256 i; i < chainIds.length; ++i) { + _outboxes[chainIds[i]] = outboxes[i]; + emit OutboxSet(chainIds[i], outboxes[i]); + } + } + + /** + * @notice Returns the order and its state with the given ID. + * @param id ID of the order. + */ + function getOrder(bytes32 id) + external + view + returns (ResolvedCrossChainOrder memory resolved, OrderState memory state) + { + SolverNet.OrderData memory orderData = _getOrderData(id); + return (_resolve(orderData), _orderState[id]); + } + + /** + * @notice Returns the next order ID. + */ + function getNextId() external view returns (bytes32) { + return _nextId(); + } + + /** + * @notice Returns the latest order with the given status. + * @param status Order status to query. + */ + function getLatestOrderIdByStatus(Status status) external view returns (bytes32) { + return _latestOrderIdByStatus[status]; + } + + /** + * @dev Validate the onchain order. + * @param order OnchainCrossChainOrder to validate. + */ + function validate(OnchainCrossChainOrder calldata order) external view returns (bool) { + _validate(order); + return true; + } + + /** + * @notice Resolve the onchain order with validation. + * @param order OnchainCrossChainOrder to resolve. + */ + function resolve(OnchainCrossChainOrder calldata order) public view returns (ResolvedCrossChainOrder memory) { + SolverNet.OrderData memory orderData = _validate(order); + return _resolve(orderData); + } + + /** + * @notice Open an order to execute a call on another chain, backed by deposits. + * @dev Token deposits are transferred from msg.sender to this inbox. + * @param order OnchainCrossChainOrder to open. + */ + function open(OnchainCrossChainOrder calldata order) external payable nonReentrant { + SolverNet.OrderData memory orderData = _validate(order); + _processDeposit(orderData.deposit); + ResolvedCrossChainOrder memory resolved = _openOrder(orderData); + + emit Open(resolved.orderId, resolved); + } + + /** + * @notice Accept an open order. + * @dev Only a whitelisted solver can accept. + * @param id ID of the order. + */ + function accept(bytes32 id) external onlyRoles(SOLVER) nonReentrant { + SolverNet.Header memory header = _orderHeader[id]; + OrderState memory state = _orderState[id]; + + if (state.status != Status.Pending) revert OrderNotPending(); + if (header.fillDeadline < block.timestamp && header.fillDeadline != 0) revert FillDeadlinePassed(); + + _upsertOrder(id, Status.Accepted, msg.sender); + + emit Accepted(id, msg.sender); + } + + /** + * @notice Reject an open order and refund deposits. + * @dev Only a whitelisted solver can reject. + * @param id ID of the order. + * @param reason Reason code for rejection. + */ + function reject(bytes32 id, uint8 reason) external onlyRoles(SOLVER) nonReentrant { + OrderState memory state = _orderState[id]; + + if (state.status != Status.Pending) { + if (state.status != Status.Accepted) revert Unauthorized(); + if (state.claimant != msg.sender) revert Unauthorized(); + } + + _upsertOrder(id, Status.Reverted, msg.sender); + _transferDeposit(id, _orderHeader[id].owner); + + emit Rejected(id, msg.sender, reason); + } + + /** + * @notice Cancel an open and refund deposits. + * @dev Only order initiator can cancel. + * @param id ID of the order. + */ + function cancel(bytes32 id) external nonReentrant { + OrderState memory state = _orderState[id]; + address user = _orderHeader[id].owner; + + if (state.status != Status.Pending) revert OrderNotPending(); + if (user != msg.sender) revert Unauthorized(); + + _upsertOrder(id, Status.Reverted, msg.sender); + _transferDeposit(id, user); + + emit Reverted(id); + } + + /** + * @notice Fill an order. + * @dev Only callable by the outbox. + * @param id ID of the order. + * @param fillHash Hash of fill instructions origin data. + * @param claimant Address to claim the order, provided by the filler. + */ + function markFilled(bytes32 id, bytes32 fillHash, address claimant) external xrecv nonReentrant { + SolverNet.Header memory header = _orderHeader[id]; + OrderState memory state = _orderState[id]; + + if (state.status != Status.Pending && state.status != Status.Accepted) { + revert OrderNotPendingOrAccepted(); + } + if (xmsg.sender != _outboxes[xmsg.sourceChainId]) revert Unauthorized(); + if (xmsg.sourceChainId != header.destChainId) revert WrongSourceChain(); + + // Ensure reported fill hash matches origin data + if (fillHash != _fillHash(id)) { + revert WrongFillHash(); + } + + _upsertOrder(id, Status.Filled, claimant); + emit Filled(id, fillHash, claimant); + } + + /** + * @notice Claim a filled order. + * @param id ID of the order. + * @param to Address to send deposits to. + */ + function claim(bytes32 id, address to) external nonReentrant { + OrderState memory state = _orderState[id]; + + if (state.status != Status.Filled) revert OrderNotFilled(); + if (state.claimant != msg.sender) revert Unauthorized(); + + _upsertOrder(id, Status.Claimed, msg.sender); + _transferDeposit(id, to); + + emit Claimed(id, msg.sender, to); + } + + /** + * @dev Return the order data for the given ID. + * @param id ID of the order. + */ + function _getOrderData(bytes32 id) internal view returns (SolverNet.OrderData memory) { + return SolverNet.OrderData({ + header: _orderHeader[id], + calls: _orderCalls[id], + deposit: _orderDeposit[id], + expenses: _orderExpenses[id] + }); + } + + /** + * @dev Parse and return order data, validate correctness. + * @param order OnchainCrossChainOrder to parse + */ + function _validate(OnchainCrossChainOrder calldata order) internal view returns (SolverNet.OrderData memory) { + // Validate OnchainCrossChainOrder + if (order.fillDeadline < block.timestamp && order.fillDeadline != 0) revert InvalidFillDeadline(); + if (order.orderDataType != ORDERDATA_TYPEHASH) revert InvalidOrderTypehash(); + if (order.orderData.length == 0) revert InvalidOrderData(); + + SolverNet.OrderData memory orderData = abi.decode(order.orderData, (SolverNet.OrderData)); + + // Validate SolverNet.OrderData.Header + SolverNet.Header memory header = orderData.header; + if (header.owner == address(0)) header.owner = msg.sender; + if (header.destChainId == 0 || header.destChainId == block.chainid) revert InvalidChainId(); + if (header.fillDeadline != order.fillDeadline) revert InvalidFillDeadline(); + + // Validate SolverNet.OrderData.Call + SolverNet.Call[] memory calls = orderData.calls; + for (uint256 i; i < calls.length; ++i) { + SolverNet.Call memory call = calls[i]; + if (call.target == address(0)) revert InvalidCallTarget(); + } + + // Validate SolverNet.OrderData.Expenses + SolverNet.Expense[] memory expenses = orderData.expenses; + for (uint256 i; i < expenses.length; ++i) { + if (expenses[i].token == address(0)) revert InvalidExpenseToken(); + if (expenses[i].amount == 0) revert InvalidExpenseAmount(); + } + + return orderData; + } + + /** + * @dev Derive the maxSpent Output for the order. + * @param orderData Order data to derive from. + */ + function _deriveMaxSpent(SolverNet.OrderData memory orderData) internal view returns (IERC7683.Output[] memory) { + SolverNet.Header memory header = orderData.header; + SolverNet.Call[] memory calls = orderData.calls; + SolverNet.Expense[] memory expenses = orderData.expenses; + + uint256 totalNativeValue; + for (uint256 i; i < calls.length; ++i) { + if (calls[i].value > 0) totalNativeValue += calls[i].value; + } + + IERC7683.Output[] memory maxSpent = + new IERC7683.Output[](totalNativeValue > 0 ? expenses.length + 1 : expenses.length); + for (uint256 i; i < expenses.length; ++i) { + maxSpent[i] = IERC7683.Output({ + token: expenses[i].token.toBytes32(), + amount: expenses[i].amount, + recipient: _outboxes[header.destChainId].toBytes32(), + chainId: header.destChainId + }); + } + if (totalNativeValue > 0) { + maxSpent[expenses.length] = IERC7683.Output({ + token: bytes32(0), + amount: totalNativeValue, + recipient: _outboxes[header.destChainId].toBytes32(), + chainId: header.destChainId + }); + } + + return maxSpent; + } + + /** + * @dev Derive the minReceived Output for the order. + * @param orderData Order data to derive from. + */ + function _deriveMinReceived(SolverNet.OrderData memory orderData) + internal + view + returns (IERC7683.Output[] memory) + { + SolverNet.Deposit memory deposit = orderData.deposit; + + IERC7683.Output[] memory minReceived = new IERC7683.Output[](deposit.amount > 0 ? 1 : 0); + if (deposit.amount > 0) { + minReceived[0] = IERC7683.Output({ + token: deposit.token.toBytes32(), + amount: deposit.amount, + recipient: bytes32(0), + chainId: block.chainid + }); + } + + return minReceived; + } + + /** + * @dev Derive the fillInstructions for the order. + * @param orderData Order data to derive from. + */ + function _deriveFillInstructions(SolverNet.OrderData memory orderData) + internal + view + returns (IERC7683.FillInstruction[] memory) + { + SolverNet.Header memory header = orderData.header; + SolverNet.Call[] memory calls = orderData.calls; + SolverNet.Expense[] memory expenses = orderData.expenses; + + IERC7683.FillInstruction[] memory fillInstructions = new IERC7683.FillInstruction[](1); + fillInstructions[0] = IERC7683.FillInstruction({ + destinationChainId: header.destChainId, + destinationSettler: _outboxes[header.destChainId].toBytes32(), + originData: abi.encode( + SolverNet.FillOriginData({ + srcChainId: uint64(block.chainid), + destChainId: header.destChainId, + fillDeadline: header.fillDeadline, + calls: calls, + expenses: expenses + }) + ) + }); + + return fillInstructions; + } + + /** + * @dev Resolve the order without validation. + * @param orderData Order data to resolve. + */ + function _resolve(SolverNet.OrderData memory orderData) internal view returns (ResolvedCrossChainOrder memory) { + SolverNet.Header memory header = orderData.header; + + IERC7683.Output[] memory maxSpent = _deriveMaxSpent(orderData); + IERC7683.Output[] memory minReceived = _deriveMinReceived(orderData); + IERC7683.FillInstruction[] memory fillInstructions = _deriveFillInstructions(orderData); + + return ResolvedCrossChainOrder({ + user: header.owner, + originChainId: block.chainid, + openDeadline: 0, + fillDeadline: header.fillDeadline, + orderId: _nextId(), + maxSpent: maxSpent, + minReceived: minReceived, + fillInstructions: fillInstructions + }); + } + + /** + * @notice Validate and intake an ERC20 or native deposit. + * @param deposit Deposit to process. + */ + function _processDeposit(SolverNet.Deposit memory deposit) internal { + if (deposit.token == address(0)) { + if (msg.value != deposit.amount) revert InvalidNativeDeposit(); + } else { + deposit.token.safeTransferFrom(msg.sender, address(this), deposit.amount); + } + } + + /** + * @dev Opens a new order by initializing its state. + * @param orderData Order data to open. + */ + function _openOrder(SolverNet.OrderData memory orderData) + internal + returns (ResolvedCrossChainOrder memory resolved) + { + resolved = _resolve(orderData); + bytes32 id = _incrementId(); + + _orderHeader[id] = orderData.header; + _orderDeposit[id] = orderData.deposit; + for (uint256 i; i < orderData.calls.length; ++i) { + _orderCalls[id].push(orderData.calls[i]); + } + for (uint256 i; i < orderData.expenses.length; ++i) { + _orderExpenses[id].push(orderData.expenses[i]); + } + + _upsertOrder(id, Status.Pending, msg.sender); + + return resolved; + } + + /** + * @dev Transfer deposit to recipient. Used for both refunds and claims. + * @param id ID of the order. + * @param to Address to send deposits to. + */ + function _transferDeposit(bytes32 id, address to) internal { + SolverNet.Deposit memory deposit = _orderDeposit[id]; + + if (deposit.amount > 0) { + if (deposit.token == address(0)) to.safeTransferETH(deposit.amount); + else deposit.token.safeTransfer(to, deposit.amount); + } + } + + /** + * @dev Update or insert order state by id. + * @param id ID of the order. + * @param status Status to upsert. + * @param updatedBy Address updating the order, only written to state if status is Accepted or Filled. + */ + function _upsertOrder(bytes32 id, Status status, address updatedBy) internal { + OrderState memory state = _orderState[id]; + + state.status = status; + state.timestamp = uint32(block.timestamp); + if (status == Status.Accepted) state.claimant = updatedBy; + if (status == Status.Filled && state.claimant == address(0)) state.claimant = updatedBy; + + _orderState[id] = state; + _latestOrderIdByStatus[status] = id; + } + + /** + * @dev Return the next order ID. + */ + function _nextId() internal view returns (bytes32) { + return bytes32(_lastId + 1); + } + + /** + * @dev Increment and return the next order ID. + */ + function _incrementId() internal returns (bytes32) { + return bytes32(++_lastId); + } + + /** + * @dev Returns call hash. Used to discern fulfillment. + * @param orderId ID of the order. + */ + function _fillHash(bytes32 orderId) internal view returns (bytes32) { + SolverNet.Header memory header = _orderHeader[orderId]; + SolverNet.Call[] memory calls = _orderCalls[orderId]; + SolverNet.Expense[] memory expenses = _orderExpenses[orderId]; + + SolverNet.FillOriginData memory fillOriginData = SolverNet.FillOriginData({ + srcChainId: uint64(block.chainid), + destChainId: header.destChainId, + fillDeadline: header.fillDeadline, + calls: calls, + expenses: expenses + }); + + return keccak256(abi.encode(orderId, fillOriginData)); + } +} diff --git a/contracts/solve/src/_optimized/SolverNetOutbox.sol b/contracts/solve/src/_optimized/SolverNetOutbox.sol new file mode 100644 index 000000000..cf7becac4 --- /dev/null +++ b/contracts/solve/src/_optimized/SolverNetOutbox.sol @@ -0,0 +1,276 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity =0.8.24; + +import { OwnableRoles } from "solady/src/auth/OwnableRoles.sol"; +import { ReentrancyGuard } from "solady/src/utils/ReentrancyGuard.sol"; +import { Initializable } from "solady/src/utils/Initializable.sol"; +import { DeployedAt } from "../util/DeployedAt.sol"; +import { XAppBase } from "core/src/pkg/XAppBase.sol"; +import { ISolverNetOutbox } from "./interfaces/ISolverNetOutbox.sol"; +import { SolverNetExecutor } from "./SolverNetExecutor.sol"; +import { SafeTransferLib } from "solady/src/utils/SafeTransferLib.sol"; +import { FixedPointMathLib } from "solady/src/utils/FixedPointMathLib.sol"; +import { ConfLevel } from "core/src/libraries/ConfLevel.sol"; +import { TypeMax } from "core/src/libraries/TypeMax.sol"; +import { SolverNet } from "./lib/SolverNet.sol"; +import { AddrUtils } from "../lib/AddrUtils.sol"; +import { ISolverNetInbox } from "./interfaces/ISolverNetInbox.sol"; + +/** + * @title SolverNetOutbox + * @notice Entrypoint for fulfillments of user solve requests. + */ +contract SolverNetOutbox is OwnableRoles, ReentrancyGuard, Initializable, DeployedAt, XAppBase, ISolverNetOutbox { + using SafeTransferLib for address; + using AddrUtils for bytes32; + + /** + * @notice Role for solvers. + * @dev _ROLE_0 evaluates to '1'. + */ + uint256 internal constant SOLVER = _ROLE_0; + + /** + * @notice Stubbed calldata for SolveInbox.markFilled. Used to estimate the gas cost. + * @dev Type maxes used to ensure no non-zero bytes in fee estimation. + */ + bytes internal constant MARK_FILLED_STUB_CDATA = + abi.encodeCall(ISolverNetInbox.markFilled, (TypeMax.Bytes32, TypeMax.Bytes32, TypeMax.Address)); + + /** + * @notice Addresses of the inbox contracts. + */ + mapping(uint64 chainId => address inbox) internal _inboxes; + + /** + * @notice Executor contract handling calls. + * @dev An executor is used so infinite approvals from solvers cannot be abused. + */ + SolverNetExecutor internal _executor; + + /** + * @notice Maps fillHash (hash of fill instruction origin data) to true, if filled. + * @dev Used to prevent duplicate fulfillment. + */ + mapping(bytes32 fillHash => bool filled) internal _filled; + + constructor() { + _disableInitializers(); + } + + /** + * @notice Initialize the contract's owner and solver. + * @dev Used instead of constructor as we want to use the transparent upgradeable proxy pattern. + * @param owner_ Address of the owner. + * @param solver_ Address of the solver. + * @param omni_ Address of the OmniPortal. + */ + function initialize(address owner_, address solver_, address omni_) external initializer { + _initializeOwner(owner_); + _grantRoles(solver_, SOLVER); + _setOmniPortal(omni_); + _executor = new SolverNetExecutor(address(this)); + } + + /** + * @notice Set the inbox addresses for the given chain IDs. + * @param chainIds IDs of the chains. + * @param inboxes Addresses of the inboxes. + */ + function setInboxes(uint64[] calldata chainIds, address[] calldata inboxes) external onlyOwner { + for (uint256 i; i < chainIds.length; ++i) { + _inboxes[chainIds[i]] = inboxes[i]; + emit InboxSet(chainIds[i], inboxes[i]); + } + } + + /** + * @notice Returns the address of the executor contract. + */ + function executor() external view returns (address) { + return address(_executor); + } + + /** + * @notice Returns the xcall fee required to mark an order filled on the source inbox. + * @param originData Data emitted on the origin to parameterize the fill. + * @return Fee amount in native currency. + */ + function fillFee(bytes calldata originData) public view returns (uint256) { + SolverNet.FillOriginData memory fillData = abi.decode(originData, (SolverNet.FillOriginData)); + return feeFor(fillData.srcChainId, MARK_FILLED_STUB_CDATA, uint64(_fillGasLimit(fillData))); + } + + /** + * @notice Returns true if the order has been filled. + * @param orderId ID of the order the source inbox. + * @param originData Data emitted on the origin to parameterize the fill + */ + function didFill(bytes32 orderId, bytes calldata originData) external view returns (bool) { + return _filled[_fillHash(orderId, originData)]; + } + + /** + * @notice Fills a particular order on the destination chain. + * @param orderId Unique order identifier for this order. + * @param originData Data emitted on the origin to parameterize the fill. + * @param fillerData ABI encoded address to mark as claimant for the order. + */ + function fill(bytes32 orderId, bytes calldata originData, bytes calldata fillerData) + external + payable + onlyRoles(SOLVER) + nonReentrant + { + SolverNet.FillOriginData memory fillData = abi.decode(originData, (SolverNet.FillOriginData)); + address claimant = msg.sender; + + if (fillData.destChainId != block.chainid) revert WrongDestChain(); + if (fillData.fillDeadline < block.timestamp && fillData.fillDeadline != 0) revert FillDeadlinePassed(); + if (fillerData.length != 0 && fillerData.length != 32) revert BadFillerData(); + if (fillerData.length == 32) claimant = abi.decode(fillerData, (address)); + + uint256 totalNativeValue = _executeCalls(fillData); + _markFilled(orderId, fillData, claimant, totalNativeValue); + } + + /** + * @notice Wrap a call with approved / enforced expenses. + * Approve spenders. Verify post-call balances match pre-call. + * @dev Expenses doesn't contain native tokens sent alongside the call. + */ + modifier withExpenses(SolverNet.Expense[] memory expenses) { + // transfer from solver, approve spenders + for (uint256 i; i < expenses.length; ++i) { + SolverNet.Expense memory expense = expenses[i]; + address spender = expense.spender; + address token = expense.token; + uint256 amount = expense.amount; + + token.safeTransferFrom(msg.sender, address(_executor), amount); + // We remotely set token approvals on executor so we don't need to reprocess Call expenses there. + if (spender != address(0)) _executor.approve(token, spender, amount); + } + + _; + + // refund excess, revoke approvals + // + // NOTE: If anyone transfers this token to this outbox outside + // SolverNetOutbox.fill(...), the next solver to fill a call with + // that token as an expense will get the balance. + // This includes the call target. + for (uint256 i; i < expenses.length; ++i) { + SolverNet.Expense memory expense = expenses[i]; + address token = expense.token; + uint256 tokenBalance = token.balanceOf(address(_executor)); + + if (tokenBalance > 0) { + address spender = expense.spender; + if (spender != address(0)) _executor.approve(token, spender, 0); + _executor.transfer(token, msg.sender, tokenBalance); + } + } + + // send any potential native refund sent to executor back to solver + uint256 nativeBalance = address(_executor).balance; + if (nativeBalance > 0) _executor.transferNative(msg.sender, nativeBalance); + } + + /** + * @notice Verify and execute a call. Expenses are processed and enforced. + * @param fillData ABI decoded fill originData. + * @return totalNativeValue total native value of the calls. + */ + function _executeCalls(SolverNet.FillOriginData memory fillData) + internal + withExpenses(fillData.expenses) + returns (uint256) + { + uint256 totalNativeValue; + + for (uint256 i; i < fillData.calls.length; ++i) { + SolverNet.Call memory call = fillData.calls[i]; + _executor.execute{ value: call.value }( + call.target, call.value, abi.encodePacked(call.selector, call.params) + ); + unchecked { + totalNativeValue += call.value; + } + } + + return totalNativeValue; + } + + /** + * @notice Mark an order as filled. Require sufficient native payment, refund excess. + * @param orderId ID of the order. + * @param fillData ABI decoded fill originData. + * @param claimant Address specified by the filler to claim the order (msg.sender if none specified). + * @param totalNativeValue Total native value of the calls. + */ + function _markFilled( + bytes32 orderId, + SolverNet.FillOriginData memory fillData, + address claimant, + uint256 totalNativeValue + ) internal { + // mark filled on outbox (here) + bytes32 fillHash = _fillHash(orderId, abi.encode(fillData)); + if (_filled[fillHash]) revert AlreadyFilled(); + _filled[fillHash] = true; + + // mark filled on inbox + uint256 fee = xcall({ + destChainId: fillData.srcChainId, + conf: ConfLevel.Finalized, + to: _inboxes[fillData.srcChainId], + data: abi.encodeCall(ISolverNetInbox.markFilled, (orderId, fillHash, claimant)), + gasLimit: uint64(_fillGasLimit(fillData)) + }); + uint256 totalSpent = totalNativeValue + fee; + if (msg.value < totalSpent) revert InsufficientFee(); + + // refund any overpayment in native currency + uint256 refund = msg.value - totalSpent; + if (refund > 0) msg.sender.safeTransferETH(refund); + + emit Filled(orderId, fillHash, msg.sender); + } + + /** + * @dev Returns call hash. Used to discern fulfillment. + */ + function _fillHash(bytes32 srcReqId, bytes memory originData) internal pure returns (bytes32) { + return keccak256(abi.encode(srcReqId, originData)); + } + + /** + * @notice Returns the gas limit required to mark an order as filled on the source inbox. + * @param fillData ABI decoded fill originData. + * @return gasLimit Gas limit for the fill. + */ + function _fillGasLimit(SolverNet.FillOriginData memory fillData) internal pure returns (uint256) { + // 2500 gas for the Metadata struct SLOAD. + uint256 metadataGas = 2500; + + // 2500 gas for Call array length SLOAD + dynamic cost of reading each call. + uint256 callsGas = 2500; + for (uint256 i; i < fillData.calls.length; ++i) { + SolverNet.Call memory call = fillData.calls[i]; + unchecked { + // 5000 gas for the two slots that hold target, selector, and value. + // 2500 gas per params slot (1 per function argument) used (minimum of 1 slot). + callsGas += 5000 + (FixedPointMathLib.divUp(call.params.length + 32, 32) * 2500); + } + } + + // 2500 gas for Expense array length SLOAD + cost of reading each expense. + uint256 expensesGas = 2500; + unchecked { + expensesGas += fillData.expenses.length * 5000; + } + + return metadataGas + callsGas + expensesGas + 100_000; // 100k base gas limit + } +} diff --git a/contracts/solve/src/_optimized/interfaces/ISolverNetExecutor.sol b/contracts/solve/src/_optimized/interfaces/ISolverNetExecutor.sol new file mode 100644 index 000000000..3da67a9fd --- /dev/null +++ b/contracts/solve/src/_optimized/interfaces/ISolverNetExecutor.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity =0.8.24; + +interface ISolverNetExecutor { + /** + * @notice Error thrown when the sender is not the outbox. + */ + error NotOutbox(); + + /** + * @notice Error thrown when the call fails. + */ + error CallFailed(); + + /** + * @notice Address of the outbox. + */ + function outbox() external view returns (address); + + /** + * @notice Approves a spender (usually call target) to spend a token held by the executor. + * @dev Called prior to `execute` in order to ensure tokens can be spent and after to purge excess approvals. + */ + function approve(address token, address spender, uint256 amount) external; + + /** + * @notice Executes a call. + * @param target Address of the contract to call. + * @param value Value to send with the call. + * @param data Data to send with the call. + */ + function execute(address target, uint256 value, bytes calldata data) external payable; + + /** + * @notice Transfers a token to a recipient. + * @dev Called after `execute` in order to refund any excess or returned tokens. + */ + function transfer(address token, address to, uint256 amount) external; + + /** + * @notice Transfers native currency to a recipient. + * @dev Called after `execute` in order to refund any native currency sent back to the executor. + */ + function transferNative(address to, uint256 amount) external; +} diff --git a/contracts/solve/src/_optimized/interfaces/ISolverNetInbox.sol b/contracts/solve/src/_optimized/interfaces/ISolverNetInbox.sol new file mode 100644 index 000000000..3f94c5be2 --- /dev/null +++ b/contracts/solve/src/_optimized/interfaces/ISolverNetInbox.sol @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity =0.8.24; + +import { IOriginSettler } from "../../erc7683/IOriginSettler.sol"; + +interface ISolverNetInbox is IOriginSettler { + // Validation errors + error InvalidOrderTypehash(); + error InvalidOrderData(); + error InvalidChainId(); + error InvalidFillDeadline(); + error InvalidCallTarget(); + error InvalidExpenseToken(); + error InvalidExpenseAmount(); + + // Open order errors + error InvalidNativeDeposit(); + + // Order accept/reject/cancel errors + error OrderNotPending(); + error FillDeadlinePassed(); + + // Order fill errors + error OrderNotPendingOrAccepted(); + error WrongSourceChain(); + error WrongFillHash(); + + // Order claim errors + error OrderNotFilled(); + + /** + * @notice Emitted when an outbox is set. + * @param chainId ID of the chain. + * @param outbox Address of the outbox. + */ + event OutboxSet(uint64 indexed chainId, address indexed outbox); + + /** + * @notice Emitted when an order is accepted. + * @param id ID of the order. + * @param by Address of the solver who accepted the order. + */ + event Accepted(bytes32 indexed id, address indexed by); + + /** + * @notice Emitted when an order is rejected. + * @param id ID of the order. + * @param by Address of the solver who rejected the order. + * @param reason Reason code for rejecting the order. + */ + event Rejected(bytes32 indexed id, address indexed by, uint8 indexed reason); + + /** + * @notice Emitted when an order is cancelled. + * @param id ID of the order. + */ + event Reverted(bytes32 indexed id); + + /** + * @notice Emitted when an order is filled. + * @param id ID of the order. + * @param callHash Hash of the call executed on another chain. + * @param creditedTo Address of the recipient credited the funds by the solver. + */ + event Filled(bytes32 indexed id, bytes32 indexed callHash, address indexed creditedTo); + + /** + * @notice Emitted when an order is claimed. + * @param id ID of the order. + * @param by The solver address that claimed the order. + * @param to The recipient of claimed deposits. + */ + event Claimed(bytes32 indexed id, address indexed by, address indexed to); + + /** + * @notice Status of an order. + */ + enum Status { + Invalid, + Pending, + Accepted, + Rejected, + Reverted, + Filled, + Claimed + } + + /** + * @notice State of an order. + * @param status Latest order status. + * @param timestamp Timestamp of the status update. + * @param claimant Address of the claimant, defined at fill. + */ + struct OrderState { + Status status; + uint32 timestamp; + address claimant; + } + + /** + * @notice Set the outbox addresses for the given chain IDs. + * @param chainIds IDs of the chains. + * @param outboxes Addresses of the outboxes. + */ + function setOutboxes(uint64[] calldata chainIds, address[] calldata outboxes) external; + + /** + * @notice Returns the order and its state with the given ID. + * @param id ID of the order. + */ + function getOrder(bytes32 id) + external + view + returns (ResolvedCrossChainOrder memory order, OrderState memory state); + + /** + * @notice Returns the next order ID. + */ + function getNextId() external view returns (bytes32); + + /** + * @notice Returns the latest order with the given status. + * @param status Order status to query. + */ + function getLatestOrderIdByStatus(Status status) external view returns (bytes32); + + /** + * @dev Validate the onchain order. + * @param order OnchainCrossChainOrder to validate. + */ + function validate(OnchainCrossChainOrder calldata order) external view returns (bool); + + /** + * @notice Accept an open order. + * @dev Only a whitelisted solver can accept. + * @param id ID of the order. + */ + function accept(bytes32 id) external; + + /** + * @notice Reject an open order and refund deposits. + * @dev Only a whitelisted solver can reject. + * @param id ID of the order. + * @param reason Reason code for rejection. + */ + function reject(bytes32 id, uint8 reason) external; + + /** + * @notice Cancel an open and refund deposits. + * @dev Only order initiator can cancel. + * @param id ID of the order. + */ + function cancel(bytes32 id) external; + + /** + * @notice Fill an order. + * @dev Only callable by the outbox. + * @param id ID of the order. + * @param fillHash Hash of fill instructions origin data. + * @param claimant Address to claim the order, provided by the filler. + */ + function markFilled(bytes32 id, bytes32 fillHash, address claimant) external; + + /** + * @notice Claim a filled order. + * @param id ID of the order. + * @param to Address to send deposits to. + */ + function claim(bytes32 id, address to) external; +} diff --git a/contracts/solve/src/_optimized/interfaces/ISolverNetOutbox.sol b/contracts/solve/src/_optimized/interfaces/ISolverNetOutbox.sol new file mode 100644 index 000000000..0d37e973c --- /dev/null +++ b/contracts/solve/src/_optimized/interfaces/ISolverNetOutbox.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity =0.8.24; + +import { IDestinationSettler } from "../../erc7683/IDestinationSettler.sol"; + +interface ISolverNetOutbox is IDestinationSettler { + error BadFillerData(); + error AlreadyFilled(); + error WrongDestChain(); + error InsufficientFee(); + error FillDeadlinePassed(); + + /** + * @notice Emitted when an inbox is set. + * @param chainId ID of the chain. + * @param inbox Address of the inbox. + */ + event InboxSet(uint64 indexed chainId, address indexed inbox); + + /** + * @notice Emitted when a cross-chain request is filled on the destination chain + * @param orderId ID of the order on the source chain + * @param fillHash Hash of the fill origin data + * @param filledBy Address of the solver that filled the oder + */ + event Filled(bytes32 indexed orderId, bytes32 indexed fillHash, address indexed filledBy); + + /** + * @notice Set the inbox addresses for the given chain IDs. + * @param chainIds IDs of the chains. + * @param inboxes Addresses of the inboxes. + */ + function setInboxes(uint64[] calldata chainIds, address[] calldata inboxes) external; + + /** + * @notice Returns the address of the executor contract. + */ + function executor() external view returns (address); + + /** + * @notice Returns the xcall fee required to mark an order filled on the source inbox. + * @param originData Data emitted on the origin to parameterize the fill. + * @return Fee amount in native currency. + */ + function fillFee(bytes calldata originData) external view returns (uint256); + + /** + * @notice Returns whether a call has been filled. + * @param srcReqId ID of the on the source inbox. + * @param originData Data emitted on the origin to parameterize the fill + * @return Whether the call has been filled. + */ + function didFill(bytes32 srcReqId, bytes calldata originData) external view returns (bool); +} diff --git a/contracts/solve/src/_optimized/lib/SolverNet.sol b/contracts/solve/src/_optimized/lib/SolverNet.sol new file mode 100644 index 000000000..cf24a65a3 --- /dev/null +++ b/contracts/solve/src/_optimized/lib/SolverNet.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity =0.8.24; + +library SolverNet { + struct OrderData { + Header header; + Deposit deposit; + Call[] calls; + Expense[] expenses; + } + + struct Header { + address owner; + uint64 destChainId; + uint32 fillDeadline; + } + + struct Call { + address target; + bytes4 selector; + uint256 value; + bytes params; + } + + struct Deposit { + address token; + uint96 amount; + } + + struct Expense { + address spender; + address token; + uint96 amount; + } + + struct FillOriginData { + uint64 srcChainId; + uint64 destChainId; + uint32 fillDeadline; + Call[] calls; + Expense[] expenses; + } +}