Skip to content

Commit

Permalink
feat(contracts/solve): implemented ERC7683 version of solve (#2717)
Browse files Browse the repository at this point in the history
Implemented an ERC7683 compliant version of the Solve contracts. No
tests have been implemented yet at the time of this PR.

issue: #2691
  • Loading branch information
Zodomo authored Dec 18, 2024
1 parent fbb5cf3 commit bc045ed
Show file tree
Hide file tree
Showing 8 changed files with 1,197 additions and 0 deletions.
494 changes: 494 additions & 0 deletions contracts/solve/src/ERC7683/SolverNetInbox.sol

Large diffs are not rendered by default.

238 changes: 238 additions & 0 deletions contracts/solve/src/ERC7683/SolverNetOutbox.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
// 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 { XAppBase } from "core/src/pkg/XAppBase.sol";

import { SafeTransferLib } from "solady/src/utils/SafeTransferLib.sol";
import { ConfLevel } from "core/src/libraries/ConfLevel.sol";
import { TypeMax } from "core/src/libraries/TypeMax.sol";

import { ISolverNetInbox } from "./interfaces/ISolverNetInbox.sol";
import { ISolverNetOutbox } from "./interfaces/ISolverNetOutbox.sol";
import { IArbSys } from "../interfaces/IArbSys.sol";

/**
* @title SolverNetOutbox
* @notice Entrypoint for fulfillments of user solve requests.
*/
contract SolverNetOutbox is OwnableRoles, ReentrancyGuard, Initializable, XAppBase, ISolverNetOutbox {
using SafeTransferLib for address;

/**
* @notice Block number at which the contract was deployed.
*/
uint256 public immutable deployedAt;

/**
* @notice Role for solvers.
* @dev _ROLE_0 evaluates to '1'.
*/
uint256 internal constant SOLVER = _ROLE_0;

/**
* @notice Arbitrum's ArbSys precompile (0x0000000000000000000000000000000000000064)
* @dev Used to get Arbitrum block number.
*/
address internal constant ARB_SYS = 0x0000000000000000000000000000000000000064;

/**
* @notice Gas limit for SolveInbox.markFulfilled callback.
*/
uint64 internal constant MARK_FULFILLED_GAS_LIMIT = 100_000;

/**
* @notice Stubbed calldata for SolveInbox.markFulfilled. Used to estimate the gas cost.
* @dev Type maxes used to ensure no non-zero bytes in fee estimation.
*/
bytes internal constant MARK_FULFILLED_STUB_CDATA =
abi.encodeCall(ISolverNetInbox.markFulfilled, (TypeMax.Bytes32, TypeMax.Bytes32));

/**
* @notice Address of the inbox contract.
*/
address internal _inbox;

/**
* @notice Mapping of allowed calls per contract.
*/
mapping(address target => mapping(bytes4 selector => bool)) public allowedCalls;

/**
* @notice Mapping of fulfilled calls.
* @dev callHash used to prevent duplicate fulfillment.
*/
mapping(bytes32 callHash => bool fulfilled) public fulfilledCalls;

constructor() {
// Must get Arbitrum block number from ArbSys precompile, block.number returns L1 block number on Arbitrum.
if (_isContract(ARB_SYS)) {
try IArbSys(ARB_SYS).arbBlockNumber() returns (uint256 arbBlockNumber) {
deployedAt = arbBlockNumber;
} catch {
deployedAt = block.number;
}
} else {
deployedAt = block.number;
}

_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.
*/
function initialize(address owner_, address solver_, address omni_, address inbox_) external initializer {
_initializeOwner(owner_);
_grantRoles(solver_, SOLVER);
_setOmniPortal(omni_);
_inbox = inbox_;
}

/**
* @notice Returns the message passing fee required to mark a request as fulfilled on the source chain
* @param srcChainId ID of the source chain.
* @return Fee amount in native currency.
*/
function fulfillFee(uint64 srcChainId) public view returns (uint256) {
return feeFor(srcChainId, MARK_FULFILLED_STUB_CDATA, MARK_FULFILLED_GAS_LIMIT);
}

/**
* @notice Check if a call has been fulfilled.
* @param srcReqId ID of the on the source inbox.
* @param originData Data emitted on the origin to parameterize the fill
*/
function didFulfill(bytes32 srcReqId, bytes calldata originData) external view returns (bool) {
return fulfilledCalls[_callHash(srcReqId, originData)];
}

/**
* @notice Set an allowed call for a target contract.
* @param target Address of the target contract.
* @param selector 4-byte selector of the function to allow.
* @param allowed Whether the call is allowed.
*/
function setAllowedCall(address target, bytes4 selector, bool allowed) external onlyOwner {
allowedCalls[target][selector] = allowed;
emit AllowedCallSet(target, selector, allowed);
}

/**
* @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
* @dev fillerData (currently unused): Data provided by the filler to inform the fill or express their preferences
*/
function fill(bytes32 orderId, bytes calldata originData, bytes calldata)
external
payable
onlyRoles(SOLVER)
nonReentrant
{
SolverNetIntent memory intent = abi.decode(originData, (SolverNetIntent));

// Check that the destination chain is the current chain
if (intent.destChainId != block.chainid) revert WrongDestChain();

// If the call has already been fulfilled, revert. Else, mark fulfilled
bytes32 callHash = _callHash(orderId, originData);
if (fulfilledCalls[callHash]) revert AlreadyFulfilled();
fulfilledCalls[callHash] = true;

// Determine tokens required, record pre-call balances, retrieve tokens from solver, and sign approvals
(address[] memory tokens, uint256[] memory preBalances) = _prepareIntent(intent);

// Execute the calls
uint256 nativeAmountRequired = _executeIntent(intent);

// Require post-call balance matches pre-call. Ensures prerequisites match call transfers.
// Native balance is validated after xcall
for (uint256 i; i < tokens.length; ++i) {
if (tokens[i] != address(0)) {
if (tokens[i].balanceOf(address(this)) != preBalances[i]) revert InvalidPrereq();
}
}

// Mark the call as fulfilled on inbox
bytes memory xcalldata = abi.encodeCall(ISolverNetInbox.markFulfilled, (orderId, callHash));
uint256 fee = xcall(uint64(intent.srcChainId), ConfLevel.Finalized, _inbox, xcalldata, MARK_FULFILLED_GAS_LIMIT);
if (msg.value - nativeAmountRequired < fee) revert InsufficientFee();

// Refund any overpayment in native currency
uint256 refund = msg.value - nativeAmountRequired - fee;
if (refund > 0) msg.sender.safeTransferETH(refund);

emit Fulfilled(orderId, callHash, msg.sender);
}

function _prepareIntent(SolverNetIntent memory intent)
internal
returns (address[] memory tokens, uint256[] memory preBalances)
{
TokenPrereq[] memory prereqs = intent.tokenPrereqs;
tokens = new address[](prereqs.length);
preBalances = new uint256[](prereqs.length);

for (uint256 i; i < prereqs.length; ++i) {
TokenPrereq memory prereq = prereqs[i];
address token = _bytes32ToAddress(prereq.token);
tokens[i] = token;

if (token == address(0)) {
if (prereq.amount >= msg.value || prereq.amount != intent.call.value) revert InvalidPrereq();
preBalances[i] = address(this).balance - msg.value;
} else {
preBalances[i] = token.balanceOf(address(this));
address spender = _bytes32ToAddress(prereq.spender);

token.safeTransferFrom(msg.sender, address(this), prereq.amount);
token.safeApprove(spender, prereq.amount);
}
}

return (tokens, preBalances);
}

function _executeIntent(SolverNetIntent memory intent) internal returns (uint256 nativeAmountRequired) {
Call memory call = intent.call;
address target = _bytes32ToAddress(call.target);

if (!allowedCalls[target][bytes4(call.callData)]) revert CallNotAllowed();

(bool success,) = payable(target).call{ value: call.value }(call.callData);
if (!success) revert CallFailed();

return call.value;
}

/**
* @dev Returns call hash. Used to discern fullfilment.
*/
function _callHash(bytes32 srcReqId, bytes memory originData) internal pure returns (bytes32) {
return keccak256(abi.encode(srcReqId, originData));
}

/**
* @dev Returns true if the address is a contract.
*/
function _isContract(address addr) internal view returns (bool) {
uint32 size;
assembly {
size := extcodesize(addr)
}
return (size > 0);
}

/**
* @dev Convert bytes32 to address.
*/
function _bytes32ToAddress(bytes32 b) internal pure returns (address) {
return address(uint160(uint256(b)));
}
}
15 changes: 15 additions & 0 deletions contracts/solve/src/ERC7683/interfaces/IDestinationSettler.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity =0.8.24;

import { IERC7683 } from "./IERC7683.sol";

/// @title IDestinationSettler
/// @notice Standard interface for settlement contracts on the destination chain
/// @dev See https://github.com/ethereum/ERCs/blob/master/ERCS/erc-7683.md
interface IDestinationSettler is IERC7683 {
/// @notice Fills a single leg of 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 Data provided by the filler to inform the fill or express their preferences
function fill(bytes32 orderId, bytes calldata originData, bytes calldata fillerData) external payable;
}
102 changes: 102 additions & 0 deletions contracts/solve/src/ERC7683/interfaces/IERC7683.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity =0.8.24;

/// @title IERC7683
/// @notice Standard type definitions for ERC-7683 compliant settlement contracts
/// @dev See https://github.com/ethereum/ERCs/blob/master/ERCS/erc-7683.md
interface IERC7683 {
/// @notice Signals that an order has been opened
/// @param orderId a unique order identifier within this settlement system
/// @param resolvedOrder resolved order that would be returned by resolve if called instead of Open
event Open(bytes32 indexed orderId, ResolvedCrossChainOrder resolvedOrder);

/// @title GaslessCrossChainOrder CrossChainOrder type
/// @notice Standard order struct to be signed by users, disseminated to fillers, and submitted to origin settler contracts
struct GaslessCrossChainOrder {
/// @dev The contract address that the order is meant to be settled by.
/// Fillers send this order to this contract address on the origin chain
address originSettler;
/// @dev The address of the user who is initiating the swap,
/// whose input tokens will be taken and escrowed
address user;
/// @dev Nonce to be used as replay protection for the order
uint256 nonce;
/// @dev The chainId of the origin chain
uint256 originChainId;
/// @dev The timestamp by which the order must be opened
uint32 openDeadline;
/// @dev The timestamp by which the order must be filled on the destination chain
uint32 fillDeadline;
/// @dev Type identifier for the order data. This is an EIP-712 typehash.
bytes32 orderDataType;
/// @dev Arbitrary implementation-specific data
/// Can be used to define tokens, amounts, destination chains, fees, settlement parameters,
/// or any other order-type specific information
bytes orderData;
}

/// @title OnchainCrossChainOrder CrossChainOrder type
/// @notice Standard order struct for user-opened orders, where the user is the msg.sender.
struct OnchainCrossChainOrder {
/// @dev The timestamp by which the order must be filled on the destination chain
uint32 fillDeadline;
/// @dev Type identifier for the order data. This is an EIP-712 typehash.
bytes32 orderDataType;
/// @dev Arbitrary implementation-specific data
/// Can be used to define tokens, amounts, destination chains, fees, settlement parameters,
/// or any other order-type specific information
bytes orderData;
}

/// @title ResolvedCrossChainOrder type
/// @notice An implementation-generic representation of an order intended for filler consumption
/// @dev Defines all requirements for filling an order by unbundling the implementation-specific orderData.
/// @dev Intended to improve integration generalization by allowing fillers to compute the exact input and output information of any order
struct ResolvedCrossChainOrder {
/// @dev The address of the user who is initiating the transfer
address user;
/// @dev The chainId of the origin chain
uint256 originChainId;
/// @dev The timestamp by which the order must be opened
uint32 openDeadline;
/// @dev The timestamp by which the order must be filled on the destination chain(s)
uint32 fillDeadline;
/// @dev The unique identifier for this order within this settlement system
bytes32 orderId;
/// @dev The max outputs that the filler will send. It's possible the actual amount depends on the state of the destination
/// chain (destination dutch auction, for instance), so these outputs should be considered a cap on filler liabilities.
Output[] maxSpent;
/// @dev The minimum outputs that must to be given to the filler as part of order settlement. Similar to maxSpent, it's possible
/// that special order types may not be able to guarantee the exact amount at open time, so this should be considered
/// a floor on filler receipts.
Output[] minReceived;
/// @dev Each instruction in this array is parameterizes a single leg of the fill. This provides the filler with the information
/// necessary to perform the fill on the destination(s).
FillInstruction[] fillInstructions;
}

/// @notice Tokens that must be receive for a valid order fulfillment
struct Output {
/// @dev The address of the ERC20 token on the destination chain
/// @dev address(0) used as a sentinel for the native token
bytes32 token;
/// @dev The amount of the token to be sent
uint256 amount;
/// @dev The address to receive the output tokens
bytes32 recipient;
/// @dev The destination chain for this output
uint256 chainId;
}

/// @title FillInstruction type
/// @notice Instructions to parameterize each leg of the fill
/// @dev Provides all the origin-generated information required to produce a valid fill leg
struct FillInstruction {
/// @dev The contract address that the order is meant to be settled by
uint64 destinationChainId;
/// @dev The contract address that the order is meant to be filled on
bytes32 destinationSettler;
/// @dev The data generated on the origin chain needed by the destinationSettler to process the fill
bytes originData;
}
}
36 changes: 36 additions & 0 deletions contracts/solve/src/ERC7683/interfaces/IOriginSettler.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity =0.8.24;

import { IERC7683 } from "./IERC7683.sol";

/// @title IOriginSettler
/// @notice Standard interface for settlement contracts on the origin chain
/// @dev See https://github.com/ethereum/ERCs/blob/master/ERCS/erc-7683.md
interface IOriginSettler is IERC7683 {
/// @notice Opens a gasless cross-chain order on behalf of a user.
/// @dev To be called by the filler.
/// @dev This method must emit the Open event
/// @param order The GaslessCrossChainOrder definition
/// @param signature The user's signature over the order
/// @param originFillerData Any filler-defined data required by the settler
// function openFor(GaslessCrossChainOrder calldata order, bytes calldata signature, bytes calldata originFillerData) external;

/// @notice Opens a cross-chain order
/// @dev To be called by the user
/// @dev This method must emit the Open event
/// @param order The OnchainCrossChainOrder definition
function open(OnchainCrossChainOrder calldata order) external payable;

/// @notice Resolves a specific GaslessCrossChainOrder into a generic ResolvedCrossChainOrder
/// @dev Intended to improve standardized integration of various order types and settlement contracts
/// @param order The GaslessCrossChainOrder definition
/// @param originFillerData Any filler-defined data required by the settler
/// @return ResolvedCrossChainOrder hydrated order data including the inputs and outputs of the order
// function resolveFor(GaslessCrossChainOrder calldata order, bytes calldata originFillerData) external view returns (ResolvedCrossChainOrder memory);

/// @notice Resolves a specific OnchainCrossChainOrder into a generic ResolvedCrossChainOrder
/// @dev Intended to improve standardized integration of various order types and settlement contracts
/// @param order The OnchainCrossChainOrder definition
/// @return ResolvedCrossChainOrder hydrated order data including the inputs and outputs of the order
function resolve(OnchainCrossChainOrder calldata order) external view returns (ResolvedCrossChainOrder memory);
}
Loading

0 comments on commit bc045ed

Please sign in to comment.