Skip to content

Commit e735b88

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 e735b88

File tree

5 files changed

+503
-4
lines changed

5 files changed

+503
-4
lines changed

crates/cast/src/args.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -751,6 +751,9 @@ pub async fn run_command(args: CastArgs) -> Result<()> {
751751
let auth: SignedAuthorization = serde_json::from_str(&auth)?;
752752
sh_println!("{}", auth.recover_authority()?)?;
753753
}
754+
CastSubcommand::ValidateAuth(cmd) => {
755+
cmd.run().await?;
756+
}
754757
CastSubcommand::TxPool { command } => command.run().await?,
755758
CastSubcommand::Erc20Token { command } => command.run().await?,
756759
CastSubcommand::DAEstimate(cmd) => {

crates/cast/src/cmd/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,5 @@ pub mod run;
2525
pub mod send;
2626
pub mod storage;
2727
pub mod txpool;
28+
pub mod validate_auth;
2829
pub mod wallet;
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
use std::collections::HashMap;
2+
3+
use alloy_consensus::{Transaction, TxEnvelope};
4+
use alloy_eips::BlockId;
5+
use alloy_network::{AnyTxEnvelope, TransactionResponse};
6+
use alloy_primitives::{Address, B256};
7+
use alloy_provider::Provider;
8+
use foundry_cli::{
9+
opts::RpcOpts,
10+
utils::{self, LoadConfig},
11+
};
12+
13+
#[derive(Debug, clap::Parser)]
14+
pub struct ValidateAuthArgs {
15+
/// Transaction hash.
16+
tx_hash: B256,
17+
18+
#[command(flatten)]
19+
rpc: RpcOpts,
20+
}
21+
22+
impl ValidateAuthArgs {
23+
pub async fn run(self) -> eyre::Result<()> {
24+
let config = self.rpc.load_config()?;
25+
let provider = utils::get_provider(&config)?;
26+
27+
let tx = provider
28+
.get_transaction_by_hash(self.tx_hash)
29+
.await?
30+
.ok_or_else(|| eyre::eyre!("tx not found: {:?}", self.tx_hash))?;
31+
32+
// Get block info for nonce calculation
33+
let block_number =
34+
tx.block_number.ok_or_else(|| eyre::eyre!("transaction is not yet mined"))?;
35+
let tx_index =
36+
tx.transaction_index.ok_or_else(|| eyre::eyre!("transaction index not available"))?;
37+
38+
// Fetch the block to get all transactions up to this one
39+
let block = provider
40+
.get_block_by_number(block_number.into())
41+
.full()
42+
.await?
43+
.ok_or_else(|| eyre::eyre!("block not found: {}", block_number))?;
44+
45+
// Build a map of address -> running nonce from txs in this block up to and including
46+
// our tx
47+
let mut running_nonces: HashMap<Address, u64> = HashMap::new();
48+
for block_tx in block.transactions.txns().take((tx_index + 1) as usize) {
49+
let from = block_tx.from();
50+
let nonce = block_tx.nonce();
51+
// Track the next expected nonce (current nonce + 1)
52+
running_nonces.insert(from, nonce + 1);
53+
}
54+
55+
let chain_id = provider.get_chain_id().await?;
56+
57+
// Extract authorization list from EIP-7702 transaction
58+
let auth_list = match &*tx.inner.inner {
59+
AnyTxEnvelope::Ethereum(TxEnvelope::Eip7702(signed_tx)) => {
60+
signed_tx.tx().authorization_list.clone()
61+
}
62+
_ => {
63+
eyre::bail!("transaction is not an EIP-7702 transaction");
64+
}
65+
};
66+
67+
sh_println!("Transaction: {}", self.tx_hash)?;
68+
sh_println!("Block: {} (tx index: {})", block_number, tx_index)?;
69+
sh_println!()?;
70+
71+
if auth_list.is_empty() {
72+
sh_println!("Authorization list is empty")?;
73+
} else {
74+
for (i, auth) in auth_list.iter().enumerate() {
75+
let valid_chain = auth.chain_id == chain_id || auth.chain_id == 0;
76+
sh_println!("Authorization #{}", i)?;
77+
sh_println!(" Decoded:")?;
78+
sh_println!(" Chain ID: {}", auth.chain_id,)?;
79+
sh_println!(" Address: {}", auth.address)?;
80+
sh_println!(" Nonce: {}", auth.nonce)?;
81+
sh_println!(" r: {}", auth.r())?;
82+
sh_println!(" s: {}", auth.s())?;
83+
sh_println!(" v: {}", auth.y_parity())?;
84+
85+
match auth.recover_authority() {
86+
Ok(authority) => {
87+
sh_println!(" Recovered Authority: {}", authority)?;
88+
89+
sh_println!(" Validation Status:")?;
90+
sh_println!(
91+
" Chain: {}",
92+
if valid_chain {
93+
"VALID".to_string()
94+
} else {
95+
format!("INVALID (expected: 0 or {chain_id})")
96+
}
97+
)?;
98+
99+
// Get the expected nonce at time of tx execution
100+
let expected_nonce = if let Some(&nonce) = running_nonces.get(&authority) {
101+
nonce
102+
} else {
103+
// Fetch nonce at block - 1 (state before this block)
104+
let prev_block = BlockId::number(block_number - 1);
105+
provider.get_transaction_count(authority).block_id(prev_block).await?
106+
};
107+
108+
let valid_nonce = auth.nonce == expected_nonce;
109+
if valid_nonce {
110+
sh_println!(" Nonce: VALID")?;
111+
} else {
112+
sh_println!(
113+
" Nonce: INVALID (expected: {}, got: {})",
114+
expected_nonce,
115+
auth.nonce
116+
)?;
117+
}
118+
119+
// If authorization was valid, update running nonce for subsequent auths
120+
if valid_chain && valid_nonce {
121+
running_nonces.insert(authority, expected_nonce + 1);
122+
}
123+
124+
// Check if the authority's code was set to the delegated address
125+
let code = provider.get_code_at(authority).await?;
126+
if code.is_empty() {
127+
sh_println!(" Code Status: No delegation (account has no code)")?;
128+
} else if code.len() == 23 && code[0..3] == [0xef, 0x01, 0x00] {
129+
// EIP-7702 delegation designator: 0xef0100 followed by 20-byte
130+
// address
131+
let delegated_to = Address::from_slice(&code[3..23]);
132+
if delegated_to == auth.address {
133+
sh_println!(
134+
" Code Status: ACTIVE (delegated to {})",
135+
delegated_to
136+
)?;
137+
} else {
138+
sh_println!(
139+
" Code Status: SUPERSEDED (currently delegated to {})",
140+
delegated_to
141+
)?;
142+
}
143+
} else {
144+
sh_println!(
145+
" Code Status: Account has contract code (not a delegation)"
146+
)?;
147+
}
148+
}
149+
Err(e) => {
150+
sh_println!(" Authority: UNKNOWN")?;
151+
sh_println!(" Signature: INVALID ({})", e)?;
152+
}
153+
}
154+
sh_println!()?;
155+
}
156+
}
157+
Ok(())
158+
}
159+
}

crates/cast/src/opts.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use crate::cmd::{
44
creation_code::CreationCodeArgs, da_estimate::DAEstimateArgs, erc20::Erc20Subcommand,
55
estimate::EstimateArgs, find_block::FindBlockArgs, interface::InterfaceArgs, logs::LogsArgs,
66
mktx::MakeTxArgs, rpc::RpcArgs, run::RunArgs, send::SendTxArgs, storage::StorageArgs,
7-
txpool::TxPoolSubcommands, wallet::WalletSubcommands,
7+
txpool::TxPoolSubcommands, validate_auth::ValidateAuthArgs, wallet::WalletSubcommands,
88
};
99
use alloy_ens::NameOrAddress;
1010
use alloy_primitives::{Address, B256, Selector, U256};
@@ -1119,6 +1119,10 @@ 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(ValidateAuthArgs),
1125+
11221126
/// Extracts function selectors and arguments from bytecode
11231127
#[command(visible_alias = "sel")]
11241128
Selectors {

0 commit comments

Comments
 (0)