diff --git a/Cargo.lock b/Cargo.lock index 87d2a6f926..94ad8a1469 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2600,6 +2600,7 @@ dependencies = [ "cow-contract-remoteerc20balances", "cow-contract-signatures", "cow-contract-solver", + "cow-contract-solver7702delegate", "cow-contract-spardose", "cow-contract-sushiswaprouter", "cow-contract-swapper", @@ -3407,6 +3408,17 @@ dependencies = [ "anyhow", ] +[[package]] +name = "cow-contract-solver7702delegate" +version = "0.1.0" +dependencies = [ + "alloy-contract", + "alloy-primitives", + "alloy-provider", + "alloy-sol-types", + "anyhow", +] + [[package]] name = "cow-contract-spardose" version = "0.1.0" diff --git a/contracts/artifacts/Solver7702Delegate.json b/contracts/artifacts/Solver7702Delegate.json new file mode 100644 index 0000000000..1aff523b5e --- /dev/null +++ b/contracts/artifacts/Solver7702Delegate.json @@ -0,0 +1,35 @@ +{ + "abi": [ + { + "inputs": [ + { + "internalType": "address[5]", + "name": "approvedCallers", + "type": "address[5]" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "stateMutability": "payable", + "type": "fallback" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "Unauthorized", + "type": "error" + } + ], + "bytecode": { + "linkReferences": {}, + "object": "0x610120604052348015610010575f5ffd5b506040516103f13803806103f183398101604081905261002f9161009c565b80516001600160a01b0390811660809081526020830151821660a0526040830151821660c0526060830151821660e05290910151166101005261011c565b634e487b7160e01b5f52604160045260245ffd5b80516001600160a01b0381168114610097575f5ffd5b919050565b5f60a082840312156100ac575f5ffd5b82601f8301126100ba575f5ffd5b60405160a081016001600160401b03811182821017156100dc576100dc61006d565b6040528060a08401858111156100f0575f5ffd5b845b818110156101115761010381610081565b8352602092830192016100f2565b509195945050505050565b60805160a05160c05160e0516101005161029c6101555f395f61011b01525f60db01525f609b01525f605b01525f601c015261029c5ff3fe60806040523373ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000000000000000000000000000000000000000000016148061007d57503373ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000000000000000000000000000000000000000000016145b806100bd57503373ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000000000000000000000000000000000000000000016145b806100fd57503373ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000000000000000000000000000000000000000000016145b8061013d57503373ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000000000000000000000000000000000000000000016145b1561014c5761014a61018c565b005b341561015457005b6040517f8e4a23d600000000000000000000000000000000000000000000000000000000815233600482015260240160405180910390fd5b601436101561019757565b5f6101a560148236816101d9565b6101ae91610200565b60601c90506014360360145f375f5f601436035f34855af13d5f5f3e8080156101d5573d5ff35b3d5ffd5b5f5f858511156101e7575f5ffd5b838611156101f3575f5ffd5b5050820193919092039150565b80357fffffffffffffffffffffffffffffffffffffffff000000000000000000000000811690601484101561025f577fffffffffffffffffffffffffffffffffffffffff000000000000000000000000808560140360031b1b82161691505b509291505056fea2646970667358221220811906fba4b240f9c63cda7dfe8ed6e7551bf4d0267aa331997e97796489e1d964736f6c63430008220033", + "sourceMap": "184:3285:0:-:0;;;967:294;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;1044:18;;-1:-1:-1;;;;;1024:38:0;;;;;;;1044:18;1092;;;1072:38;;;;1140:18;;;;1120:38;;;;1188:18;;;;1168:38;;;;1236:18;;;;1216:38;;;184:3285;;14:127:1;75:10;70:3;66:20;63:1;56:31;106:4;103:1;96:15;130:4;127:1;120:15;146:177;225:13;;-1:-1:-1;;;;;267:31:1;;257:42;;247:70;;313:1;310;303:12;247:70;146:177;;;:::o;328:789::-;421:6;474:3;462:9;453:7;449:23;445:33;442:53;;;491:1;488;481:12;442:53;540:7;533:4;522:9;518:20;514:34;504:62;;562:1;559;552:12;504:62;595:2;589:9;637:3;625:16;;-1:-1:-1;;;;;656:34:1;;692:22;;;653:62;650:88;;;718:18;;:::i;:::-;754:2;747:22;789:6;833:3;818:19;;849;;;846:39;;;881:1;878;871:12;846:39;905:9;923:163;939:6;934:3;931:15;923:163;;;1007:34;1037:3;1007:34;:::i;:::-;995:47;;1071:4;1062:14;;;;956;923:163;;;-1:-1:-1;1105:6:1;;328:789;-1:-1:-1;;;;;328:789:1:o;1122:127::-;184:3285:0;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;" + } +} \ No newline at end of file diff --git a/contracts/generated/Cargo.lock b/contracts/generated/Cargo.lock index 469f17b142..55b402ca78 100644 --- a/contracts/generated/Cargo.lock +++ b/contracts/generated/Cargo.lock @@ -1078,6 +1078,7 @@ dependencies = [ "cow-contract-remoteerc20balances", "cow-contract-signatures", "cow-contract-solver", + "cow-contract-solver7702delegate", "cow-contract-spardose", "cow-contract-sushiswaprouter", "cow-contract-swapper", @@ -1799,6 +1800,17 @@ dependencies = [ "anyhow", ] +[[package]] +name = "cow-contract-solver7702delegate" +version = "0.1.0" +dependencies = [ + "alloy-contract", + "alloy-primitives", + "alloy-provider", + "alloy-sol-types", + "anyhow", +] + [[package]] name = "cow-contract-spardose" version = "0.1.0" diff --git a/contracts/generated/contracts-facade/Cargo.toml b/contracts/generated/contracts-facade/Cargo.toml index 63330b909e..2be950a2db 100644 --- a/contracts/generated/contracts-facade/Cargo.toml +++ b/contracts/generated/contracts-facade/Cargo.toml @@ -72,6 +72,7 @@ cow-contract-permit2 = { path = "../contracts-generated/permit2" } cow-contract-remoteerc20balances = { path = "../contracts-generated/remoteerc20balances" } cow-contract-signatures = { path = "../contracts-generated/signatures" } cow-contract-solver = { path = "../contracts-generated/solver" } +cow-contract-solver7702delegate = { path = "../contracts-generated/solver7702delegate" } cow-contract-spardose = { path = "../contracts-generated/spardose" } cow-contract-sushiswaprouter = { path = "../contracts-generated/sushiswaprouter" } cow-contract-swapper = { path = "../contracts-generated/swapper" } diff --git a/contracts/generated/contracts-facade/src/lib.rs b/contracts/generated/contracts-facade/src/lib.rs index ecdaf9cf72..1117260f9e 100644 --- a/contracts/generated/contracts-facade/src/lib.rs +++ b/contracts/generated/contracts-facade/src/lib.rs @@ -56,6 +56,7 @@ pub use { cow_contract_liquoricesettlement as LiquoriceSettlement, cow_contract_pancakerouter as PancakeRouter, cow_contract_permit2 as Permit2, + cow_contract_solver7702delegate as Solver7702Delegate, cow_contract_sushiswaprouter as SushiSwapRouter, cow_contract_swaprrouter as SwaprRouter, cow_contract_testnetuniswapv2router02 as TestnetUniswapV2Router02, diff --git a/contracts/generated/contracts-generated/solver7702delegate/Cargo.toml b/contracts/generated/contracts-generated/solver7702delegate/Cargo.toml new file mode 100644 index 0000000000..d0406845fd --- /dev/null +++ b/contracts/generated/contracts-generated/solver7702delegate/Cargo.toml @@ -0,0 +1,19 @@ +# Auto-generated by contracts-generate. Do not edit. +[package] +name = "cow-contract-solver7702delegate" +version = "0.1.0" +edition = "2024" +publish = false + +[lib] +doctest = false + +[dependencies] +alloy-contract = { workspace = true } +alloy-primitives = { workspace = true } +alloy-provider = { workspace = true } +alloy-sol-types = { workspace = true } +anyhow = { workspace = true } + +[lints] +workspace = true diff --git a/contracts/generated/contracts-generated/solver7702delegate/src/lib.rs b/contracts/generated/contracts-generated/solver7702delegate/src/lib.rs new file mode 100644 index 0000000000..371f25b77d --- /dev/null +++ b/contracts/generated/contracts-generated/solver7702delegate/src/lib.rs @@ -0,0 +1,545 @@ +#![allow( + unused_imports, + unused_attributes, + clippy::all, + rustdoc::all, + non_snake_case +)] +//! Auto-generated contract bindings. Do not edit. +/** + +Generated by the following Solidity interface... +```solidity +interface Solver7702Delegate { + error Unauthorized(address sender); + + constructor(address[5] approvedCallers); + + fallback() external payable; +} +``` + +...which was generated by the following JSON ABI: +```json +[ + { + "type": "constructor", + "inputs": [ + { + "name": "approvedCallers", + "type": "address[5]", + "internalType": "address[5]" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "fallback", + "stateMutability": "payable" + }, + { + "type": "error", + "name": "Unauthorized", + "inputs": [ + { + "name": "sender", + "type": "address", + "internalType": "address" + } + ] + } +] +```*/ +#[allow( + non_camel_case_types, + non_snake_case, + clippy::pub_underscore_fields, + clippy::style, + clippy::empty_structs_with_brackets +)] +pub mod Solver7702Delegate { + use {super::*, alloy_sol_types}; + /// The creation / init bytecode of the contract. + /// + /// ```text + ///0x610120604052348015610010575f5ffd5b506040516103f13803806103f183398101604081905261002f9161009c565b80516001600160a01b0390811660809081526020830151821660a0526040830151821660c0526060830151821660e05290910151166101005261011c565b634e487b7160e01b5f52604160045260245ffd5b80516001600160a01b0381168114610097575f5ffd5b919050565b5f60a082840312156100ac575f5ffd5b82601f8301126100ba575f5ffd5b60405160a081016001600160401b03811182821017156100dc576100dc61006d565b6040528060a08401858111156100f0575f5ffd5b845b818110156101115761010381610081565b8352602092830192016100f2565b509195945050505050565b60805160a05160c05160e0516101005161029c6101555f395f61011b01525f60db01525f609b01525f605b01525f601c015261029c5ff3fe60806040523373ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000000000000000000000000000000000000000000016148061007d57503373ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000000000000000000000000000000000000000000016145b806100bd57503373ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000000000000000000000000000000000000000000016145b806100fd57503373ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000000000000000000000000000000000000000000016145b8061013d57503373ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000000000000000000000000000000000000000000016145b1561014c5761014a61018c565b005b341561015457005b6040517f8e4a23d600000000000000000000000000000000000000000000000000000000815233600482015260240160405180910390fd5b601436101561019757565b5f6101a560148236816101d9565b6101ae91610200565b60601c90506014360360145f375f5f601436035f34855af13d5f5f3e8080156101d5573d5ff35b3d5ffd5b5f5f858511156101e7575f5ffd5b838611156101f3575f5ffd5b5050820193919092039150565b80357fffffffffffffffffffffffffffffffffffffffff000000000000000000000000811690601484101561025f577fffffffffffffffffffffffffffffffffffffffff000000000000000000000000808560140360031b1b82161691505b509291505056fea2646970667358221220811906fba4b240f9c63cda7dfe8ed6e7551bf4d0267aa331997e97796489e1d964736f6c63430008220033 + /// ``` + #[rustfmt::skip] + #[allow(clippy::all)] + pub static BYTECODE: alloy_sol_types::private::Bytes = alloy_sol_types::private::Bytes::from_static( + b"a\x01 `@R4\x80\x15a\0\x10W__\xFD[P`@Qa\x03\xF18\x03\x80a\x03\xF1\x839\x81\x01`@\x81\x90Ra\0/\x91a\0\x9CV[\x80Q`\x01`\x01`\xA0\x1B\x03\x90\x81\x16`\x80\x90\x81R` \x83\x01Q\x82\x16`\xA0R`@\x83\x01Q\x82\x16`\xC0R``\x83\x01Q\x82\x16`\xE0R\x90\x91\x01Q\x16a\x01\0Ra\x01\x1CV[cNH{q`\xE0\x1B_R`A`\x04R`$_\xFD[\x80Q`\x01`\x01`\xA0\x1B\x03\x81\x16\x81\x14a\0\x97W__\xFD[\x91\x90PV[_`\xA0\x82\x84\x03\x12\x15a\0\xACW__\xFD[\x82`\x1F\x83\x01\x12a\0\xBAW__\xFD[`@Q`\xA0\x81\x01`\x01`\x01`@\x1B\x03\x81\x11\x82\x82\x10\x17\x15a\0\xDCWa\0\xDCa\0mV[`@R\x80`\xA0\x84\x01\x85\x81\x11\x15a\0\xF0W__\xFD[\x84[\x81\x81\x10\x15a\x01\x11Wa\x01\x03\x81a\0\x81V[\x83R` \x92\x83\x01\x92\x01a\0\xF2V[P\x91\x95\x94PPPPPV[`\x80Q`\xA0Q`\xC0Q`\xE0Qa\x01\0Qa\x02\x9Ca\x01U_9_a\x01\x1B\x01R_`\xDB\x01R_`\x9B\x01R_`[\x01R_`\x1C\x01Ra\x02\x9C_\xF3\xFE`\x80`@R3s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x7F\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x16\x14\x80a\0}WP3s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x7F\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x16\x14[\x80a\0\xBDWP3s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x7F\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x16\x14[\x80a\0\xFDWP3s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x7F\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x16\x14[\x80a\x01=WP3s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x7F\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x16\x14[\x15a\x01LWa\x01Ja\x01\x8CV[\0[4\x15a\x01TW\0[`@Q\x7F\x8EJ#\xD6\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x81R3`\x04\x82\x01R`$\x01`@Q\x80\x91\x03\x90\xFD[`\x146\x10\x15a\x01\x97WV[_a\x01\xA5`\x14\x826\x81a\x01\xD9V[a\x01\xAE\x91a\x02\0V[``\x1C\x90P`\x146\x03`\x14_7__`\x146\x03_4\x85Z\xF1=__>\x80\x80\x15a\x01\xD5W=_\xF3[=_\xFD[__\x85\x85\x11\x15a\x01\xE7W__\xFD[\x83\x86\x11\x15a\x01\xF3W__\xFD[PP\x82\x01\x93\x91\x90\x92\x03\x91PV[\x805\x7F\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\0\0\0\0\0\0\0\0\0\0\0\0\x81\x16\x90`\x14\x84\x10\x15a\x02_W\x7F\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\0\0\0\0\0\0\0\0\0\0\0\0\x80\x85`\x14\x03`\x03\x1B\x1B\x82\x16\x16\x91P[P\x92\x91PPV\xFE\xA2dipfsX\"\x12 \x81\x19\x06\xFB\xA4\xB2@\xF9\xC6<\xDA}\xFE\x8E\xD6\xE7U\x1B\xF4\xD0&z\xA31\x99~\x97yd\x89\xE1\xD9dsolcC\0\x08\"\x003", + ); + #[derive(Default, Debug, PartialEq, Eq, Hash)] + /**Custom error with signature `Unauthorized(address)` and selector `0x8e4a23d6`. + ```solidity + error Unauthorized(address sender); + ```*/ + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct Unauthorized { + #[allow(missing_docs)] + pub sender: alloy_sol_types::private::Address, + } + #[allow( + non_camel_case_types, + non_snake_case, + clippy::pub_underscore_fields, + clippy::style + )] + const _: () = { + use alloy_sol_types; + #[doc(hidden)] + #[allow(dead_code)] + type UnderlyingSolTuple<'a> = (alloy_sol_types::sol_data::Address,); + #[doc(hidden)] + type UnderlyingRustTuple<'a> = (alloy_sol_types::private::Address,); + #[cfg(test)] + #[allow(dead_code, unreachable_patterns)] + fn _type_assertion(_t: alloy_sol_types::private::AssertTypeEq) { + match _t { + alloy_sol_types::private::AssertTypeEq::< + ::RustType, + >(_) => {} + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From for UnderlyingRustTuple<'_> { + fn from(value: Unauthorized) -> Self { + (value.sender,) + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> for Unauthorized { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self { sender: tuple.0 } + } + } + #[automatically_derived] + impl alloy_sol_types::SolError for Unauthorized { + type Parameters<'a> = UnderlyingSolTuple<'a>; + type Token<'a> = as alloy_sol_types::SolType>::Token<'a>; + + const SELECTOR: [u8; 4] = [142u8, 74u8, 35u8, 214u8]; + const SIGNATURE: &'static str = "Unauthorized(address)"; + + #[inline] + fn new<'a>( + tuple: as alloy_sol_types::SolType>::RustType, + ) -> Self { + tuple.into() + } + + #[inline] + fn tokenize(&self) -> Self::Token<'_> { + ( + ::tokenize( + &self.sender, + ), + ) + } + + #[inline] + fn abi_decode_raw_validate(data: &[u8]) -> alloy_sol_types::Result { + as alloy_sol_types::SolType>::abi_decode_sequence_validate( + data, + ) + .map(Self::new) + } + } + }; + /**Constructor`. + ```solidity + constructor(address[5] approvedCallers); + ```*/ + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct constructorCall { + #[allow(missing_docs)] + pub approvedCallers: [alloy_sol_types::private::Address; 5usize], + } + const _: () = { + use alloy_sol_types; + { + #[doc(hidden)] + #[allow(dead_code)] + type UnderlyingSolTuple<'a> = ( + alloy_sol_types::sol_data::FixedArray, + ); + #[doc(hidden)] + type UnderlyingRustTuple<'a> = ([alloy_sol_types::private::Address; 5usize],); + #[cfg(test)] + #[allow(dead_code, unreachable_patterns)] + fn _type_assertion(_t: alloy_sol_types::private::AssertTypeEq) { + match _t { + alloy_sol_types::private::AssertTypeEq::< + ::RustType, + >(_) => {} + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From for UnderlyingRustTuple<'_> { + fn from(value: constructorCall) -> Self { + (value.approvedCallers,) + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> for constructorCall { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self { + approvedCallers: tuple.0, + } + } + } + } + #[automatically_derived] + impl alloy_sol_types::SolConstructor for constructorCall { + type Parameters<'a> = ( + alloy_sol_types::sol_data::FixedArray, + ); + type Token<'a> = as alloy_sol_types::SolType>::Token<'a>; + + #[inline] + fn new<'a>( + tuple: as alloy_sol_types::SolType>::RustType, + ) -> Self { + tuple.into() + } + + #[inline] + fn tokenize(&self) -> Self::Token<'_> { + ( as alloy_sol_types::SolType>::tokenize( + &self.approvedCallers, + ),) + } + } + }; + ///Container for all the [`Solver7702Delegate`](self) custom errors. + #[derive(Clone, Debug, PartialEq, Eq, Hash)] + pub enum Solver7702DelegateErrors { + #[allow(missing_docs)] + Unauthorized(Unauthorized), + } + impl Solver7702DelegateErrors { + /// All the selectors of this enum. + /// + /// Note that the selectors might not be in the same order as the + /// variants. No guarantees are made about the order of the + /// selectors. + /// + /// Prefer using `SolInterface` methods instead. + pub const SELECTORS: &'static [[u8; 4usize]] = &[[142u8, 74u8, 35u8, 214u8]]; + /// The signatures in the same order as `SELECTORS`. + pub const SIGNATURES: &'static [&'static str] = + &[::SIGNATURE]; + /// The names of the variants in the same order as `SELECTORS`. + pub const VARIANT_NAMES: &'static [&'static str] = &[::core::stringify!(Unauthorized)]; + + /// Returns the signature for the given selector, if known. + #[inline] + pub fn signature_by_selector( + selector: [u8; 4usize], + ) -> ::core::option::Option<&'static str> { + match Self::SELECTORS.binary_search(&selector) { + ::core::result::Result::Ok(idx) => { + ::core::option::Option::Some(Self::SIGNATURES[idx]) + } + ::core::result::Result::Err(_) => ::core::option::Option::None, + } + } + + /// Returns the enum variant name for the given selector, if known. + #[inline] + pub fn name_by_selector(selector: [u8; 4usize]) -> ::core::option::Option<&'static str> { + let sig = Self::signature_by_selector(selector)?; + sig.split_once('(').map(|(name, _)| name) + } + } + #[automatically_derived] + impl alloy_sol_types::SolInterface for Solver7702DelegateErrors { + const COUNT: usize = 1usize; + const MIN_DATA_LENGTH: usize = 32usize; + const NAME: &'static str = "Solver7702DelegateErrors"; + + #[inline] + fn selector(&self) -> [u8; 4] { + match self { + Self::Unauthorized(_) => ::SELECTOR, + } + } + + #[inline] + fn selector_at(i: usize) -> ::core::option::Option<[u8; 4]> { + Self::SELECTORS.get(i).copied() + } + + #[inline] + fn valid_selector(selector: [u8; 4]) -> bool { + Self::SELECTORS.binary_search(&selector).is_ok() + } + + #[inline] + #[allow(non_snake_case)] + fn abi_decode_raw(selector: [u8; 4], data: &[u8]) -> alloy_sol_types::Result { + static DECODE_SHIMS: &[fn( + &[u8], + ) + -> alloy_sol_types::Result] = &[{ + fn Unauthorized(data: &[u8]) -> alloy_sol_types::Result { + ::abi_decode_raw(data) + .map(Solver7702DelegateErrors::Unauthorized) + } + Unauthorized + }]; + let Ok(idx) = Self::SELECTORS.binary_search(&selector) else { + return Err(alloy_sol_types::Error::unknown_selector( + ::NAME, + selector, + )); + }; + DECODE_SHIMS[idx](data) + } + + #[inline] + #[allow(non_snake_case)] + fn abi_decode_raw_validate( + selector: [u8; 4], + data: &[u8], + ) -> alloy_sol_types::Result { + static DECODE_VALIDATE_SHIMS: &[fn( + &[u8], + ) -> alloy_sol_types::Result< + Solver7702DelegateErrors, + >] = &[{ + fn Unauthorized(data: &[u8]) -> alloy_sol_types::Result { + ::abi_decode_raw_validate(data) + .map(Solver7702DelegateErrors::Unauthorized) + } + Unauthorized + }]; + let Ok(idx) = Self::SELECTORS.binary_search(&selector) else { + return Err(alloy_sol_types::Error::unknown_selector( + ::NAME, + selector, + )); + }; + DECODE_VALIDATE_SHIMS[idx](data) + } + + #[inline] + fn abi_encoded_size(&self) -> usize { + match self { + Self::Unauthorized(inner) => { + ::abi_encoded_size(inner) + } + } + } + + #[inline] + fn abi_encode_raw(&self, out: &mut alloy_sol_types::private::Vec) { + match self { + Self::Unauthorized(inner) => { + ::abi_encode_raw(inner, out) + } + } + } + } + use alloy_contract; + /**Creates a new wrapper around an on-chain [`Solver7702Delegate`](self) contract instance. + + See the [wrapper's documentation](`Solver7702DelegateInstance`) for more details.*/ + #[inline] + pub const fn new< + P: alloy_contract::private::Provider, + N: alloy_contract::private::Network, + >( + address: alloy_sol_types::private::Address, + __provider: P, + ) -> Solver7702DelegateInstance { + Solver7702DelegateInstance::::new(address, __provider) + } + /**Deploys this contract using the given `provider` and constructor arguments, if any. + + Returns a new instance of the contract, if the deployment was successful. + + For more fine-grained control over the deployment process, use [`deploy_builder`] instead.*/ + #[inline] + pub fn deploy, N: alloy_contract::private::Network>( + __provider: P, + approvedCallers: [alloy_sol_types::private::Address; 5usize], + ) -> impl ::core::future::Future>> + { + Solver7702DelegateInstance::::deploy(__provider, approvedCallers) + } + /**Creates a `RawCallBuilder` for deploying this contract using the given `provider` + and constructor arguments, if any. + + This is a simple wrapper around creating a `RawCallBuilder` with the data set to + the bytecode concatenated with the constructor's ABI-encoded arguments.*/ + #[inline] + pub fn deploy_builder< + P: alloy_contract::private::Provider, + N: alloy_contract::private::Network, + >( + __provider: P, + approvedCallers: [alloy_sol_types::private::Address; 5usize], + ) -> alloy_contract::RawCallBuilder { + Solver7702DelegateInstance::::deploy_builder(__provider, approvedCallers) + } + /**A [`Solver7702Delegate`](self) instance. + + Contains type-safe methods for interacting with an on-chain instance of the + [`Solver7702Delegate`](self) contract located at a given `address`, using a given + provider `P`. + + If the contract bytecode is available (see the [`sol!`](alloy_sol_types::sol!) + documentation on how to provide it), the `deploy` and `deploy_builder` methods can + be used to deploy a new instance of the contract. + + See the [module-level documentation](self) for all the available methods.*/ + #[derive(Clone)] + pub struct Solver7702DelegateInstance { + address: alloy_sol_types::private::Address, + provider: P, + _network: ::core::marker::PhantomData, + } + #[automatically_derived] + impl ::core::fmt::Debug for Solver7702DelegateInstance { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.debug_tuple("Solver7702DelegateInstance") + .field(&self.address) + .finish() + } + } + /// Instantiation and getters/setters. + impl, N: alloy_contract::private::Network> + Solver7702DelegateInstance + { + /**Creates a new wrapper around an on-chain [`Solver7702Delegate`](self) contract instance. + + See the [wrapper's documentation](`Solver7702DelegateInstance`) for more details.*/ + #[inline] + pub const fn new(address: alloy_sol_types::private::Address, __provider: P) -> Self { + Self { + address, + provider: __provider, + _network: ::core::marker::PhantomData, + } + } + + /**Deploys this contract using the given `provider` and constructor arguments, if any. + + Returns a new instance of the contract, if the deployment was successful. + + For more fine-grained control over the deployment process, use [`deploy_builder`] instead.*/ + #[inline] + pub async fn deploy( + __provider: P, + approvedCallers: [alloy_sol_types::private::Address; 5usize], + ) -> alloy_contract::Result> { + let call_builder = Self::deploy_builder(__provider, approvedCallers); + let contract_address = call_builder.deploy().await?; + Ok(Self::new(contract_address, call_builder.provider)) + } + + /**Creates a `RawCallBuilder` for deploying this contract using the given `provider` + and constructor arguments, if any. + + This is a simple wrapper around creating a `RawCallBuilder` with the data set to + the bytecode concatenated with the constructor's ABI-encoded arguments.*/ + #[inline] + pub fn deploy_builder( + __provider: P, + approvedCallers: [alloy_sol_types::private::Address; 5usize], + ) -> alloy_contract::RawCallBuilder { + alloy_contract::RawCallBuilder::new_raw_deploy( + __provider, + [ + &BYTECODE[..], + &alloy_sol_types::SolConstructor::abi_encode(&constructorCall { + approvedCallers, + })[..], + ] + .concat() + .into(), + ) + } + + /// Returns a reference to the address. + #[inline] + pub const fn address(&self) -> &alloy_sol_types::private::Address { + &self.address + } + + /// Sets the address. + #[inline] + pub fn set_address(&mut self, address: alloy_sol_types::private::Address) { + self.address = address; + } + + /// Sets the address and returns `self`. + pub fn at(mut self, address: alloy_sol_types::private::Address) -> Self { + self.set_address(address); + self + } + + /// Returns a reference to the provider. + #[inline] + pub const fn provider(&self) -> &P { + &self.provider + } + } + impl Solver7702DelegateInstance<&P, N> { + /// Clones the provider and returns a new instance with the cloned + /// provider. + #[inline] + pub fn with_cloned_provider(self) -> Solver7702DelegateInstance { + Solver7702DelegateInstance { + address: self.address, + provider: ::core::clone::Clone::clone(&self.provider), + _network: ::core::marker::PhantomData, + } + } + } + /// Function calls. + impl, N: alloy_contract::private::Network> + Solver7702DelegateInstance + { + /// Creates a new call builder using this contract instance's provider + /// and address. + /// + /// Note that the call can be any function call, not just those defined + /// in this contract. Prefer using the other methods for + /// building type-safe contract calls. + pub fn call_builder( + &self, + call: &C, + ) -> alloy_contract::SolCallBuilder<&P, C, N> { + alloy_contract::SolCallBuilder::new_sol(&self.provider, &self.address, call) + } + } + /// Event filters. + impl, N: alloy_contract::private::Network> + Solver7702DelegateInstance + { + /// Creates a new event filter using this contract instance's provider + /// and address. + /// + /// Note that the type can be any event, not just those defined in this + /// contract. Prefer using the other methods for building + /// type-safe event filters. + pub fn event_filter( + &self, + ) -> alloy_contract::Event<&P, E, N> { + alloy_contract::Event::new_sol(&self.provider, &self.address) + } + } +} +pub type Instance = Solver7702Delegate::Solver7702DelegateInstance<::alloy_provider::DynProvider>; diff --git a/contracts/src/main.rs b/contracts/src/main.rs index d07ad7ef5f..2c46fc1944 100644 --- a/contracts/src/main.rs +++ b/contracts/src/main.rs @@ -425,6 +425,7 @@ fn build_module() -> Module { PLASMA => "0x9da8b48441583a2b93e2ef8213aad0ec0b392c69", INK => "0x9da8b48441583a2b93e2ef8213aad0ec0b392c69", ])) + .add_contract(Contract::new("Solver7702Delegate")) .add_contract(Contract::new("ICowWrapper")) .add_contract(Contract::new("ChainalysisOracle").with_networks(networks![ MAINNET => "0x40C57923924B5c5c5455c48D93317139ADDaC8fb", diff --git a/contracts/src/vendor.rs b/contracts/src/vendor.rs index 4c9584243e..cb369dab57 100644 --- a/contracts/src/vendor.rs +++ b/contracts/src/vendor.rs @@ -36,6 +36,12 @@ pub fn run(artifacts_dir: &Path) -> Result<()> { "balancer-labs/balancer-v2-monorepo/a3b570a2aa655d4c4941a67e3db6a06fbd72ef09/pkg/\ deployments/deployed/mainnet/WeightedPool2TokensFactory.json", )? + .github( + "Solver7702Delegate", + // TODO(post-audit): bump to the audited commit hash or release tag. + "cowprotocol/solver-7702-delegate/5b6bd5faf05bed6c08c827f9f8e33cd4e33b7533/out/\ + Solver7702Delegate.sol/Solver7702Delegate.json", + )? .npm( "CowProtocolToken", "@cowprotocol/token@1.1.0/build/artifacts/src/contracts/CowProtocolToken.sol/\ diff --git a/crates/driver/src/domain/mempools.rs b/crates/driver/src/domain/mempools.rs index 7be80cf8a9..d71eceda70 100644 --- a/crates/driver/src/domain/mempools.rs +++ b/crates/driver/src/domain/mempools.rs @@ -4,7 +4,7 @@ use { domain::{blockchain::TxStatus, competition::solution::Settlement}, infra::{self, Ethereum, observe}, }, - alloy::{consensus::Transaction, eips::eip1559::Eip1559Estimation}, + alloy::{consensus::Transaction, eips::eip1559::Eip1559Estimation, primitives::Bytes}, anyhow::{Context, anyhow}, eth_domain_types::{self as eth, BlockNo, TxId}, ethrpc::block_stream::into_stream, @@ -30,12 +30,12 @@ pub enum SubmissionMode { /// Solver EOA signs and submits directly to the settlement contract. Direct(eth::Address), /// A dedicated submission EOA signs and pays for the tx while routing it - /// through the solver's EIP-7702 delegated forwarder contract. + /// through the solver's EIP-7702 delegate. Delegated { /// The address that signs the transaction and whose nonce is used. submitter_eoa: eth::Address, /// The solver EOA address. In EIP-7702 mode tx.to is set to this - /// address (which delegates to a forwarder contract), instead of the + /// address (which delegates to Solver7702Delegate), instead of the /// settlement contract. solver_eoa: eth::Address, }, @@ -502,10 +502,11 @@ fn apply_gas_fee_override( } } -/// In EIP-7702 mode, reroute the tx through the solver EOA's delegated -/// forwarder contract. The original target and calldata are wrapped in a -/// `forward()` call. `from` is set to the submission EOA so that simulations -/// see the correct `msg.sender` for the forwarder's caller whitelist. +/// In EIP-7702 mode, reroute the tx through the solver EOA's delegate. Its +/// fallback expects the 20-byte target address followed by target calldata. +/// `from` is the submitter EOA so simulations see the correct `msg.sender` +/// for the delegate's caller whitelist. The solver EOA is in `tx.to` and +/// becomes `address(this)` when the delegate runs. fn prepare_submission(tx: ð::Tx, mode: &SubmissionMode) -> eth::Tx { let mut tx = tx.clone(); match mode { @@ -517,13 +518,22 @@ fn prepare_submission(tx: ð::Tx, mode: &SubmissionMode) -> eth::Tx { submitter_eoa, solver_eoa, } => { + let original_target = tx.to; tx.from = *submitter_eoa; tx.to = *solver_eoa; + tx.input = delegated_calldata(original_target, &tx.input); tx } } } +fn delegated_calldata(target: eth::Address, calldata: &Bytes) -> Bytes { + let mut input = Vec::with_capacity(target.len() + calldata.len()); + input.extend_from_slice(target.as_slice()); + input.extend_from_slice(calldata); + input.into() +} + pub struct SubmissionSuccess { pub tx_hash: eth::TxId, /// At which block we started to submit the transaction. @@ -611,3 +621,43 @@ impl Error { Some(end.saturating_sub(start).0) } } + +#[cfg(test)] +mod tests { + use { + super::*, + alloy::primitives::{Bytes, address}, + }; + + const ORIGINAL_FROM: eth::Address = address!("0000000000000000000000000000000000000001"); + const SETTLEMENT: eth::Address = address!("0000000000000000000000000000000000000002"); + const SOLVER: eth::Address = address!("0000000000000000000000000000000000000003"); + const SUBMITTER: eth::Address = address!("0000000000000000000000000000000000000004"); + + fn tx(input: Bytes) -> eth::Tx { + eth::Tx { + from: ORIGINAL_FROM, + to: SETTLEMENT, + value: 0.into(), + input, + access_list: Default::default(), + } + } + + #[test] + fn delegated_submission_rewrites_transaction() { + let prepared = prepare_submission( + &tx(Bytes::from_static(&[0xaa, 0xbb])), + &SubmissionMode::Delegated { + submitter_eoa: SUBMITTER, + solver_eoa: SOLVER, + }, + ); + let mut expected = SETTLEMENT.as_slice().to_vec(); + expected.extend_from_slice(&[0xaa, 0xbb]); + + assert_eq!(prepared.from, SUBMITTER); + assert_eq!(prepared.to, SOLVER); + assert_eq!(prepared.input, Bytes::from(expected)); + } +} diff --git a/crates/driver/src/infra/solver/eip7702.rs b/crates/driver/src/infra/solver/eip7702.rs index 4f34b0f2c8..b34a3fa890 100644 --- a/crates/driver/src/infra/solver/eip7702.rs +++ b/crates/driver/src/infra/solver/eip7702.rs @@ -2,73 +2,62 @@ use { super::{Config, Solver}, crate::infra::blockchain::Ethereum, alloy::{ - eips::eip7702::Authorization, - network::{TransactionBuilder7702, TxSigner}, - primitives::{Address, U256}, + eips::eip7702::{Authorization, SignedAuthorization}, + network::{ReceiptResponse, TransactionBuilder7702, TxSigner}, + primitives::{Address, B256, Bytes, U256, address}, providers::Provider, rpc::types::TransactionRequest, - sol_types::SolCall, + sol_types::SolConstructor, }, anyhow::Context, - contracts::CowSettlementForwarder::CowSettlementForwarder, - futures::future::try_join_all, - std::time::Duration, + contracts::Solver7702Delegate::Solver7702Delegate, + hex_literal::hex, + std::{collections::HashSet, time::Duration}, tracing::instrument, }; /// EIP-7702 delegation prefix stored as account code prefix. If you call /// eth_getCode on a delegated EOA, instead of getting empty bytes (normal EOA), /// you get 0xef0100<20-byte contract address>. -const DELEGATION_PREFIX: [u8; 3] = [0xef, 0x01, 0x00]; +pub const DELEGATION_PREFIX: [u8; 3] = [0xef, 0x01, 0x00]; +const DELEGATION_CODE_LEN: usize = DELEGATION_PREFIX.len() + Address::len_bytes(); +/// The maximum number of approved callers allowed by the Solver7702Delegate +/// ABI. +pub const MAX_APPROVED_CALLERS: usize = 5; +type ApprovedCallers = [Address; MAX_APPROVED_CALLERS]; +// Arachnid's deterministic-deployment-proxy. It is deployed at this same +// address on many EVM chains. Sending 32-byte salt || init code to it deploys +// that init code with CREATE2. We use it to derive and deploy the exact +// Solver7702Delegate address from this proxy address, zero salt, and init code. +pub const CREATE2_DEPLOYER: Address = address!("4e59b44847b379578588920cA78FbF26c0B4956C"); +const CREATE2_DEPLOYER_CODE: &[u8] = &hex!( + "7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf3" +); +// The ordered caller slots and zero salt are part of the CREATE2 input, so the +// same caller set keeps resolving to the same delegate target. +pub const CREATE2_SALT: B256 = B256::ZERO; -/// Ensure EIP-7702 delegation and caller approval are set up for all solvers -/// with parallel submission accounts. Called once at driver startup. +/// Ensure EIP-7702 delegate deployment and solver delegation are set up for all +/// solvers with parallel submission accounts. Called once at driver startup. /// /// # Errors -/// - `max-solutions-to-propose > 1` without any `submission-accounts` -/// configured (parallel submission is required for multi-solution mode). -/// - `submission-accounts` configured without a `forwarder-contract` address. -/// - `submission-accounts` configured but the main solver account is read-only, -/// so it cannot sign the EIP-7702 authorization. -/// - The EIP-7702 delegation tx lands but the on-chain code does not reflect -/// the expected designator (e.g. a concurrent tx shifted the nonce). -/// - Any underlying RPC error (code fetch, chain id, tx send, receipt). +/// - A solver has `submission-accounts`, but its main account is read-only and +/// cannot sign the EIP-7702 authorization. +/// - The deterministic CREATE2 deployer is missing or has unexpected code. +/// - The solver EOA already delegates to another target, or has non-delegation +/// code. +/// - The EIP-7702 authorization lands but the on-chain code does not reflect +/// the expected delegate, for example because a concurrent tx shifted the +/// nonce. +/// - Any underlying RPC error while fetching code, chain id, nonces, sending a +/// tx, or waiting for a receipt. #[instrument(name = "setup_eip7702", skip_all)] pub async fn setup(solvers: &[Solver], eth: &Ethereum) -> anyhow::Result<()> { for solver in solvers { let config = solver.config(); if config.submission_accounts.is_empty() { - anyhow::ensure!( - config.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)", - config.name, - ); - continue; - } - if config - .submission_accounts - .iter() - .all(|a| matches!(a, super::Account::Address(_))) - { - tracing::debug!( - solver = %config.name, - "all submission accounts are read-only, skipping EIP-7702 setup" - ); continue; } - anyhow::ensure!( - !matches!(config.account, super::Account::Address(_)), - "solver '{}': main account must be a signer to set up EIP-7702 delegation when \ - submission accounts are configured", - config.name, - ); - let forwarder = config.forwarder_contract.ok_or_else(|| { - anyhow::anyhow!( - "solver {}: submission_accounts configured but forwarder_contract missing", - config.name - ) - })?; // Register solver + submission accounts with the main wallet so we can // send transactions via the provider during setup. @@ -78,81 +67,316 @@ pub async fn setup(solvers: &[Solver], eth: &Ethereum) -> anyhow::Result<()> { web3.wallet.register_signer(acc.clone()); } - setup_solver(config, forwarder, eth).await?; + let submission_addresses = config + .submission_accounts + .iter() + .map(TxSigner::address) + .collect::>(); + let (delegate, approved_callers, init_code) = delegate_deployment(&submission_addresses)?; + + setup_solver(config, delegate, &approved_callers, &init_code, eth).await?; } Ok(()) } -#[instrument(skip_all)] -async fn setup_solver(config: &Config, forwarder: Address, eth: &Ethereum) -> anyhow::Result<()> { - let solver_address: Address = config.account.address(); - let provider = ð.web3().provider; +pub fn delegate_address(callers: &[Address]) -> anyhow::Result
{ + Ok(delegate_deployment(callers)?.0) +} + +fn delegate_deployment(callers: &[Address]) -> anyhow::Result<(Address, ApprovedCallers, Bytes)> { + anyhow::ensure!( + callers.len() <= MAX_APPROVED_CALLERS, + "Solver7702Delegate supports at most {MAX_APPROVED_CALLERS} submission accounts" + ); + anyhow::ensure!( + callers.iter().all(|caller| *caller != Address::ZERO), + "submission accounts cannot include the zero address" + ); + let mut seen = HashSet::with_capacity(callers.len()); + anyhow::ensure!( + callers.iter().all(|caller| seen.insert(*caller)), + "submission accounts must be unique" + ); - // 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]; + approved_callers[..callers.len()].copy_from_slice(callers); - let submission_addresses: Vec
= config - .submission_accounts + anyhow::ensure!( + !Solver7702Delegate::BYTECODE.is_empty(), + "Solver7702Delegate creation bytecode is missing" + ); + let init_code = Solver7702Delegate::BYTECODE .iter() - .map(TxSigner::address) - .collect(); + .chain(&SolConstructor::abi_encode( + &Solver7702Delegate::constructorCall { + approvedCallers: approved_callers, + }, + )) + .copied() + .collect::(); - if needs_delegation { - setup_delegation_and_approve(config, forwarder, &submission_addresses, eth).await?; - } else { - // Skip delegation, but make sure submission accounts are approved callers. - let unapproved = - check_unapproved_callers(eth, solver_address, &submission_addresses).await?; - approve_submitters(config, &unapproved, eth).await?; + // The submission account order is part of the constructor args, so changing + // TOML order changes the CREATE2 delegate address. + let target = CREATE2_DEPLOYER.create2_from_code(CREATE2_SALT, &init_code); + + Ok((target, approved_callers, init_code)) +} + +#[instrument(skip_all, fields(delegate = ?delegate))] +async fn setup_solver( + config: &Config, + delegate: Address, + approved_callers: &ApprovedCallers, + init_code: &Bytes, + eth: &Ethereum, +) -> anyhow::Result<()> { + let provider = ð.web3().provider; + let solver_address = config.account.address(); + let delegate_code = provider + .get_code_at(delegate) + .await + .context("reading Solver7702Delegate code")?; + let solver_code = provider + .get_code_at(solver_address) + .await + .context("reading solver EOA code")?; + + let delegate_missing = delegate_code.is_empty(); + match (DelegationStatus::from_code(&solver_code), delegate_missing) { + // The solver EOA already delegates somewhere else. Do not silently undo + // a manual change or incident response action. + (DelegationStatus::DelegatedTo(target), _) if target != delegate => anyhow::bail!( + "solver '{}': solver EOA {:?} already delegates to {:?}, expected {:?}; refusing to \ + re-delegate automatically on startup. Clear the existing delegation manually if this \ + is intentional.", + config.name, + solver_address, + target, + delegate, + ), + // The solver account has code that is not an EIP-7702 delegation. This + // is unexpected for an EOA, so fail instead of overwriting it. + (DelegationStatus::OtherCode, _) => anyhow::bail!( + "solver '{}': solver EOA {:?} has non-empty code that is not an EIP-7702 delegation; \ + refusing to overwrite it on startup", + config.name, + solver_address, + ), + // A previous setup attempt may have set the delegation but failed + // before CREATE2 deployment succeeded. The EOA already points to the + // right counterfactual address, so only deploy the missing code. + (DelegationStatus::DelegatedTo(_), true) => { + deploy_delegate( + config, + delegate, + approved_callers, + init_code, + DeploymentMode::DeployOnly, + eth, + ) + .await + } + // Everything is already set up. + (DelegationStatus::DelegatedTo(_), false) => { + tracing::info!( + solver = %config.name, + delegate = ?delegate, + "solver EOA already delegates to Solver7702Delegate" + ); + Ok(()) + } + // Fresh setup: neither the EOA delegation nor the CREATE2 delegate + // exists, so deploy and delegate in one transaction. + (DelegationStatus::Empty, true) => { + deploy_delegate( + config, + delegate, + approved_callers, + init_code, + DeploymentMode::DeployAndDelegate, + eth, + ) + .await + } + // The delegate was deployed already, but this solver EOA has no + // delegation yet. This can happen when another startup process deployed + // the shared CREATE2 target first, so warn and set delegation now. + (DelegationStatus::Empty, false) => { + tracing::warn!( + solver = %config.name, + solver_eoa = ?solver_address, + delegate = ?delegate, + "solver EOA has no EIP-7702 delegation but expected delegate already exists; \ + setting delegation" + ); + setup_delegation(config, delegate, eth).await + } } +} - Ok(()) +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum DeploymentMode { + DeployOnly, + DeployAndDelegate, } -/// Check whether the account's code is an EIP-7702 delegation to -/// `expected_forwarder`. -fn is_delegated_to(code: &[u8], expected_forwarder: Address) -> bool { - // EIP-7702 delegation designator: 0xef0100 || 20-byte address - code.len() == 23 && code.starts_with(&DELEGATION_PREFIX) && code[3..] == expected_forwarder.0.0 +impl DeploymentMode { + fn includes_delegation(self) -> bool { + matches!(self, Self::DeployAndDelegate) + } } -/// Check which submission accounts are already approved callers on the -/// solver's delegated forwarder. Uses `join_all` which auto-batches through -/// ethrpc's batching layer. -#[instrument(skip_all)] -async fn check_unapproved_callers( +#[instrument(skip_all, fields(delegate = ?delegate))] +async fn deploy_delegate( + config: &Config, + delegate: Address, + approved_callers: &ApprovedCallers, + init_code: &Bytes, + mode: DeploymentMode, eth: &Ethereum, - solver: Address, - callers: &[Address], -) -> anyhow::Result> { +) -> anyhow::Result<()> { let provider = ð.web3().provider; + let deployer_code = provider + .get_code_at(CREATE2_DEPLOYER) + .await + .context("reading CREATE2 deployer code")?; + anyhow::ensure!( + deployer_code.as_ref() == CREATE2_DEPLOYER_CODE, + "CREATE2 deployer {CREATE2_DEPLOYER:?} has unexpected code", + ); - let unapproved: Vec
= - try_join_all(callers.iter().copied().map(move |caller| async move { - let forwarder = CowSettlementForwarder::new(solver, provider); - let approved = forwarder.isApprovedCaller(caller).call().await?; - Ok::<_, anyhow::Error>((!approved).then_some(caller)) - })) - .await? - .into_iter() - .flatten() - .collect(); - - Ok(unapproved) + let tx_sender = match mode { + DeploymentMode::DeployOnly => config + .submission_accounts + .first() + .map(TxSigner::address) + .unwrap_or_else(|| config.account.address()), + DeploymentMode::DeployAndDelegate => config.account.address(), + }; + let tx_nonce = wait_for_pending_txs(provider, tx_sender).await?; + let signed_auth = if mode.includes_delegation() { + let chain_id = provider + .get_chain_id() + .await + .context("reading chain id for EIP-7702 authorization")?; + Some(sign_authorization(config, chain_id, delegate, tx_nonce + 1).await?) + } else { + None + }; + let input = CREATE2_SALT + .iter() + .chain(init_code) + .copied() + .collect::(); + + tracing::info!( + delegate = ?delegate, + approved_callers = ?approved_callers, + tx_sender = ?tx_sender, + tx_nonce, + mode = ?mode, + "deploying Solver7702Delegate with CREATE2" + ); + let mut tx = TransactionRequest::default() + .from(tx_sender) + .to(CREATE2_DEPLOYER) + .nonce(tx_nonce) + .input(input.into()); + if let Some(signed_auth) = signed_auth { + tx = tx.with_authorization_list(vec![signed_auth]); + } + + let pending = provider + .send_transaction(tx) + .await + .context("sending Solver7702Delegate CREATE2 deployment tx")?; + let receipt = pending + .get_receipt() + .await + .context("waiting for Solver7702Delegate CREATE2 deployment receipt")?; + receipt + .ensure_success() + .context("Solver7702Delegate CREATE2 deployment tx reverted")?; + + let code = provider + .get_code_at(delegate) + .await + .context("reading Solver7702Delegate code after deployment")?; + anyhow::ensure!( + !code.is_empty(), + "Solver7702Delegate deployment tx {:?} did not create code at {:?}", + receipt.transaction_hash, + delegate, + ); + if mode.includes_delegation() { + let solver_code = provider + .get_code_at(tx_sender) + .await + .context("reading solver EOA code after combined deployment and delegation")?; + anyhow::ensure!( + is_delegated_to(&solver_code, delegate), + "Solver7702Delegate deployment tx {:?} did not delegate solver EOA {:?} to {:?}. \ + Expected auth_nonce={} (solver_nonce={} + 1). Check that no pending txs changed the \ + nonce between query and submission.", + receipt.transaction_hash, + tx_sender, + delegate, + tx_nonce + 1, + tx_nonce, + ); + } + tracing::info!( + tx_hash = ?receipt.transaction_hash, + block = ?receipt.block_number, + delegate = ?delegate, + mode = ?mode, + "Solver7702Delegate CREATE2 deployment confirmed" + ); + + Ok(()) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum DelegationStatus { + /// No code (`eth_getCode` returns empty); undelegated EOA. + Empty, + /// EIP-7702 delegation: code is [`DELEGATION_PREFIX`] followed by this + /// implementation address. + DelegatedTo(Address), + /// Non-empty code that is not an EIP-7702 delegation prefix. + OtherCode, +} + +impl DelegationStatus { + fn from_code(code: &[u8]) -> Self { + if code.is_empty() { + Self::Empty + } else if code.len() == DELEGATION_CODE_LEN && code.starts_with(&DELEGATION_PREFIX) { + Self::DelegatedTo(Address::from_slice(&code[DELEGATION_PREFIX.len()..])) + } else { + Self::OtherCode + } + } +} + +/// Check whether the account's code is an EIP-7702 delegation to +/// `expected_delegate`. +fn is_delegated_to(code: &[u8], expected_delegate: Address) -> bool { + matches!(DelegationStatus::from_code(code), DelegationStatus::DelegatedTo(delegate) if delegate == expected_delegate) } -/// Set up EIP-7702 delegation and approve callers in a single transaction. -/// The solver signs the authorization and self-calls `setApprovedCallers`. +/// Set up EIP-7702 delegation with a zero-address authorization transaction. #[instrument(skip_all)] -async fn setup_delegation_and_approve( +async fn setup_delegation( config: &Config, - forwarder: Address, - unapproved: &[Address], + delegate: Address, eth: &Ethereum, ) -> anyhow::Result<()> { let provider = ð.web3().provider; - let chain_id = provider.get_chain_id().await?; + let chain_id = provider + .get_chain_id() + .await + .context("reading chain id for EIP-7702 authorization")?; let solver_address: Address = config.account.address(); // Wait for any pending solver txs to clear (e.g. in-flight settlements @@ -161,48 +385,38 @@ async fn setup_delegation_and_approve( let solver_nonce = wait_for_pending_txs(provider, solver_address).await?; tracing::info!( - ?forwarder, + ?delegate, solver_nonce, auth_nonce = solver_nonce + 1, - unapproved_callers = unapproved.len(), - "setting up EIP-7702 delegation" + "setting up EIP-7702 solver delegation" ); // The auth nonce must be solver_nonce + 1: in EIP-7702 the sender's nonce // is incremented before the authorization list is processed. Since the // solver is both sender and authority, the nonce will already be // solver_nonce + 1 by the time the auth is checked. - let auth = Authorization { - chain_id: U256::from(chain_id), - address: forwarder, - nonce: solver_nonce + 1, - }; - let sig = config - .account - .sign_hash(&auth.signature_hash()) - .await - .context("failed to sign EIP-7702 authorization")?; - let signed_auth = auth.into_signed(sig); + let signed_auth = sign_authorization(config, chain_id, delegate, solver_nonce + 1).await?; - // Explicitly set the tx nonce to `solver_nonce` so the provider's nonce - // filler cannot assign a different value. - let mut tx = TransactionRequest::default() + // This path is used when the CREATE2 delegate already exists. The tx only + // carries the auth, so use an inert zero-value call. + let tx = TransactionRequest::default() .from(solver_address) - .to(solver_address) + .to(Address::ZERO) + .value(U256::ZERO) .nonce(solver_nonce) .with_authorization_list(vec![signed_auth]); - if !unapproved.is_empty() { - let calldata = CowSettlementForwarder::setApprovedCallersCall { - callers: unapproved.to_vec(), - approved: true, - } - .abi_encode(); - tx = tx.input(calldata.into()); - } - - let pending = provider.send_transaction(tx).await?; - let receipt = pending.get_receipt().await?; + let pending = provider + .send_transaction(tx) + .await + .context("sending EIP-7702 delegation tx")?; + let receipt = pending + .get_receipt() + .await + .context("waiting for EIP-7702 delegation receipt")?; + receipt + .ensure_success() + .context("EIP-7702 delegation tx reverted")?; tracing::info!( tx_hash = ?receipt.transaction_hash, block = ?receipt.block_number, @@ -211,8 +425,11 @@ async fn setup_delegation_and_approve( // Verify the delegation was actually applied (EIP-7702 silently skips // authorizations with mismatched nonces). - let code = provider.get_code_at(solver_address).await?; - if !is_delegated_to(&code, forwarder) { + let code = provider + .get_code_at(solver_address) + .await + .context("reading solver EOA code after EIP-7702 delegation tx")?; + if !is_delegated_to(&code, delegate) { anyhow::bail!( "EIP-7702 delegation not applied after tx {:?}. Expected auth_nonce={} \ (solver_nonce={} + 1). Check that no pending txs changed the nonce between query and \ @@ -226,44 +443,24 @@ async fn setup_delegation_and_approve( Ok(()) } -/// Approve callers via a solver self-call (delegation already active). -#[instrument(skip_all)] -async fn approve_submitters( +async fn sign_authorization( config: &Config, - unapproved: &[Address], - eth: &Ethereum, -) -> anyhow::Result<()> { - if unapproved.is_empty() { - tracing::info!("no sumitters to approve, skipping"); - return Ok(()); - } - tracing::info!( - unapproved_callers = unapproved.len(), - "approving new submission callers" - ); - let provider = ð.web3().provider; - let solver_address: Address = config.account.address(); - - let calldata = CowSettlementForwarder::setApprovedCallersCall { - callers: unapproved.to_vec(), - approved: true, - } - .abi_encode(); - - let tx = TransactionRequest::default() - .from(solver_address) - .to(solver_address) - .input(calldata.into()); - - let pending = provider.send_transaction(tx).await?; - let receipt = pending.get_receipt().await?; - tracing::info!( - tx_hash = ?receipt.transaction_hash, - block = ?receipt.block_number, - "setApprovedCallers tx confirmed" - ); + chain_id: u64, + delegate: Address, + auth_nonce: u64, +) -> anyhow::Result { + let auth = Authorization { + chain_id: U256::from(chain_id), + address: delegate, + nonce: auth_nonce, + }; + let sig = config + .account + .sign_hash(&auth.signature_hash()) + .await + .context("failed to sign EIP-7702 authorization")?; - Ok(()) + Ok(auth.into_signed(sig)) } /// Wait until the solver has no pending transactions in the mempool. @@ -275,10 +472,19 @@ async fn wait_for_pending_txs(provider: &impl Provider, address: Address) -> any let deadline = tokio::time::Instant::now() + MAX_WAIT; loop { + // Startup can happen while transactions from the previous driver process + // are still pending. Reusing that nonce would replace them. // only counts txs in mined blocks - let latest = provider.get_transaction_count(address).await?; + let latest = provider + .get_transaction_count(address) + .await + .context("reading latest solver nonce before EIP-7702 setup")?; // also count txs in the mempool - let pending = provider.get_transaction_count(address).pending().await?; + let pending = provider + .get_transaction_count(address) + .pending() + .await + .context("reading pending solver nonce before EIP-7702 setup")?; if pending <= latest { return Ok(latest); } @@ -298,3 +504,80 @@ async fn wait_for_pending_txs(provider: &impl Provider, address: Address) -> any tokio::time::sleep(POLL_INTERVAL).await; } } + +#[cfg(test)] +mod tests { + use {super::*, alloy::primitives::address}; + + const CALLER_A: Address = address!("0000000000000000000000000000000000000001"); + const CALLER_B: Address = address!("0000000000000000000000000000000000000002"); + const CALLER_C: Address = address!("0000000000000000000000000000000000000003"); + const CALLER_D: Address = address!("0000000000000000000000000000000000000004"); + const CALLER_E: Address = address!("0000000000000000000000000000000000000005"); + const CALLER_F: Address = address!("0000000000000000000000000000000000000006"); + + #[test] + fn delegate_target_is_stable_and_caller_sensitive() { + let (first, _, _) = delegate_deployment(&[CALLER_A, CALLER_B]).unwrap(); + let (same, _, _) = delegate_deployment(&[CALLER_A, CALLER_B]).unwrap(); + let (reordered, _, _) = delegate_deployment(&[CALLER_B, CALLER_A]).unwrap(); + + assert_eq!(first, same); + assert_ne!(first, reordered); + } + + #[test] + fn pads_approved_callers_to_contract_capacity() { + let (_, approved_callers, _) = delegate_deployment(&[CALLER_A, CALLER_B]).unwrap(); + + assert_eq!( + approved_callers, + [ + CALLER_A, + CALLER_B, + Address::ZERO, + Address::ZERO, + Address::ZERO + ] + ); + } + + #[test] + fn rejects_more_callers_than_the_delegate_supports() { + let err = + delegate_deployment(&[CALLER_A, CALLER_B, CALLER_C, CALLER_D, CALLER_E, CALLER_F]) + .unwrap_err(); + + assert!(err.to_string().contains("at most 5")); + } + + #[test] + fn rejects_zero_submission_account() { + let err = delegate_deployment(&[CALLER_A, Address::ZERO]).unwrap_err(); + + assert!(err.to_string().contains("zero address")); + } + + #[test] + fn rejects_duplicate_submission_accounts() { + let err = delegate_deployment(&[CALLER_A, CALLER_A]).unwrap_err(); + + assert!(err.to_string().contains("must be unique")); + } + + #[test] + fn detects_eip7702_delegation_target() { + let delegate = address!("0000000000000000000000000000000000000007"); + let other = address!("0000000000000000000000000000000000000008"); + let mut code = Vec::from(DELEGATION_PREFIX); + code.extend_from_slice(delegate.as_slice()); + + assert_eq!(DelegationStatus::from_code(&[]), DelegationStatus::Empty); + assert_eq!( + DelegationStatus::from_code(&[0x60, 0x00]), + DelegationStatus::OtherCode + ); + assert!(is_delegated_to(&code, delegate)); + assert!(!is_delegated_to(&code, other)); + } +} diff --git a/crates/driver/src/infra/solver/mod.rs b/crates/driver/src/infra/solver/mod.rs index 9c783fd41a..89e46af47c 100644 --- a/crates/driver/src/infra/solver/mod.rs +++ b/crates/driver/src/infra/solver/mod.rs @@ -41,17 +41,7 @@ use { }; pub mod dto; -// The old implementation still lives in eip7702.rs for the stacked delegate PR, -// but it depends on the removed forwarder binding. This minimal stub allows the -// CI to pass. -pub mod eip7702 { - use {super::Solver, crate::infra::blockchain::Ethereum, tracing::instrument}; - - #[instrument(name = "setup_eip7702", skip_all)] - pub async fn setup(_solvers: &[Solver], _eth: &Ethereum) -> anyhow::Result<()> { - Ok(()) - } -} +pub mod eip7702; // TODO At some point I should be checking that the names are unique, I don't // think I'm doing that. @@ -224,7 +214,7 @@ pub struct Config { pub haircut_bps: u32, /// Additional EOAs for parallel settlement submission via EIP-7702. /// When non-empty, these accounts submit txs to the solver EOA (which - /// delegates to a forwarder contract), enabling concurrent submissions. + /// delegates to Solver7702Delegate), enabling concurrent submissions. pub submission_accounts: Vec, /// Maximum number of solutions the driver proposes to the autopilot per /// auction. When 1 (the default), only the best-scoring solution is sent. @@ -233,6 +223,8 @@ pub struct Config { impl Solver { pub async fn try_new(config: Config, eth: Ethereum) -> Result { + config.validate()?; + let mut headers = reqwest::header::HeaderMap::new(); headers.insert( reqwest::header::CONTENT_TYPE, @@ -522,6 +514,134 @@ impl Solver { } } +impl Config { + fn validate(&self) -> Result<()> { + if self.submission_accounts.is_empty() { + anyhow::ensure!( + self.max_solutions_to_propose.get() == 1, + "solver '{}': max-solutions-to-propose > 1 requires non-empty submission-accounts \ + (EIP-7702 parallel submission must be enabled)", + self.name, + ); + return Ok(()); + } + + anyhow::ensure!( + !self + .submission_accounts + .iter() + .any(|account| matches!(account, Account::Address(_))), + "solver '{}': EIP-7702 submission accounts must be signers; address-only accounts \ + cannot sign delegated settlement transactions", + self.name, + ); + anyhow::ensure!( + !matches!(self.account, Account::Address(_)), + "solver '{}': main account must be a signer to set up EIP-7702 delegation when \ + submission accounts are configured", + self.name, + ); + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use { + super::*, + alloy::primitives::{address, b256}, + std::num::NonZeroUsize, + }; + + const SOLVER: Address = address!("0000000000000000000000000000000000000001"); + const SUBMITTER: Address = address!("0000000000000000000000000000000000000002"); + + fn signer() -> Account { + Account::PrivateKey( + PrivateKeySigner::from_bytes(&b256!( + "59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d" + )) + .unwrap(), + ) + } + + fn config() -> Config { + Config { + endpoint: "http://localhost/solve".parse().unwrap(), + name: Name("solver".to_string()), + slippage: Slippage { + relative: BigRational::from_integer(0.into()), + absolute: None, + }, + liquidity: Liquidity::Fetch, + account: Account::Address(SOLVER), + timeouts: Timeouts { + http_delay: chrono::Duration::seconds(1), + solving_share_of_deadline: 1.0.try_into().unwrap(), + }, + request_headers: Default::default(), + fee_handler: FeeHandler::Driver, + quote_using_limit_orders: false, + merge_solutions: SolutionMerging::Forbidden, + s3: None, + solver_native_token: ManageNativeToken { + wrap_address: false, + insert_unwraps: false, + }, + quote_tx_origin: None, + response_size_limit_max_bytes: 1024, + bad_order_detection: BadOrderDetection { + tokens_supported: Default::default(), + enable_simulation_strategy: false, + enable_metrics_strategy: false, + metrics_strategy_failure_ratio: 0.9, + metrics_strategy_required_measurements: 20, + metrics_strategy_log_only: true, + metrics_strategy_order_freeze_time: Duration::ZERO, + metrics_strategy_cache_gc_interval: Duration::ZERO, + metrics_strategy_cache_max_age: Duration::ZERO, + }, + settle_queue_size: 0, + flashloans_enabled: false, + fetch_liquidity_at_block: infra::liquidity::AtBlock::Latest, + haircut_bps: 0, + submission_accounts: vec![], + max_solutions_to_propose: NonZeroUsize::new(1).unwrap(), + } + } + + #[test] + fn rejects_multiple_proposed_solutions_without_submission_accounts() { + let mut config = config(); + config.max_solutions_to_propose = NonZeroUsize::new(2).unwrap(); + + let err = config.validate().unwrap_err(); + + assert!(err.to_string().contains("requires non-empty")); + } + + #[test] + fn rejects_read_only_submission_accounts() { + let mut config = config(); + config.submission_accounts = vec![Account::Address(SUBMITTER)]; + + let err = config.validate().unwrap_err(); + + assert!(err.to_string().contains("must be signers")); + } + + #[test] + fn rejects_read_only_main_account_with_submission_accounts() { + let mut config = config(); + config.submission_accounts = vec![signer()]; + + let err = config.validate().unwrap_err(); + + assert!(err.to_string().contains("main account must be a signer")); + } +} + /// Controls whether or not the driver is allowed to merge multiple solutions /// of the same solver to produce an overall better solution. #[derive(Debug, Clone, Copy)] diff --git a/crates/driver/src/run.rs b/crates/driver/src/run.rs index 83cfe9b00e..a8649382d8 100644 --- a/crates/driver/src/run.rs +++ b/crates/driver/src/run.rs @@ -85,8 +85,8 @@ async fn run_with(args: cli::Args, addr_sender: Option PrivateKeySigner { + // Well-known Anvil test key #1. Do not use as a production key. + PrivateKeySigner::from_bytes(&b256!( + "59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d" + )) + .unwrap() +} + /// Test that the best-scoring solution is picked when the /solve endpoint /// returns multiple valid solutions. #[tokio::test] @@ -36,7 +44,7 @@ async fn all_proposed() { .solvers(vec![ test_solver() .max_solutions_to_propose(5) - .submission_account(address!("0000000000000000000000000000000000000001")), + .submission_account(submission_account()), ]) .pool(ab_pool()) .pool(ad_pool()) @@ -74,7 +82,7 @@ async fn capped() { .solvers(vec![ test_solver() .max_solutions_to_propose(1) - .submission_account(address!("0000000000000000000000000000000000000001")), + .submission_account(submission_account()), ]) .pool(ab_pool()) .pool(ad_pool()) @@ -100,7 +108,7 @@ async fn only_proposes_valid_solutions() { .solvers(vec![ test_solver() .max_solutions_to_propose(5) - .submission_account(address!("0000000000000000000000000000000000000001")), + .submission_account(submission_account()), ]) .pool(ab_pool()) .order(order.clone()) diff --git a/crates/driver/src/tests/setup/driver.rs b/crates/driver/src/tests/setup/driver.rs index be91f6ccab..099a7f7037 100644 --- a/crates/driver/src/tests/setup/driver.rs +++ b/crates/driver/src/tests/setup/driver.rs @@ -344,7 +344,7 @@ async fn create_config_file( let accounts = solver .submission_accounts .iter() - .map(|a| format!("\"{a}\"")) + .map(|a| format!("\"{}\"", a.to_bytes())) .collect::>() .join(", "); writeln!(file, " submission-accounts = [{accounts}]").unwrap(); diff --git a/crates/driver/src/tests/setup/mod.rs b/crates/driver/src/tests/setup/mod.rs index 39a61b2b38..2180cddae6 100644 --- a/crates/driver/src/tests/setup/mod.rs +++ b/crates/driver/src/tests/setup/mod.rs @@ -364,7 +364,7 @@ pub struct Solver { /// Maximum number of solutions the driver proposes per auction. max_solutions_to_propose: usize, /// Additional submission accounts for EIP-7702 parallel settlement. - submission_accounts: Vec, + submission_accounts: Vec, } #[derive(Debug, Clone)] @@ -445,8 +445,8 @@ impl Solver { self } - pub fn submission_account(mut self, address: eth::Address) -> Self { - self.submission_accounts.push(address); + pub fn submission_account(mut self, signer: PrivateKeySigner) -> Self { + self.submission_accounts.push(signer); self } } diff --git a/crates/e2e/tests/e2e/main.rs b/crates/e2e/tests/e2e/main.rs index 30f0969882..9711aee7e6 100644 --- a/crates/e2e/tests/e2e/main.rs +++ b/crates/e2e/tests/e2e/main.rs @@ -28,7 +28,7 @@ mod liquidity_source_notification; mod malformed_requests; mod order_cancellation; mod order_simulation; -// mod parallel_settlement; +mod parallel_settlement; mod partial_fill; mod partially_fillable_balance; mod partially_fillable_pool; diff --git a/crates/e2e/tests/e2e/parallel_settlement.rs b/crates/e2e/tests/e2e/parallel_settlement.rs index d1b5d1e372..42d1a96d41 100644 --- a/crates/e2e/tests/e2e/parallel_settlement.rs +++ b/crates/e2e/tests/e2e/parallel_settlement.rs @@ -1,9 +1,12 @@ use { ::alloy::{ consensus::Transaction as _, - primitives::{Address, U256}, + primitives::{Address, Bytes, U256}, providers::{Provider, ext::TxPoolApi}, + sol_types::SolConstructor, }, + contracts::Solver7702Delegate::Solver7702Delegate, + driver::infra::solver::eip7702::{DELEGATION_PREFIX, MAX_APPROVED_CALLERS, delegate_address}, e2e::setup::{colocation, *}, ethrpc::{ Web3, @@ -24,10 +27,10 @@ use { /// Bypasses the autopilot (which settles one solution per auction) and sends /// two /solve + /settle requests directly to the driver. /// -/// Uses EIP-7702 delegation: a forwarder contract is deployed and the -/// solver EOA delegates its code to it. Two whitelisted submission accounts -/// send settlement txs through the solver EOA in parallel, each using their own -/// nonce. +/// Uses EIP-7702 delegation: the driver deploys Solver7702Delegate for the +/// submission accounts and delegates the solver EOA to it. Two submission +/// accounts send settlement txs through the solver EOA in parallel, each using +/// their own nonce. #[tokio::test] #[ignore] async fn local_node_parallel_settlement_submission() { @@ -94,6 +97,12 @@ async fn test_parallel_settlement_submission(web3: Web3) { }) .await .expect("driver did not start in time"); + assert_solver_delegates_to_expected_contract( + &web3, + solver.address(), + [submitter_a.address(), submitter_b.address()], + ) + .await; let valid_to = model::time::now_in_epoch_seconds() + 300; let make_buy_order = |buy_token: Address| { @@ -159,7 +168,7 @@ async fn test_parallel_settlement_submission(web3: Web3) { // Assert that TWO settlement txs are pending simultaneously: one direct // (solver EOA → settlement contract) and one delegated (submission EOA → - // solver EOA via EIP-7702 forwarding). The driver uses the direct slot + // solver EOA via EIP-7702 delegation). The driver uses the direct slot // when no settlement is in flight (cheaper), and falls back to 7702 for // concurrent submissions. let solver_address = solver.address(); @@ -219,6 +228,50 @@ async fn test_parallel_settlement_submission(web3: Web3) { } } +async fn assert_solver_delegates_to_expected_contract( + web3: &Web3, + solver: Address, + callers: [Address; 2], +) { + let delegate = solver_delegate_address(callers); + let delegate_code = web3.provider.get_code_at(delegate).await.unwrap(); + assert!( + !delegate_code.is_empty(), + "delegate contract was not deployed" + ); + + let solver_code = web3.provider.get_code_at(solver).await.unwrap(); + let mut expected_solver_code = Vec::from(DELEGATION_PREFIX); + expected_solver_code.extend_from_slice(delegate.as_slice()); + assert_eq!( + solver_code.as_ref(), + expected_solver_code, + "solver EOA does not delegate to expected Solver7702Delegate" + ); +} + +fn solver_delegate_address(callers: [Address; 2]) -> Address { + let mut approved_callers = [Address::ZERO; MAX_APPROVED_CALLERS]; + approved_callers[..callers.len()].copy_from_slice(&callers); + let init_code = Solver7702Delegate::BYTECODE + .iter() + .chain(&SolConstructor::abi_encode( + &Solver7702Delegate::constructorCall { + approvedCallers: approved_callers, + }, + )) + .copied() + .collect::(); + + let local = delegate_address(&callers).unwrap(); + assert_eq!( + local, + driver::infra::solver::eip7702::CREATE2_DEPLOYER + .create2_from_code(driver::infra::solver::eip7702::CREATE2_SALT, &init_code) + ); + local +} + /// Sends a /solve request to the driver for a single order and returns the /// solution_id from the response. async fn solve_order( diff --git a/playground/configs/driver.toml b/playground/configs/driver.toml index de231abfb9..955db85ebb 100644 --- a/playground/configs/driver.toml +++ b/playground/configs/driver.toml @@ -8,6 +8,11 @@ endpoint = "http://baseline" absolute-slippage = "40000000000000000" # Denominated in wei, optional relative-slippage = "0.1" # Percentage in the [0, 1] range account = "0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6" # Known test private key +# Known local dev private keys from the default Hardhat/Anvil test account set. +submission-accounts = [ + "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d", # Hardhat/Anvil account #1 + "0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a", # Hardhat/Anvil account #2 +] [submission] gas-price-cap = "1000000000000"