Skip to content

feat: add solver 7702 delegate#4430

Open
igorroncevic wants to merge 11 commits into
mainfrom
feat/solver-7702-delegate
Open

feat: add solver 7702 delegate#4430
igorroncevic wants to merge 11 commits into
mainfrom
feat/solver-7702-delegate

Conversation

@igorroncevic
Copy link
Copy Markdown
Contributor

@igorroncevic igorroncevic commented May 22, 2026

Description

This PR adds the new Solver7702Delegate flow for EIP-7702 parallel settlement submission.

  • The old forwarder flow required a separately deployed contract address in solver config and separate caller approval handling.
  • The new delegate makes setup more self-contained:
    • the driver deploys the delegate deterministically from the configured submission accounts,
    • delegates the solver EOA to it
    • and then routes delegated settlement transactions through the solver EOA.

Solver7702Delegate is also much more efficient and secure that its predecessor, due to a minimal attack surface. The contract itself is vendored from https://github.com/cowprotocol/solver-7702-delegate

Changes

  • Add the vendored Solver7702Delegate artifact and generated Rust bindings.
  • Wire Solver7702Delegate into contract generation and the contracts facade.
  • Update EIP-7702 startup setup to:
    • validate that submission accounts are signers,
    • build delegate init code with approved callers,
    • deploy the delegate with CREATE2 when missing,
    • delegate the solver EOA to the deployed delegate,
    • verify the final EIP-7702 delegation target.
  • Replace the old forward(target, data) calldata wrapping with the new delegate calldata format targetAddress || targetCalldata.
  • Add focused tests for delegate address derivation, caller limits, config validation, delegation detection, and delegated transaction rewriting.

How to test

  1. just generate-contracts
  2. just fmt --check
  3. just clippy
  4. just test-unit
  5. just test-e2e parallel_settlement

@igorroncevic igorroncevic requested a review from a team as a code owner May 22, 2026 11:21
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 replaces the CowSettlementForwarder contract with Solver7702Delegate to support EIP-7702 parallel settlement submissions. Key changes include the addition of the Solver7702Delegate artifact and its generated Rust bindings, a refactor of the driver's EIP-7702 setup to use deterministic CREATE2 deployment, and updates to the submission logic to handle the new delegate's fallback mechanism. The PR also adds unit tests for these components and reorganizes existing test files. I have no feedback to provide.

@igorroncevic igorroncevic force-pushed the feat/solver-7702-delegate branch from 8af659d to fdcb176 Compare May 22, 2026 11:32
@igorroncevic igorroncevic force-pushed the feat/remove_old_forwarder_contract branch from 4dc8037 to e94152c Compare May 22, 2026 11:32
Copy link
Copy Markdown
Contributor

@kaze-cow kaze-cow left a comment

Choose a reason for hiding this comment

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

wow the changes seem very straightforward, nice work! I haven't finished running the actual code locally so give me some time but if the paralell submission test is already working thats a great sign 👍

Comment thread playground/configs/driver.toml 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 Outdated
Comment thread crates/driver/src/infra/solver/eip7702.rs
Comment thread crates/driver/src/infra/config/file/tests.rs Outdated
Comment thread crates/driver/src/infra/solver/eip7702/tests.rs Outdated
Comment thread crates/driver/src/infra/solver/eip7702/tests.rs Outdated
Comment thread crates/e2e/tests/e2e/parallel_settlement.rs
Comment thread crates/driver/src/domain/mempools/tests.rs Outdated
Comment thread contracts/src/vendor.rs Outdated
Copy link
Copy Markdown
Member

@AryanGodara AryanGodara left a comment

Choose a reason for hiding this comment

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

looks good mostly, just 2 comments. 1 med, 1 small.
Also replid to the "lets get opinion of backend team" threads 😅

Comment thread crates/driver/src/infra/solver/eip7702.rs Outdated
Comment thread crates/driver/src/infra/solver/eip7702.rs
Base automatically changed from feat/remove_old_forwarder_contract to main May 25, 2026 14:56
…gate

