diff --git a/executors/src/eoa/worker/transaction.rs b/executors/src/eoa/worker/transaction.rs index 6e45422..765d160 100644 --- a/executors/src/eoa/worker/transaction.rs +++ b/executors/src/eoa/worker/transaction.rs @@ -12,8 +12,9 @@ use alloy::{ consensus::{ SignableTransaction, Signed, TxEip4844Variant, TxEip4844WithSidecar, TypedTransaction, }, + eips::eip7702::SignedAuthorization, network::{TransactionBuilder, TransactionBuilder7702}, - primitives::{Bytes, U256}, + primitives::{Address, Bytes, U256}, providers::Provider, rpc::types::TransactionRequest as AlloyTransactionRequest, signers::Signature, @@ -26,6 +27,7 @@ use engine_core::{ signer::{AccountSigner, EoaSigningOptions}, transaction::TransactionTypeData, }; +use engine_eip7702_core::constants::{EIP_7702_DELEGATION_CODE_LENGTH, EIP_7702_DELEGATION_PREFIX}; use crate::eoa::{ EoaTransactionRequest, @@ -249,6 +251,63 @@ impl EoaExecutorWorker { } } + /// Filter out authorization list entries where the authority is already delegated + /// to the target contract. This prevents Etherlink (and potentially other strict chains) + /// from rejecting type-4 transactions that include redundant/stale authorizations. + /// + /// On most chains, a stale authorization is simply skipped. On Etherlink, the entire + /// transaction is rejected at the RPC level, causing it to be silently dropped. + async fn filter_already_delegated_authorizations( + &self, + authorization_list: &[SignedAuthorization], + to: Option
, + ) -> Vec { + // If we have a `to` address, check if it's already delegated to any of the + // authorization targets. In the 7702 relayer flow, `to` is the user's smart + // account and the authorization targets the delegation contract. + if let Some(account_address) = to { + match self.chain.provider().get_code_at(account_address).await { + Ok(code) => { + let prefix_len = EIP_7702_DELEGATION_PREFIX.len(); + if code.len() >= EIP_7702_DELEGATION_CODE_LENGTH + && code.starts_with(&EIP_7702_DELEGATION_PREFIX) + { + let delegated_to = Address::from_slice(&code[prefix_len..prefix_len + 20]); + + // Filter out any auth entries whose target matches the existing delegation + let filtered: Vec<_> = authorization_list + .iter() + .filter(|auth| { + if auth.address == delegated_to { + tracing::info!( + account = ?account_address, + delegation_target = ?delegated_to, + "Stripping redundant authorization - account already delegated to target" + ); + false + } else { + true + } + }) + .cloned() + .collect(); + + return filtered; + } + } + Err(e) => { + tracing::warn!( + account = ?account_address, + error = ?e, + "Failed to check delegation status, keeping all authorizations" + ); + } + } + } + + authorization_list.to_vec() + } + pub async fn build_typed_transaction( &self, request: &EoaTransactionRequest, @@ -301,7 +360,12 @@ impl EoaExecutorWorker { TransactionTypeData::Eip7702(data) => { let mut req = tx_request; if let Some(authorization_list) = &data.authorization_list { - req = req.with_authorization_list(authorization_list.clone()); + let filtered = self + .filter_already_delegated_authorizations(authorization_list, request.to) + .await; + if !filtered.is_empty() { + req = req.with_authorization_list(filtered); + } } if let Some(max_fee) = data.max_fee_per_gas { req = req.with_max_fee_per_gas(max_fee);