Skip to content
Open
Show file tree
Hide file tree
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
3 changes: 3 additions & 0 deletions crates/cast/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -751,6 +751,9 @@ pub async fn run_command(args: CastArgs) -> Result<()> {
let auth: SignedAuthorization = serde_json::from_str(&auth)?;
sh_println!("{}", auth.recover_authority()?)?;
}
CastSubcommand::ValidateAuth(cmd) => {
cmd.run().await?;
}
CastSubcommand::TxPool { command } => command.run().await?,
CastSubcommand::Erc20Token { command } => command.run().await?,
CastSubcommand::DAEstimate(cmd) => {
Expand Down
1 change: 1 addition & 0 deletions crates/cast/src/cmd/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,5 @@ pub mod run;
pub mod send;
pub mod storage;
pub mod txpool;
pub mod validate_auth;
pub mod wallet;
159 changes: 159 additions & 0 deletions crates/cast/src/cmd/validate_auth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
use std::collections::HashMap;

use alloy_consensus::{Transaction, TxEnvelope};
use alloy_eips::BlockId;
use alloy_network::{AnyTxEnvelope, TransactionResponse};
use alloy_primitives::{Address, B256};
use alloy_provider::Provider;
use foundry_cli::{
opts::RpcOpts,
utils::{self, LoadConfig},
};

#[derive(Debug, clap::Parser)]
pub struct ValidateAuthArgs {
/// Transaction hash.
tx_hash: B256,

#[command(flatten)]
rpc: RpcOpts,
}

impl ValidateAuthArgs {
pub async fn run(self) -> eyre::Result<()> {
let config = self.rpc.load_config()?;
let provider = utils::get_provider(&config)?;

let tx = provider
.get_transaction_by_hash(self.tx_hash)
.await?
.ok_or_else(|| eyre::eyre!("tx not found: {:?}", self.tx_hash))?;

// Get block info for nonce calculation
let block_number =
tx.block_number.ok_or_else(|| eyre::eyre!("transaction is not yet mined"))?;
let tx_index =
tx.transaction_index.ok_or_else(|| eyre::eyre!("transaction index not available"))?;

// Fetch the block to get all transactions up to this one
let block = provider
.get_block_by_number(block_number.into())
.full()
.await?
.ok_or_else(|| eyre::eyre!("block not found: {}", block_number))?;

// Build a map of address -> running nonce from txs in this block up to and including
// our tx
let mut running_nonces: HashMap<Address, u64> = HashMap::new();
for block_tx in block.transactions.txns().take((tx_index + 1) as usize) {
let from = block_tx.from();
let nonce = block_tx.nonce();
// Track the next expected nonce (current nonce + 1)
running_nonces.insert(from, nonce + 1);
}

let chain_id = provider.get_chain_id().await?;

// Extract authorization list from EIP-7702 transaction
let auth_list = match &*tx.inner.inner {
AnyTxEnvelope::Ethereum(TxEnvelope::Eip7702(signed_tx)) => {
signed_tx.tx().authorization_list.clone()
}
_ => {
eyre::bail!("transaction is not an EIP-7702 transaction");
}
};

sh_println!("Transaction: {}", self.tx_hash)?;
sh_println!("Block: {} (tx index: {})", block_number, tx_index)?;
sh_println!()?;

if auth_list.is_empty() {
sh_println!("Authorization list is empty")?;
} else {
for (i, auth) in auth_list.iter().enumerate() {
let valid_chain = auth.chain_id == chain_id || auth.chain_id == 0;
sh_println!("Authorization #{}", i)?;
sh_println!(" Decoded:")?;
sh_println!(" Chain ID: {}", auth.chain_id,)?;
sh_println!(" Address: {}", auth.address)?;
sh_println!(" Nonce: {}", auth.nonce)?;
sh_println!(" r: {}", auth.r())?;
sh_println!(" s: {}", auth.s())?;
sh_println!(" v: {}", auth.y_parity())?;

match auth.recover_authority() {
Ok(authority) => {
sh_println!(" Recovered Authority: {}", authority)?;

sh_println!(" Validation Status:")?;
sh_println!(
" Chain: {}",
if valid_chain {
"VALID".to_string()
} else {
format!("INVALID (expected: 0 or {chain_id})")
}
)?;

// Get the expected nonce at time of tx execution
let expected_nonce = if let Some(&nonce) = running_nonces.get(&authority) {
nonce
} else {
// Fetch nonce at block - 1 (state before this block)
let prev_block = BlockId::number(block_number - 1);
provider.get_transaction_count(authority).block_id(prev_block).await?
};

let valid_nonce = auth.nonce == expected_nonce;
if valid_nonce {
sh_println!(" Nonce: VALID")?;
} else {
sh_println!(
" Nonce: INVALID (expected: {}, got: {})",
expected_nonce,
auth.nonce
)?;
}

// If authorization was valid, update running nonce for subsequent auths
if valid_chain && valid_nonce {
running_nonces.insert(authority, expected_nonce + 1);
}

// Check if the authority's code was set to the delegated address
let code = provider.get_code_at(authority).await?;
if code.is_empty() {
sh_println!(" Code Status: No delegation (account has no code)")?;
} else if code.len() == 23 && code[0..3] == [0xef, 0x01, 0x00] {
// EIP-7702 delegation designator: 0xef0100 followed by 20-byte
// address
let delegated_to = Address::from_slice(&code[3..23]);
if delegated_to == auth.address {
sh_println!(
" Code Status: ACTIVE (delegated to {})",
delegated_to
)?;
} else {
sh_println!(
" Code Status: SUPERSEDED (currently delegated to {})",
delegated_to
)?;
}
} else {
sh_println!(
" Code Status: Account has contract code (not a delegation)"
)?;
}
}
Err(e) => {
sh_println!(" Authority: UNKNOWN")?;
sh_println!(" Signature: INVALID ({})", e)?;
}
}
sh_println!()?;
}
}
Ok(())
}
}
6 changes: 5 additions & 1 deletion crates/cast/src/opts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::cmd::{
creation_code::CreationCodeArgs, da_estimate::DAEstimateArgs, erc20::Erc20Subcommand,
estimate::EstimateArgs, find_block::FindBlockArgs, interface::InterfaceArgs, logs::LogsArgs,
mktx::MakeTxArgs, rpc::RpcArgs, run::RunArgs, send::SendTxArgs, storage::StorageArgs,
txpool::TxPoolSubcommands, wallet::WalletSubcommands,
txpool::TxPoolSubcommands, validate_auth::ValidateAuthArgs, wallet::WalletSubcommands,
};
use alloy_ens::NameOrAddress;
use alloy_primitives::{Address, B256, Selector, U256};
Expand Down Expand Up @@ -1119,6 +1119,10 @@ pub enum CastSubcommand {
#[command(visible_aliases = &["decode-auth"])]
RecoverAuthority { auth: String },

/// Validate EIP-7702 authorizations in a transaction and print validity status.
Copy link
Member

Choose a reason for hiding this comment

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

what is being validated here?
just the auth itself?
because for this we only need to fetch the tx itself and perform recovery on this, then this can be simplified

this info is also already included in cast tx (only displays the recovered auth if valid)
so I'm not actually sure how exactly this new command is different

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't think the recoveredAuthority in itself tells you whether the authorization was valid because, the signature can be valid, but the nonce may be outdated or too ahead(there is only one acceptable nonce), the chain id can be invalid, etc. Essentially, my intention was to determine which authorizations got "executed", and which got rejected, and this is my attempt at displaying that information.

I think it shows the decoded data and recovered authority from the signature, but it doesn't show the the validity. I had to refer to etherscan for instance when I couldn't figure out why my eoa was not getting delegated
image

#[command(name = "validate-auth", visible_aliases = &["va", "validate-auths"])]
ValidateAuth(ValidateAuthArgs),

/// Extracts function selectors and arguments from bytecode
#[command(visible_alias = "sel")]
Selectors {
Expand Down
Loading
Loading