Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 66 additions & 2 deletions executors/src/eoa/worker/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -249,6 +251,63 @@ impl<C: Chain> EoaExecutorWorker<C> {
}
}

/// 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<Address>,
) -> Vec<SignedAuthorization> {
Comment on lines +260 to +264

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Check delegation on the authority (from) account, not to.

Line 269 checks bytecode at to, but EIP-7702 delegation bytecode is on the delegating EOA. This can miss redundant-authorizations when to != from, so strict-chain rejections can still happen. This also differs from eip7702-core/src/delegated_account.rs:30-73, which checks self.eoa_address.

Suggested fix
-    async fn filter_already_delegated_authorizations(
-        &self,
-        authorization_list: &[SignedAuthorization],
-        to: Option<Address>,
-    ) -> Vec<SignedAuthorization> {
-        // 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 {
+    async fn filter_already_delegated_authorizations(
+        &self,
+        authorization_list: &[SignedAuthorization],
+        authority: Address,
+    ) -> Vec<SignedAuthorization> {
+        let account_address = authority;
+        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]);
@@
-                }
-                Err(e) => {
+                }
+                Err(e) => {
                     tracing::warn!(
                         account = ?account_address,
                         error = ?e,
                         "Failed to check delegation status, keeping all authorizations"
                     );
                 }
-            }
         }
-
         authorization_list.to_vec()
     }
-                        let filtered = self
-                            .filter_already_delegated_authorizations(authorization_list, request.to)
+                        let filtered = self
+                            .filter_already_delegated_authorizations(authorization_list, request.from)
                             .await;

Also applies to: 268-270, 363-364

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@executors/src/eoa/worker/transaction.rs` around lines 260 - 264, The function
filter_already_delegated_authorizations currently checks EIP-7702 delegation
bytecode at the `to` address, but the delegation bytecode lives on the
delegating/authority account (the authorization's `from`/authorizer), so update
the checks in filter_already_delegated_authorizations (and the related checks at
the other occurrences noted) to inspect the bytecode/account state of the
authorization's authorizer (the `SignedAuthorization`'s from/authorizer address)
instead of `to`; specifically, replace uses of `to` when querying bytecode or
delegation status with the SignedAuthorization's source address (e.g.,
authorization.from or authorization.authorizer) so redundant-authorizations are
detected correctly even when `to != from`.

// 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,
Expand Down Expand Up @@ -301,7 +360,12 @@ impl<C: Chain> EoaExecutorWorker<C> {
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);
Expand Down