From 30e34f909d8e8926433e6a94b8166fdc475a7090 Mon Sep 17 00:00:00 2001 From: 0xFirekeeper <0xFirekeeper@gmail.com> Date: Sun, 29 Mar 2026 16:51:24 +0700 Subject: [PATCH 1/2] Strip redundant EIP-7702 authorizations Add logic to filter out signed EIP-7702 authorizations that are already delegated to the target contract. Introduces a helper (filter_already_delegated_authorizations) which queries the account code, detects an existing delegation via EIP-7702 prefix/length, extracts the delegation target, and removes any matching authorizations from the list. This prevents RPC-level rejections on strict chains (e.g. Etherlink) caused by sending stale/redundant authorizations. The build path now uses the filtered list and only attaches an authorization_list if non-empty; provider lookup failures are logged and result in leaving the list unchanged. --- executors/src/eoa/worker/transaction.rs | 67 ++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 2 deletions(-) diff --git a/executors/src/eoa/worker/transaction.rs b/executors/src/eoa/worker/transaction.rs index 6e45422..479684d 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,62 @@ 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) => { + if code.len() >= EIP_7702_DELEGATION_CODE_LENGTH + && code.starts_with(&EIP_7702_DELEGATION_PREFIX) + { + let delegated_to = Address::from_slice(&code[3..23]); + + // 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 +359,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); From 3c2d4cbb7f3613fb68e582218a4eb029a6eb22c7 Mon Sep 17 00:00:00 2001 From: 0xFirekeeper <0xFirekeeper@gmail.com> Date: Sun, 29 Mar 2026 16:59:31 +0700 Subject: [PATCH 2/2] nit --- executors/src/eoa/worker/transaction.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/executors/src/eoa/worker/transaction.rs b/executors/src/eoa/worker/transaction.rs index 479684d..765d160 100644 --- a/executors/src/eoa/worker/transaction.rs +++ b/executors/src/eoa/worker/transaction.rs @@ -268,10 +268,11 @@ impl EoaExecutorWorker { 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[3..23]); + 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