# Conflicts:
#	contracts/generated/contracts-generated/solver7702delegate/Cargo.toml
#	contracts/src/main.rs
#	crates/driver/src/domain/mempools.rs
#	crates/e2e/tests/e2e/parallel_settlement.rs
Copy link
Copy Markdown
Contributor

@jmg-duarte jmg-duarte left a comment

Choose a reason for hiding this comment

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

this is quite big which makes it harder to review, overall it looks ok to me but it's a bit hard to visualize the impact of the change in my head

Comment thread crates/driver/src/infra/solver/mod.rs Outdated
Comment on lines +521 to +523
self.max_solutions_to_propose.get() == 1,
"solver '{}': max-solutions-to-propose > 1 requires at least one \
submission-account (EIP-7702 parallel submission must be enabled)",
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.

x == 1 & "at least one" are not the same

which one is the correct one?

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.

Its definitely worded wierdly but I think its referring to two different vars. if the var max-solutions-to-propose > 1, then submission-account.length > 0

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.

Good point. The check is about two different fields, but the message was confusing. I changed it to say that max-solutions-to-propose > 1 requires non-empty submission-accounts.

Comment thread crates/driver/src/infra/solver/mod.rs Outdated
anyhow::ensure!(
self.submission_accounts
.iter()
.all(|account| !matches!(account, Account::Address(_))),
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.

same thing but there's no ! almost hiding

Suggested change
.all(|account| !matches!(account, Account::Address(_))),
.any(|account| matches!(account, Account::Address(_))),

Comment on lines +11 to +14
PrivateKeySigner::from_bytes(&b256!(
"59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d"
))
.unwrap()
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.

just in case: leave a note explaining NOT to use this as a production key

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.

its anvil well-known mnemonic signer #1 btw

std::time::Duration,
};

const CREATE2_DEPLOYER: Address = address!("4e59b44847b379578588920cA78FbF26c0B4956C");
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.

you can use the value from the driver

}

