Skip to content

Commit b7c7cfe

Browse files
authored
feat(cast): wallet sign-auth --self-broadcast (#12624)
Add a new option `--self-broadcast` to indicate the signed authorization is intended to be broadcasted by the signing account. This is used to determine the nonce for the authorization that will be valid for the broadcasting transaction. If the signing account itself broadcasts the authorization, the nonce is incremented and the authorization becomes invalid if it is signed for the `next_nonce` instead of `next_nonce + 1`.
1 parent 229e969 commit b7c7cfe

File tree

2 files changed

+110
-2
lines changed

2 files changed

+110
-2
lines changed

crates/cast/src/cmd/wallet/mod.rs

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,12 @@ pub enum WalletSubcommands {
136136
#[arg(long)]
137137
chain: Option<Chain>,
138138

139+
/// If set, indicates the authorization will be broadcast by the signing account itself.
140+
/// This means the nonce used will be the current nonce + 1 (to account for the
141+
/// transaction that will include this authorization).
142+
#[arg(long, conflicts_with = "nonce")]
143+
self_broadcast: bool,
144+
139145
#[command(flatten)]
140146
wallet: WalletOpts,
141147
},
@@ -542,13 +548,20 @@ impl WalletSubcommands {
542548
sh_println!("0x{}", hex::encode(sig.as_bytes()))?;
543549
}
544550
}
545-
Self::SignAuth { rpc, nonce, chain, wallet, address } => {
551+
Self::SignAuth { rpc, nonce, chain, wallet, address, self_broadcast } => {
546552
let wallet = wallet.signer().await?;
547553
let provider = utils::get_provider(&rpc.load_config()?)?;
548554
let nonce = if let Some(nonce) = nonce {
549555
nonce
550556
} else {
551-
provider.get_transaction_count(wallet.address()).await?
557+
let current_nonce = provider.get_transaction_count(wallet.address()).await?;
558+
if self_broadcast {
559+
// When self-broadcasting, the authorization nonce needs to be +1
560+
// because the transaction itself will consume the current nonce
561+
current_nonce + 1
562+
} else {
563+
current_nonce
564+
}
552565
};
553566
let chain_id = if let Some(chain) = chain {
554567
chain.id()
@@ -998,4 +1011,20 @@ mod tests {
9981011
_ => panic!("expected WalletSubcommands::ChangePassword"),
9991012
}
10001013
}
1014+
1015+
#[test]
1016+
fn wallet_sign_auth_nonce_and_self_broadcast_conflict() {
1017+
let result = WalletSubcommands::try_parse_from([
1018+
"foundry-cli",
1019+
"sign-auth",
1020+
"0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF",
1021+
"--nonce",
1022+
"42",
1023+
"--self-broadcast",
1024+
]);
1025+
assert!(
1026+
result.is_err(),
1027+
"expected error when both --nonce and --self-broadcast are provided"
1028+
);
1029+
}
10011030
}

crates/cast/tests/cli/main.rs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -615,6 +615,85 @@ casttest!(wallet_sign_auth, |_prj, cmd| {
615615
"#]]);
616616
});
617617

618+
// tests that `cast wallet sign-auth --self-broadcast` uses nonce + 1
619+
casttest!(wallet_sign_auth_self_broadcast, async |_prj, cmd| {
620+
use alloy_rlp::Decodable;
621+
use alloy_signer_local::PrivateKeySigner;
622+
623+
let (_, handle) =
624+
anvil::spawn(NodeConfig::test().with_hardfork(Some(EthereumHardfork::Prague.into()))).await;
625+
let endpoint = handle.http_endpoint();
626+
627+
let private_key = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
628+
let signer: PrivateKeySigner = private_key.parse().unwrap();
629+
let signer_address = signer.address();
630+
let delegate_address = address!("0x70997970C51812dc3A010C7d01b50e0d17dc79C8");
631+
632+
// Get the current nonce from the RPC
633+
let provider = ProviderBuilder::new().connect_http(endpoint.parse().unwrap());
634+
let current_nonce = provider.get_transaction_count(signer_address).await.unwrap();
635+
636+
// First, get the auth without --self-broadcast (should use current nonce)
637+
let output_normal = cmd
638+
.args([
639+
"wallet",
640+
"sign-auth",
641+
"--private-key",
642+
private_key,
643+
"--rpc-url",
644+
&endpoint,
645+
&delegate_address.to_string(),
646+
])
647+
.assert_success()
648+
.get_output()
649+
.stdout_lossy()
650+
.trim()
651+
.to_string();
652+
653+
// Then, get the auth with --self-broadcast (should use current nonce + 1)
654+
let output_self_broadcast = cmd
655+
.cast_fuse()
656+
.args([
657+
"wallet",
658+
"sign-auth",
659+
"--private-key",
660+
private_key,
661+
"--rpc-url",
662+
&endpoint,
663+
"--self-broadcast",
664+
&delegate_address.to_string(),
665+
])
666+
.assert_success()
667+
.get_output()
668+
.stdout_lossy()
669+
.trim()
670+
.to_string();
671+
672+
// The outputs should be different due to different nonces
673+
assert_ne!(
674+
output_normal, output_self_broadcast,
675+
"self-broadcast should produce different signature due to nonce + 1"
676+
);
677+
678+
// Decode the RLP to verify the nonces
679+
let normal_bytes = hex::decode(output_normal.strip_prefix("0x").unwrap()).unwrap();
680+
let self_broadcast_bytes =
681+
hex::decode(output_self_broadcast.strip_prefix("0x").unwrap()).unwrap();
682+
683+
let normal_auth =
684+
alloy_eips::eip7702::SignedAuthorization::decode(&mut normal_bytes.as_slice()).unwrap();
685+
let self_broadcast_auth =
686+
alloy_eips::eip7702::SignedAuthorization::decode(&mut self_broadcast_bytes.as_slice())
687+
.unwrap();
688+
689+
assert_eq!(normal_auth.nonce(), current_nonce, "normal auth should have current nonce");
690+
assert_eq!(
691+
self_broadcast_auth.nonce(),
692+
current_nonce + 1,
693+
"self-broadcast auth should have current nonce + 1"
694+
);
695+
});
696+
618697
// tests that `cast wallet list` outputs the local accounts
619698
casttest!(wallet_list_local_accounts, |prj, cmd| {
620699
let keystore_path = prj.root().join("keystore");

0 commit comments

Comments
 (0)