Skip to content

Commit fb9a8bc

Browse files
authored
Limit uint to u64::MAX for fuzz tests (#507)
1 parent 5f69b6f commit fb9a8bc

File tree

16 files changed

+314
-44
lines changed

16 files changed

+314
-44
lines changed

crates/config/src/fuzz.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ pub struct FuzzConfig {
3232
pub show_logs: bool,
3333
/// Optional timeout (in seconds) for each property test
3434
pub timeout: Option<u32>,
35+
/// Maximum value for fuzzed integers, used to simulate smaller integer types.
36+
/// When set, unsigned integers are clamped to [0, max_fuzz_int] and signed integers
37+
/// are clamped to [-(max_fuzz_int+1), max_fuzz_int] to match real signed type ranges.
38+
/// Useful for Polkadot compatibility where balances are u128.
39+
#[serde(default, skip_serializing_if = "Option::is_none")]
40+
pub max_fuzz_int: Option<U256>,
3541
}
3642

3743
impl Default for FuzzConfig {
@@ -47,6 +53,7 @@ impl Default for FuzzConfig {
4753
failure_persist_file: None,
4854
show_logs: false,
4955
timeout: None,
56+
max_fuzz_int: None,
5057
}
5158
}
5259
}

crates/config/src/invariant.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! Configuration for invariant testing
22
33
use crate::fuzz::FuzzDictionaryConfig;
4+
use alloy_primitives::U256;
45
use serde::{Deserialize, Serialize};
56
use std::path::PathBuf;
67

@@ -45,6 +46,12 @@ pub struct InvariantConfig {
4546
pub show_solidity: bool,
4647
/// Whether to collect and display edge coverage metrics.
4748
pub show_edge_coverage: bool,
49+
/// Maximum value for fuzzed integers, used to simulate smaller integer types.
50+
/// When set, unsigned integers are clamped to [0, max_fuzz_int] and signed integers
51+
/// are clamped to [-(max_fuzz_int+1), max_fuzz_int] to match real signed type ranges.
52+
/// Used for Polkadot compatibility where balances are u128.
53+
#[serde(default, skip_serializing_if = "Option::is_none")]
54+
pub max_fuzz_int: Option<U256>,
4855
}
4956

5057
impl Default for InvariantConfig {
@@ -67,6 +74,7 @@ impl Default for InvariantConfig {
6774
timeout: None,
6875
show_solidity: false,
6976
show_edge_coverage: false,
77+
max_fuzz_int: None,
7078
}
7179
}
7280
}
@@ -92,6 +100,7 @@ impl InvariantConfig {
92100
timeout: None,
93101
show_solidity: false,
94102
show_edge_coverage: false,
103+
max_fuzz_int: None,
95104
}
96105
}
97106
}

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,10 @@ impl FuzzedExecutor {
9090
let execution_data = RefCell::new(FuzzTestData::default());
9191
let state = self.build_fuzz_state(deployed_libs);
9292
let dictionary_weight = self.config.dictionary.dictionary_weight.min(100);
93+
let max_fuzz_int = self.config.max_fuzz_int;
9394
let strategy = proptest::prop_oneof![
94-
100 - dictionary_weight => fuzz_calldata(func.clone(), fuzz_fixtures),
95-
dictionary_weight => fuzz_calldata_from_state(func.clone(), &state),
95+
100 - dictionary_weight => fuzz_calldata(func.clone(), fuzz_fixtures, max_fuzz_int),
96+
dictionary_weight => fuzz_calldata_from_state(func.clone(), &state, max_fuzz_int),
9697
];
9798
// We want to collect at least one trace which will be displayed to user.
9899
let max_traces_to_collect = std::cmp::max(1, self.config.gas_report_samples) as usize;

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,9 @@ pub struct TxCorpusManager {
152152
failed_replays: usize,
153153
// Corpus metrics.
154154
pub(crate) metrics: CorpusMetrics,
155+
// Maximum value for fuzzed integers, used to simulate smaller integer types.
156+
// Unsigned: [0, max], Signed: [-(max+1), max].
157+
max_fuzz_int: Option<U256>,
155158
}
156159

157160
impl TxCorpusManager {
@@ -176,6 +179,7 @@ impl TxCorpusManager {
176179
let corpus_gzip = invariant_config.corpus_gzip;
177180
let corpus_min_mutations = invariant_config.corpus_min_mutations;
178181
let corpus_min_size = invariant_config.corpus_min_size;
182+
let max_fuzz_int = invariant_config.max_fuzz_int;
179183
let mut failed_replays = 0;
180184

181185
// Early return if corpus dir / coverage guided fuzzing not configured.
@@ -191,6 +195,7 @@ impl TxCorpusManager {
191195
current_mutated: None,
192196
failed_replays,
193197
metrics: CorpusMetrics::default(),
198+
max_fuzz_int,
194199
});
195200
};
196201

@@ -273,6 +278,7 @@ impl TxCorpusManager {
273278
current_mutated: None,
274279
failed_replays,
275280
metrics,
281+
max_fuzz_int,
276282
})
277283
}
278284