fn solver_delegate_address(callers: [Address; 2]) -> Address {
let mut approved_callers = [Address::ZERO; 5];
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.

use MAX_ALLOWED_CALLERS (or what was the name of the var)

Comment on lines +39 to +40
/// Ensure EIP-7702 delegate deployment and solver delegation are set up for all
/// solvers with parallel submission accounts. Called once at driver startup.
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.

the error descriptions disappeared

Comment on lines -66 to -71
let forwarder = config.forwarder_contract.ok_or_else(|| {
anyhow::anyhow!(
"solver {}: submission_accounts configured but forwarder_contract missing",
config.name
)
})?;
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.

thought that this had been removed already

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.

This check is now moved out of EIP-7702 setup and into config validation, where it belongs.

fn delegation_status(code: &[u8]) -> DelegationStatus {
if code.is_empty() {
DelegationStatus::Empty
} else if code.len() == 23 && code.starts_with(&DELEGATION_PREFIX) {
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.

23 is a magic number

Comment on lines +313 to +315
Empty,
DelegatedTo(Address),
OtherCode,
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.

docs for the variants

OtherCode,
}

fn delegation_status(code: &[u8]) -> DelegationStatus {
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.

take it or leave it: maybe?

Suggested change
fn delegation_status(code: &[u8]) -> DelegationStatus {
impl DelegationStatus {
fn from_code(code: &[u8]) -> Self {
...
}
}

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.

Take it!

Comment thread crates/driver/src/domain/mempools.rs Outdated
// Check delegation status.
let code = provider.get_code_at(solver_address).await?;
let needs_delegation = !is_delegated_to(&code, forwarder);
let mut approved_callers = [Address::ZERO; MAX_APPROVED_CALLERS];
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.

Worth rejecting duplicates and Address::ZERO here. Both change (or alias with the padding for) the CREATE2 target with no signal to the operator. A typo in the TOML would silently produce a working-but-wrong delegate.

anyhow::ensure!(
    callers.iter().all(|a| *a != Address::ZERO),
    "submission accounts cannot include the zero address"
);
let mut seen = std::collections::HashSet::with_capacity(callers.len());
anyhow::ensure!(
    callers.iter().all(|a| seen.insert(*a)),
    "submission accounts must be unique"
);

Also worth a comment noting that reordering submission accounts in the TOML changes the delegate address. The test asserts it, but operators won't read the test.

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.

Agreed, added these checks

Comment thread contracts/src/vendor.rs Outdated
.github(
"Solver7702Delegate",
// TODO(post-audit): bump to the audited commit hash or release tag.
"cowprotocol/solver-7702-delegate/acde4b0cd452d208c2ea0f0fbbf83e3698decbf8/out/\
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.

The vendor URL is at commit acde4b0c..., but the committed JSON's sourceCommit is 4273853b.... Re-running the vendor command would pull a different artifact and overwrite the checked-in one. Please pin them to the same commit (the post-audit TODO can update both at once).

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.

Thanks, I did not realize out folder needed to be exposed for this to work, so now that it's exposed I've pinned the commit to the latest one.

"CREATE2 deployer {CREATE2_DEPLOYER:?} has unexpected code",
);

let tx_sender = config.account.address();
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 recovery path pays gas from the solver EOA. If the operator only funds submission accounts (reasonable setup), startup fails with a confusing OOF error. No auth is needed in DeployOnly mode, so a submission account could send the CREATE2 deploy instead. Non-blocking.

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.

Good point. In DeployOnly mode the deploy tx can be sent from a submission account, so I changed it to use the first submission account when available.

}

impl Config {
fn validate(&self) -> 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.

The submission-accounts-must-be-signers check lives in Config::validate, but the main-account-must-be-signer check is over in eip7702::setup. Both are the same kind of EIP-7702 precondition. Moving the main-account check into validate would catch misconfig at construction. Minor.

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.

Agreed. I moved the main-account-must-be-signer check into Config::validate, next to the submission account signer validation.

Copy link
Copy Markdown
Contributor

@kaze-cow kaze-cow left a comment

Choose a reason for hiding this comment

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

my feedback was addressed or superseded by other feedbacks. in other news, I was able to test with the playground and verify the wholistic change is working as expected, so looks good to me 👍

metalurgical pushed a commit to metalurgical/services that referenced this pull request May 26, 2026
# Description

This PR removes the old `CowSettlementForwarder` implementation and its
config surface.

The forwarder is being replaced by `Solver7702Delegate` in the follow-up
PR. Removing the old contract, artifact, bindings, and explicit
`forwarder-contract` config first keeps the stack easier to review: this
PR is only the cleanup of the old path, while cowprotocol#4430 introduces the new
delegate behavior.

  # Changes

  * Remove `CowSettlementForwarder.sol` and its artifact.
* Remove generated `cowsettlementforwarder` bindings and facade wiring.
* Remove `forwarder_contract` / `forwarder-contract` from driver config
loading and solver config.
* Remove e2e setup fields and test code that deployed or passed a
forwarder address.
* Keep the replacement `Solver7702Delegate` implementation for the
follow-up PR.

  ## How to test

This PR is intended to be validated together with cowprotocol#4430, which adds the
replacement implementation.

  1. `just generate-contracts`
  2. `just fmt --check`
  3. `just clippy`
  4. `just test-unit`
@igorroncevic
Copy link
Copy Markdown
Contributor Author

Thinking that just generate-contracts might not be handling Foundry artifacts properly.

It says that deployedBytecode isn't present, but it's clearly available in the out/Solver7702Delegate.sol/Solver7702Delegate.json but under deployedBytecode.object.

Copy link
Copy Markdown
Contributor

@squadgazzz squadgazzz left a comment

Choose a reason for hiding this comment

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

LGTM

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.

5 participants