Skip to content

Allow multiple parallel settlements from one solver#4167

Merged
fafk merged 37 commits intomainfrom
parallel-submission-7702
Mar 4, 2026
Merged

Allow multiple parallel settlements from one solver#4167
fafk merged 37 commits intomainfrom
parallel-submission-7702

Conversation

@fafk
Copy link
Copy Markdown
Contributor

@fafk fafk commented Feb 18, 2026

Description

Resolves #3966.

Enable parallel settlement submission from a single solver by using EIP-7702 delegation. When a solver wins multiple solutions in overlapping auctions, each settlement can now be submitted concurrently through independent submission EOAs instead of waiting in a sequential queue.

The solver EOA delegates its code to a CowSettlementForwarder contract via EIP-7702. Approved submission EOAs call forward(target, data) on the solver EOA, which forwards to the actual target (settlement contract, wrapper, or flashloan router) with msg.sender = solver EOA — preserving the existing whitelisting.

Changes

  • CowSettlementForwarder.sol — New forwarder contract with caller whitelist. Supports forwarding to any target (settlement contract, wrappers, flashloan router) via forward(address target, bytes data)
  • SubmissionAccountPool type: a channel-based pool that lends submission accounts for the duration of a settlement and reclaims them afterward. In EIP-7702 mode, settle requests are spawned as concurrent tasks instead of processed sequentially
  • submission_accounts config that allows specifying accounts that are whitelisted to submit settlements. If omitted we fallback to non-delegated submissions directly from the solver EOA

Requirements before deployment

  • Deploy & verify the CowSettlementForwarder contract
  • Set up EIP7022 delegation to whitelisted solver EOAs to the newly deployed contract
  • Create new submissions accounts and fund them with eth (or other base token) & monitor their eth balance
  • Whitelist the submissions accounts on the solver EOA (CowSettlementForwarder code)

Only then can the submissions accounts be configured and deployed.

How to test

cargo nextest run -p e2e local_node_parallel_settlement --test-threads 1 --run-ignored ignored-only

@fafk fafk requested a review from a team as a code owner February 18, 2026 11:03
@fafk fafk marked this pull request as draft February 18, 2026 11:03
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces parallel settlement submission for a single solver using EIP-7702 delegation, which is a significant improvement. The implementation involves a new forwarder contract, a submission account pool, and updates to the settlement processing logic. My review identified two main issues: a critical discrepancy between the Solidity source code and its compiled artifact, and a high-severity resource leak risk in the submission account pool handling. Addressing these will improve the robustness and security of the new functionality.

Comment thread crates/contracts/solidity/CowSettlementForwarder.sol
Comment thread crates/driver/src/domain/competition/mod.rs Outdated
Copy link
Copy Markdown
Contributor

@MartinquaXD MartinquaXD left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks really good already.
Nice test and I like the logic of passing around account claims via channels.

@fafk fafk changed the title Allow multiple parallel settlements from 1 solver Allow multiple parallel settlements from one solver Feb 23, 2026
@fafk fafk marked this pull request as ready for review February 24, 2026 15:43
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a well-designed implementation for parallel settlement submission using EIP-7702, featuring the new CowSettlementForwarder contract, SubmissionAccountPool, and robust delegation setup. However, a critical privilege escalation vulnerability was identified in the CowSettlementForwarder smart contract, where an approved caller can use the forward function to modify the whitelist via setApprovedCallers on the solver EOA. This requires mitigation by preventing self-calls in the forward function. Additionally, a high-severity issue exists in the AccountGuard's drop implementation, which could lead to a panic during application shutdown.