@@ -495,10 +501,12 @@ impl TxCorpusManager {
495501
.expect("fuzzed_artifacts returned wrong sig");
496502
// For now, only new inputs are generated, no existing inputs are
497503
// mutated.
504+
let max_fuzz_int = self.max_fuzz_int;
498505
let mut gen_input = |input: &alloy_json_abi::Param| {
499506
fuzz_param_from_state(
500507
&input.selector_type().parse().unwrap(),
501508
&test.fuzz_state,
509+
max_fuzz_int,
502510
)
503511
.new_tree(test_runner)
504512
.expect("Could not generate case")

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -597,6 +597,7 @@ impl<'a> InvariantExecutor<'a> {
597597
targeted_contracts.clone(),
598598
self.config.dictionary.dictionary_weight,
599599
fuzz_fixtures.clone(),
600+
self.config.max_fuzz_int,
600601
)
601602
.no_shrink();
602603

@@ -614,6 +615,7 @@ impl<'a> InvariantExecutor<'a> {
614615
targeted_contracts.clone(),
615616
target_contract_ref.clone(),
616617
fuzz_fixtures.clone(),
618+
self.config.max_fuzz_int,
617619
),
618620
target_contract_ref,
619621
));

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@ use crate::{
44
};
55
use alloy_dyn_abi::JsonAbiExt;
66
use alloy_json_abi::Function;
7-
use alloy_primitives::Bytes;
7+
use alloy_primitives::{Bytes, U256};
88
use proptest::prelude::Strategy;
99

