Skip to content
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

Closed
wants to merge 1 commit into from

Conversation

Zodomo
Copy link
Contributor

@Zodomo Zodomo commented Jan 29, 2025

SolverNet Call, OrderData, and FillOriginData 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

@Zodomo Zodomo self-assigned this Jan 29, 2025
@Zodomo Zodomo requested a review from kevinhalliday as a code owner January 29, 2025 23:33
Comment on lines 47 to 54
*/
struct OrderData {
address owner;
Call call;
uint64 destChainId;
Call[] calls;
Deposit[] deposits;
TokenExpense[] expenses;
}
Copy link
Contributor

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 👍

Comment on lines +157 to +169
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);
}
Copy link
Contributor

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);
    }

}

Comment on lines +157 to +169
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);
}
Copy link
Contributor

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);
    }

}

Comment on lines +308 to +345
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;
}
}
Copy link
Contributor

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();
}

Copy link
Contributor

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

@Zodomo
Copy link
Contributor Author

Zodomo commented Feb 3, 2025

Closing as #2954 implemented multicall.

@Zodomo Zodomo closed this Feb 3, 2025
@Zodomo Zodomo deleted the zodomo/sn-multicall branch February 3, 2025 23:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants