Skip to content

Commit 59f354c

Browse files
authored
fix(fuzz): exclude exernal libraries addresses from fuzz inputs (#9527)
1 parent 09894ef commit 59f354c

File tree

9 files changed

+98
-13
lines changed

9 files changed

+98
-13
lines changed

crates/evm/evm/src/executors/fuzz/mod.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,18 +77,20 @@ impl FuzzedExecutor {
7777
/// test case.
7878
///
7979
/// Returns a list of all the consumed gas and calldata of every fuzz case
80+
#[allow(clippy::too_many_arguments)]
8081
pub fn fuzz(
8182
&self,
8283
func: &Function,
8384
fuzz_fixtures: &FuzzFixtures,
85+
deployed_libs: &[Address],
8486
address: Address,
8587
should_fail: bool,
8688
rd: &RevertDecoder,
8789
progress: Option<&ProgressBar>,
8890
) -> FuzzTestResult {
8991
// Stores the fuzz test execution data.
9092
let execution_data = RefCell::new(FuzzTestData::default());
91-
let state = self.build_fuzz_state();
93+
let state = self.build_fuzz_state(deployed_libs);
9294
let dictionary_weight = self.config.dictionary.dictionary_weight.min(100);
9395
let strategy = proptest::prop_oneof![
9496
100 - dictionary_weight => fuzz_calldata(func.clone(), fuzz_fixtures),
@@ -274,11 +276,15 @@ impl FuzzedExecutor {
274276
}
275277

276278
/// Stores fuzz state for use with [fuzz_calldata_from_state]
277-
pub fn build_fuzz_state(&self) -> EvmFuzzState {
279+
pub fn build_fuzz_state(&self, deployed_libs: &[Address]) -> EvmFuzzState {
278280
if let Some(fork_db) = self.executor.backend().active_fork_db() {
279-
EvmFuzzState::new(fork_db, self.config.dictionary)
281+
EvmFuzzState::new(fork_db, self.config.dictionary, deployed_libs)
280282
} else {
281-
EvmFuzzState::new(self.executor.backend().mem_db(), self.config.dictionary)
283+
EvmFuzzState::new(
284+
self.executor.backend().mem_db(),
285+
self.config.dictionary,
286+
deployed_libs,
287+
)
282288
}
283289
}
284290
}

crates/evm/evm/src/executors/invariant/mod.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,7 @@ impl<'a> InvariantExecutor<'a> {
323323
&mut self,
324324
invariant_contract: InvariantContract<'_>,
325325
fuzz_fixtures: &FuzzFixtures,
326+
deployed_libs: &[Address],
326327
progress: Option<&ProgressBar>,
327328
) -> Result<InvariantFuzzTestResult> {
328329
// Throw an error to abort test run if the invariant function accepts input params
@@ -331,7 +332,7 @@ impl<'a> InvariantExecutor<'a> {
331332
}
332333

333334
let (invariant_test, invariant_strategy) =
334-
self.prepare_test(&invariant_contract, fuzz_fixtures)?;
335+
self.prepare_test(&invariant_contract, fuzz_fixtures, deployed_libs)?;
335336

336337
// Start timer for this invariant test.
337338
let timer = FuzzTestTimer::new(self.config.timeout);
@@ -506,15 +507,19 @@ impl<'a> InvariantExecutor<'a> {
506507
&mut self,
507508
invariant_contract: &InvariantContract<'_>,
508509
fuzz_fixtures: &FuzzFixtures,
510+
deployed_libs: &[Address],
509511
) -> Result<(InvariantTest, impl Strategy<Value = BasicTxDetails>)> {
510512
// Finds out the chosen deployed contracts and/or senders.
511513
self.select_contract_artifacts(invariant_contract.address)?;
512514
let (targeted_senders, targeted_contracts) =
513515
self.select_contracts_and_senders(invariant_contract.address)?;
514516

515517
// Stores fuzz state for use with [fuzz_calldata_from_state].
516-
let fuzz_state =
517-
EvmFuzzState::new(self.executor.backend().mem_db(), self.config.dictionary);
518+
let fuzz_state = EvmFuzzState::new(
519+
self.executor.backend().mem_db(),
520+
self.config.dictionary,
521+
deployed_libs,
522+
);
518523

519524
// Creates the invariant strategy.
520525
let strategy = invariant_strat(

crates/evm/fuzz/src/strategies/param.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,19 @@ pub fn fuzz_param_from_state(
130130
// Convert the value based on the parameter type
131131
match *param {
132132
DynSolType::Address => {
133-
value().prop_map(move |value| DynSolValue::Address(Address::from_word(value))).boxed()
133+
let deployed_libs = state.deployed_libs.clone();
134+
value()
135+
.prop_filter_map("filter address fuzzed from state", move |value| {
136+
let fuzzed_addr = Address::from_word(value);
137+
// Do not use addresses of deployed libraries as fuzz input.
138+
// See <https://github.com/foundry-rs/foundry/issues/8639>.
139+
if !deployed_libs.contains(&fuzzed_addr) {
140+
Some(DynSolValue::Address(fuzzed_addr))
141+
} else {
142+
None
143+
}
144+
})
145+
.boxed()
134146
}
135147
DynSolType::Function => value()
136148
.prop_map(move |value| {
@@ -217,7 +229,7 @@ mod tests {
217229
let f = "testArray(uint64[2] calldata values)";
218230
let func = get_func(f).unwrap();
219231
let db = CacheDB::new(EmptyDB::default());
220-
let state = EvmFuzzState::new(&db, FuzzDictionaryConfig::default());
232+
let state = EvmFuzzState::new(&db, FuzzDictionaryConfig::default(), &[]);
221233
let strategy = proptest::prop_oneof![
222234
60 => fuzz_calldata(func.clone(), &FuzzFixtures::default()),
223235
40 => fuzz_calldata_from_state(func, &state),

crates/evm/fuzz/src/strategies/state.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,24 @@ const PUSH_BYTE_ANALYSIS_LIMIT: usize = 24 * 1024;
2727
#[derive(Clone, Debug)]
2828
pub struct EvmFuzzState {
2929
inner: Arc<RwLock<FuzzDictionary>>,
30+
/// Addresses of external libraries deployed in test setup, excluded from fuzz test inputs.
31+
pub deployed_libs: Vec<Address>,
3032
}
3133

3234
impl EvmFuzzState {
33-
pub fn new<DB: DatabaseRef>(db: &CacheDB<DB>, config: FuzzDictionaryConfig) -> Self {
35+
pub fn new<DB: DatabaseRef>(
36+
db: &CacheDB<DB>,
37+
config: FuzzDictionaryConfig,
38+
deployed_libs: &[Address],
39+
) -> Self {
3440
// Sort accounts to ensure deterministic dictionary generation from the same setUp state.
3541
let mut accs = db.accounts.iter().collect::<Vec<_>>();
3642
accs.sort_by_key(|(address, _)| *address);
3743

3844
// Create fuzz dictionary and insert values from db state.
3945
let mut dictionary = FuzzDictionary::new(config);
4046
dictionary.insert_db_values(accs);
41-
Self { inner: Arc::new(RwLock::new(dictionary)) }
47+
Self { inner: Arc::new(RwLock::new(dictionary)), deployed_libs: deployed_libs.to_vec() }
4248
}
4349

4450
pub fn collect_values(&self, values: impl IntoIterator<Item = B256>) {

crates/forge/src/result.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -752,6 +752,8 @@ pub struct TestSetup {
752752
pub traces: Traces,
753753
/// Coverage info during setup.
754754
pub coverage: Option<HitMaps>,
755+
/// Addresses of external libraries deployed during setup.
756+
pub deployed_libs: Vec<Address>,
755757

756758
/// The reason the setup failed, if it did.
757759
pub reason: Option<String>,

crates/forge/src/runner.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,12 @@ impl<'a> ContractRunner<'a> {
130130
U256::ZERO,
131131
Some(&self.mcr.revert_decoder),
132132
);
133+
134+
// Record deployed library address.
135+
if let Ok(deployed) = &deploy_result {
136+
result.deployed_libs.push(deployed.address);
137+
}
138+
133139
let (raw, reason) = RawCallResult::from_evm_result(deploy_result.map(Into::into))?;
134140
result.extend(raw, TraceKind::Deployment);
135141
if reason.is_some() {
@@ -614,6 +620,7 @@ impl<'a> FunctionRunner<'a> {
614620
let invariant_result = match evm.invariant_fuzz(
615621
invariant_contract.clone(),
616622
&self.setup.fuzz_fixtures,
623+
&self.setup.deployed_libs,
617624
progress.as_ref(),
618625
) {
619626
Ok(x) => x,
@@ -728,6 +735,7 @@ impl<'a> FunctionRunner<'a> {
728735
let result = fuzzed_executor.fuzz(
729736
func,
730737
&self.setup.fuzz_fixtures,
738+
&self.setup.deployed_libs,
731739
self.address,
732740
should_fail,
733741
&self.cr.mcr.revert_decoder,

crates/forge/tests/it/core.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -742,8 +742,8 @@ async fn test_trace() {
742742

743743
assert_eq!(
744744
deployment_traces.count(),
745-
12,
746-
"Test {test_name} did not have exactly 12 deployment trace."
745+
13,
746+
"Test {test_name} did not have exactly 13 deployment trace."
747747
);
748748
assert!(setup_traces.count() <= 1, "Test {test_name} had more than 1 setup trace.");
749749
assert_eq!(

crates/forge/tests/it/repros.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,3 +386,6 @@ test_repro!(8971; |config| {
386386
prj_config.isolate = true;
387387
config.runner.config = Arc::new(prj_config);
388388
});
389+
390+
// https://github.com/foundry-rs/foundry/issues/8639
391+
test_repro!(8639);
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// SPDX-License-Identifier: MIT OR Apache-2.0
2+
pragma solidity ^0.8.18;
3+
4+
import "ds-test/test.sol";
5+
6+
library ExternalLibrary {
7+
function doWork(uint256 a) public returns (uint256) {
8+
return a++;
9+
}
10+
}
11+
12+
contract Counter {
13+
uint256 public number;
14+
15+
function setNumber(uint256 newNumber) public {
16+
ExternalLibrary.doWork(1);
17+
}
18+
19+
function increment() public {}
20+
}
21+
22+
// https://github.com/foundry-rs/foundry/issues/8639
23+
contract Issue8639Test is DSTest {
24+
Counter counter;
25+
26+
function setUp() public {
27+
counter = new Counter();
28+
}
29+
30+
/// forge-config: default.fuzz.runs = 1000
31+
/// forge-config: default.fuzz.seed = '100'
32+
function test_external_library_address(address test) public {
33+
require(test != address(ExternalLibrary));
34+
}
35+
}
36+
37+
contract Issue8639AnotherTest is DSTest {
38+
/// forge-config: default.fuzz.runs = 1000
39+
/// forge-config: default.fuzz.seed = '100'
40+
function test_another_external_library_address(address test) public {
41+
require(test != address(ExternalLibrary));
42+
}
43+
}

0 commit comments

Comments
 (0)