diff --git a/crates/cast/src/cmd/wallet/mod.rs b/crates/cast/src/cmd/wallet/mod.rs index 7c9a99d08b0ce..d7f7ffefe9d5f 100644 --- a/crates/cast/src/cmd/wallet/mod.rs +++ b/crates/cast/src/cmd/wallet/mod.rs @@ -136,6 +136,12 @@ pub enum WalletSubcommands { #[arg(long)] chain: Option, + /// If set, indicates the authorization will be broadcast by the signing account itself. + /// This means the nonce used will be the current nonce + 1 (to account for the + /// transaction that will include this authorization). + #[arg(long, conflicts_with = "nonce")] + self_broadcast: bool, + #[command(flatten)] wallet: WalletOpts, }, @@ -542,13 +548,20 @@ impl WalletSubcommands { sh_println!("0x{}", hex::encode(sig.as_bytes()))?; } } - Self::SignAuth { rpc, nonce, chain, wallet, address } => { + Self::SignAuth { rpc, nonce, chain, wallet, address, self_broadcast } => { let wallet = wallet.signer().await?; let provider = utils::get_provider(&rpc.load_config()?)?; let nonce = if let Some(nonce) = nonce { nonce } else { - provider.get_transaction_count(wallet.address()).await? + let current_nonce = provider.get_transaction_count(wallet.address()).await?; + if self_broadcast { + // When self-broadcasting, the authorization nonce needs to be +1 + // because the transaction itself will consume the current nonce + current_nonce + 1 + } else { + current_nonce + } }; let chain_id = if let Some(chain) = chain { chain.id() @@ -998,4 +1011,20 @@ mod tests { _ => panic!("expected WalletSubcommands::ChangePassword"), } } + + #[test] + fn wallet_sign_auth_nonce_and_self_broadcast_conflict() { + let result = WalletSubcommands::try_parse_from([ + "foundry-cli", + "sign-auth", + "0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF", + "--nonce", + "42", + "--self-broadcast", + ]); + assert!( + result.is_err(), + "expected error when both --nonce and --self-broadcast are provided" + ); + } } diff --git a/crates/cast/tests/cli/main.rs b/crates/cast/tests/cli/main.rs index 89cf822be7077..cb8b30c538d1f 100644 --- a/crates/cast/tests/cli/main.rs +++ b/crates/cast/tests/cli/main.rs @@ -615,6 +615,85 @@ casttest!(wallet_sign_auth, |_prj, cmd| { "#]]); }); +// tests that `cast wallet sign-auth --self-broadcast` uses nonce + 1 +casttest!(wallet_sign_auth_self_broadcast, async |_prj, cmd| { + use alloy_rlp::Decodable; + use alloy_signer_local::PrivateKeySigner; + + let (_, handle) = + anvil::spawn(NodeConfig::test().with_hardfork(Some(EthereumHardfork::Prague.into()))).await; + let endpoint = handle.http_endpoint(); + + let private_key = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; + let signer: PrivateKeySigner = private_key.parse().unwrap(); + let signer_address = signer.address(); + let delegate_address = address!("0x70997970C51812dc3A010C7d01b50e0d17dc79C8"); + + // Get the current nonce from the RPC + let provider = ProviderBuilder::new().connect_http(endpoint.parse().unwrap()); + let current_nonce = provider.get_transaction_count(signer_address).await.unwrap(); + + // First, get the auth without --self-broadcast (should use current nonce) + let output_normal = cmd + .args([ + "wallet", + "sign-auth", + "--private-key", + private_key, + "--rpc-url", + &endpoint, + &delegate_address.to_string(), + ]) + .assert_success() + .get_output() + .stdout_lossy() + .trim() + .to_string(); + + // Then, get the auth with --self-broadcast (should use current nonce + 1) + let output_self_broadcast = cmd + .cast_fuse() + .args([ + "wallet", + "sign-auth", + "--private-key", + private_key, + "--rpc-url", + &endpoint, + "--self-broadcast", + &delegate_address.to_string(), + ]) + .assert_success() + .get_output() + .stdout_lossy() + .trim() + .to_string(); + + // The outputs should be different due to different nonces + assert_ne!( + output_normal, output_self_broadcast, + "self-broadcast should produce different signature due to nonce + 1" + ); + + // Decode the RLP to verify the nonces + let normal_bytes = hex::decode(output_normal.strip_prefix("0x").unwrap()).unwrap(); + let self_broadcast_bytes = + hex::decode(output_self_broadcast.strip_prefix("0x").unwrap()).unwrap(); + + let normal_auth = + alloy_eips::eip7702::SignedAuthorization::decode(&mut normal_bytes.as_slice()).unwrap(); + let self_broadcast_auth = + alloy_eips::eip7702::SignedAuthorization::decode(&mut self_broadcast_bytes.as_slice()) + .unwrap(); + + assert_eq!(normal_auth.nonce(), current_nonce, "normal auth should have current nonce"); + assert_eq!( + self_broadcast_auth.nonce(), + current_nonce + 1, + "self-broadcast auth should have current nonce + 1" + ); +}); + // tests that `cast wallet list` outputs the local accounts casttest!(wallet_list_local_accounts, |prj, cmd| { let keystore_path = prj.root().join("keystore");