Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
ae0107c
feat: add native symbolic testing
mattsse May 16, 2026
babfe79
fix: satisfy symbolic clippy lints
mattsse May 17, 2026
deae7d1
Merge branch 'master' into mattsse/native-symbolic-testing
grandizzy May 19, 2026
0ce92b5
fix: set symbolic counterexample value
decofe May 19, 2026
eecc0a5
fix: narrow symbolic test classification
decofe May 19, 2026
34d045a
test(symbolic): add fuzzer/standard-lib parity tests (#14809)
grandizzy May 19, 2026
c9976b5
refactor(symbolic): split executor into modules (#14825)
figtracer May 19, 2026
72a7bf8
feat(symbolic): add named dynamic ABI bounds (#14814)
figtracer May 19, 2026
5cf1f58
feat(symbolic): add solver selection plumbing (#14835)
figtracer May 20, 2026
dc92a41
feat(symbolic): add portfolio solver diagnostics (#14846)
figtracer May 20, 2026
7411445
feat(symbolic): stage portfolio solver launches (#14847)
figtracer May 20, 2026
877b5b0
feat(symbolic): summarize portfolio diagnostics (#14849)
figtracer May 21, 2026
509f192
fix(symbolic): defer diagnostics during progress output (#14861)
figtracer May 21, 2026
8bd13b7
feat(symbolic): show query progress (#14862)
figtracer May 21, 2026
26432fb
feat(symbolic): adapt portfolio scheduling (#14864)
figtracer May 21, 2026
e3cf6ac
feat(symbolic): configure exploration order (#14869)
figtracer May 22, 2026
cc1089b
refactor(symbolic): consolidate constants into a `consts` module (#14…
mablr May 22, 2026
5ddf05d
refactor(symbolic): split symbolic test file by feature (#14881)
mablr May 22, 2026
ec43311
fix(symbolic): harden soundness fallbacks (#14877)
figtracer May 22, 2026
7228f88
refactor(symbolic): split executor into modules (#14879)
figtracer May 25, 2026
0af4478
refactor(symbolic): replace `String` errors in solver config with `en…
mablr May 25, 2026
2199ad4
Merge remote-tracking branch 'origin/master' into mattsse/native-symb…
decofe May 25, 2026
f443e58
fix: stabilize symbolic CI tests (#14900)
decofe May 25, 2026
2eeabaf
test: snapshot symbolic CLI output checks (#14903)
decofe May 25, 2026
a0a1de4
feat(symbolic): add tracing to executor and solver (#14904)
mablr May 25, 2026
3dffbfe
refactor(common): extract wallet helpers (#14906)
mablr May 25, 2026
4913daf
test(symbolic): stabilize cheatcode expectations (#14910)
figtracer May 25, 2026
724cda5
feat(symbolic): normalize hard arithmetic queries (#14901)
figtracer May 26, 2026
42facf5
feat(symbolic): cache satisfiability queries (#14917)
figtracer May 26, 2026
430bc1c
feat(symbolic): cache solver models (#14921)
figtracer May 27, 2026
06b74cb
test(symbolic): avoid cache hits in scheduler tests (#14935)
figtracer May 27, 2026
aa7f675
Merge master into mattsse/native-symbolic-testing
figtracer May 27, 2026
788f514
test(symbolic): add soundness regression tests (#14936)
grandizzy May 27, 2026
87a30ac
fix(symbolic): report non-replaying counterexamples as incomplete (#1…
figtracer May 27, 2026
afc682b
fix(symbolic): fail closed on gasleft (#14939)
figtracer May 27, 2026
b23e9cd
fix(symbolic): surface Keccak heuristic in incomplete results (#14940)
figtracer May 27, 2026
27a6aff
docs(symbolic): document preview result semantics (#14941)
figtracer May 27, 2026
e64c5be
feat(symbolic): allow GAS as call operand (#14942)
figtracer May 27, 2026
a623da1
feat(symbolic): support symbolic `vm.deal` values (#14952)
figtracer May 28, 2026
c91bef5
docs(symbolic): document known incomplete surfaces
figtracer May 28, 2026
a790370
docs(symbolic): clarify bounded preview gaps
figtracer May 28, 2026
ef4eb4a
Merge remote-tracking branch 'origin/master' into mattsse/native-symb…
figtracer May 28, 2026
8593d97
Merge branch 'master' into mattsse/native-symbolic-testing
grandizzy May 28, 2026
f948122
fix(symbolic): fail closed on false-pass surfaces
figtracer May 28, 2026
8d5ac2f
Merge branch 'master' into mattsse/native-symbolic-testing
figtracer May 28, 2026
f7e563e
Merge branch 'master' into mattsse/native-symbolic-testing
grandizzy Jun 1, 2026
80fe520
fix(symbolic): fail closed on validity-sensitive helpers (#15024)
figtracer Jun 2, 2026
b9e65eb
Merge remote-tracking branch 'origin/master' into mattsse/native-symb…
decofe Jun 3, 2026
d964e47
Merge remote-tracking branch 'origin/master' into mattsse/native-symb…
figtracer Jun 3, 2026
002b22b
test: update default config snapshot
mablr Jun 4, 2026
d1d2144
fix: make clippy happy
mablr Jun 4, 2026
a2dc642
Merge branch 'master' into mattsse/native-symbolic-testing
mablr Jun 4, 2026
49ab330
perf(symbolic): improve solver query handling (#15065)
figtracer Jun 5, 2026
021a57b
perf(symbolic): reduce solver work for guarded division checks (#15074)
figtracer Jun 5, 2026
743a11b
perf(symbolic): reduce solver work with path-bounded arithmetic guard…
figtracer Jun 5, 2026
1bd2053
perf(symbolic): reduce solver queries with witnesses and exact folds …
figtracer Jun 5, 2026
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
20 changes: 20 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ members = [
"crates/evm/evm/",
"crates/evm/fuzz/",
"crates/evm/sancov/",
"crates/evm/symbolic/",
"crates/evm/hardforks/",
"crates/evm/traces/",
"crates/fmt/",
Expand Down Expand Up @@ -333,6 +334,7 @@ foundry-evm-hardforks = { path = "crates/evm/hardforks", default-features = fals
foundry-evm-networks = { path = "crates/evm/networks", default-features = false }
foundry-evm-fuzz = { path = "crates/evm/fuzz", default-features = false }
foundry-evm-sancov = { path = "crates/evm/sancov" }
foundry-evm-symbolic = { path = "crates/evm/symbolic", default-features = false }
foundry-evm-traces = { path = "crates/evm/traces", default-features = false }
foundry-macros = { path = "crates/macros" }
foundry-test-utils = { path = "crates/test-utils", default-features = false }
Expand Down
33 changes: 6 additions & 27 deletions crates/cheatcodes/src/crypto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use alloy_signer_local::{
},
};
use alloy_sol_types::SolValue;
use foundry_common::wallet::{derive_private_key, derive_private_key_with_language};
use foundry_evm_core::evm::FoundryEvmNetwork;
use k256::{
FieldBytes, Scalar,
Expand Down Expand Up @@ -486,36 +487,14 @@ pub(super) fn parse_wallet(private_key: &U256) -> Result<PrivateKeySigner> {
}

fn derive_key_str(mnemonic: &str, path: &str, index: u32, language: &str) -> Result {
match language {
"chinese_simplified" => derive_key::<ChineseSimplified>(mnemonic, path, index),
"chinese_traditional" => derive_key::<ChineseTraditional>(mnemonic, path, index),
"czech" => derive_key::<Czech>(mnemonic, path, index),
"english" => derive_key::<English>(mnemonic, path, index),
"french" => derive_key::<French>(mnemonic, path, index),
"italian" => derive_key::<Italian>(mnemonic, path, index),
"japanese" => derive_key::<Japanese>(mnemonic, path, index),
"korean" => derive_key::<Korean>(mnemonic, path, index),
"portuguese" => derive_key::<Portuguese>(mnemonic, path, index),
"spanish" => derive_key::<Spanish>(mnemonic, path, index),
_ => Err(fmt_err!("unsupported mnemonic language: {language:?}")),
}
let private_key = derive_private_key_with_language(mnemonic, path, index, language)
.map_err(|e| fmt_err!("{e}"))?;
Ok(private_key.abi_encode())
}

fn derive_key<W: Wordlist>(mnemonic: &str, path: &str, index: u32) -> Result {
fn derive_key_path(path: &str, index: u32) -> String {
let mut out = path.to_string();
if !out.ends_with('/') {
out.push('/');
}
out.push_str(&index.to_string());
out
}

let wallet = MnemonicBuilder::<W>::default()
.phrase(mnemonic)
.derivation_path(derive_key_path(path, index))?
.build()?;
let private_key = U256::from_be_bytes(wallet.credential().to_bytes().into());
let private_key =
derive_private_key::<W>(mnemonic, path, index).map_err(|e| fmt_err!("{e}"))?;
Ok(private_key.abi_encode())
}

Expand Down
2 changes: 1 addition & 1 deletion crates/cheatcodes/src/json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -855,7 +855,7 @@ mod tests {
any::<u32>().prop_map(|v| DynSolValue::Uint(U256::from(v), 256)),
any::<[u8; 20]>().prop_map(Address::from).prop_map(DynSolValue::Address),
any::<[u8; 32]>().prop_map(B256::from).prop_map(|b| DynSolValue::FixedBytes(b, 32)),
".*".prop_map(DynSolValue::String),
".*".prop_filter("invalid string value", |s| s != "{}").prop_map(DynSolValue::String),
];

// Combine them to create a list of unique fields that preserve the random order.
Expand Down
2 changes: 1 addition & 1 deletion crates/common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ mpp.workspace = true
foundry-wallets = { workspace = true, features = ["browser", "tempo"] }
tokio-tungstenite.workspace = true
futures.workspace = true
alloy-signer-local.workspace = true
alloy-signer-local = { workspace = true, features = ["mnemonic-all-languages"] }
base64.workspace = true
sha2 = "0.10"
tempfile.workspace = true
Expand Down
1 change: 1 addition & 0 deletions crates/common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ pub mod traits;
pub mod transactions;
mod utils;
pub mod version;
pub mod wallet;

pub use compile::Analysis;
pub use constants::*;
Expand Down
20 changes: 19 additions & 1 deletion crates/common/src/traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@ pub trait TestFunctionExt {
self.test_function_kind().is_invariant_test()
}

/// Returns `true` if this function is a symbolic test.
fn is_symbolic_test(&self) -> bool {
self.test_function_kind().is_symbolic_test()
}

/// Returns `true` if this function is an `afterInvariant` function.
fn is_after_invariant(&self) -> bool {
self.test_function_kind().is_after_invariant()
Expand Down Expand Up @@ -158,6 +163,8 @@ pub enum TestFunctionKind {
InvariantTest,
/// `table*`, with arguments.
TableTest,
/// `check*` or `prove*`, when selected by symbolic test mode.
SymbolicTest,
/// `afterInvariant`.
AfterInvariant,
/// `fixture*`.
Expand Down Expand Up @@ -199,6 +206,7 @@ impl TestFunctionKind {
Self::FuzzTest { should_fail: true } => "fuzz fail",
Self::InvariantTest => "invariant",
Self::TableTest => "table",
Self::SymbolicTest => "symbolic",
Self::AfterInvariant => "afterInvariant",
Self::Fixture => "fixture",
Self::Unknown => "unknown",
Expand All @@ -216,7 +224,11 @@ impl TestFunctionKind {
pub const fn is_any_test(&self) -> bool {
matches!(
self,
Self::UnitTest { .. } | Self::FuzzTest { .. } | Self::TableTest | Self::InvariantTest
Self::UnitTest { .. }
| Self::FuzzTest { .. }
| Self::TableTest
| Self::InvariantTest
| Self::SymbolicTest
)
}

Expand Down Expand Up @@ -250,6 +262,12 @@ impl TestFunctionKind {
matches!(self, Self::TableTest)
}

/// Returns `true` if this function is a symbolic test.
#[inline]
pub const fn is_symbolic_test(&self) -> bool {
matches!(self, Self::SymbolicTest)
}

/// Returns `true` if this function is an `afterInvariant` function.
#[inline]
pub const fn is_after_invariant(&self) -> bool {
Expand Down
68 changes: 68 additions & 0 deletions crates/common/src/wallet.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
use alloy_primitives::U256;
use alloy_signer_local::{
MnemonicBuilder, PrivateKeySigner,
coins_bip39::{
ChineseSimplified, ChineseTraditional, Czech, English, French, Italian, Japanese, Korean,
Portuguese, Spanish, Wordlist,
},
};

/// Appends `index` to `path`, inserting a `/` separator when needed.
pub fn derive_key_path(path: &str, index: u32) -> String {
let mut out = path.to_string();
if !out.ends_with('/') {
out.push('/');
}
out.push_str(&index.to_string());
out
}

/// Derives a private key from a BIP-39 mnemonic using the given BIP-32 path and index.
pub fn derive_private_key<W: Wordlist>(
mnemonic: &str,
path: &str,
index: u32,
) -> Result<U256, String> {
let wallet = MnemonicBuilder::<W>::default()
.phrase(mnemonic)
.derivation_path(derive_key_path(path, index))
.map_err(|e| e.to_string())?
.build()
.map_err(|e| e.to_string())?;
Ok(U256::from_be_bytes(wallet.credential().to_bytes().into()))
}

/// Derives a private key from a BIP-39 mnemonic, selecting the wordlist by name.
///
/// Recognised language names: `chinese_simplified`, `chinese_traditional`, `czech`, `english`,
/// `french`, `italian`, `japanese`, `korean`, `portuguese`, `spanish`.
pub fn derive_private_key_with_language(
mnemonic: &str,
path: &str,
index: u32,
language: &str,
) -> Result<U256, String> {
match language {
"chinese_simplified" => derive_private_key::<ChineseSimplified>(mnemonic, path, index),
"chinese_traditional" => derive_private_key::<ChineseTraditional>(mnemonic, path, index),
"czech" => derive_private_key::<Czech>(mnemonic, path, index),
"english" => derive_private_key::<English>(mnemonic, path, index),
"french" => derive_private_key::<French>(mnemonic, path, index),
"italian" => derive_private_key::<Italian>(mnemonic, path, index),
"japanese" => derive_private_key::<Japanese>(mnemonic, path, index),
"korean" => derive_private_key::<Korean>(mnemonic, path, index),
"portuguese" => derive_private_key::<Portuguese>(mnemonic, path, index),
"spanish" => derive_private_key::<Spanish>(mnemonic, path, index),
_ => Err(format!("unsupported mnemonic language: {language:?}")),
}
}

/// Constructs a [`PrivateKeySigner`] from a raw private key value.
///
/// Returns `Err` when `private_key` is zero or its bytes are not a valid secp256k1 scalar.
pub fn private_key_from_u256(private_key: U256) -> Result<PrivateKeySigner, String> {
if private_key.is_zero() {
return Err("private key cannot be zero".to_string());
}
PrivateKeySigner::from_slice(&private_key.to_be_bytes::<32>()).map_err(|e| e.to_string())
}
Loading
Loading