Skip to content

fix(driver): fall back to another EOA when a submission account is unusable#4547

Open
frdrckj wants to merge 1 commit into
cowprotocol:mainfrom
frdrckj:fix/eip7702-submitter-fallback
Open

fix(driver): fall back to another EOA when a submission account is unusable#4547
frdrckj wants to merge 1 commit into
cowprotocol:mainfrom
frdrckj:fix/eip7702-submitter-fallback

Conversation

@frdrckj

@frdrckj frdrckj commented Jun 22, 2026

Copy link
Copy Markdown

Description

EIP-7702 parallel submission selects a submission account from a FIFO pool. When the selected account cannot broadcast the transaction for an account-specific reason (no balance for gas, a stale nonce, or a pending tx that cannot be replaced), the settlement currently fails even though another funded account may be available. This is the missing fallback described in #4541.

This PR adds the reactive fallback: on an account-specific submission failure, the settlement is retried from another eligible account while the submission deadline still holds.

Changes

  • Add mempools::Error::SubmitterUnusable(AccountFailure) and a small classify_submission_failure helper that recognises pre-broadcast node rejections (insufficient funds, nonce too low, ... underpriced). already known is intentionally excluded.
  • Mempool::submit now returns this variant for those broadcast-time rejections instead of the generic Other.
  • Competition::process_settle_request retries the settlement from another account on SubmitterUnusable. Spent accounts are held until the request finishes so each retry uses a different one, and a new non-blocking SubmitterPool::try_acquire avoids deadlocking against concurrent settlements.
  • Metrics and solver notifications account for the new outcome.

Retrying is safe against double-submission: a settlement that actually lands always returns Ok, so only failures (where nothing was broadcast) are ever retried. That is also why already known is excluded, since it means our exact transaction is already in the mempool and may still be mined.

This implements the reactive fallback and failure classification. Proactive balance/nonce pre-checks before account selection, and a persistent cooldown for repeatedly-failing accounts, are left as follow-ups since they involve tuning choices worth agreeing on separately.

How to test

RUST_MIN_STACK=3145728 cargo nextest run -p driver

New unit tests cover the classifier in both directions: classifies_account_specific_submission_failures and does_not_classify_non_account_failures_as_account_specific (the latter guards against retrying reverts, already known, rate limits, and transport errors).

Towards #4541.

…usable

EIP-7702 parallel submission picks an account from a FIFO pool but never
falls back when the chosen account can't broadcast (no gas for the tx,
stale nonce, or a pending tx that can't be replaced). A valid settlement
then fails even when another funded account is available.

Classify pre-broadcast node rejections as account-specific
(`mempools::Error::SubmitterUnusable`) and, on such a failure, retry the
settlement from another account while the submission deadline still holds.
Spent accounts are held for the duration of the request so each retry picks
a different one, and `try_acquire` keeps the fallback non-blocking to avoid
deadlocking against concurrent settlements.

Retrying is safe against double-submission: a settlement that actually
lands always returns `Ok`, so only failures (where nothing was broadcast)
are ever retried. `already known` is deliberately not treated as
account-specific, since it means our exact tx is already in the mempool and
may still be mined.

Towards cowprotocol#4541.
@frdrckj frdrckj requested a review from a team as a code owner June 22, 2026 06:53
@github-actions

github-actions Bot commented Jun 22, 2026

Copy link
Copy Markdown

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

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

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.

Code Review

This pull request introduces a fallback mechanism to retry transaction submissions using alternative accounts when encountering account-specific failures (such as insufficient funds, nonce issues, or underpriced transactions). Feedback focuses on improving the robustness of the error classification: first, by formatting the full anyhow::Error chain using format!("{err:#}") instead of err.to_string() to prevent underlying node errors from being hidden; and second, by loosening the substring matching logic to support non-Geth clients like Nethermind which format error messages differently.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

// A rejection at broadcast time means nothing was sent, so an
// account-specific failure (no gas, stale nonce, ...) can be
// retried from a different submission account.
match mempools::classify_submission_failure(&err.to_string()) {

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 err.to_string() on an anyhow::Error only returns the outermost error message. If context is added to the error in the future (e.g., via .context()), the underlying node error message containing 'insufficient funds' or 'nonce too low' will be hidden, silently breaking the classification and fallback mechanism. Use format!("{err:#}") to format the entire error chain, ensuring robustness against future refactoring.

Suggested change
match mempools::classify_submission_failure(&err.to_string()) {
match mempools::classify_submission_failure(&format!("{err:#}")) {

Comment on lines +682 to +693
pub fn classify_submission_failure(message: &str) -> Option<AccountFailure> {
let message = message.to_lowercase();
if message.contains("insufficient funds") {
Some(AccountFailure::InsufficientFunds)
} else if message.contains("nonce too low") {
Some(AccountFailure::Nonce)
} else if message.contains("underpriced") {
Some(AccountFailure::Underpriced)
} else {
None
}
}

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

Matching exact substrings like "insufficient funds" or "nonce too low" is fragile and can fail on non-Geth clients (such as Nethermind) which may return error variants like "SenderInsufficientFunds" or "NonceTooLow" without spaces. Checking for the presence of both keywords (e.g., "insufficient" and "funds", or "nonce" and "low") makes the classification significantly more robust across different Ethereum clients.

pub fn classify_submission_failure(message: &str) -> Option<AccountFailure> {
    let message = message.to_lowercase();
    if message.contains("insufficient") && message.contains("funds") {
        Some(AccountFailure::InsufficientFunds)
    } else if message.contains("nonce") && message.contains("low") {
        Some(AccountFailure::Nonce)
    } else if message.contains("underpriced") {
        Some(AccountFailure::Underpriced)
    } else {
        None
    }
}

@frdrckj

frdrckj commented Jun 22, 2026

Copy link
Copy Markdown
Author

I have read the CLA Document and I hereby sign the CLA

github-actions Bot added a commit that referenced this pull request Jun 22, 2026
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.

1 participant