Comment on lines +23 to +31
function forward(address target, bytes calldata data) external payable {
if (!isApprovedCaller[msg.sender]) revert Unauthorized();
(bool success, bytes memory result) = target.call{value: msg.value}(data);
assembly {
switch success
case 0 { revert(add(result, 32), mload(result)) }
default { return(add(result, 32), mload(result)) }
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The forward function allows an approved caller to execute arbitrary calls as the solver EOA. If the target is set to address(this), the call is executed with msg.sender set to the solver EOA itself. This allows an approved caller to call setApprovedCallers and modify the whitelist, bypassing the intended access control which requires msg.sender == address(this). This is a privilege escalation vulnerability where a submission EOA can take full control over the whitelist and potentially other sensitive functions of the solver EOA.

Recommendation: Add a check in the forward function to ensure that the target is not address(this).

    function forward(address target, bytes calldata data) external payable {
        if (!isApprovedCaller[msg.sender]) revert Unauthorized();
        if (target == address(this)) revert Unauthorized();
        (bool success, bytes memory result) = target.call{value: msg.value}(data);
        assembly {
            switch success
            case 0 { revert(add(result, 32), mload(result)) }
            default { return(add(result, 32), mload(result)) }
        }
    }

Comment on lines +107 to +114
if let Some(account) = self.account.take() {
let sender = self.release_sender.clone();
tokio::spawn(async move {
if sender.send(account).await.is_err() {
tracing::error!("failed to return submission account to pool: channel closed");
}
});
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using tokio::spawn directly within a Drop implementation can lead to a panic if the AccountGuard is dropped while the Tokio runtime is shutting down. This would cause an ungraceful shutdown. A more robust approach is to acquire a handle to the current runtime and spawn the task on it, which gracefully handles cases where the runtime is not available or is shutting down.

        if let Some(account) = self.account.take() {
            let sender = self.release_sender.clone();
            if let Ok(handle) = tokio::runtime::Handle::try_current() {
                handle.spawn(async move {
                    if sender.send(account).await.is_err() {
                        tracing::error!("failed to return submission account to pool: channel closed");
                    }
                });
            } else {
                // This can happen during a panic or when the runtime is shutting down.
                // The account is not returned to the pool, which is acceptable.
                tracing::warn!("no tokio runtime, submission account will not be returned to pool");
            }
        }

Copy link
Copy Markdown
Contributor

@MartinquaXD MartinquaXD left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks really good over all. Mostly nits and code golf suggestions.

Comments on the contract code mostly have production deployment in mind but for e2e tests it's definitely fine.

Comment thread crates/contracts/solidity/CowSettlementForwarder.sol

/// @notice Set approved callers.
function setApprovedCallers(address[] calldata callers, bool approved) external {
if (msg.sender != address(this)) revert Unauthorized();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might be missing some solidity details but it seems like the way to use this contract is to first have an EOA delegate to it and then send a message from the EOA to itself to call this function. Is that correct?

IIRC the state (allow list mapping) will be stored in the EOA so theoretically all solvers could use the same instance of this contract, right?

We should probably address the question whether we want to have a function cleaning up the EOAs storage or leave things as is and consider the EOAs as forever tainted once they use this contract. Probably not a big concern since attacks would require the EOA to delegate to a second malicious contract that exploits the set state.

/// is deployed once and shared across all solver EOAs — each gets its own
/// independent `isApprovedCaller` mapping in its own storage.
contract CowSettlementForwarder {
mapping(address => bool) public isApprovedCaller;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Storing the approved addresses in storage means we'd have to do an additional SLOAD for every settlement. Not sure if the SC team will have concerns about that. At least for e2e testing purposes this contract seems to be sufficient.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there an alternative? I can only think of baking the submission EOAs into the contract itself via a constructor, but that would make it very inflexible to update.

Also SOAD is 2100 gas which is ~nothing compared to the total costs of forwarding the calldata. 🤔

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, baking the addresses into the contract is what some external solvers teams did when they rolled their own 7702 solution. I think the SC team should ultimately give the green light for the final contract. Just wanted to point that out since @fedgiac always sounds so scared about introducing SLOADs. 😄

Comment thread crates/driver/src/domain/competition/mod.rs Outdated
Comment thread crates/driver/src/domain/competition/mod.rs Outdated
Comment thread crates/driver/src/infra/solver/eip7702.rs Outdated
Comment thread crates/driver/src/infra/solver/eip7702.rs Outdated
Comment thread crates/driver/src/infra/solver/eip7702.rs
Comment on lines +251 to +252
const POLL_INTERVAL: Duration = Duration::from_secs(3);
const MAX_WAIT: Duration = Duration::from_secs(90);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a potentially long restart that can ultimately lead to a panic. I'm not really sure if the driver should automatically set up new callers TBH. Have to think more about it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking about doing this outside of the driver, but we're using KMS here which the driver is well equipped to use unlike a script where you need AWS access.

It is one-time cost, I think the submission acc set is not really going to be changing often at all.

Comment thread crates/driver/src/infra/solver/eip7702.rs Outdated
@fafk fafk force-pushed the parallel-submission-7702 branch from fa14015 to a498412 Compare March 2, 2026 14:54
Comment thread crates/driver/src/domain/competition/mod.rs
fn deref(&self) -> &Account {
&self.account
impl SubmitterGuard {
fn delegation_context(&self) -> Option<DelegatedSubmission> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if it would be nicer to return an enum here so that we don't have to rely on the tx having the correct from set in the Direct case. 🤔

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mhm. We would have to carry around the solver address in Direct as well we wouldn't actually get to delete wrap_for_delegated_submission, so I don't think it's a big win.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was not about deleting any code - just about making the important information available in the same place. It's a bit strange that submission data for EIP 7702 comes from the SubmitterPool while direct submission relies on the tx being created with the correct from address.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fafk fafk force-pushed the parallel-submission-7702 branch from 31d1eec to d679caa Compare March 3, 2026 13:07
Comment thread crates/driver/src/domain/competition/mod.rs Outdated
Copy link
Copy Markdown
Contributor

@MartinquaXD MartinquaXD left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All my comments got addressed. 👍

@fafk fafk added this pull request to the merge queue Mar 4, 2026
Merged via the queue into main with commit b79d812 Mar 4, 2026
19 checks passed
@fafk fafk deleted the parallel-submission-7702 branch March 4, 2026 08:39
@github-actions github-actions Bot locked and limited conversation to collaborators Mar 4, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

support parallel transaction submission using EIP 7702

2 participants