Skip to content

Commit 113f249

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 a063402 commit 113f249

File tree

5 files changed

+664
-4
lines changed

5 files changed

+664
-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: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
use std::collections::HashMap;
2+
3+
use alloy_consensus::Transaction;
4+
use alloy_eips::{BlockId, eip7702::SignedAuthorization};
5+
use alloy_network::{AnyNetwork, TransactionResponse};
6+
use alloy_primitives::{Address, B256};
7+
use alloy_provider::Provider;
8+
use eyre::OptionExt;
9+
use foundry_cli::{
10+
opts::RpcOpts,
11+
utils::{self, LoadConfig, init_progress},
12+
};
13+
use foundry_common::shell;
14+
15+
#[derive(Debug, clap::Parser)]
16+
pub struct ValidateAuthArgs {
17+
/// Transaction hash.
18+
tx_hash: B256,
19+
20+
#[command(flatten)]
21+
rpc: RpcOpts,
22+
}
23+
24+
/// Validates an authorization and updates the running nonce map if valid.
25+
/// Returns (valid_chain, valid_nonce, expected_nonce, authority) if signature recovery succeeds.
26+
async fn validate_and_update_nonce<P: Provider<AnyNetwork>>(
27+
auth: &SignedAuthorization,
28+
chain_id: u64,
29+
block_number: u64,
30+
running_nonces: &mut HashMap<Address, u64>,
31+
provider: &P,
32+
) -> eyre::Result<Option<(bool, bool, u64, Address)>> {
33+
let valid_chain = auth.chain_id == chain_id || auth.chain_id == 0;
34+
35+
match auth.recover_authority() {
36+
Ok(authority) => {
37+
// Get expected nonce for this authority
38+
let expected_nonce = if let Some(&nonce) = running_nonces.get(&authority) {
39+
nonce
40+
} else {
41+
// Fetch nonce at block - 1 (state before this block)
42+
let prev_block = BlockId::number(block_number - 1);
43+
provider.get_transaction_count(authority).block_id(prev_block).await?
44+
};
45+
46+
let valid_nonce = auth.nonce == expected_nonce;
47+
48+
// If authorization was valid, update running nonce
49+
if valid_chain && valid_nonce {
50+
running_nonces.insert(authority, expected_nonce + 1);
51+
}
52+
53+
Ok(Some((valid_chain, valid_nonce, expected_nonce, authority)))
54+
}
55+
Err(_) => Ok(None),
56+
}
57+
}
58+
59+
impl ValidateAuthArgs {
60+
/// Validates all the authorizations in an EIP-7702 transaction. It does so by validating the
61+
/// nonce and chain id for each of the recovered authority from the given authorizations.
62+
///
63+
/// For nonce validation, it builds a "running nonce" map by processing all transactions
64+
/// before the target transaction in the same block:
65+
/// - For each transaction sender, tracks their next expected nonce (tx.nonce + 1)
66+
/// - For each valid authorization in previous transactions, also increments that authority's
67+
/// running nonce (since valid authorizations execute and increment the authority's nonce)
68+
/// - If an authority is not in the running nonce map, fetches their nonce at block - 1
69+
///
70+
/// Then, for each authorization in the target transaction:
71+
/// - Validates nonce against the running nonce
72+
/// - If the authorization is valid (both chain and nonce), increments the running nonce for
73+
/// that authority (for subsequent authorizations in the same transaction)
74+
///
75+
/// For chain id validation, it checks if it is zero or the same chainid as the current chain.
76+
pub async fn run(self) -> eyre::Result<()> {
77+
let config = self.rpc.load_config()?;
78+
let provider = utils::get_provider(&config)?;
79+
80+
let tx = provider
81+
.get_transaction_by_hash(self.tx_hash)
82+
.await?
83+
.ok_or_else(|| eyre::eyre!("tx not found: {:?}", self.tx_hash))?;
84+
85+
// Get block info for nonce calculation
86+
let block_number = tx.block_number.ok_or_eyre("transaction is not yet mined")?;
87+
let tx_index = tx.transaction_index.ok_or_eyre("transaction index not available")?;
88+
89+
// Fetch the block to get all transactions up to this one
90+
let block = provider
91+
.get_block_by_number(block_number.into())
92+
.full()
93+
.await?
94+
.ok_or_else(|| eyre::eyre!("block not found: {}", block_number))?;
95+
96+
let chain_id = provider.get_chain_id().await?;
97+
98+
// Build a map of address -> running nonce from txs in this block up to (but not
99+
// including) our tx. We need to process both sender nonces AND any valid authorizations
100+
// from previous transactions, since those affect the running nonce.
101+
let mut running_nonces: HashMap<Address, u64> = HashMap::new();
102+
103+
// Check if there are any previous transactions
104+
if tx_index > 0 {
105+
if !shell::is_json() {
106+
sh_println!("Executing previous transactions from the block.")?;
107+
}
108+
109+
let pb = init_progress(tx_index as u64, "tx");
110+
pb.set_position(0);
111+
112+
// Process all transactions BEFORE our target transaction
113+
for (index, block_tx) in block.transactions.txns().take(tx_index as usize).enumerate() {
114+
let from = block_tx.from();
115+
let nonce = block_tx.nonce();
116+
// Track the next expected nonce (current nonce + 1)
117+
running_nonces.insert(from, nonce + 1);
118+
119+
// Also process any valid authorizations in this previous transaction
120+
if let Some(auth_list) = block_tx.authorization_list() {
121+
for auth in auth_list {
122+
validate_and_update_nonce(
123+
auth,
124+
chain_id,
125+
block_number,
126+
&mut running_nonces,
127+
&provider,
128+
)
129+
.await?;
130+
}
131+
}
132+
133+
pb.set_position((index + 1) as u64);
134+
}
135+
136+
if !shell::is_json() {
137+
sh_println!()?;
138+
}
139+
}
140+
141+
// Also track our target transaction's sender nonce
142+
running_nonces.insert(tx.from(), tx.nonce() + 1);
143+
144+
// Extract authorization list from EIP-7702 transaction
145+
let auth_list =
146+
tx.authorization_list().ok_or_eyre("Transaction has no authorization list")?;
147+
148+
sh_println!("Transaction: {}", self.tx_hash)?;
149+
sh_println!("Block: {} (tx index: {})", block_number, tx_index)?;
150+
sh_println!()?;
151+
152+
if auth_list.is_empty() {
153+
sh_println!("Authorization list is empty")?;
154+
} else {
155+
for (i, auth) in auth_list.iter().enumerate() {
156+
sh_println!("Authorization #{}", i)?;
157+
sh_println!(" Decoded:")?;
158+
sh_println!(" Chain ID: {}", auth.chain_id,)?;
159+
sh_println!(" Address: {}", auth.address)?;
160+
sh_println!(" Nonce: {}", auth.nonce)?;
161+
sh_println!(" r: {}", auth.r())?;
162+
sh_println!(" s: {}", auth.s())?;
163+
sh_println!(" v: {}", auth.y_parity())?;
164+
165+
match validate_and_update_nonce(
166+
auth,
167+
chain_id,
168+
block_number,
169+
&mut running_nonces,
170+
&provider,
171+
)
172+
.await?
173+
{
174+
Some((valid_chain, valid_nonce, expected_nonce, authority)) => {
175+
sh_println!(" Recovered Authority: {}", authority)?;
176+
177+
sh_println!(" Validation Status:")?;
178+
sh_println!(
179+
" Chain: {}",
180+
if valid_chain {
181+
"VALID".to_string()
182+
} else {
183+
format!("INVALID (expected: 0 or {chain_id})")
184+
}
185+
)?;
186+
187+
if valid_nonce {
188+
sh_println!(" Nonce: VALID")?;
189+
} else {
190+
sh_println!(
191+
" Nonce: INVALID (expected: {}, got: {})",
192+
expected_nonce,
193+
auth.nonce
194+
)?;
195+
}
196+
197+
// Check if the authority's code was set to the delegated address
198+
// Fetch code at the transaction's block to see state after tx execution
199+
let code = provider
200+
.get_code_at(authority)
201+
.block_id(BlockId::number(block_number))
202+
.await?;
203+
sh_println!(" Code Status (at end of block {}):", block_number)?;
204+
if code.is_empty() {
205+
sh_println!(" No delegation (account has no code)")?;
206+
} else if code.len() == 23 && code[0..3] == [0xef, 0x01, 0x00] {
207+
// EIP-7702 delegation designator: 0xef0100 followed by 20-byte
208+
// address
209+
let delegated_to = Address::from_slice(&code[3..23]);
210+
if delegated_to == auth.address {
211+
sh_println!(" ACTIVE (delegated to {})", delegated_to)?;
212+
} else {
213+
sh_println!(" SUPERSEDED (delegated to {})", delegated_to)?;
214+
}
215+
} else {
216+
sh_println!(" Account has contract code (not a delegation)")?;
217+
}
218+
}
219+
None => {
220+
sh_println!(" Authority: UNKNOWN")?;
221+
sh_println!(" Signature: INVALID (recovery failed)")?;
222+
}
223+
}
224+
sh_println!()?;
225+
}
226+
}
227+
Ok(())
228+
}
229+
}

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)