Skip to content

Commit 3a2b65c

Browse files
authored
Implement delegation contract handling in EIP-7702 executor and bundler client (#41)
* Implement delegation contract handling in EIP-7702 executor and bundler client - Added `TwGetDelegationContractResponse` struct to encapsulate the delegation contract address. - Introduced `tw_get_delegation_contract` method in `BundlerClient` to fetch the delegation contract. - Updated transaction signing methods in `DelegatedAccount` and `MinimalAccountTransaction` to accept delegation contract as a parameter. - Refactored integration tests to utilize the new delegation contract handling. - Created a `DelegationContractCache` for efficient retrieval of delegation contracts in the executor. These changes enhance the EIP-7702 framework by integrating delegation contract management, improving transaction processing and observability. * don't fail job on bundler request failure * add "pop_id" to lease token * don't release lock in case of lock lost error cases * retry if fail to fetch delegation target address * rename ambiguous field
1 parent bf2d156 commit 3a2b65c

File tree

14 files changed

+260
-76
lines changed

14 files changed

+260
-76
lines changed

core/src/rpc_clients/bundler.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,12 @@ pub enum TwGetTransactionHashStatus {
8888
Success,
8989
}
9090

91+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
92+
#[serde(rename_all = "camelCase")]
93+
pub struct TwGetDelegationContractResponse {
94+
pub delegation_contract: Address,
95+
}
96+
9197
impl BundlerClient {
9298
/// Create a new bundler client with the given transport
9399
pub fn new(transport: impl IntoBoxTransport) -> Self {
@@ -170,4 +176,9 @@ impl BundlerClient {
170176

171177
Ok(response)
172178
}
179+
180+
pub async fn tw_get_delegation_contract(&self) -> TransportResult<TwGetDelegationContractResponse> {
181+
let response: TwGetDelegationContractResponse = self.inner.request("tw_getDelegationContract", ()).await?;
182+
Ok(response)
183+
}
173184
}

eip7702-core/src/constants.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
use alloy::primitives::{Address, address};
22

3-
/// The minimal account implementation address used for EIP-7702 delegation
4-
pub const MINIMAL_ACCOUNT_IMPLEMENTATION_ADDRESS: Address =
5-
address!("0xD6999651Fc0964B9c6B444307a0ab20534a66560");
3+
// The minimal account implementation address used for EIP-7702 delegation
4+
// pub const MINIMAL_ACCOUNT_IMPLEMENTATION_ADDRESS: Address =
5+
// address!("0xD6999651Fc0964B9c6B444307a0ab20534a66560");
6+
// NOTE!: do not hardcode. If needed later, use tw_getDelegationContract
67

78
/// EIP-7702 delegation prefix bytes
89
pub const EIP_7702_DELEGATION_PREFIX: [u8; 3] = [0xef, 0x01, 0x00];

eip7702-core/src/delegated_account.rs

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,7 @@ use engine_core::{
1010
};
1111
use rand::Rng;
1212

13-
use crate::constants::{
14-
EIP_7702_DELEGATION_CODE_LENGTH, EIP_7702_DELEGATION_PREFIX,
15-
MINIMAL_ACCOUNT_IMPLEMENTATION_ADDRESS,
16-
};
13+
use crate::constants::{EIP_7702_DELEGATION_CODE_LENGTH, EIP_7702_DELEGATION_PREFIX};
1714

1815
/// Represents an EOA address that can have EIP-7702 delegation, associated with a specific chain
1916
#[derive(Clone, Debug)]
@@ -62,21 +59,26 @@ impl<C: Chain> DelegatedAccount<C> {
6259

6360
// Extract the target address from bytes 3-23 (20 bytes for address)
6461
// EIP-7702 format: 0xef0100 + 20 bytes address
65-
let target_bytes = &code[3..23];
66-
let target_address = Address::from_slice(target_bytes);
6762

68-
// Compare with the minimal account implementation address
69-
let is_delegated = target_address == MINIMAL_ACCOUNT_IMPLEMENTATION_ADDRESS;
63+
// NOTE!: skip the actual delegated target address check for now
64+
// extremely unlikely that an EOA being used with engine is delegated to a non-minimal account
65+
// Potential source for fringe edge cases, please verify delegated target address if debugging 7702 execution issues
7066

71-
tracing::debug!(
72-
eoa_address = ?self.eoa_address,
73-
target_address = ?target_address,
74-
minimal_account_address = ?MINIMAL_ACCOUNT_IMPLEMENTATION_ADDRESS,
75-
has_delegation = is_delegated,
76-
"EIP-7702 delegation check result"
77-
);
67+
// let target_bytes = &code[3..23];
68+
// let target_address = Address::from_slice(target_bytes);
69+
70+
// // Compare with the minimal account implementation address
71+
// let is_delegated = target_address == MINIMAL_ACCOUNT_IMPLEMENTATION_ADDRESS;
72+
73+
// tracing::debug!(
74+
// eoa_address = ?self.eoa_address,
75+
// target_address = ?target_address,
76+
// minimal_account_address = ?MINIMAL_ACCOUNT_IMPLEMENTATION_ADDRESS,
77+
// has_delegation = is_delegated,
78+
// "EIP-7702 delegation check result"
79+
// );
7880

79-
Ok(is_delegated)
81+
Ok(true)
8082
}
8183

8284
/// Get the EOA address
@@ -103,6 +105,7 @@ impl<C: Chain> DelegatedAccount<C> {
103105
&self,
104106
eoa_signer: &S,
105107
credentials: &SigningCredential,
108+
delegation_contract: Address,
106109
) -> Result<alloy::eips::eip7702::SignedAuthorization, EngineError> {
107110
let nonce = self.get_nonce().await?;
108111

@@ -115,7 +118,7 @@ impl<C: Chain> DelegatedAccount<C> {
115118
.sign_authorization(
116119
signing_options,
117120
self.chain.chain_id(),
118-
MINIMAL_ACCOUNT_IMPLEMENTATION_ADDRESS,
121+
delegation_contract,
119122
nonce,
120123
credentials,
121124
)

eip7702-core/src/transaction.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,12 +193,13 @@ impl<C: Chain> MinimalAccountTransaction<C> {
193193
mut self,
194194
signer: &S,
195195
credentials: &SigningCredential,
196+
delegation_contract: Address,
196197
) -> Result<Self, EngineError> {
197198
if self.account.is_minimal_account().await? {
198199
return Ok(self);
199200
}
200201

201-
let authorization = self.account.sign_authorization(signer, credentials).await?;
202+
let authorization = self.account.sign_authorization(signer, credentials, delegation_contract).await?;
202203
self.authorization = Some(authorization);
203204
Ok(self)
204205
}

eip7702-core/tests/integration_tests.rs

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ use engine_core::{
2727
transaction::InnerTransaction,
2828
};
2929
use engine_eip7702_core::{
30-
constants::MINIMAL_ACCOUNT_IMPLEMENTATION_ADDRESS,
3130
delegated_account::DelegatedAccount,
3231
transaction::{CallSpec, LimitType, SessionSpec, WrappedCalls},
3332
};
@@ -290,6 +289,7 @@ struct TestSetup {
290289
user_address: Address,
291290

292291
signer: MockEoaSigner,
292+
delegation_contract: Option<Address>,
293293
}
294294

295295
const ANVIL_PORT: u16 = 8545;
@@ -358,6 +358,7 @@ impl TestSetup {
358358
signer,
359359
mock_erc20_contract: contract,
360360
anvil_provider: provider,
361+
delegation_contract: None,
361362
})
362363
}
363364

@@ -376,22 +377,32 @@ impl TestSetup {
376377
Ok(())
377378
}
378379

379-
async fn fetch_and_set_bytecode(&self) -> Result<(), Box<dyn std::error::Error>> {
380+
async fn fetch_and_set_bytecode(&mut self) -> Result<(), Box<dyn std::error::Error>> {
380381
// Fetch bytecode from Base Sepolia
381382
let base_sepolia_url = "https://84532.rpc.thirdweb.com".parse()?;
382383
let base_sepolia_provider = ProviderBuilder::new().connect_http(base_sepolia_url);
383384

385+
let delegation_contract_response = self
386+
.chain
387+
.bundler_client()
388+
.tw_get_delegation_contract()
389+
.await?;
390+
391+
// Store the delegation contract address for later use
392+
self.delegation_contract = Some(delegation_contract_response.delegation_contract);
393+
384394
let bytecode = base_sepolia_provider
385-
.get_code_at(MINIMAL_ACCOUNT_IMPLEMENTATION_ADDRESS)
395+
.get_code_at(delegation_contract_response.delegation_contract)
386396
.await?;
387397
// Set bytecode on our Anvil chain
388398
let _: () = self
389399
.anvil_provider
390-
.anvil_set_code(MINIMAL_ACCOUNT_IMPLEMENTATION_ADDRESS, bytecode)
400+
.anvil_set_code(delegation_contract_response.delegation_contract, bytecode)
391401
.await?;
392402

393403
println!(
394-
"Set bytecode for minimal account implementation at {MINIMAL_ACCOUNT_IMPLEMENTATION_ADDRESS}"
404+
"Set bytecode for minimal account implementation at {:?}",
405+
delegation_contract_response.delegation_contract
395406
);
396407

397408
Ok(())
@@ -645,7 +656,7 @@ impl TestSetup {
645656
#[tokio::test]
646657
async fn test_eip7702_integration() -> Result<(), Box<dyn std::error::Error>> {
647658
// Set up test environment
648-
let setup = TestSetup::new().await?;
659+
let mut setup = TestSetup::new().await?;
649660

650661
// Step 1: Fetch and set bytecode from Base Sepolia
651662
setup.fetch_and_set_bytecode().await?;
@@ -683,7 +694,11 @@ async fn test_eip7702_integration() -> Result<(), Box<dyn std::error::Error>> {
683694
let developer_tx = developer_account
684695
.clone()
685696
.owner_transaction(&[mint_transaction])
686-
.add_authorization_if_needed(&setup.signer, &setup.developer_credentials)
697+
.add_authorization_if_needed(
698+
&setup.signer,
699+
&setup.developer_credentials,
700+
setup.delegation_contract.expect("Delegation contract should be set")
701+
)
687702
.await?;
688703

689704
let (wrapped_calls_json, signature) = developer_tx
@@ -723,7 +738,11 @@ async fn test_eip7702_integration() -> Result<(), Box<dyn std::error::Error>> {
723738
// Step 8: Delegate user account (session key granter)
724739
// User signs authorization but executor broadcasts it (user has no funds)
725740
let user_authorization = user_account
726-
.sign_authorization(&setup.signer, &setup.user_credentials)
741+
.sign_authorization(
742+
&setup.signer,
743+
&setup.user_credentials,
744+
setup.delegation_contract.expect("Delegation contract should be set")
745+
)
727746
.await?;
728747

729748
// Executor broadcasts the user's delegation transaction

executors/src/eip7702_executor/confirm.rs

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ use twmq::{
1818

1919
use crate::eip7702_executor::send::Eip7702Sender;
2020
use crate::{
21-
metrics::{record_transaction_queued_to_confirmed, current_timestamp_ms, calculate_duration_seconds_from_twmq},
21+
metrics::{
22+
calculate_duration_seconds_from_twmq, current_timestamp_ms,
23+
record_transaction_queued_to_confirmed,
24+
},
2225
transaction_registry::TransactionRegistry,
2326
webhook::{
2427
WebhookJobHandler,
@@ -194,10 +197,18 @@ where
194197
.bundler_client()
195198
.tw_get_transaction_hash(&job_data.bundler_transaction_id)
196199
.await
197-
.map_err(|e| Eip7702ConfirmationError::TransactionHashError {
198-
message: e.to_string(),
200+
.map_err(|e| {
201+
tracing::error!(
202+
bundler_transaction_id = job_data.bundler_transaction_id,
203+
sender_details = ?job_data.sender_details,
204+
error = ?e,
205+
"Failed to get transaction hash from bundler"
206+
);
207+
Eip7702ConfirmationError::TransactionHashError {
208+
message: e.to_string(),
209+
}
199210
})
200-
.map_err_fail()?;
211+
.map_err_nack(Some(Duration::from_secs(10)), RequeuePosition::Last)?;
201212

202213
let transaction_hash = match transaction_hash_res {
203214
TwGetTransactionHashResponse::Success { transaction_hash } => {
@@ -266,12 +277,17 @@ where
266277
"Transaction confirmed successfully"
267278
);
268279

269-
// Record metrics if original timestamp is available
270-
if let Some(original_timestamp) = job_data.original_queued_timestamp {
271-
let confirmed_timestamp = current_timestamp_ms();
272-
let queued_to_confirmed_duration = calculate_duration_seconds_from_twmq(original_timestamp, confirmed_timestamp);
273-
record_transaction_queued_to_confirmed("eip7702", job_data.chain_id, queued_to_confirmed_duration);
274-
}
280+
// Record metrics if original timestamp is available
281+
if let Some(original_timestamp) = job_data.original_queued_timestamp {
282+
let confirmed_timestamp = current_timestamp_ms();
283+
let queued_to_confirmed_duration =
284+
calculate_duration_seconds_from_twmq(original_timestamp, confirmed_timestamp);
285+
record_transaction_queued_to_confirmed(
286+
"eip7702",
287+
job_data.chain_id,
288+
queued_to_confirmed_duration,
289+
);
290+
}
275291

276292
Ok(Eip7702ConfirmationResult {
277293
transaction_id: job_data.transaction_id.clone(),
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
use std::{ops::Deref, sync::Arc};
2+
3+
use alloy::primitives::Address;
4+
use engine_core::{
5+
chain::Chain,
6+
error::{AlloyRpcErrorToEngineError, EngineError},
7+
rpc_clients::TwGetDelegationContractResponse,
8+
};
9+
use moka::future::Cache;
10+
11+
/// Cache key for delegation contract - uses chain_id as the key since each chain has one delegation contract
12+
#[derive(Hash, Eq, PartialEq, Clone, Debug)]
13+
pub struct DelegationContractCacheKey {
14+
chain_id: u64,
15+
}
16+
17+
/// Cache for delegation contract addresses to avoid repeated RPC calls
18+
#[derive(Clone)]
19+
pub struct DelegationContractCache {
20+
pub inner: moka::future::Cache<DelegationContractCacheKey, Address>,
21+
}
22+
23+
impl DelegationContractCache {
24+
/// Create a new delegation contract cache with the provided moka cache
25+
pub fn new(cache: Cache<DelegationContractCacheKey, Address>) -> Self {
26+
Self { inner: cache }
27+
}
28+
29+
/// Get the delegation contract address for a chain, fetching it if not cached
30+
pub async fn get_delegation_contract<C: Chain>(
31+
&self,
32+
chain: &C,
33+
) -> Result<Address, EngineError> {
34+
let cache_key = DelegationContractCacheKey {
35+
chain_id: chain.chain_id(),
36+
};
37+
38+
// Use try_get_with for SWR behavior - this will fetch if not cached or expired
39+
let result = self
40+
.inner
41+
.try_get_with(cache_key, async {
42+
tracing::debug!(
43+
chain_id = chain.chain_id(),
44+
"Fetching delegation contract from bundler"
45+
);
46+
47+
let TwGetDelegationContractResponse {
48+
delegation_contract,
49+
} = chain
50+
.bundler_client()
51+
.tw_get_delegation_contract()
52+
.await
53+
.map_err(|e| e.to_engine_bundler_error(chain))?;
54+
55+
tracing::debug!(
56+
chain_id = chain.chain_id(),
57+
delegation_contract = ?delegation_contract,
58+
"Successfully fetched and cached delegation contract"
59+
);
60+
61+
Ok(delegation_contract)
62+
})
63+
.await
64+
.map_err(|e: Arc<EngineError>| e.deref().clone())?;
65+
66+
Ok(result)
67+
}
68+
}

executors/src/eip7702_executor/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
pub mod send;
2-
pub mod confirm;
2+
pub mod confirm;
3+
pub mod delegation_cache;

0 commit comments

Comments
 (0)