1010
/// Given a function, it returns a strategy which generates valid calldata
1111
/// for that function's input types, following declared test fixtures.
1212
pub fn fuzz_calldata(
1313
func: Function,
1414
fuzz_fixtures: &FuzzFixtures,
15+
max_fuzz_int: Option<U256>,
1516
) -> impl Strategy<Value = Bytes> + use<> {
1617
// We need to compose all the strategies generated for each parameter in all
1718
// possible combinations, accounting any parameter declared fixture
@@ -23,6 +24,7 @@ pub fn fuzz_calldata(
2324
&input.selector_type().parse().unwrap(),
2425
fuzz_fixtures.param_fixtures(&input.name),
2526
&input.name,
27+
max_fuzz_int,
2628
)
2729
})
2830
.collect::<Vec<_>>();
@@ -43,11 +45,14 @@ pub fn fuzz_calldata(
4345
pub fn fuzz_calldata_from_state(
4446
func: Function,
4547
state: &EvmFuzzState,
48+
max_fuzz_int: Option<U256>,
4649
) -> impl Strategy<Value = Bytes> + use<> {
4750
let strats = func
4851
.inputs
4952
.iter()
50-
.map(|input| fuzz_param_from_state(&input.selector_type().parse().unwrap(), state))
53+
.map(|input| {
54+
fuzz_param_from_state(&input.selector_type().parse().unwrap(), state, max_fuzz_int)
55+
})
5156
.collect::<Vec<_>>();
5257
strats
5358
.prop_map(move |values| {
@@ -83,7 +88,7 @@ mod tests {
8388
);
8489

8590
let expected = function.abi_encode_input(&[address_fixture]).unwrap();
86-
let strategy = fuzz_calldata(function, &FuzzFixtures::new(fixtures));
91+
let strategy = fuzz_calldata(function, &FuzzFixtures::new(fixtures), None);
8792
let _ = strategy.prop_map(move |fuzzed| {
8893
assert_eq!(expected, fuzzed);
8994
});

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

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,20 @@ use proptest::{
66
test_runner::TestRunner,
77
};
88

9+
/// Clamps a signed integer to the range [-(max+1), max] to match real signed type ranges.
10+
/// For example, i128 range is [-2^127, 2^127-1], not [-2^127+1, 2^127-1].
11+
pub fn clamp(value: I256, max: U256) -> I256 {
12+
let max_i256 = I256::from_raw(max);
13+
let min_i256 = I256::overflowing_from_sign_and_abs(Sign::Negative, max + U256::from(1)).0;
14+
if value > max_i256 {
15+
max_i256
16+
} else if value < min_i256 {
17+
min_i256
18+
} else {
19+
value
20+
}
21+
}
22+
923
/// Value tree for signed ints (up to int256).
1024
pub struct IntValueTree {
1125
/// Lower base (by absolute value)
@@ -103,28 +117,40 @@ pub struct IntStrategy {
103117
fixtures_weight: usize,
104118
/// The weight for purely random values
105119
random_weight: usize,
120+
/// Optional maximum value for generated integers, used to simulate smaller signed types.
121+
/// When set, generated values will be clamped to [-(max_value+1), max_value] to match
122+
/// real signed integer type ranges (e.g., i128 range is [-2^127, 2^127-1]).
123+
max_value: Option<U256>,
106124
}
107125

108126
impl IntStrategy {
109127
/// Create a new strategy.
110-
/// #Arguments
111-
/// * `bits` - Size of uint in bits
128+
/// # Arguments
129+
/// * `bits` - Size of int in bits
112130
/// * `fixtures` - A set of fixed values to be generated (according to fixtures weight)
113-
pub fn new(bits: usize, fixtures: Option<&[DynSolValue]>) -> Self {
131+
/// * `max_value` - Optional maximum value to simulate smaller signed types. Values will be
132+
/// clamped to [-(max_value+1), max_value].
133+
pub fn new(bits: usize, fixtures: Option<&[DynSolValue]>, max_value: Option<U256>) -> Self {
114134
Self {
115135
bits,
116136
fixtures: Vec::from(fixtures.unwrap_or_default()),
117137
edge_weight: 10usize,
118138
fixtures_weight: 40usize,
119139
random_weight: 50usize,
140+
max_value,
120141
}
121142
}
122143

144+
fn effective_max(&self) -> U256 {
145+
let type_max: U256 = (U256::from(1) << (self.bits - 1)) - U256::from(1);
146+
self.max_value.map(|m| type_max.min(m)).unwrap_or(type_max)
147+
}
148+
123149
fn generate_edge_tree(&self, runner: &mut TestRunner) -> NewTree<Self> {
124150
let rng = runner.rng();
125151

126152
let offset = I256::from_raw(U256::from(rng.random_range(0..4)));
127-
let umax: U256 = (U256::from(1) << (self.bits - 1)) - U256::from(1);
153+
let umax = self.effective_max();
128154
// Choose if we want values around min, -0, +0, or max
129155
let kind = rng.random_range(0..4);
130156
let start = match kind {
@@ -136,7 +162,7 @@ impl IntStrategy {
136162
3 => I256::overflowing_from_sign_and_abs(Sign::Positive, umax).0 - offset,
137163
_ => unreachable!(),
138164
};
139-
Ok(IntValueTree::new(start, false))
165+
Ok(IntValueTree::new(clamp(start, self.effective_max()), false))
140166
}
141167

142168
fn generate_fixtures_tree(&self, runner: &mut TestRunner) -> NewTree<Self> {
@@ -150,7 +176,7 @@ impl IntStrategy {
150176
if let Some(int_fixture) = fixture.as_int()
151177
&& int_fixture.1 == self.bits
152178
{
153-
return Ok(IntValueTree::new(int_fixture.0, false));
179+
return Ok(IntValueTree::new(clamp(int_fixture.0, self.effective_max()), false));
154180
}
155181

156182
// If fixture is not a valid type, raise error and generate random value.
@@ -195,7 +221,7 @@ impl IntStrategy {
195221
let sign = if rng.random::<bool>() { Sign::Positive } else { Sign::Negative };
196222
let (start, _) = I256::overflowing_from_sign_and_abs(sign, U256::from_limbs(inner));
197223

198-
Ok(IntValueTree::new(start, false))
224+
Ok(IntValueTree::new(clamp(start, self.effective_max()), false))
199225
}
200226
}
201227

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

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use crate::{
55
strategies::{EvmFuzzState, fuzz_calldata_from_state, fuzz_param},
66
};
77
use alloy_json_abi::Function;
8-
use alloy_primitives::Address;
8+
use alloy_primitives::{Address, U256};
99
use parking_lot::RwLock;
1010
use proptest::prelude::*;
1111
use rand::seq::IteratorRandom;
@@ -17,6 +17,7 @@ pub fn override_call_strat(
1717
contracts: FuzzRunIdentifiedContracts,
1818
target: Arc<RwLock<Address>>,
1919
fuzz_fixtures: FuzzFixtures,
20+
max_fuzz_int: Option<U256>,
2021
) -> impl Strategy<Value = CallDetails> + Send + Sync + 'static {
2122
let contracts_ref = contracts.targets.clone();
2223
proptest::prop_oneof![
@@ -41,7 +42,13 @@ pub fn override_call_strat(
4142
};
4243

4344
func.prop_flat_map(move |func| {
44-
fuzz_contract_with_calldata(&fuzz_state, &fuzz_fixtures, target_address, func)
45+
fuzz_contract_with_calldata(
46+
&fuzz_state,
47+
&fuzz_fixtures,
48+
target_address,
49+
func,
50+
max_fuzz_int,
51+
)
4552
})
4653
})
4754
}
@@ -62,19 +69,22 @@ pub fn invariant_strat(
6269
contracts: FuzzRunIdentifiedContracts,
6370
dictionary_weight: u32,
6471
fuzz_fixtures: FuzzFixtures,
72+
max_fuzz_int: Option<U256>,
6573
) -> impl Strategy<Value = BasicTxDetails> {
6674
let senders = Rc::new(senders);
6775
any::<prop::sample::Selector>()
6876
.prop_flat_map(move |selector| {
6977
let contracts = contracts.targets.lock();
7078
let functions = contracts.fuzzed_functions();
7179
let (target_address, target_function) = selector.select(functions);
72-
let sender = select_random_sender(&fuzz_state, senders.clone(), dictionary_weight);
80+
let sender =
81+
select_random_sender(&fuzz_state, senders.clone(), dictionary_weight, max_fuzz_int);
7382
let call_details = fuzz_contract_with_calldata(
7483
&fuzz_state,
7584
&fuzz_fixtures,
7685
*target_address,
7786
target_function.clone(),
87+
max_fuzz_int,
7888
);
7989
(sender, call_details)
8090
})
@@ -88,14 +98,15 @@ fn select_random_sender(
8898
fuzz_state: &EvmFuzzState,
8999
senders: Rc<SenderFilters>,
90100
dictionary_weight: u32,
101+
max_fuzz_int: Option<U256>,
91102
) -> impl Strategy<Value = Address> + use<> {
92103
if !senders.targeted.is_empty() {
93104
any::<prop::sample::Index>().prop_map(move |index| *index.get(&senders.targeted)).boxed()
94105
} else {
95106
assert!(dictionary_weight <= 100, "dictionary_weight must be <= 100");
96107
proptest::prop_oneof![
97-
100 - dictionary_weight => fuzz_param(&alloy_dyn_abi::DynSolType::Address),
98-
dictionary_weight => fuzz_param_from_state(&alloy_dyn_abi::DynSolType::Address, fuzz_state),
108+
100 - dictionary_weight => fuzz_param(&alloy_dyn_abi::DynSolType::Address, max_fuzz_int),
109+
dictionary_weight => fuzz_param_from_state(&alloy_dyn_abi::DynSolType::Address, fuzz_state, max_fuzz_int),
99110
]
100111
.prop_map(move |addr| {
101112
let mut addr = addr.as_address().unwrap();
@@ -122,13 +133,14 @@ pub fn fuzz_contract_with_calldata(
122133
fuzz_fixtures: &FuzzFixtures,
123134
target: Address,
124135
func: Function,
136+
max_fuzz_int: Option<U256>,
125137
) -> impl Strategy<Value = CallDetails> + use<> {
126138
// We need to compose all the strategies generated for each parameter in all possible
127139
// combinations.
128140
// `prop_oneof!` / `TupleUnion` `Arc`s for cheap cloning.
129141
prop_oneof![
130-
60 => fuzz_calldata(func.clone(), fuzz_fixtures),
131-
40 => fuzz_calldata_from_state(func, fuzz_state),
142+
60 => fuzz_calldata(func.clone(), fuzz_fixtures, max_fuzz_int),
143+
40 => fuzz_calldata_from_state(func, fuzz_state, max_fuzz_int),
132144
]
133145
.prop_map(move |calldata| {
134146
trace!(input=?calldata);

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
mod int;
2-
pub use int::IntStrategy;
2+
pub use int::{IntStrategy, clamp};
33

44
mod uint;
55
pub use uint::UintStrategy;

0 commit comments

Comments
 (0)