Skip to content

Commit 0584a58

Browse files
authored
feat(forge): warp and roll on invariant tx (#12616)
* feat(forge): warp and roll on invariant tx * Target test
1 parent a949348 commit 0584a58

File tree

12 files changed

+269
-60
lines changed

12 files changed

+269
-60
lines changed

crates/config/src/invariant.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ pub struct InvariantConfig {
3737
pub timeout: Option<u32>,
3838
/// Display counterexample as solidity calls.
3939
pub show_solidity: bool,
40+
/// Maximum time (in seconds) between generated txs.
41+
pub max_time_delay: Option<u32>,
42+
/// Maximum number of blocks elapsed between generated txs.
43+
pub max_block_delay: Option<u32>,
4044
}
4145

4246
impl Default for InvariantConfig {
@@ -55,6 +59,8 @@ impl Default for InvariantConfig {
5559
show_metrics: true,
5660
timeout: None,
5761
show_solidity: false,
62+
max_time_delay: None,
63+
max_block_delay: None,
5864
}
5965
}
6066
}

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

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
use crate::executors::{Executor, RawCallResult};
1+
use crate::executors::{Executor, RawCallResult, invariant::execute_tx};
22
use alloy_dyn_abi::JsonAbiExt;
33
use alloy_json_abi::Function;
4-
use alloy_primitives::{Bytes, U256};
4+
use alloy_primitives::Bytes;
55
use eyre::eyre;
66
use foundry_config::FuzzCorpusConfig;
77
use foundry_evm_fuzz::{
@@ -225,15 +225,7 @@ impl CorpusManager {
225225
let mut executor = executor.clone();
226226
for tx in &tx_seq {
227227
if can_replay_tx(tx) {
228-
let mut call_result = executor
229-
.call_raw(
230-
tx.sender,
231-
tx.call_details.target,
232-
tx.call_details.calldata.clone(),
233-
U256::ZERO,
234-
)
235-
.map_err(|e| eyre!(format!("Could not make raw evm call: {e}")))?;
236-
228+
let mut call_result = execute_tx(&mut executor, tx)?;
237229
let (new_coverage, is_edge) =
238230
call_result.merge_edge_coverage(&mut history_map);
239231
if new_coverage {
@@ -648,6 +640,8 @@ mod tests {
648640

649641
fn basic_tx() -> BasicTxDetails {
650642
BasicTxDetails {
643+
warp: None,
644+
roll: None,
651645
sender: Address::ZERO,
652646
call_details: foundry_evm_fuzz::CallDetails {
653647
target: Address::ZERO,

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,8 @@ impl FuzzedExecutor {
113113
dictionary_weight => fuzz_calldata_from_state(func.clone(), &state),
114114
]
115115
.prop_map(move |calldata| BasicTxDetails {
116+
warp: None,
117+
roll: None,
116118
sender: Default::default(),
117119
call_details: CallDetails { target: Default::default(), calldata },
118120
});
@@ -321,6 +323,8 @@ impl FuzzedExecutor {
321323
let new_coverage = coverage_metrics.merge_edge_coverage(&mut call);
322324
coverage_metrics.process_inputs(
323325
&[BasicTxDetails {
326+
warp: None,
327+
roll: None,
324328
sender: self.sender,
325329
call_details: CallDetails { target: address, calldata: calldata.clone() },
326330
}],

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

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -395,16 +395,7 @@ impl<'a> InvariantExecutor<'a> {
395395

396396
// Execute call from the randomly generated sequence without committing state.
397397
// State is committed only if call is not a magic assume.
398-
let mut call_result = current_run
399-
.executor
400-
.call_raw(
401-
tx.sender,
402-
tx.call_details.target,
403-
tx.call_details.calldata.clone(),
404-
U256::ZERO,
405-
)
406-
.map_err(|e| eyre!(format!("Could not make raw evm call: {e}")))?;
407-
398+
let mut call_result = execute_tx(&mut current_run.executor, tx)?;
408399
let discarded = call_result.result.as_ref() == MAGIC_ASSUME;
409400
if self.config.show_metrics {
410401
invariant_test.record_metrics(tx, call_result.reverted, discarded);
@@ -583,7 +574,7 @@ impl<'a> InvariantExecutor<'a> {
583574
fuzz_state.clone(),
584575
targeted_senders,
585576
targeted_contracts.clone(),
586-
self.config.dictionary.dictionary_weight,
577+
self.config.clone(),
587578
fuzz_fixtures.clone(),
588579
)
589580
.no_shrink();
@@ -1037,3 +1028,29 @@ pub(crate) fn call_invariant_function(
10371028
let success = executor.is_raw_call_mut_success(address, &mut call_result, false);
10381029
Ok((call_result, success))
10391030
}
1031+
1032+
/// Calls the invariant selector and returns call result and if succeeded.
1033+
/// Updates the block number and block timestamp if configured.
1034+
pub(crate) fn execute_tx(executor: &mut Executor, tx: &BasicTxDetails) -> Result<RawCallResult> {
1035+
// Apply pre-call block adjustments.
1036+
if let Some(warp) = tx.warp {
1037+
executor.env_mut().evm_env.block_env.timestamp += warp;
1038+
}
1039+
if let Some(roll) = tx.roll {
1040+
executor.env_mut().evm_env.block_env.number += roll;
1041+
}
1042+
1043+
// Perform the raw call.
1044+
let mut call_result = executor
1045+
.call_raw(tx.sender, tx.call_details.target, tx.call_details.calldata.clone(), U256::ZERO)
1046+
.map_err(|e| eyre!(format!("Could not make raw evm call: {e}")))?;
1047+
1048+
// Propagate block adjustments to call result which will be committed.
1049+
if let Some(warp) = tx.warp {
1050+
call_result.env.evm_env.block_env.timestamp += warp;
1051+
}
1052+
if let Some(roll) = tx.roll {
1053+
call_result.env.evm_env.block_env.number += roll;
1054+
}
1055+
Ok(call_result)
1056+
}

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

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
use super::{call_after_invariant_function, call_invariant_function};
1+
use super::{call_after_invariant_function, call_invariant_function, execute_tx};
22
use crate::executors::{EarlyExit, Executor, invariant::shrink::shrink_sequence};
33
use alloy_dyn_abi::JsonAbiExt;
4-
use alloy_primitives::{Log, U256, map::HashMap};
4+
use alloy_primitives::{Log, map::HashMap};
55
use eyre::Result;
66
use foundry_common::{ContractsByAddress, ContractsByArtifact};
77
use foundry_config::InvariantConfig;
@@ -36,13 +36,7 @@ pub fn replay_run(
3636

3737
// Replay each call from the sequence, collect logs, traces and coverage.
3838
for tx in inputs {
39-
let call_result = executor.transact_raw(
40-
tx.sender,
41-
tx.call_details.target,
42-
tx.call_details.calldata.clone(),
43-
U256::ZERO,
44-
)?;
45-
39+
let call_result = execute_tx(&mut executor, tx)?;
4640
logs.extend(call_result.logs);
4741
traces.push((TraceKind::Execution, call_result.traces.clone().unwrap()));
4842
HitMaps::merge_opt(line_coverage, call_result.line_coverage);
@@ -53,9 +47,7 @@ pub fn replay_run(
5347

5448
// Create counter example to be used in failed case.
5549
counterexample_sequence.push(BaseCounterExample::from_invariant_call(
56-
tx.sender,
57-
tx.call_details.target,
58-
&tx.call_details.calldata,
50+
tx,
5951
&ided_contracts,
6052
call_result.traces,
6153
show_solidity,

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

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
use crate::executors::{
22
EarlyExit, Executor,
3-
invariant::{call_after_invariant_function, call_invariant_function},
3+
invariant::{call_after_invariant_function, call_invariant_function, execute_tx},
44
};
5-
use alloy_primitives::{Address, Bytes, U256};
5+
use alloy_primitives::{Address, Bytes};
66
use foundry_config::InvariantConfig;
77
use foundry_evm_core::constants::MAGIC_ASSUME;
88
use foundry_evm_fuzz::{BasicTxDetails, invariant::InvariantContract};
@@ -118,12 +118,8 @@ pub fn check_sequence(
118118
// Apply the call sequence.
119119
for call_index in sequence {
120120
let tx = &calls[call_index];
121-
let call_result = executor.transact_raw(
122-
tx.sender,
123-
tx.call_details.target,
124-
tx.call_details.calldata.clone(),
125-
U256::ZERO,
126-
)?;
121+
let mut call_result = execute_tx(&mut executor, tx)?;
122+
executor.commit(&mut call_result);
127123
// Ignore calls reverted with `MAGIC_ASSUME`. This is needed to handle failed scenarios that
128124
// are replayed with a modified version of test driver (that use new `vm.assume`
129125
// cheatcodes).

crates/evm/fuzz/src/invariant/call_override.rs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,9 @@ impl RandomCallGenerator {
7575
*self.target_reference.write() = original_caller;
7676

7777
// `original_caller` has a 80% chance of being the `new_target`.
78-
let choice = self
79-
.strategy
80-
.new_tree(&mut self.runner.lock())
81-
.unwrap()
82-
.current()
83-
.map(|call_details| BasicTxDetails { sender, call_details });
78+
let choice = self.strategy.new_tree(&mut self.runner.lock()).unwrap().current().map(
79+
|call_details| BasicTxDetails { warp: None, roll: None, sender, call_details },
80+
);
8481

8582
self.last_sequence.write().push(choice.clone());
8683
choice

crates/evm/fuzz/src/lib.rs

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ extern crate tracing;
1010

1111
use alloy_dyn_abi::{DynSolValue, JsonAbiExt};
1212
use alloy_primitives::{
13-
Address, Bytes, Log,
13+
Address, Bytes, Log, U256,
1414
map::{AddressHashMap, HashMap},
1515
};
1616
use foundry_common::{calc, contracts::ContractsByAddress};
@@ -36,6 +36,10 @@ pub use inspector::Fuzzer;
3636
/// Details of a transaction generated by fuzz strategy for fuzzing a target.
3737
#[derive(Clone, Debug, Serialize, Deserialize)]
3838
pub struct BasicTxDetails {
39+
// Time (in seconds) to increase block timestamp before executing the tx.
40+
pub warp: Option<U256>,
41+
// Number to increase block number before executing the tx.
42+
pub roll: Option<U256>,
3943
// Transaction sender address.
4044
pub sender: Address,
4145
// Transaction call details.
@@ -62,6 +66,10 @@ pub enum CounterExample {
6266

6367
#[derive(Clone, Debug, Serialize, Deserialize)]
6468
pub struct BaseCounterExample {
69+
// Amount to increase block timestamp.
70+
pub warp: Option<U256>,
71+
// Amount to increase block number.
72+
pub roll: Option<U256>,
6573
/// Address which makes the call.
6674
pub sender: Option<Address>,
6775
/// Address to which to call to.
@@ -89,21 +97,26 @@ pub struct BaseCounterExample {
8997
impl BaseCounterExample {
9098
/// Creates counter example representing a step from invariant call sequence.
9199
pub fn from_invariant_call(
92-
sender: Address,
93-
addr: Address,
94-
bytes: &Bytes,
100+
tx: &BasicTxDetails,
95101
contracts: &ContractsByAddress,
96102
traces: Option<SparsedTraceArena>,
97103
show_solidity: bool,
98104
) -> Self {
99-
if let Some((name, abi)) = &contracts.get(&addr)
105+
let sender = tx.sender;
106+
let target = tx.call_details.target;
107+
let bytes = &tx.call_details.calldata;
108+
let warp = tx.warp;
109+
let roll = tx.roll;
110+
if let Some((name, abi)) = &contracts.get(&target)
100111
&& let Some(func) = abi.functions().find(|f| f.selector() == bytes[..4])
101112
{
102113
// skip the function selector when decoding
103114
if let Ok(args) = func.abi_decode_input(&bytes[4..]) {
104115
return Self {
116+
warp,
117+
roll,
105118
sender: Some(sender),
106-
addr: Some(addr),
119+
addr: Some(target),
107120
calldata: bytes.clone(),
108121
contract_name: Some(name.clone()),
109122
func_name: Some(func.name.clone()),
@@ -119,8 +132,10 @@ impl BaseCounterExample {
119132
}
120133

121134
Self {
135+
warp,
136+
roll,
122137
sender: Some(sender),
123-
addr: Some(addr),
138+
addr: Some(target),
124139
calldata: bytes.clone(),
125140
contract_name: None,
126141
func_name: None,
@@ -139,6 +154,8 @@ impl BaseCounterExample {
139154
traces: Option<SparsedTraceArena>,
140155
) -> Self {
141156
Self {
157+
warp: None,
158+
roll: None,
142159
sender: None,
143160
addr: None,
144161
calldata: bytes,
@@ -160,6 +177,12 @@ impl fmt::Display for BaseCounterExample {
160177
&& let (Some(sender), Some(contract), Some(address), Some(func_name), Some(args)) =
161178
(&self.sender, &self.contract_name, &self.addr, &self.func_name, &self.raw_args)
162179
{
180+
if let Some(warp) = &self.warp {
181+
writeln!(f, "\t\tvm.warp(block.timestamp + {warp});")?;
182+
}
183+
if let Some(roll) = &self.roll {
184+
writeln!(f, "\t\tvm.roll(block.number + {roll});")?;
185+
}
163186
writeln!(f, "\t\tvm.prank({sender});")?;
164187
write!(
165188
f,
@@ -186,6 +209,13 @@ impl fmt::Display for BaseCounterExample {
186209
write!(f, "{addr} ")?
187210
}
188211

212+
if let Some(warp) = &self.warp {
213+
write!(f, "warp={warp} ")?;
214+
}
215+
if let Some(roll) = &self.roll {
216+
write!(f, "roll={roll} ")?;
217+
}
218+
189219
if let Some(sig) = &self.signature {
190220
write!(f, "calldata={sig}")?
191221
} else {

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

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ 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};
9+
use foundry_config::InvariantConfig;
910
use parking_lot::RwLock;
1011
use proptest::prelude::*;
1112
use rand::seq::IteratorRandom;
@@ -60,25 +61,44 @@ pub fn invariant_strat(
6061
fuzz_state: EvmFuzzState,
6162
senders: SenderFilters,
6263
contracts: FuzzRunIdentifiedContracts,
63-
dictionary_weight: u32,
64+
config: InvariantConfig,
6465
fuzz_fixtures: FuzzFixtures,
6566
) -> impl Strategy<Value = BasicTxDetails> {
6667
let senders = Rc::new(senders);
68+
let dictionary_weight = config.dictionary.dictionary_weight;
69+
70+
// Strategy to generate values for tx warp and roll.
71+
let warp_roll_strat = |cond: bool| {
72+
if cond { any::<U256>().prop_map(Some).boxed() } else { Just(None).boxed() }
73+
};
74+
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);
80+
7281
let sender = select_random_sender(&fuzz_state, senders.clone(), dictionary_weight);
82+
7383
let call_details = fuzz_contract_with_calldata(
7484
&fuzz_state,
7585
&fuzz_fixtures,
7686
*target_address,
7787
target_function.clone(),
7888
);
79-
(sender, call_details)
89+
90+
let warp = warp_roll_strat(config.max_time_delay.is_some());
91+
let roll = warp_roll_strat(config.max_block_delay.is_some());
92+
93+
(warp, roll, sender, call_details)
94+
})
95+
.prop_map(move |(warp, roll, sender, call_details)| {
96+
let warp =
97+
warp.map(|time| time % U256::from(config.max_time_delay.unwrap_or_default()));
98+
let roll =
99+
roll.map(|block| block % U256::from(config.max_block_delay.unwrap_or_default()));
100+
BasicTxDetails { warp, roll, sender, call_details }
80101
})
81-
.prop_map(|(sender, call_details)| BasicTxDetails { sender, call_details })
82102
}
83103

84104
/// Strategy to select a sender address:

crates/forge/src/runner.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -780,6 +780,8 @@ impl<'a> FunctionRunner<'a> {
780780
.map(|seq| {
781781
seq.show_solidity = show_solidity;
782782
BasicTxDetails {
783+
warp: seq.warp,
784+
roll: seq.roll,
783785
sender: seq.sender.unwrap_or_default(),
784786
call_details: CallDetails {
785787
target: seq.addr.unwrap_or_default(),

0 commit comments

Comments
 (0)