Skip to content

Commit 1f4db8d

Browse files
authored
Strip redundant EIP-7702 authorizations (#104)
* 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. * nit
1 parent f6e6c55 commit 1f4db8d

1 file changed

Lines changed: 66 additions & 2 deletions

File tree

executors/src/eoa/worker/transaction.rs

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ use alloy::{
1212
consensus::{
1313
SignableTransaction, Signed, TxEip4844Variant, TxEip4844WithSidecar, TypedTransaction,
1414
},
15+
eips::eip7702::SignedAuthorization,
1516
network::{TransactionBuilder, TransactionBuilder7702},
16-
primitives::{Bytes, U256},
17+
primitives::{Address, Bytes, U256},
1718
providers::Provider,
1819
rpc::types::TransactionRequest as AlloyTransactionRequest,
1920
signers::Signature,
@@ -26,6 +27,7 @@ use engine_core::{
2627
signer::{AccountSigner, EoaSigningOptions},
2728
transaction::TransactionTypeData,
2829
};
30+
use engine_eip7702_core::constants::{EIP_7702_DELEGATION_CODE_LENGTH, EIP_7702_DELEGATION_PREFIX};
2931

3032
use crate::eoa::{
3133
EoaTransactionRequest,
@@ -249,6 +251,63 @@ impl<C: Chain> EoaExecutorWorker<C> {
249251
}
250252
}
251253

254+
/// Filter out authorization list entries where the authority is already delegated
255+
/// to the target contract. This prevents Etherlink (and potentially other strict chains)
256+
/// from rejecting type-4 transactions that include redundant/stale authorizations.
257+
///
258+
/// On most chains, a stale authorization is simply skipped. On Etherlink, the entire
259+
/// transaction is rejected at the RPC level, causing it to be silently dropped.
260+
async fn filter_already_delegated_authorizations(
261+
&self,
262+
authorization_list: &[SignedAuthorization],
263+
to: Option<Address>,
264+
) -> Vec<SignedAuthorization> {
265+
// If we have a `to` address, check if it's already delegated to any of the
266+
// authorization targets. In the 7702 relayer flow, `to` is the user's smart
267+
// account and the authorization targets the delegation contract.
268+
if let Some(account_address) = to {
269+
match self.chain.provider().get_code_at(account_address).await {
270+
Ok(code) => {
271+
let prefix_len = EIP_7702_DELEGATION_PREFIX.len();
272+
if code.len() >= EIP_7702_DELEGATION_CODE_LENGTH
273+
&& code.starts_with(&EIP_7702_DELEGATION_PREFIX)
274+
{
275+
let delegated_to = Address::from_slice(&code[prefix_len..prefix_len + 20]);
276+
277+
// Filter out any auth entries whose target matches the existing delegation
278+
let filtered: Vec<_> = authorization_list
279+
.iter()
280+
.filter(|auth| {
281+
if auth.address == delegated_to {
282+
tracing::info!(
283+
account = ?account_address,
284+
delegation_target = ?delegated_to,
285+
"Stripping redundant authorization - account already delegated to target"
286+
);
287+
false
288+
} else {
289+
true
290+
}
291+
})
292+
.cloned()
293+
.collect();
294+
295+
return filtered;
296+
}
297+
}
298+
Err(e) => {
299+
tracing::warn!(
300+
account = ?account_address,
301+
error = ?e,
302+
"Failed to check delegation status, keeping all authorizations"
303+
);
304+
}
305+
}
306+
}
307+
308+
authorization_list.to_vec()
309+
}
310+
252311
pub async fn build_typed_transaction(
253312
&self,
254313
request: &EoaTransactionRequest,
@@ -301,7 +360,12 @@ impl<C: Chain> EoaExecutorWorker<C> {
301360
TransactionTypeData::Eip7702(data) => {
302361
let mut req = tx_request;
303362
if let Some(authorization_list) = &data.authorization_list {
304-
req = req.with_authorization_list(authorization_list.clone());
363+
let filtered = self
364+
.filter_already_delegated_authorizations(authorization_list, request.to)
365+
.await;
366+
if !filtered.is_empty() {
367+
req = req.with_authorization_list(filtered);
368+
}
305369
}
306370
if let Some(max_fee) = data.max_fee_per_gas {
307371
req = req.with_max_fee_per_gas(max_fee);

0 commit comments

Comments
 (0)