Skip to content

chore: validate submission accounts on deserialization#4450

Open
dymchenkko wants to merge 1 commit into
cowprotocol:mainfrom
dymchenkko:chore/validate-submission-accounts-deser
Open

chore: validate submission accounts on deserialization#4450
dymchenkko wants to merge 1 commit into
cowprotocol:mainfrom
dymchenkko:chore/validate-submission-accounts-deser

Conversation

@dymchenkko
Copy link
Copy Markdown

@dymchenkko dymchenkko commented May 27, 2026

Description

submission-accounts is already validated at runtime in solver::Config::validate() (added in #4430). As suggested in this review comment, this additionally enforces the "submission accounts must be signers" invariant at config deserialization via a wrapper type, so an invalid config fails to load and the type can't be constructed in an invalid state.

Changes

  • Add a SubmissionAccounts(Vec<Account>) wrapper in config/file/mod.rs whose new() constructor and custom Deserialize impl reject any address-only Account (only signers can sign EIP-7702 delegated settlements). Config loading now fails fast with a clear error.
  • Use SubmissionAccounts for the submission_accounts config field; load.rs unwraps the validated list via into_inner().
  • The existing runtime check in solver::Config::validate() is left in place as defense-in-depth (also guards programmatic construction); no change there.
  • Add TOML deserialization tests: omitted/default → empty, signer accounts accepted, and address-only accounts rejected during deserialization.

How to test

  1. cargo test -p driver --lib submission_accounts
  2. Manually: add an address-only entry to submission-accounts in a driver config (e.g. submission-accounts = ["0x0000000000000000000000000000000000000002"]) and start the driver — it refuses to start with EIP-7702 submission accounts must be signers; address-only accounts cannot sign delegated settlement transactions. A private-key entry starts normally.

Related Issues

Fixes #4449

@dymchenkko dymchenkko requested a review from a team as a code owner May 27, 2026 16:01
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 refactors the validation of EIP-7702 submission accounts by enforcing that they must be signers (and not address-only accounts) during deserialization using a new SubmissionAccounts wrapper type. Feedback on this change suggests retaining the validation check within solver::Config::validate() to ensure that programmatic constructions of the configuration remain safe and validated.

Comment thread crates/driver/src/infra/solver/mod.rs Outdated
Comment on lines +236 to +238
// The invariant that submission accounts are signers (never
// `Account::Address`) is enforced when the configuration is
// deserialized, see `config::file::SubmissionAccounts`.
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

While enforcing the invariant at the deserialization layer (config::file::SubmissionAccounts) is excellent for failing fast during config loading, removing the check from solver::Config::validate() leaves programmatic construction of solver::Config (e.g., in unit/integration tests or future programmatic APIs) unvalidated.

To adhere to defensive programming principles and ensure that solver::Config remains valid by construction/validation regardless of how it is instantiated, we should retain this check in validate().

        anyhow::ensure!(
            self.submission_accounts
                .iter()
                .all(|account| !matches!(account, Account::Address(_))),
            "solver '{}': EIP-7702 submission accounts must be signers; address-only accounts \\
             cannot sign delegated settlement transactions",
            self.name,
        );

@dymchenkko dymchenkko force-pushed the chore/validate-submission-accounts-deser branch 2 times, most recently from db90c24 to 36b9ca3 Compare May 27, 2026 16:17
#[derive(Debug, Default)]
struct SubmissionAccounts(Vec<Account>);

impl SubmissionAccounts {

This comment was marked as resolved.

Comment on lines +404 to +413
impl<'de> Deserialize<'de> for SubmissionAccounts {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let accounts = Vec::<Account>::deserialize(deserializer)?;
Self::new(accounts).map_err(serde::de::Error::custom)
}
}

This comment was marked as outdated.

@failuresmith

This comment was marked as resolved.

@dymchenkko dymchenkko force-pushed the chore/validate-submission-accounts-deser branch from 36b9ca3 to 2248d96 Compare May 27, 2026 18:19
@dymchenkko
Copy link
Copy Markdown
Author

@failuresmith Thanks for the review! Applied the changes.

@failuresmith
Copy link
Copy Markdown

Longer term, I think the wrapper may belong in infra::solver, not the file-config layer. The invariant is about runtime solver behavior, so ideally solver::Config would store SubmissionAccounts instead of Vec<Account>, and both TOML loading and programmatic construction would go through the same constructor.

The current PR still improves the file-loading path and keeps validate() as a fallback, so I don’t think it has to block this change. But a domain-level wrapper would avoid maintaining the same invariant in two places.

@failuresmith
Copy link
Copy Markdown

If we want one canonical check, it should live just in the runtime/domain type, not only in file config.

The stronger design is:

// infra::solver
pub struct SubmissionAccounts(Vec<Account>);

impl SubmissionAccounts {
    pub fn new(accounts: Vec<Account>) -> anyhow::Result<Self> {
        // reject Account::Address once
    }
}

Then:

pub struct Config {
    pub submission_accounts: SubmissionAccounts,
}

Now both paths must go through the same constructor:

  • TOML loading parses file::Account, converts to solver::Account, then calls solver::SubmissionAccounts::new(...).
  • Programmatic construction also has to call solver::SubmissionAccounts::new(...).

The tradeoff is scope: moving it into solver::Config touches more call sites than #4449 likely intended.

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.

chore: validate submission accounts on deserialization

2 participants