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 has_delay = tx.warp.is_some_and(|delay| !delay.is_zero())
90                    || tx.roll.is_some_and(|delay| !delay.is_zero());
91                let snapshot = (matches!(self.mode, FuzzCampaignMode::Invariant { .. })
92                    && has_delay)
93                    .then(|| BlockSnapshot::new(executor));
94                let result = match self.mode {
95                    FuzzCampaignMode::Stateless => executor.call_raw(
96                        tx.sender,
97                        tx.call_details.target,
98                        tx.call_details.calldata.clone(),
99                        tx.call_details.value.unwrap_or_default(),
100                    )?,
101                    FuzzCampaignMode::Invariant { .. } => execute_invariant_tx(executor, tx)?,
102                };
103                (result, snapshot)
104            };
105            if result.execution_cancelled {
106                if let Some(snapshot) = block_snapshot {
107                    let (executor, _) = parts(state);
108                    snapshot.restore(executor);
109                }
110                return Ok(CampaignSequenceOutcome::Cancelled);
111            }
112            on_event(state, CampaignEvent::Feedback(&mut result))?;
113            let discarded = result.result.as_ref() == MAGIC_ASSUME;
114            let kind = if discarded {
115                if let Some(snapshot) = block_snapshot {
116                    let (executor, _) = parts(state);
117                    snapshot.restore(executor);
118                }
119                CampaignCallKind::AssumptionRejected
120            } else {
121                if matches!(self.mode, FuzzCampaignMode::Invariant { .. }) {
122                    let (executor, _) = parts(state);
123                    executor.commit(&mut result);
124                }
125                CampaignCallKind::Accepted
126            };
127            let should_check = match self.mode {
128                FuzzCampaignMode::Stateless => true,
129                FuzzCampaignMode::Invariant { optimization: true, .. } => true,
130                FuzzCampaignMode::Invariant { check_interval: 0, .. } => last_call,
131                FuzzCampaignMode::Invariant { check_interval, .. } => {
132                    check_interval == 1
133                        || (current_depth + 1).is_multiple_of(check_interval)
134                        || last_call
135                }
136            };
137            let mut result = Some(result);
138            if on_event(state, CampaignEvent::Check { result: &mut result, kind, should_check })?
139                == CampaignControl::Stop
140            {
141                break CampaignSequenceOutcome::Stopped;
142            }
143            if !discarded {
144                on_event(state, CampaignEvent::Advance)?;
145            }
146            let next_depth = depth(state);
147            on_event(state, CampaignEvent::Next { discarded, depth: next_depth })?;
148        };
149        on_event(state, CampaignEvent::PostCheck)?;
150        Ok(outcome)
151    }
152}
153
154struct BlockSnapshot<FEN: FoundryEvmNetwork> {
155    env: BlockEnvFor<FEN>,
156    cheatcode: Option<BlockEnvFor<FEN>>,
157}
158
159impl<FEN: FoundryEvmNetwork> BlockSnapshot<FEN> {
160    fn new(executor: &Executor<FEN>) -> Self {
161        Self {
162            env: executor.evm_env().block_env.clone(),
163            cheatcode: executor.inspector().cheatcodes.as_ref().and_then(|c| c.block.clone()),
164        }
165    }
166
167    fn restore(self, executor: &mut Executor<FEN>) {
168        executor.evm_env_mut().block_env = self.env;
169        if let Some(cheatcodes) = executor.inspector_mut().cheatcodes.as_mut() {
170            cheatcodes.block = self.cheatcode;
171        }
172    }
173}
174
175pub(super) fn execute_invariant_tx<FEN: FoundryEvmNetwork>(
176    executor: &mut Executor<FEN>,
177    tx: &mut BasicTxDetails,
178) -> Result<RawCallResult<FEN>> {
179    let warp = tx.warp.unwrap_or_default();
180    let roll = tx.roll.unwrap_or_default();
181    if warp > 0 || roll > 0 {
182        let needs_cheatcode_block = executor
183            .inspector()
184            .cheatcodes
185            .as_ref()
186            .is_some_and(|cheatcodes| cheatcodes.block.is_none());
187        let block_env = {
188            let block_env = &mut executor.evm_env_mut().block_env;
189            block_env.set_timestamp(block_env.timestamp() + warp);
190            block_env.set_number(block_env.number() + roll);
191            needs_cheatcode_block.then(|| block_env.clone())
192        };
193        if let Some(cheatcodes) = executor.inspector_mut().cheatcodes.as_mut() {
194            if let Some(block) = cheatcodes.block.as_mut() {
195                block.set_timestamp(block.timestamp() + warp);
196                block.set_number(block.number() + roll);
197            } else {
198                cheatcodes.block = Some(block_env.unwrap());
199            }
200        }
201    }
202    let value = match tx.call_details.value {
203        Some(requested) if !requested.is_zero() => requested.min(executor.get_balance(tx.sender)?),
204        _ => U256::ZERO,
205    };
206    // Persist exactly what was sent so replay, shrinking, and corpus entries do not claim an
207    // unavailable value was executed.
208    tx.call_details.value = (!value.is_zero()).then_some(value);
209    executor
210        .call_raw(tx.sender, tx.call_details.target, tx.call_details.calldata.clone(), value)
211        .map_err(|error| eyre!("Could not make raw evm call: {error}"))
212}