Skip to content

Commit

Permalink
chore(contracts/solve): refactor fill and order data
Browse files Browse the repository at this point in the history
  • Loading branch information
kevinhalliday committed Dec 20, 2024
1 parent eee9c7e commit b228b55
Show file tree
Hide file tree
Showing 5 changed files with 184 additions and 167 deletions.
115 changes: 60 additions & 55 deletions contracts/solve/src/ERC7683/SolverNetInbox.sol
Original file line number Diff line number Diff line change
Expand Up @@ -101,53 +101,52 @@ contract SolverNetInbox is OwnableRoles, ReentrancyGuard, Initializable, Deploye
* @param order OnchainCrossChainOrder to validate.
*/
function validateOrder(OnchainCrossChainOrder calldata order) external view returns (bool) {
_validateOrder(order);
_parseOrder(order);
return true;
}

/**
* @dev Resolve the onchain order.
* @notice Resolve the onchain order.
* @param order OnchainCrossChainOrder to resolve.
*/
function resolve(OnchainCrossChainOrder calldata order) public view returns (ResolvedCrossChainOrder memory) {
SolverNetOrderData memory orderData = abi.decode(order.orderData, (SolverNetOrderData));
SolverNetIntent memory intent = orderData.intent;
TokenPrereq[] memory prereqs = intent.tokenPrereqs;
TokenDeposit[] memory deposits = orderData.deposits;
OrderData memory orderData = abi.decode(order.orderData, (OrderData));
Call memory call = orderData.call;
Deposit[] memory deposits = orderData.deposits;

Output[] memory maxSpent = new Output[](prereqs.length);
for (uint256 i; i < prereqs.length; ++i) {
Output[] memory maxSpent = new Output[](call.expenses.length);
for (uint256 i; i < call.expenses.length; ++i) {
maxSpent[i] = Output({
token: prereqs[i].token,
amount: prereqs[i].amount,
recipient: _addressToBytes32(_outbox),
chainId: intent.destChainId
token: call.expenses[i].token,
amount: call.expenses[i].amount,
recipient: _addressToBytes32(_outbox), // for solver, recipient is always outbox
chainId: call.destChainId
});
}

Output[] memory minReceived = new Output[](deposits.length);
for (uint256 i; i < deposits.length; ++i) {
minReceived[i] = Output({
token: _addressToBytes32(deposits[i].token),
token: deposits[i].token,
amount: deposits[i].amount,
recipient: bytes32(0),
recipient: bytes32(0), // recipient is solver, which is only known on acceptance
chainId: block.chainid
});
}

FillInstruction[] memory fillInstructions = new FillInstruction[](1);
fillInstructions[0] = FillInstruction({
destinationChainId: intent.destChainId,
destinationChainId: call.destChainId,
destinationSettler: _addressToBytes32(_outbox),
originData: abi.encode(intent)
originData: abi.encode(FillOriginData({ srcChainId: uint64(block.chainid), call: call }))
});

return ResolvedCrossChainOrder({
user: msg.sender,
originChainId: block.chainid,
openDeadline: uint32(block.timestamp),
fillDeadline: order.fillDeadline,
orderId: _nextId(),
orderId: _nextId(), // use next id for view, may not be id when opened
maxSpent: maxSpent,
minReceived: minReceived,
fillInstructions: fillInstructions
Expand All @@ -160,7 +159,7 @@ contract SolverNetInbox is OwnableRoles, ReentrancyGuard, Initializable, Deploye
* @param order OnchainCrossChainOrder to open.
*/
function open(OnchainCrossChainOrder calldata order) external payable nonReentrant {
SolverNetOrderData memory orderData = _validateOrder(order);
OrderData memory orderData = _parseOrder(order);

_processDeposits(orderData.deposits);

Expand All @@ -176,7 +175,7 @@ contract SolverNetInbox is OwnableRoles, ReentrancyGuard, Initializable, Deploye
*/
function accept(bytes32 id) external onlyRoles(SOLVER) nonReentrant {
OrderState memory state = _orderState[id];
if (state.status != Status.Pending) revert NotPending();
if (state.status != Status.Pending) revert OrderNotPending();

state.status = Status.Accepted;
state.acceptedBy = msg.sender;
Expand All @@ -193,7 +192,7 @@ contract SolverNetInbox is OwnableRoles, ReentrancyGuard, Initializable, Deploye
*/
function reject(bytes32 id, uint8 reason) external onlyRoles(SOLVER) nonReentrant {
OrderState memory state = _orderState[id];
if (state.status != Status.Pending) revert NotPending();
if (state.status != Status.Pending) revert OrderNotPending();

state.status = Status.Rejected;
_upsertOrder(id, state);
Expand All @@ -210,7 +209,7 @@ contract SolverNetInbox is OwnableRoles, ReentrancyGuard, Initializable, Deploye
ResolvedCrossChainOrder memory order = _orders[id];
OrderState memory state = _orderState[id];
if (state.status != Status.Pending && state.status != Status.Rejected) {
revert NotPendingOrRejected();
revert OrderNotPendingOrRejected();
}
if (order.user != msg.sender) revert Unauthorized();

Expand All @@ -230,7 +229,7 @@ contract SolverNetInbox is OwnableRoles, ReentrancyGuard, Initializable, Deploye
function markFilled(bytes32 id, bytes32 fillHash) external xrecv nonReentrant {
ResolvedCrossChainOrder memory order = _orders[id];
OrderState memory state = _orderState[id];
if (state.status != Status.Accepted) revert NotAccepted();
if (state.status != Status.Accepted) revert OrderNotAccepted();
if (xmsg.sender != _outbox) revert NotOutbox();
if (xmsg.sourceChainId != order.fillInstructions[0].destinationChainId) revert WrongSourceChain();

Expand All @@ -253,7 +252,7 @@ contract SolverNetInbox is OwnableRoles, ReentrancyGuard, Initializable, Deploye
function claim(bytes32 id, address to) external nonReentrant {
ResolvedCrossChainOrder memory order = _orders[id];
OrderState memory state = _orderState[id];
if (state.status != Status.Filled) revert NotFilled();
if (state.status != Status.Filled) revert OrderNotFilled();
if (state.acceptedBy != msg.sender) revert Unauthorized();

state.status = Status.Claimed;
Expand All @@ -265,55 +264,61 @@ contract SolverNetInbox is OwnableRoles, ReentrancyGuard, Initializable, Deploye
}

/**
* @dev Validate all order fields.
* @param order OnchainCrossChainOrder to validate.
* @dev Parse and return order data, validate correctness.
* @param order OnchainCrossChainOrder to parse
*/
function _validateOrder(OnchainCrossChainOrder calldata order) internal view returns (SolverNetOrderData memory) {
function _parseOrder(OnchainCrossChainOrder calldata order) internal view returns (OrderData memory) {
if (order.fillDeadline < block.timestamp) revert InvalidFillDeadline();
if (order.orderDataType != ORDER_DATA_TYPEHASH) revert InvalidOrderDataTypehash();
if (order.orderData.length == 0) revert InvalidOrderData();

SolverNetOrderData memory orderData = abi.decode(order.orderData, (SolverNetOrderData));
SolverNetIntent memory intent = orderData.intent;

if (intent.srcChainId != block.chainid) revert InvalidSrcChain();
// We should perform a chainId => outbox address lookup here in the future to validate the route
if (intent.destChainId == 0 || intent.destChainId == block.chainid) revert InvalidDestChain();
if (intent.call.target == bytes32(0)) revert ZeroAddress();
if (intent.call.callData.length == 0) revert NoCalldata();
if (orderData.deposits.length == 0) revert NoDeposits(); // Should we prevent requests without deposits?
for (uint256 i; i < intent.tokenPrereqs.length; ++i) {
TokenPrereq memory prereq = intent.tokenPrereqs[i];
if (prereq.token != bytes32(0) && prereq.spender == bytes32(0)) revert NoSpender();
if (prereq.amount == 0) revert ZeroAmount();
OrderData memory orderData = abi.decode(order.orderData, (OrderData));
Call memory call = orderData.call;
Deposit[] memory deposits = orderData.deposits;

if (call.target == bytes32(0)) revert NoCallTarget();
if (call.data.length == 0) revert NoCallData();
if (deposits.length == 0) revert NoDeposits();

for (uint256 i; i < call.expenses.length; ++i) {
TokenExpense memory expense = call.expenses[i];
if (expense.token == bytes32(0)) revert NoExpenseToken();
if (expense.spender == bytes32(0)) revert NoExpenseSender();
if (expense.amount == 0) revert NoExpenseAmount();
}

return orderData;
}

/**
* @dev Process and validate all deposits.
* @dev Native deposit is validated by checking msg.value against deposit amount, and must be included in array.
* @param deposits Array of TokenDeposit to process.
* @notice Validate and intake all deposits.
* @dev If msg.value > 0, exactly one corresponding native deposit (address == 0x0) is expected.
* @param deposits Deposits to intake.
*/
function _processDeposits(TokenDeposit[] memory deposits) internal {
bool nativeDepositValidated = msg.value > 0 ? false : true;
function _processDeposits(Deposit[] memory deposits) internal {
bool hasNative = msg.value > 0;
bool processedNative = false;

for (uint256 i; i < deposits.length; ++i) {
TokenDeposit memory deposit = deposits[i];
Deposit memory deposit = deposits[i];

// Handle native deposit
if (deposit.token == address(0)) {
if (nativeDepositValidated) revert InvalidNativeDeposit();
if (deposit.token == bytes32(0)) {
if (!hasNative) revert InvalidNativeDeposit();
if (deposit.amount != msg.value) revert InvalidNativeDeposit();
nativeDepositValidated = true;
if (processedNative) revert DuplicateNativeDeposit();
processedNative = true;
}

// Handle ERC20 deposit
if (deposit.token != address(0)) {
if (deposit.amount == 0) revert ZeroAmount();
deposit.token.safeTransferFrom(msg.sender, address(this), deposit.amount);
if (deposit.token != bytes32(0)) {
if (deposit.amount == 0) revert NoDepositAmount();
address token = _bytes32ToAddress(deposit.token);
token.safeTransferFrom(msg.sender, address(this), deposit.amount);
}
}
// Validate frontend properly processed native deposit
if (!nativeDepositValidated) revert InvalidNativeDeposit();

if (hasNative && !processedNative) revert InvalidNativeDeposit();
}

/**
Expand All @@ -325,9 +330,9 @@ contract SolverNetInbox is OwnableRoles, ReentrancyGuard, Initializable, Deploye
internal
returns (ResolvedCrossChainOrder storage resolved)
{
bytes32 id = _incrementId();

ResolvedCrossChainOrder memory _resolved = resolve(order);

bytes32 id = _incrementId();
resolved = _orders[id];
resolved.user = _resolved.user;
resolved.originChainId = _resolved.originChainId;
Expand Down
116 changes: 59 additions & 57 deletions contracts/solve/src/ERC7683/SolverNetOutbox.sol
Original file line number Diff line number Diff line change
Expand Up @@ -111,80 +111,82 @@ contract SolverNetOutbox is OwnableRoles, ReentrancyGuard, Initializable, Deploy
onlyRoles(SOLVER)
nonReentrant
{
SolverNetIntent memory intent = abi.decode(originData, (SolverNetIntent));
FillOriginData memory fillData = abi.decode(originData, (FillOriginData));
Call memory call = fillData.call;

// Check that the destination chain is the current chain
if (intent.destChainId != block.chainid) revert WrongDestChain();
_executeCall(call);
_markFilled(orderId, fillData.srcChainId, call, _fillHash(orderId, originData));
}

// If the order has already been filled, revert. Else, mark filled
bytes32 fillHash = _fillHash(orderId, originData);
if (_filled[fillHash]) revert AlreadyFilled();
_filled[fillHash] = true;
/**
* @notice Wrap a call with approved / enforced expenses.
* Approve spenders. Verify post-call balances match pre-call.
*/
modifier withExpenses(TokenExpense[] memory expenses) {
address[] memory tokens = new address[](expenses.length);
uint256[] memory preBalances = new uint256[](expenses.length);

// Determine tokens required, record pre-call balances, retrieve tokens from solver, and sign approvals
(address[] memory tokens, uint256[] memory preBalances) = _prepareIntent(intent);
for (uint256 i; i < expenses.length; ++i) {
TokenExpense memory expense = expenses[i];
address token = _bytes32ToAddress(expense.token);

// Execute the calls
uint256 nativeAmountRequired = _executeIntent(intent);
tokens[i] = token;
preBalances[i] = token.balanceOf(address(this));

// 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();
}
address spender = _bytes32ToAddress(expense.spender);
token.safeTransferFrom(msg.sender, address(this), expense.amount);
token.safeApprove(spender, expense.amount);
}

// Mark the call as filled on inbox
bytes memory xcalldata = abi.encodeCall(ISolverNetInbox.markFilled, (orderId, fillHash));
uint256 fee = xcall(uint64(intent.srcChainId), ConfLevel.Finalized, _inbox, xcalldata, MARK_FILLED_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 Filled(orderId, fillHash, msg.sender);
for (uint256 i; i < tokens.length; ++i) {
if (tokens[i].balanceOf(address(this)) != preBalances[i]) revert InvalidExpenses();
}
}

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;
/**
* @notice Verifiy and execute a call. Expenses are processed and enforced.
* @param call Call to execute.
*/
function _executeCall(Call memory call) internal withExpenses(call.expenses) {
if (call.destChainId != block.chainid) revert WrongDestChain();

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);
address target = _bytes32ToAddress(call.target);

token.safeTransferFrom(msg.sender, address(this), prereq.amount);
token.safeApprove(spender, prereq.amount);
}
}
if (!allowedCalls[target][bytes4(call.data)]) revert CallNotAllowed();

return (tokens, preBalances);
(bool success,) = payable(target).call{ value: call.value }(call.data);
if (!success) revert CallFailed();
}

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();
/**
* @notice Mark an order as filled. Require sufficient native payment, refund excess.
* @param orderId ID of the order.
* @param srcChainId Chain ID on which the order was opened.
* @param call Call executed.
* @param fillHash Hash of fill data, verifies fill matches order.
*/
function _markFilled(bytes32 orderId, uint64 srcChainId, Call memory call, bytes32 fillHash) internal {
// mark filled on outbox (here)
if (_filled[fillHash]) revert AlreadyFilled();
_filled[fillHash] = true;

(bool success,) = payable(target).call{ value: call.value }(call.callData);
if (!success) revert CallFailed();
// mark filled on inbox
uint256 fee = xcall({
destChainId: srcChainId,
conf: ConfLevel.Finalized,
to: _inbox,
data: abi.encodeCall(ISolverNetInbox.markFilled, (orderId, fillHash)),
gasLimit: MARK_FILLED_GAS_LIMIT
});
if (msg.value - call.value < fee) revert InsufficientFee();

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

return call.value;
emit Filled(orderId, fillHash, msg.sender);
}

/**
Expand Down
Loading

0 comments on commit b228b55

Please sign in to comment.