-
Notifications
You must be signed in to change notification settings - Fork 63
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(contracts/solve): add multicall support to SolverNet #2940
Conversation
*/ | ||
struct OrderData { | ||
address owner; | ||
Call call; | ||
uint64 destChainId; | ||
Call[] calls; | ||
Deposit[] deposits; | ||
TokenExpense[] expenses; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we rename TokenExpense
to Expense
? now that we are putting native expenses in there too
does to have top level destChainId
. supporting multi dest fills will require a lot of other work. no need to have data structures support it now 👍
if (token != address(0)) { | ||
address spender = expense.spender.toAddress(); | ||
uint256 tokenBalance = token.balanceOf(address(_executor)); | ||
|
||
if (spender != address(0)) { | ||
if (IERC20(token).allowance(address(_executor), spender) > 0) { | ||
_executor.approve(token, spender, 0); | ||
} | ||
} | ||
|
||
if (tokenBalance > 0) { | ||
_executor.transfer(token, msg.sender, tokenBalance); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
feel like this withExpenses bit was a little already cluttered (could have used improving as s)
And these nested ifs make things a bit harder to read. Some ideas
// add the following to executor
contract SolverNetExecutor {
// ...
function transferAll(address token, address to) external onlyOutbox {
uint256 balance = token.balanceOf(address(this));
if (balance > 0) token.safeTransfer(to, balance);
}
function resetAllowance(address token, address spender) external onlyOutbox {
uint256 allowance = IERC20(token).allowance(address(this), spender);
if (allowance > 0) IERC20(token).approve(spender, 0);
}
function transferAllNative(address to) external onlyOutbox {
uint256 balance = address(this).balance;
if (balance > 0) to.safeTransferETH(balance);
}
}
// update with expenses. prefer breaks over nested blocks
contract SolverNetOutbox {
// ...
modifier withExpenses(TokenExpense[] memory expenses) {
// transfer from solver, approve spenders
for (uint256 i; i < expenses.length; ++i) {
TokenExpense memory expense = expenses[i];
address token = expense.token.toAddress();
// native, no approval needed
if (token == address(0)) continue;
address spender = expense.spender.toAddress();
token.safeTransferFrom(msg.sender, address(_executor), expense.amount);
if (spender != address(0)) _executor.approve(token, spender, expense.amount);
}
_;
for (uint256 i; i < expenses.length; ++i) {
TokenExpense memory expense = expenses[i];
address token = expense.token.toAddress();
// native refund handled at end
if (token == address(0)) continue;
// refund excess
_executor.transferAll(token, msg.sender);
// spender is executor, allowance reset not needed
if (spender != address(0)) continue;
// reset allowance
_executor.resetAllowance(token, spender);
}
// refund native
_executor.transferAllNative(msg.sender);
}
}
if (token != address(0)) { | ||
address spender = expense.spender.toAddress(); | ||
uint256 tokenBalance = token.balanceOf(address(_executor)); | ||
|
||
if (spender != address(0)) { | ||
if (IERC20(token).allowance(address(_executor), spender) > 0) { | ||
_executor.approve(token, spender, 0); | ||
} | ||
} | ||
|
||
if (tokenBalance > 0) { | ||
_executor.transfer(token, msg.sender, tokenBalance); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
feel like this withExpenses bit was a little already cluttered (could have used improving as s)
And these nested ifs make things a bit harder to read. Some ideas
// add the following to executor
contract SolverNetExecutor {
// ...
function transferAll(address token, address to) external onlyOutbox {
uint256 balance = token.balanceOf(address(this));
if (balance > 0) token.safeTransfer(to, balance);
}
function resetAllowance(address token, address spender) external onlyOutbox {
uint256 allowance = IERC20(token).allowance(address(this), spender);
if (allowance > 0) IERC20(token).approve(spender, 0);
}
function transferAllNative(address to) external onlyOutbox {
uint256 balance = address(this).balance;
if (balance > 0) to.safeTransferETH(balance);
}
}
// update with expenses. prefer breaks over nested blocks
contract SolverNetOutbox {
// ...
modifier withExpenses(TokenExpense[] memory expenses) {
// transfer from solver, approve spenders
for (uint256 i; i < expenses.length; ++i) {
TokenExpense memory expense = expenses[i];
address token = expense.token.toAddress();
// native, no approval needed
if (token == address(0)) continue;
address spender = expense.spender.toAddress();
token.safeTransferFrom(msg.sender, address(_executor), expense.amount);
if (spender != address(0)) _executor.approve(token, spender, expense.amount);
}
_;
for (uint256 i; i < expenses.length; ++i) {
TokenExpense memory expense = expenses[i];
address token = expense.token.toAddress();
// native refund handled at end
if (token == address(0)) continue;
// refund excess
_executor.transferAll(token, msg.sender);
// spender is executor, allowance reset not needed
if (spender != address(0)) continue;
// reset allowance
_executor.resetAllowance(token, spender);
}
// refund native
_executor.transferAllNative(msg.sender);
}
}
if (orderData.destChainId == 0 || orderData.destChainId == block.chainid) revert InvalidDestChainId(); | ||
|
||
uint256 callNativeValue; | ||
if (calls.length == 0) revert NoCalls(); | ||
for (uint256 i; i < calls.length; ++i) { | ||
Call memory call = calls[i]; | ||
if (call.target == bytes32(0)) revert NoCallTarget(); | ||
if (call.value == 0 && call.data.length == 0) revert NoCalldata(); | ||
|
||
unchecked { | ||
if (call.value > 0) callNativeValue += call.value; | ||
} | ||
} | ||
|
||
bool hasNative; | ||
bool hasNativeDeposit; | ||
if (deposits.length == 0) revert NoDeposits(); | ||
for (uint256 i; i < deposits.length; ++i) { | ||
Deposit memory deposit = deposits[i]; | ||
if (deposit.amount == 0) revert NoDepositAmount(); | ||
if (deposit.token == bytes32(0)) { | ||
if (hasNative) revert DuplicateNativeDeposit(); | ||
hasNative = true; | ||
if (hasNativeDeposit) revert DuplicateNativeDeposit(); | ||
hasNativeDeposit = true; | ||
} | ||
} | ||
|
||
for (uint256 i; i < call.expenses.length; ++i) { | ||
TokenExpense memory expense = call.expenses[i]; | ||
if (expense.token == bytes32(0)) revert NoExpenseToken(); | ||
bool hasNativeExpense; | ||
uint256 expenseNativeValue; | ||
for (uint256 i; i < expenses.length; ++i) { | ||
TokenExpense memory expense = expenses[i]; | ||
if (expense.amount == 0) revert NoExpenseAmount(); | ||
if (expense.token == bytes32(0)) { | ||
if (hasNativeExpense) revert DuplicateNativeExpense(); | ||
hasNativeExpense = true; | ||
|
||
unchecked { | ||
expenseNativeValue += expense.amount; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar thoughts on prefering breaks over nested ifs as below.
And with all these for loops and if things are starting to feel cluttered here too. Maybe time to break things out
Some ideas
function _parseOrder(OnchainCrossChainOrder calldata order) internal view returns (OrderData memory) {
if (order.fillDeadline < block.timestamp && order.fillDeadline != 0) revert InvalidFillDeadline();
if (order.orderDataType != ORDER_DATA_TYPEHASH) revert InvalidOrderDataTypehash();
if (order.orderData.length == 0) revert InvalidOrderData();
OrderData memory orderData = abi.decode(order.orderData, (OrderData));
Call[] memory calls = orderData.calls;
Deposit[] memory deposits = orderData.deposits;
TokenExpense[] memory expenses = orderData.expenses;
if (orderData.destChainId == 0 || orderData.destChainId == block.chainid) revert InvalidDestChainId();
if (deposits.length == 0) revert NoDeposits();
_parseDeposits(deposits);
uint256 totalNativeCallValue = _parseCalls(calls);
_parseExpenses(expenses, totalNativeCallValue);
return orderData;
}
function _parseCalls(Call[] memory calls) internal pure returns (uint256 totalValue) {
if (calls.length == 0) revert NoCalls();
for (uint256 i; i < calls.length; ++i) {
Call memory call = calls[i];
if (call.target == bytes32(0)) revert NoCallTarget();
if (call.value == 0 && call.data.length == 0) revert NoCalldata();
unchecked {
totalValue += call.value;
}
}
}
function _parseDeposits(Deposit[] memory deposits) internal pure {
if (deposits.length == 0) revert NoDeposits();
bool hasNative = false;
for (uint256 i; i < deposits.length; ++i) {
Deposit memory deposit = deposits[i];
if (deposit.token != bytes32(0)) cotinue;
if (hasNative) revert DuplicateNativeDeposit();
hasNative = true;
}
}
function _parseExpenses(TokenExpense[] memory expenses, uint256 expectedNative) internal pure {
uint256 native;
for (uint256 i; i < expenses.length; ++i) {
TokenExpense memory expense = expenses[i];
if (expense.amount == 0) revert NoExpenseAmount();
if (expense.token != bytes32(0)) continue;
if (nativeExpense > 0) revert DuplicateNativeExpense();
unchecked {
native += expense.amount;
}
}
if (native != expectedNative) revert NativeExpenseMismatch();
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These might be more like _verify*
functions, but I think _parse*
also works. we do some reduction on data (like summing and returning native call value). And it matches _parseOrder
Closing as #2954 implemented multicall. |
SolverNet
Call
,OrderData
, andFillOriginData
structs were refactored to enable destination multicall support. As of the time of this PR, tests specifically for the multicall logic still need to be implemented.issue: #2879