diff --git a/src/Orders.sol b/src/Orders.sol index 9776025..8b79e58 100644 --- a/src/Orders.sol +++ b/src/Orders.sol @@ -5,103 +5,120 @@ import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; /// @notice Contract capable of processing fulfillment of intent-based Orders. abstract contract OrderDestination { - /// @notice Emitted when an swap order is fulfilled by the Builder. - /// @param originChainId - The chainId on which the swap order was submitted. - /// @param token - The address of the token transferred to the recipient. address(0) corresponds to native Ether. + /// @notice Emitted when an Order's Output is sent to the recipient. + /// @dev There may be multiple Outputs per Order. + /// @param originChainId - The chainId on which the Order was initiated. /// @param recipient - The recipient of the token. + /// @param token - The address of the token transferred to the recipient. address(0) corresponds to native Ether. /// @param amount - The amount of the token transferred to the recipient. - event SwapFulfilled( - uint256 indexed originChainId, address indexed token, address indexed recipient, uint256 amount - ); - - /// @notice Fulfill a rollup Swap order. - /// The user calls `swap` on a rollup; the Builder calls `fulfillSwap` on the target chain. - /// @custom:emits SwapFulfilled - /// @param originChainId - The chainId of the rollup on which `swap` was called. + event OutputFilled(uint256 indexed originChainId, address indexed recipient, address indexed token, uint256 amount); + + /// @notice Send the Output(s) of an Order to fulfill it. + /// The user calls `initiate` on a rollup; the Builder calls `fill` on the target chain for each Output. + /// @custom:emits OutputFilled + /// @param originChainId - The chainId on which the Order was initiated. + /// @param recipient - The recipient of the token. /// @param token - The address of the token to be transferred to the recipient. /// address(0) corresponds to native Ether. - /// @param recipient - The recipient of the token. /// @param amount - The amount of the token to be transferred to the recipient. - function fulfillSwap(uint256 originChainId, address token, address recipient, uint256 amount) external payable { + function fill(uint256 originChainId, address recipient, address token, uint256 amount) external payable { if (token == address(0)) { require(amount == msg.value); payable(recipient).transfer(msg.value); } else { IERC20(token).transferFrom(msg.sender, recipient, amount); } - emit SwapFulfilled(originChainId, token, recipient, amount); + emit OutputFilled(originChainId, recipient, token, amount); } } /// @notice Contract capable of registering initiation of intent-based Orders. abstract contract OrderOrigin { - /// @notice Thrown when an swap transaction is submitted with a deadline that has passed. + /// @notice Tokens sent by the swapper as inputs to the order + /// @dev From ERC-7683 + struct Input { + /// @dev The address of the ERC20 token on the origin chain + address token; + /// @dev The amount of the token to be sent + uint256 amount; + } + + /// @notice Tokens that must be receive for a valid order fulfillment + /// @dev From ERC-7683 + struct Output { + /// @dev The address of the ERC20 token on the destination chain + /// @dev address(0) used as a sentinel for the native token + address token; + /// @dev The amount of the token to be sent + uint256 amount; + /// @dev The address to receive the output tokens + address recipient; + /// @dev The destination chain for this output + uint32 chainId; + } + + /// @notice Thrown when an Order is submitted with a deadline that has passed. error OrderExpired(); - /// @notice Emitted when an swap order is successfully processed, indicating it was also fulfilled on the target chain. - /// @dev See `swap` for parameter docs. - event Swap( - uint256 indexed targetChainId, - address indexed tokenIn, - address indexed tokenOut, - address recipient, - uint256 deadline, - uint256 amountIn, - uint256 amountOut - ); + /// @notice Thrown when trying to call `sweep` if not the Builder of the block. + error OnlyBuilder(); + + /// @notice Emitted when an Order is submitted for fulfillment. + event Order(uint256 deadline, Input[] inputs, Output[] outputs); /// @notice Emitted when tokens or native Ether is swept from the contract. /// @dev Intended to improve visibility for Builders to ensure Sweep isn't called unexpectedly. /// Intentionally does not bother to emit which token(s) were swept, nor their amounts. - event Sweep(address indexed token, address indexed recipient, uint256 amount); + event Sweep(address indexed recipient, address indexed token, uint256 amount); /// @notice Request to swap ERC20s. - /// @dev tokenIn is provided on the rollup; in exchange, - /// tokenOut is expected to be received on targetChainId. - /// @dev targetChainId may be the current chainId, the Host chainId, or.. - /// @dev Fees paid to the Builders for fulfilling the swap orders - /// can be included within the "exchange rate" between tokenIn and tokenOut. - /// @dev The Builder claims the tokenIn from the contract by submitting a transaction to `sweep` the tokens within the same block. - /// @dev The Rollup STF MUST NOT apply `swap` transactions to the rollup state - /// UNLESS a sufficient SwapFulfilled event is emitted on the target chain within the same block. - /// @param targetChainId - The chain on which tokens should be output. - /// @param tokenIn - The address of the token the user supplies as the input on the rollup for the trade. - /// @param tokenOut - The address of the token the user expects to receive on the target chain. - /// @param recipient - The address of the recipient of tokenOut on the target chain. - /// @param deadline - The deadline by which the swap order must be fulfilled. - /// @param amountIn - The amount of tokenIn the user supplies as the input on the rollup for the trade. - /// @param amountOut - The minimum amount of tokenOut the user expects to receive on the target chain. - /// @custom:reverts Expired if the deadline has passed. - /// @custom:emits Swap if the swap transaction succeeds. - function swap( - uint256 targetChainId, - address tokenIn, - address tokenOut, - address recipient, - uint256 deadline, - uint256 amountIn, - uint256 amountOut - ) external payable { + /// @dev inputs are provided on the rollup; in exchange, + /// outputs are expected to be received on the target chain(s). + /// @dev Fees paid to the Builders for fulfilling the Orders + /// can be included within the "exchange rate" between inputs and outputs. + /// @dev The Builder claims the inputs from the contract by submitting `sweep` transactions within the same block. + /// @dev The Rollup STF MUST NOT apply `initiate` transactions to the rollup state + /// UNLESS the outputs are delivered on the target chains within the same block. + /// @param deadline - The deadline by which the Order must be fulfilled. + /// @param inputs - The token amounts offered by the swapper in exchange for the outputs. + /// @param outputs - The token amounts that must be received on their target chain(s) in order for the Order to be executed. + /// @custom:reverts OrderExpired if the deadline has passed. + /// @custom:emits Order if the transaction mines. + function initiate(uint256 deadline, Input[] memory inputs, Output[] memory outputs) external payable { // check that the deadline hasn't passed if (block.timestamp >= deadline) revert OrderExpired(); - if (tokenIn == address(0)) { - require(amountIn == msg.value); - } else { - IERC20(tokenIn).transferFrom(msg.sender, address(this), amountIn); - } + // transfer inputs to this contract + _transferInputs(inputs); + + // emit + emit Order(deadline, inputs, outputs); + } - // emit the swap event - emit Swap(targetChainId, tokenIn, tokenOut, recipient, deadline, amountIn, amountOut); + /// @notice Transfer the Order inputs to this contract, where they can be collected by the Order filler. + function _transferInputs(Input[] memory inputs) internal { + uint256 value = msg.value; + for (uint256 i; i < inputs.length; i++) { + if (inputs[i].token == address(0)) { + // this line should underflow if there's an attempt to spend more ETH than is attached to the transaction + value -= inputs[i].amount; + } else { + IERC20(inputs[i].token).transferFrom(msg.sender, address(this), inputs[i].amount); + } + } } /// @notice Transfer the entire balance of ERC20 tokens to the recipient. - /// @dev Called by the Builder within the same block as users' `swap` transactions - /// to claim the amounts of `tokenIn`. - /// @dev Builder MUST ensure that no other account calls `sweep` before them. - /// @param token - The token to transfer. + /// @dev Called by the Builder within the same block as users' `initiate` transactions + /// to claim the `inputs`. + /// @dev Builder MUST call `sweep` atomically with `fill` (claim Inputs atomically with sending Outputs). /// @param recipient - The address to receive the tokens. - function sweep(address token, address recipient) public { + /// @param token - The token to transfer. + /// @custom:emits Sweep + /// @custom:reverts OnlyBuilder if called by non-block builder + function sweep(address recipient, address token) public { + if (msg.sender != block.coinbase) revert OnlyBuilder(); + // send ETH or tokens uint256 balance; if (token == address(0)) { balance = address(this).balance; @@ -110,7 +127,7 @@ abstract contract OrderOrigin { balance = IERC20(token).balanceOf(address(this)); IERC20(token).transfer(recipient, balance); } - emit Sweep(token, recipient, balance); + emit Sweep(recipient, token, balance); } }