Skip to content

Commit 5f1a14b

Browse files
authored
quantum phase 3: add KeyVault lifecycle UX and read-path compatibility — introduces cast quantum subcommand group (bootstrap / add-key / remove-key / update-key-auth) that reuses the shared CastTxBuilder + ML-DSA signing pipeline, routes through `Quan (#4)
<!-- Thank you for your Pull Request. Please provide a description above and review the requirements below. Bug fixes and new features should include tests. Contributors guide: https://github.com/foundry-rs/foundry/blob/HEAD/CONTRIBUTING.md The contributors guide includes instructions for running rustfmt and building the documentation. --> <!-- ** Please select "Allow edits from maintainers" in the PR Options ** --> ## Motivation <!-- Explain the context and why you're making that change. What is the problem you're trying to solve? In some cases there is not a problem and this can be thought of as being the motivation for your change. --> ## Solution <!-- Summarize the solution and provide any necessary context needed to understand the code change. --> ## PR Checklist - [ ] Added Tests - [ ] Added Documentation - [ ] Breaking changes
1 parent 975b38d commit 5f1a14b

9 files changed

Lines changed: 776 additions & 19 deletions

File tree

crates/cast/src/args.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,7 @@ pub async fn run_command(args: CastArgs) -> Result<()> {
572572
}
573573
CastSubcommand::Run(cmd) => cmd.run().await?,
574574
CastSubcommand::SendTx(cmd) => cmd.run().await?,
575+
CastSubcommand::Quantum(cmd) => cmd.run().await?,
575576
CastSubcommand::BatchMakeTx(cmd) => cmd.run().await?,
576577
CastSubcommand::BatchSend(cmd) => cmd.run().await?,
577578
CastSubcommand::Tx { tx_hash, from, nonce, field, raw, rpc, to_request, network } => {

crates/cast/src/cmd/call.rs

Lines changed: 108 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@ use foundry_cli::{
2020
utils::{LoadConfig, TraceResult, parse_ether_value},
2121
};
2222
use foundry_common::{
23-
FoundryTransactionBuilder,
23+
FoundryTransactionBuilder, QUANTUM_CALL_LIFECYCLE_REJECTION_MESSAGE,
2424
abi::{encode_function_args, get_func},
2525
provider::{ProviderBuilder, curl_transport::generate_curl_command},
26-
sh_println, shell,
26+
quantum_call_is_unsupported_lifecycle_calldata, sh_println, shell,
2727
};
2828
use foundry_compilers::artifacts::EvmVersion;
2929
use foundry_config::{
@@ -221,6 +221,11 @@ impl CallArgs {
221221
if self.rpc.curl {
222222
return self.run_curl().await;
223223
}
224+
// `cast call` is a read path and must stay on ordinary RPC simulation.
225+
// Quantum write-only options (`--quantum*`) and KeyVault lifecycle
226+
// selectors cannot be simulated; reject them with an actionable error
227+
// instead of letting them reach `eth_call`.
228+
self.reject_quantum_read_path_misuse()?;
224229
if self.tx.tempo.is_tempo() {
225230
self.run_with_network::<TempoEvmNetwork>().await
226231
} else {
@@ -484,6 +489,51 @@ impl CallArgs {
484489
Ok(())
485490
}
486491

492+
/// Fail closed when `cast call` is invoked with Quantum write-only options or
493+
/// against a KeyVault lifecycle selector.
494+
fn reject_quantum_read_path_misuse(&self) -> Result<()> {
495+
if self.tx.quantum.is_quantum() {
496+
eyre::bail!(
497+
"`cast call` does not support Quantum write-only flags (--quantum*); use `cast call` without them for reads, or `cast quantum` / `cast send --quantum` for writes"
498+
);
499+
}
500+
let calldata = self.read_path_calldata()?;
501+
if let Some(bytes) = calldata
502+
&& quantum_call_is_unsupported_lifecycle_calldata(&bytes)
503+
{
504+
eyre::bail!(QUANTUM_CALL_LIFECYCLE_REJECTION_MESSAGE);
505+
}
506+
Ok(())
507+
}
508+
509+
/// Decode the `--data`/`sig` input for read-path selector checks. Returns
510+
/// `None` when no call input is provided.
511+
fn read_path_calldata(&self) -> Result<Option<Vec<u8>>> {
512+
if let Some(data) = &self.data {
513+
let trimmed = data.trim().trim_start_matches("0x");
514+
return Ok(Some(hex::decode(trimmed)?));
515+
}
516+
if let Some(sig) = &self.sig {
517+
let trimmed = sig.trim();
518+
if let Some(hex_body) = trimmed.strip_prefix("0x").or_else(|| {
519+
// Some callers pass a bare hex string without `0x`; treat short
520+
// inputs that start with a known selector as already-encoded.
521+
if trimmed.len() >= 8 && trimmed.chars().all(|c| c.is_ascii_hexdigit()) {
522+
Some(trimmed)
523+
} else {
524+
None
525+
}
526+
}) && let Ok(bytes) = hex::decode(hex_body)
527+
{
528+
return Ok(Some(bytes));
529+
}
530+
if let Ok(func) = get_func(trimmed) {
531+
return Ok(Some(encode_function_args(&func, &self.args)?));
532+
}
533+
}
534+
Ok(None)
535+
}
536+
487537
/// Parse state overrides from command line arguments.
488538
pub fn get_state_overrides(&self) -> eyre::Result<Option<StateOverride>> {
489539
// Early return if no override set - <https://github.com/foundry-rs/foundry/issues/10705>
@@ -797,6 +847,62 @@ mod tests {
797847
assert_eq!(args.args, vec!["-999999"]);
798848
}
799849

850+
#[test]
851+
fn cast_call_rejects_keyvault_bootstrap_selector() {
852+
let args = CallArgs::parse_from([
853+
"foundry-cli",
854+
"0x0000000000000000000000000000000000001000",
855+
"--data",
856+
"0x5e8e7a13",
857+
]);
858+
let err = args.reject_quantum_read_path_misuse().unwrap_err();
859+
assert!(
860+
err.to_string().contains("cannot be simulated via eth_call"),
861+
"unexpected error: {err}"
862+
);
863+
}
864+
865+
#[test]
866+
fn cast_call_rejects_keyvault_add_key_selector_via_sig() {
867+
let args = CallArgs::parse_from([
868+
"foundry-cli",
869+
"0x0000000000000000000000000000000000001000",
870+
"0x32bc2919",
871+
]);
872+
let err = args.reject_quantum_read_path_misuse().unwrap_err();
873+
assert!(
874+
err.to_string().contains("cannot be simulated via eth_call"),
875+
"unexpected error: {err}"
876+
);
877+
}
878+
879+
#[test]
880+
fn cast_call_rejects_quantum_write_flags() {
881+
let args = CallArgs::parse_from([
882+
"foundry-cli",
883+
"--quantum",
884+
"0xDeaDBeeFcAfEbAbEfAcEfEeDcBaDbEeFcAfEbAbE",
885+
"balanceOf(address)",
886+
"0x000000000000000000000000000000000000dEaD",
887+
]);
888+
let err = args.reject_quantum_read_path_misuse().unwrap_err();
889+
assert!(
890+
err.to_string().contains("does not support Quantum write-only flags"),
891+
"unexpected error: {err}"
892+
);
893+
}
894+
895+
#[test]
896+
fn cast_call_allows_ordinary_view_call() {
897+
let args = CallArgs::parse_from([
898+
"foundry-cli",
899+
"0xDeaDBeeFcAfEbAbEfAcEfEeDcBaDbEeFcAfEbAbE",
900+
"balanceOf(address)",
901+
"0x000000000000000000000000000000000000dEaD",
902+
]);
903+
args.reject_quantum_read_path_misuse().expect("ordinary reads must not be rejected");
904+
}
905+
800906
#[test]
801907
fn test_transaction_opts_with_trace() {
802908
// Test that transaction options are correctly parsed when using --trace

crates/cast/src/cmd/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ pub mod interface;
2323
pub mod keychain;
2424
pub mod logs;
2525
pub mod mktx;
26+
pub mod quantum;
2627
pub mod rpc;
2728
pub mod run;
2829
pub mod send;

0 commit comments

Comments
 (0)