From 347b5d5735ee1ead052d3ccb792c079eee3e4293 Mon Sep 17 00:00:00 2001 From: Joaquim Verges Date: Thu, 2 Oct 2025 08:50:12 +1300 Subject: [PATCH] Add delegation contract verification for EIP-7702 accounts --- eip7702-core/src/constants.rs | 7 ----- eip7702-core/src/delegated_account.rs | 39 +++++++++++++----------- eip7702-core/src/transaction.rs | 2 +- eip7702-core/tests/integration_tests.rs | 11 ++++--- executors/src/eoa/authorization_cache.rs | 5 ++- executors/src/eoa/worker/mod.rs | 2 +- server/src/execution_router/mod.rs | 2 +- 7 files changed, 34 insertions(+), 34 deletions(-) diff --git a/eip7702-core/src/constants.rs b/eip7702-core/src/constants.rs index 06405b3..a8e7a56 100644 --- a/eip7702-core/src/constants.rs +++ b/eip7702-core/src/constants.rs @@ -1,10 +1,3 @@ -use alloy::primitives::{Address, address}; - -// The minimal account implementation address used for EIP-7702 delegation -// pub const MINIMAL_ACCOUNT_IMPLEMENTATION_ADDRESS: Address = -// address!("0xD6999651Fc0964B9c6B444307a0ab20534a66560"); -// NOTE!: do not hardcode. If needed later, use tw_getDelegationContract - /// EIP-7702 delegation prefix bytes pub const EIP_7702_DELEGATION_PREFIX: [u8; 3] = [0xef, 0x01, 0x00]; diff --git a/eip7702-core/src/delegated_account.rs b/eip7702-core/src/delegated_account.rs index d47eb8a..6b6aa75 100644 --- a/eip7702-core/src/delegated_account.rs +++ b/eip7702-core/src/delegated_account.rs @@ -28,7 +28,7 @@ impl DelegatedAccount { } /// Check if the EOA has EIP-7702 delegation to the minimal account implementation - pub async fn is_minimal_account(&self) -> Result { + pub async fn is_minimal_account(&self, delegation_contract: Option
) -> Result { // Get the bytecode at the EOA address using eth_getCode let code = self .chain @@ -60,25 +60,28 @@ impl DelegatedAccount { // Extract the target address from bytes 3-23 (20 bytes for address) // EIP-7702 format: 0xef0100 + 20 bytes address - // NOTE!: skip the actual delegated target address check for now - // extremely unlikely that an EOA being used with engine is delegated to a non-minimal account - // Potential source for fringe edge cases, please verify delegated target address if debugging 7702 execution issues - - // let target_bytes = &code[3..23]; - // let target_address = Address::from_slice(target_bytes); - - // // Compare with the minimal account implementation address - // let is_delegated = target_address == MINIMAL_ACCOUNT_IMPLEMENTATION_ADDRESS; + let target_bytes = &code[3..23]; + let target_address = Address::from_slice(target_bytes); + + // Compare with the minimal account implementation address + let is_delegated = match delegation_contract { + Some(delegation_contract) => { + target_address == delegation_contract + } + None => { + true + } + }; - // tracing::debug!( - // eoa_address = ?self.eoa_address, - // target_address = ?target_address, - // minimal_account_address = ?MINIMAL_ACCOUNT_IMPLEMENTATION_ADDRESS, - // has_delegation = is_delegated, - // "EIP-7702 delegation check result" - // ); + tracing::debug!( + eoa_address = ?self.eoa_address, + target_address = ?target_address, + minimal_account_address = ?delegation_contract, + has_delegation = is_delegated, + "EIP-7702 delegation check result" + ); - Ok(true) + Ok(is_delegated) } /// Get the EOA address diff --git a/eip7702-core/src/transaction.rs b/eip7702-core/src/transaction.rs index 0798f2d..8676d63 100644 --- a/eip7702-core/src/transaction.rs +++ b/eip7702-core/src/transaction.rs @@ -195,7 +195,7 @@ impl MinimalAccountTransaction { credentials: &SigningCredential, delegation_contract: Address, ) -> Result { - if self.account.is_minimal_account().await? { + if self.account.is_minimal_account(Some(delegation_contract)).await? { return Ok(self); } diff --git a/eip7702-core/tests/integration_tests.rs b/eip7702-core/tests/integration_tests.rs index 1c59fa5..82f85ca 100644 --- a/eip7702-core/tests/integration_tests.rs +++ b/eip7702-core/tests/integration_tests.rs @@ -657,6 +657,7 @@ impl TestSetup { async fn test_eip7702_integration() -> Result<(), Box> { // Set up test environment let mut setup = TestSetup::new().await?; + let delegation_contract = setup.delegation_contract.expect("Delegation contract should be set"); // Step 1: Fetch and set bytecode from Base Sepolia setup.fetch_and_set_bytecode().await?; @@ -666,11 +667,11 @@ async fn test_eip7702_integration() -> Result<(), Box> { // Step 3: Test is_minimal_account - all should be false initially assert!( - !developer_account.is_minimal_account().await?, + !developer_account.is_minimal_account(Some(delegation_contract)).await?, "Developer should not be minimal account initially" ); assert!( - !user_account.is_minimal_account().await?, + !user_account.is_minimal_account(Some(delegation_contract)).await?, "User should not be minimal account initially" ); println!("✓ All accounts are not minimal accounts initially"); @@ -731,7 +732,7 @@ async fn test_eip7702_integration() -> Result<(), Box> { ); assert!( - developer_account.is_minimal_account().await?, + developer_account.is_minimal_account(Some(delegation_contract)).await?, "Developer should be minimal account after minting" ); @@ -751,14 +752,14 @@ async fn test_eip7702_integration() -> Result<(), Box> { .await?; assert!( - user_account.is_minimal_account().await?, + user_account.is_minimal_account(Some(delegation_contract)).await?, "User (session key granter) should be minimal account after delegation" ); println!("✓ User (session key granter) is now a minimal account (delegated by executor)"); // Step 9: Developer is already delegated via add_authorization_if_needed in owner_transaction assert!( - developer_account.is_minimal_account().await?, + developer_account.is_minimal_account(Some(delegation_contract)).await?, "Developer (session key grantee) should already be minimal account from earlier delegation" ); println!("✓ Developer (session key grantee) was already delegated in previous step"); diff --git a/executors/src/eoa/authorization_cache.rs b/executors/src/eoa/authorization_cache.rs index 6a2c5d5..87ecbe0 100644 --- a/executors/src/eoa/authorization_cache.rs +++ b/executors/src/eoa/authorization_cache.rs @@ -9,6 +9,7 @@ use moka::future::Cache; pub struct AuthorizationCacheKey { eoa_address: Address, chain_id: u64, + delegation_contract: Address, } #[derive(Clone)] @@ -24,14 +25,16 @@ impl EoaAuthorizationCache { pub async fn is_minimal_account( &self, delegated_account: &DelegatedAccount, + delegation_contract: Option
, ) -> Result { self.inner .try_get_with( AuthorizationCacheKey { eoa_address: delegated_account.eoa_address, chain_id: delegated_account.chain.chain_id(), + delegation_contract: delegation_contract.unwrap_or(Address::ZERO), }, - delegated_account.is_minimal_account(), + delegated_account.is_minimal_account(delegation_contract), ) .await .map_err(|e| e.deref().clone()) diff --git a/executors/src/eoa/worker/mod.rs b/executors/src/eoa/worker/mod.rs index 176df7a..364c2ee 100644 --- a/executors/src/eoa/worker/mod.rs +++ b/executors/src/eoa/worker/mod.rs @@ -172,7 +172,7 @@ where // if there's an error checking 7702 delegation here, we'll just assume it's not a minimal account for the purposes of max in flight let is_minimal_account = self .authorization_cache - .is_minimal_account(&delegated_account) + .is_minimal_account(&delegated_account, None) .await .inspect_err(|e| { tracing::error!(error = ?e, "Error checking 7702 delegation"); diff --git a/server/src/execution_router/mod.rs b/server/src/execution_router/mod.rs index 9f80ba1..7e070bc 100644 --- a/server/src/execution_router/mod.rs +++ b/server/src/execution_router/mod.rs @@ -405,7 +405,7 @@ impl ExecutionRouter { let delegated_account = DelegatedAccount::new(eoa_execution_options.from, chain); let is_minimal_account = self .authorization_cache - .is_minimal_account(&delegated_account) + .is_minimal_account(&delegated_account, None) .await .map_err(|e| EngineError::InternalError { message: format!("Failed to check 7702 delegation: {e:?}"),