Skip to main content

foundry_evm/executors/
campaign.rs

1//! Worker-local call execution policy shared by stateless and invariant fuzz campaigns.
2
3use super::{Executor, RawCallResult};
4use alloy_primitives::U256;
5use eyre::{Result, eyre};
6use foundry_evm_core::{
7    FoundryBlock,
8    constants::MAGIC_ASSUME,
9    evm::{BlockEnvFor, FoundryEvmNetwork},
10};
11use foundry_evm_fuzz::BasicTxDetails;
12use revm::context::Block;
13
14/// The small set of execution policies which differ between fuzzing modes.
15#[derive(Clone, Copy, Debug)]
16pub(crate) enum FuzzCampaignMode {
17    /// Stateless calls never mutate the worker executor.
18    Stateless,
19    /// Accepted invariant calls mutate it and predicates run at the configured cadence.
20    Invariant { check_interval: u32, optimization: bool },
21}
22
23/// Classification produced by the common call loop.
24#[derive(Clone, Copy, Debug, PartialEq, Eq)]
25pub(crate) enum CampaignCallKind {
26    Accepted,
27    AssumptionRejected,
28}
29
30pub(crate) enum CampaignEvent<'a, FEN: FoundryEvmNetwork> {
31    Feedback(&'a mut RawCallResult<FEN>),
32    Check { result: &'a mut Option<RawCallResult<FEN>>, kind: CampaignCallKind, should_check: bool },
33    Advance,
34    Next { discarded: bool, depth: u32 },
35    PostCheck,
36}
37
38#[derive(Clone, Copy, Debug, PartialEq, Eq)]
39pub(crate) enum CampaignControl {
40    Continue,
41    Stop,
42}
43
44#[derive(Clone, Copy, Debug, PartialEq, Eq)]
45pub(crate) enum CampaignSequenceOutcome {
46    Complete,
47    Cancelled,
48    Stopped,
49}
50
51/// Concrete worker-local campaign execution policy.
52pub(crate) struct FuzzCampaign {
53    mode: FuzzCampaignMode,
54}
55
56impl FuzzCampaign {
57    pub(crate) const fn new(mode: FuzzCampaignMode) -> Self {
58        Self { mode }
59    }
60
61    /// Drives one concrete sequence, including execution, classification, and lifecycle events.
62    pub(crate) fn run_sequence<S, FEN, Parts, Depth, Stop, Event>(
63        &self,
64        state: &mut S,
65        run_depth: u32,
66        mut parts: Parts,
67        mut depth: Depth,
68        mut should_stop: Stop,
69        mut on_event: Event,
70    ) -> Result<CampaignSequenceOutcome>
71    where
72        FEN: FoundryEvmNetwork,
73        Parts: for<'a> FnMut(&'a mut S) -> (&'a mut Executor<FEN>, &'a mut BasicTxDetails),
74        Depth: FnMut(&S) -> u32,
75        Stop: FnMut(&S) -> bool,
76        Event: for<'a> FnMut(&mut S, CampaignEvent<'a, FEN>) -> Result<CampaignControl>,
77    {
78        let outcome = loop {
79            if depth(state) >= run_depth {
80                break CampaignSequenceOutcome::Complete;
81            }
82            if should_stop(state) {
83                return Ok(CampaignSequenceOutcome::Cancelled);
84            }
85            let current_depth = depth(state);
86            let last_call = current_depth == run_depth - 1;
87            let (mut result, block_snapshot) = {
88                let (executor, tx) = parts(state);
89                let snapshot = matches!(self.mode, FuzzCampaignMode::Invariant { .. })
90                    .then(|| BlockSnapshot::for_delayed_call(executor, tx))
91                    .flatten();
92                let result = match self.mode {
93                    FuzzCampaignMode::Stateless => executor.call_raw(
94                        tx.sender,
95                        tx.call_details.target,
96                        tx.call_details.calldata.clone(),
97                        tx.call_details.value.unwrap_or_default(),
98                    )?,
99                    FuzzCampaignMode::Invariant { .. } => execute_invariant_tx(executor, tx)?,
100                };
101                (result, snapshot)
102            };
103            if result.execution_cancelled {
104                if let Some(snapshot) = block_snapshot {
105                    let (executor, _) = parts(state);
106                    snapshot.restore(executor);
107                }
108                return Ok(CampaignSequenceOutcome::Cancelled);
109            }
110            on_event(state, CampaignEvent::Feedback(&mut result))?;
111            let kind = match self.mode {
112                FuzzCampaignMode::Invariant { .. } => {
113                    let (executor, _) = parts(state);
114                    transition_invariant_call(executor, &mut result, block_snapshot)
115                }
116                FuzzCampaignMode::Stateless => {
117                    if result.result.as_ref() == MAGIC_ASSUME {
118                        CampaignCallKind::AssumptionRejected
119                    } else {
120                        CampaignCallKind::Accepted
121                    }
122                }
123            };
124            let discarded = kind == CampaignCallKind::AssumptionRejected;
125            let should_check = match self.mode {
126                FuzzCampaignMode::Stateless => true,
127                FuzzCampaignMode::Invariant { optimization: true, .. } => true,
128                FuzzCampaignMode::Invariant { check_interval: 0, .. } => last_call,
129                FuzzCampaignMode::Invariant { check_interval, .. } => {
130                    check_interval == 1
131                        || (current_depth + 1).is_multiple_of(check_interval)
132                        || last_call
133                }
134            };
135            let mut result = Some(result);
136            if on_event(state, CampaignEvent::Check { result: &mut result, kind, should_check })?
137                == CampaignControl::Stop
138            {
139                break CampaignSequenceOutcome::Stopped;
140            }
141            if !discarded {
142                on_event(state, CampaignEvent::Advance)?;
143            }
144            let next_depth = depth(state);
145            on_event(state, CampaignEvent::Next { discarded, depth: next_depth })?;
146        };
147        on_event(state, CampaignEvent::PostCheck)?;
148        Ok(outcome)
149    }
150}
151
152struct BlockSnapshot<FEN: FoundryEvmNetwork> {
153    env: BlockEnvFor<FEN>,
154    cheatcode: Option<BlockEnvFor<FEN>>,
155}
156
157impl<FEN: FoundryEvmNetwork> BlockSnapshot<FEN> {
158    fn for_delayed_call(executor: &Executor<FEN>, tx: &BasicTxDetails) -> Option<Self> {
159        let has_delay = tx.warp.is_some_and(|delay| !delay.is_zero())
160            || tx.roll.is_some_and(|delay| !delay.is_zero());
161        has_delay.then(|| Self::new(executor))
162    }
163
164    fn new(executor: &Executor<FEN>) -> Self {
165        Self {
166            env: executor.evm_env().block_env.clone(),
167            cheatcode: executor.inspector().cheatcodes.as_ref().and_then(|c| c.block.clone()),
168        }
169    }
170
171    fn restore(self, executor: &mut Executor<FEN>) {
172        executor.evm_env_mut().block_env = self.env;
173        if let Some(cheatcodes) = executor.inspector_mut().cheatcodes.as_mut() {
174            cheatcodes.block = self.cheatcode;
175        }
176    }
177}
178
179fn transition_invariant_call<FEN: FoundryEvmNetwork>(
180    executor: &mut Executor<FEN>,
181    result: &mut RawCallResult<FEN>,
182    block_snapshot: Option<BlockSnapshot<FEN>>,
183) -> CampaignCallKind {
184    if result.result.as_ref() == MAGIC_ASSUME {
185        if let Some(snapshot) = block_snapshot {
186            snapshot.restore(executor);
187        }
188        CampaignCallKind::AssumptionRejected
189    } else {
190        executor.commit(result);
191        CampaignCallKind::Accepted
192    }
193}
194
195/// Executes and commits one invariant replay call with live campaign semantics.
196pub(super) fn execute_invariant_replay_tx<FEN: FoundryEvmNetwork>(
197    executor: &mut Executor<FEN>,
198    tx: &BasicTxDetails,
199) -> Result<(CampaignCallKind, RawCallResult<FEN>)> {
200    let block_snapshot = BlockSnapshot::for_delayed_call(executor, tx);
201    let mut result = execute_invariant_tx(executor, &mut tx.clone())?;
202    if result.execution_cancelled {
203        if let Some(snapshot) = block_snapshot {
204            snapshot.restore(executor);
205        }
206        return Err(eyre!("invariant replay call was cancelled"));
207    }
208    let kind = transition_invariant_call(executor, &mut result, block_snapshot);
209    Ok((kind, result))
210}
211
212pub(super) fn execute_invariant_tx<FEN: FoundryEvmNetwork>(
213    executor: &mut Executor<FEN>,
214    tx: &mut BasicTxDetails,
215) -> Result<RawCallResult<FEN>> {
216    let warp = tx.warp.unwrap_or_default();
217    let roll = tx.roll.unwrap_or_default();
218    if warp > 0 || roll > 0 {
219        let needs_cheatcode_block = executor
220            .inspector()
221            .cheatcodes
222            .as_ref()
223            .is_some_and(|cheatcodes| cheatcodes.block.is_none());
224        let block_env = {
225            let block_env = &mut executor.evm_env_mut().block_env;
226            block_env.set_timestamp(block_env.timestamp() + warp);
227            block_env.set_number(block_env.number() + roll);
228            needs_cheatcode_block.then(|| block_env.clone())
229        };
230        if let Some(cheatcodes) = executor.inspector_mut().cheatcodes.as_mut() {
231            if let Some(block) = cheatcodes.block.as_mut() {
232                block.set_timestamp(block.timestamp() + warp);
233                block.set_number(block.number() + roll);
234            } else {
235                cheatcodes.block = Some(block_env.unwrap());
236            }
237        }
238    }
239    let value = match tx.call_details.value {
240        Some(requested) if !requested.is_zero() => requested.min(executor.get_balance(tx.sender)?),
241        _ => U256::ZERO,
242    };
243    // Persist exactly what was sent so replay, shrinking, and corpus entries do not claim an
244    // unavailable value was executed.
245    tx.call_details.value = (!value.is_zero()).then_some(value);
246    executor
247        .call_raw(tx.sender, tx.call_details.target, tx.call_details.calldata.clone(), value)
248        .map_err(|error| eyre!("Could not make raw evm call: {error}"))
249}