Skip to content
Merged
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
33 changes: 31 additions & 2 deletions crates/cast/src/cmd/wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,12 @@ pub enum WalletSubcommands {
#[arg(long)]
chain: Option<Chain>,

/// 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,
},
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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"
);
}
}
79 changes: 79 additions & 0 deletions crates/cast/tests/cli/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
Loading