-
Notifications
You must be signed in to change notification settings - Fork 2.5k
Expand file tree
/
Copy pathinvariants.rs
More file actions
173 lines (158 loc) · 7.06 KB
/
invariants.rs
File metadata and controls
173 lines (158 loc) · 7.06 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
use super::{fuzz_calldata, fuzz_msg_value, fuzz_param_from_state};
use crate::{
BasicTxDetails, CallDetails, FuzzFixtures,
invariant::{FuzzRunIdentifiedContracts, SenderFilters},
strategies::{EvmFuzzState, fuzz_calldata_from_state, fuzz_param},
};
use alloy_json_abi::Function;
use alloy_primitives::{Address, U256};
use foundry_config::InvariantConfig;
use parking_lot::RwLock;
use proptest::prelude::*;
use rand::seq::IteratorRandom;
use std::{rc::Rc, sync::Arc};
/// Given a target address, we generate random calldata.
pub fn override_call_strat(
fuzz_state: EvmFuzzState,
contracts: FuzzRunIdentifiedContracts,
target: Arc<RwLock<Address>>,
fuzz_fixtures: FuzzFixtures,
) -> impl Strategy<Value = CallDetails> + Send + Sync + 'static {
let contracts_ref = contracts.targets.clone();
proptest::prop_oneof![
80 => proptest::strategy::LazyJust::new(move || *target.read()),
20 => any::<prop::sample::Selector>()
.prop_map(move |selector| *selector.select(contracts_ref.lock().keys())),
]
.prop_flat_map(move |target_address| {
let fuzz_state = fuzz_state.clone();
let fuzz_fixtures = fuzz_fixtures.clone();
let (actual_target, func) = {
let contracts = contracts.targets.lock();
// If the target address is in the contracts map, use it directly.
// Otherwise, fall back to a random contract from the targeted contracts.
// This can happen when call_override sets target_reference to a contract
// that is not in targetContracts (e.g., the protocol contract during reentrancy).
let (actual_target, contract) =
contracts.get(&target_address).map(|c| (target_address, c)).unwrap_or_else(|| {
let entry = contracts
.iter()
.choose(&mut rand::rng())
.expect("at least one target contract");
(*entry.0, entry.1)
});
let fuzzed_functions: Vec<_> = contract.abi_fuzzed_functions().cloned().collect();
(
actual_target,
any::<prop::sample::Index>()
.prop_map(move |index| index.get(&fuzzed_functions).clone()),
)
};
func.prop_flat_map(move |func| {
fuzz_contract_with_calldata(&fuzz_state, &fuzz_fixtures, actual_target, func)
})
})
}
/// Creates the invariant strategy.
///
/// Given the known and future contracts, it generates the next call by fuzzing the `caller`,
/// `calldata` and `target`. The generated data is evaluated lazily for every single call to fully
/// leverage the evolving fuzz dictionary.
///
/// The fuzzed parameters can be filtered through different methods implemented in the test
/// contract:
///
/// `targetContracts()`, `targetSenders()`, `excludeContracts()`, `targetSelectors()`
pub fn invariant_strat(
fuzz_state: EvmFuzzState,
senders: SenderFilters,
contracts: FuzzRunIdentifiedContracts,
config: InvariantConfig,
fuzz_fixtures: FuzzFixtures,
) -> impl Strategy<Value = BasicTxDetails> {
let senders = Rc::new(senders);
let dictionary_weight = config.dictionary.dictionary_weight;
// Strategy to generate values for tx warp and roll.
let warp_roll_strat = |cond: bool| {
if cond { any::<U256>().prop_map(Some).boxed() } else { Just(None).boxed() }
};
any::<prop::sample::Selector>()
.prop_flat_map(move |selector| {
let contracts = contracts.targets.lock();
let functions = contracts.fuzzed_functions();
let (target_address, target_function) = selector.select(functions);
let sender = select_random_sender(&fuzz_state, senders.clone(), dictionary_weight);
let call_details = fuzz_contract_with_calldata(
&fuzz_state,
&fuzz_fixtures,
*target_address,
target_function.clone(),
);
let warp = warp_roll_strat(config.max_time_delay.is_some());
let roll = warp_roll_strat(config.max_block_delay.is_some());
(warp, roll, sender, call_details)
})
.prop_map(move |(warp, roll, sender, call_details)| {
let warp =
warp.map(|time| time % U256::from(config.max_time_delay.unwrap_or_default()));
let roll =
roll.map(|block| block % U256::from(config.max_block_delay.unwrap_or_default()));
BasicTxDetails { warp, roll, sender, call_details }
})
}
/// Strategy to select a sender address:
/// * If `senders` is empty, then it's either a random address (10%) or from the dictionary (90%).
/// * If `senders` is not empty, a random address is chosen from the list of senders.
fn select_random_sender(
fuzz_state: &EvmFuzzState,
senders: Rc<SenderFilters>,
dictionary_weight: u32,
) -> impl Strategy<Value = Address> + use<> {
if !senders.targeted.is_empty() {
any::<prop::sample::Index>().prop_map(move |index| *index.get(&senders.targeted)).boxed()
} else {
assert!(dictionary_weight <= 100, "dictionary_weight must be <= 100");
proptest::prop_oneof![
100 - dictionary_weight => fuzz_param(&alloy_dyn_abi::DynSolType::Address),
dictionary_weight => fuzz_param_from_state(&alloy_dyn_abi::DynSolType::Address, fuzz_state),
]
.prop_map(move |addr| {
let mut addr = addr.as_address().unwrap();
// Make sure the selected address is not in the list of excluded senders.
// We don't use proptest's filter to avoid reaching the `PROPTEST_MAX_LOCAL_REJECTS`
// max rejects and exiting test before all runs completes.
// See <https://github.com/foundry-rs/foundry/issues/11369>.
loop {
if !senders.excluded.contains(&addr) {
break;
}
addr = Address::random();
}
addr
})
.boxed()
}
}
/// Given a function, it returns a proptest strategy which generates valid abi-encoded calldata
/// for that function's input types.
pub fn fuzz_contract_with_calldata(
fuzz_state: &EvmFuzzState,
fuzz_fixtures: &FuzzFixtures,
target: Address,
func: Function,
) -> impl Strategy<Value = CallDetails> + use<> {
let is_payable = func.state_mutability == alloy_json_abi::StateMutability::Payable;
// We need to compose all the strategies generated for each parameter in all possible
// combinations.
// `prop_oneof!` / `TupleUnion` `Arc`s for cheap cloning.
let calldata_strategy = prop_oneof![
60 => fuzz_calldata(func.clone(), fuzz_fixtures),
40 => fuzz_calldata_from_state(func, fuzz_state),
];
// For payable functions, generate random value using shared strategy.
let value_strategy = if is_payable { fuzz_msg_value().boxed() } else { Just(None).boxed() };
(calldata_strategy, value_strategy).prop_map(move |(calldata, value)| {
trace!(input=?calldata, ?value);
CallDetails { target, calldata, value }
})
}