Skip to content

Commit 4b83973

Browse files
committed
feat(cast): validate-auth
There is no way to determine the validity of the authorizations just looking at the RPC responses with `cast tx` or `cast receipt`. So, we need to build a new command that manually validates the authorizations with the logic laid out in EIP-7702.
1 parent d9f47a7 commit 4b83973

File tree

3 files changed

+494
-3
lines changed

3 files changed

+494
-3
lines changed

crates/cast/src/args.rs

Lines changed: 148 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,20 @@ use crate::{
44
opts::{Cast as CastArgs, CastSubcommand, ToBaseArgs},
55
traces::identifier::SignaturesIdentifier,
66
};
7-
use alloy_consensus::transaction::{Recovered, SignerRecoverable};
7+
use alloy_consensus::{
8+
Transaction, TxEnvelope,
9+
transaction::{Recovered, SignerRecoverable},
10+
};
811
use alloy_dyn_abi::{DynSolValue, ErrorExt, EventExt};
912
use alloy_eips::eip7702::SignedAuthorization;
1013
use alloy_ens::{ProviderEnsExt, namehash};
14+
use alloy_network::{AnyTxEnvelope, TransactionResponse};
1115
use alloy_primitives::{Address, B256, eip191_hash_message, hex, keccak256};
1216
use alloy_provider::Provider;
1317
use alloy_rpc_types::{BlockId, BlockNumberOrTag::Latest};
1418
use clap::{CommandFactory, Parser};
1519
use clap_complete::generate;
16-
use eyre::Result;
20+
use eyre::{Result, WrapErr};
1721
use foundry_cli::{utils, utils::LoadConfig};
1822
use foundry_common::{
1923
abi::{get_error, get_event},
@@ -26,7 +30,7 @@ use foundry_common::{
2630
},
2731
shell, stdin,
2832
};
29-
use std::time::Instant;
33+
use std::{collections::HashMap, time::Instant};
3034

3135
/// Run the `cast` command-line interface.
3236
pub fn run() -> Result<()> {
@@ -751,6 +755,147 @@ pub async fn run_command(args: CastArgs) -> Result<()> {
751755
let auth: SignedAuthorization = serde_json::from_str(&auth)?;
752756
sh_println!("{}", auth.recover_authority()?)?;
753757
}
758+
CastSubcommand::ValidateAuth { tx_hash, rpc } => {
759+
let config = rpc.load_config()?;
760+
let provider = utils::get_provider(&config)?;
761+
762+
let tx_hash = tx_hash.parse::<B256>().wrap_err("invalid tx hash")?;
763+
let tx = provider
764+
.get_transaction_by_hash(tx_hash)
765+
.await?
766+
.ok_or_else(|| eyre::eyre!("tx not found: {:?}", tx_hash))?;
767+
768+
// Get block info for nonce calculation
769+
let block_number =
770+
tx.block_number.ok_or_else(|| eyre::eyre!("transaction is not yet mined"))?;
771+
let tx_index = tx
772+
.transaction_index
773+
.ok_or_else(|| eyre::eyre!("transaction index not available"))?;
774+
775+
// Fetch the block to get all transactions up to this one
776+
let block = provider
777+
.get_block_by_number(block_number.into())
778+
.full()
779+
.await?
780+
.ok_or_else(|| eyre::eyre!("block not found: {}", block_number))?;
781+
782+
// Build a map of address -> running nonce from txs in this block up to and including
783+
// our tx
784+
let mut running_nonces: HashMap<Address, u64> = HashMap::new();
785+
for block_tx in block.transactions.txns().take((tx_index + 1) as usize) {
786+
let from = block_tx.from();
787+
let nonce = block_tx.nonce();
788+
// Track the next expected nonce (current nonce + 1)
789+
running_nonces.insert(from, nonce + 1);
790+
}
791+
792+
let chain_id = provider.get_chain_id().await?;
793+
794+
// Extract authorization list from EIP-7702 transaction
795+
let auth_list = match &*tx.inner.inner {
796+
AnyTxEnvelope::Ethereum(TxEnvelope::Eip7702(signed_tx)) => {
797+
signed_tx.tx().authorization_list.clone()
798+
}
799+
_ => {
800+
eyre::bail!("transaction is not an EIP-7702 transaction");
801+
}
802+
};
803+
804+
sh_println!("Transaction: {}", tx_hash)?;
805+
sh_println!("Block: {} (tx index: {})", block_number, tx_index)?;
806+
sh_println!()?;
807+
808+
if auth_list.is_empty() {
809+
sh_println!("Authorization list is empty")?;
810+
} else {
811+
for (i, auth) in auth_list.iter().enumerate() {
812+
let valid_chain = auth.chain_id == chain_id || auth.chain_id == 0;
813+
sh_println!("Authorization #{}", i)?;
814+
sh_println!(" Decoded:")?;
815+
sh_println!(" Chain ID: {}", auth.chain_id,)?;
816+
sh_println!(" Address: {}", auth.address)?;
817+
sh_println!(" Nonce: {}", auth.nonce)?;
818+
sh_println!(" r: {}", auth.r())?;
819+
sh_println!(" s: {}", auth.s())?;
820+
sh_println!(" v: {}", auth.y_parity())?;
821+
822+
match auth.recover_authority() {
823+
Ok(authority) => {
824+
sh_println!(" Recovered Authority: {}", authority)?;
825+
826+
sh_println!(" Validation Status:")?;
827+
sh_println!(
828+
" Chain: {}",
829+
if valid_chain {
830+
"VALID".to_string()
831+
} else {
832+
format!("INVALID (expected: 0 or {chain_id})")
833+
}
834+
)?;
835+
836+
// Get the expected nonce at time of tx execution
837+
let expected_nonce =
838+
if let Some(&nonce) = running_nonces.get(&authority) {
839+
nonce
840+
} else {
841+
// Fetch nonce at block - 1 (state before this block)
842+
let prev_block = BlockId::number(block_number - 1);
843+
provider
844+
.get_transaction_count(authority)
845+
.block_id(prev_block)
846+
.await?
847+
};
848+
849+
let valid_nonce = auth.nonce == expected_nonce;
850+
if valid_nonce {
851+
sh_println!(" Nonce: VALID")?;
852+
} else {
853+
sh_println!(
854+
" Nonce: INVALID (expected: {}, got: {})",
855+
expected_nonce,
856+
auth.nonce
857+
)?;
858+
}
859+
860+
// If authorization was valid, update running nonce for subsequent auths
861+
if valid_chain && valid_nonce {
862+
running_nonces.insert(authority, expected_nonce + 1);
863+
}
864+
865+
// Check if the authority's code was set to the delegated address
866+
let code = provider.get_code_at(authority).await?;
867+
if code.is_empty() {
868+
sh_println!(" Code Status: No delegation (account has no code)")?;
869+
} else if code.len() == 23 && code[0..3] == [0xef, 0x01, 0x00] {
870+
// EIP-7702 delegation designator: 0xef0100 followed by 20-byte
871+
// address
872+
let delegated_to = Address::from_slice(&code[3..23]);
873+
if delegated_to == auth.address {
874+
sh_println!(
875+
" Code Status: ACTIVE (delegated to {})",
876+
delegated_to
877+
)?;
878+
} else {
879+
sh_println!(
880+
" Code Status: SUPERSEDED (currently delegated to {})",
881+
delegated_to
882+
)?;
883+
}
884+
} else {
885+
sh_println!(
886+
" Code Status: Account has contract code (not a delegation)"
887+
)?;
888+
}
889+
}
890+
Err(e) => {
891+
sh_println!(" Authority: UNKNOWN")?;
892+
sh_println!(" Signature: INVALID ({})", e)?;
893+
}
894+
}
895+
sh_println!()?;
896+
}
897+
}
898+
}
754899
CastSubcommand::TxPool { command } => command.run().await?,
755900
CastSubcommand::Erc20Token { command } => command.run().await?,
756901
CastSubcommand::DAEstimate(cmd) => {

crates/cast/src/opts.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1119,6 +1119,16 @@ pub enum CastSubcommand {
11191119
#[command(visible_aliases = &["decode-auth"])]
11201120
RecoverAuthority { auth: String },
11211121

1122+
/// Validate EIP-7702 authorizations in a transaction and print validity status.
1123+
#[command(name = "validate-auth", visible_aliases = &["va", "validate-auths"])]
1124+
ValidateAuth {
1125+
/// Transaction hash.
1126+
tx_hash: String,
1127+
1128+
#[command(flatten)]
1129+
rpc: RpcOpts,
1130+
},
1131+
11221132
/// Extracts function selectors and arguments from bytecode
11231133
#[command(visible_alias = "sel")]
11241134
Selectors {

0 commit comments

Comments
 (0)