Skip to main content

forge_lint/sol/med/
cheatcode_environment.rs

1//! Finds raw environment reads whose values can cross a Foundry environment mutation.
2//!
3//! A bounded source-level interpreter follows scalar locals, tuple assignments, internal calls,
4//! and modifier placeholders. An environment setter marks matching live origins; a subsequent use
5//! reports the original read. Reads on both sides of a mutation are also reported because a
6//! compiler may reuse the earlier read. Branches retain separate states, and return, revert,
7//! break, and continue stop the corresponding path. External calls are not inlined: the callee
8//! has a separate EVM frame and its return data is already materialized.
9//! Differing helper return values and conditional-expression values are discarded instead of
10//! combining components from mutually exclusive outcomes.
11//!
12//! This analysis runs before optimization and never changes executable code. Cheatcodes are
13//! recognized by their resolved signature and constant receiver address, including local aliases
14//! and helper arguments, rather than by a variable or interface name. Getter results carry no
15//! raw-read origin. Heap/storage aliases, indirect calls, low-level cheatcode calls, recursion,
16//! and paths beyond the explicit analysis limits are not modeled. This is a source warning,
17//! not a proof of a particular optimizer's scheduling decisions.
18
19use super::CheatcodeEnvironment;
20use crate::{
21    linter::{Lint, ProjectLintEmitter, ProjectLintPass, ProjectSource},
22    sol::{
23        Severity, SolLint,
24        analysis::{arg_for_param, dispatched_function, for_each_child, is_exit_call, loop_update},
25    },
26};
27use alloy_primitives::{U256, keccak256, uint};
28use solar::{
29    ast::{BinOpKind, ElementaryType, FunctionKind, UnOpKind},
30    interface::{Span, diagnostics::DiagId, source_map::FileName},
31    sema::{
32        Gcx,
33        builtins::Builtin,
34        eval::ConstValue,
35        hir::{self, Expr, ExprKind, Function, FunctionId, Stmt, StmtKind, VariableId},
36        ty::TyKind,
37    },
38};
39use std::collections::HashMap;
40
41declare_forge_lint!(
42    ENVIRONMENT_READ_ACROSS_MUTATION,
43    Severity::Med,
44    "environment-read-across-mutation",
45    "environment read may be reused across a Foundry environment mutation"
46);
47
48const CHEATCODE_ADDRESS: U256 = uint!(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D_U256);
49const MAX_STEPS: usize = 16_384;
50const MAX_PATHS: usize = 32;
51const MAX_CALL_DEPTH: usize = 8;
52const MAX_LOOP_ITERATIONS: usize = 2;
53
54#[derive(Clone, Copy, PartialEq, Eq)]
55enum Environment {
56    Number,
57    Timestamp,
58    ChainId,
59    Coinbase,
60    Difficulty,
61    Prevrandao,
62    BaseFee,
63    BlobBaseFee,
64    GasLimit,
65    SlotNumber,
66    GasPrice,
67    BlockHash,
68    BlobHash,
69}
70
71impl Environment {
72    /// Block/configuration fields and blockhash history replaced by fork operations.
73    const BLOCK: &'static [Self] = &[
74        Self::Number,
75        Self::Timestamp,
76        Self::ChainId,
77        Self::Coinbase,
78        Self::Difficulty,
79        Self::Prevrandao,
80        Self::BaseFee,
81        Self::BlobBaseFee,
82        Self::GasLimit,
83        Self::SlotNumber,
84        Self::BlockHash,
85    ];
86
87    /// Snapshots also restore cheatcode overrides of transaction properties.
88    const ALL: &'static [Self] = &[
89        Self::Number,
90        Self::Timestamp,
91        Self::ChainId,
92        Self::Coinbase,
93        Self::Difficulty,
94        Self::Prevrandao,
95        Self::BaseFee,
96        Self::BlobBaseFee,
97        Self::GasLimit,
98        Self::SlotNumber,
99        Self::BlockHash,
100        Self::GasPrice,
101        Self::BlobHash,
102    ];
103
104    const fn from_builtin(builtin: Builtin) -> Option<Self> {
105        Some(match builtin {
106            Builtin::BlockNumber => Self::Number,
107            Builtin::BlockTimestamp => Self::Timestamp,
108            Builtin::BlockChainid => Self::ChainId,
109            Builtin::BlockCoinbase => Self::Coinbase,
110            Builtin::BlockDifficulty => Self::Difficulty,
111            Builtin::BlockPrevrandao => Self::Prevrandao,
112            Builtin::BlockBasefee => Self::BaseFee,
113            Builtin::BlockBlobbasefee => Self::BlobBaseFee,
114            Builtin::BlockGaslimit => Self::GasLimit,
115            Builtin::BlockSlotnum => Self::SlotNumber,
116            Builtin::TxGasPrice => Self::GasPrice,
117            Builtin::Blockhash => Self::BlockHash,
118            Builtin::Blobhash => Self::BlobHash,
119            _ => return None,
120        })
121    }
122
123    const fn name(self) -> &'static str {
124        match self {
125            Self::Number => "block.number",
126            Self::Timestamp => "block.timestamp",
127            Self::ChainId => "block.chainid",
128            Self::Coinbase => "block.coinbase",
129            Self::Difficulty => "block.difficulty",
130            Self::Prevrandao => "block.prevrandao",
131            Self::BaseFee => "block.basefee",
132            Self::BlobBaseFee => "block.blobbasefee",
133            Self::GasLimit => "block.gaslimit",
134            Self::SlotNumber => "block.slotnum",
135            Self::GasPrice => "tx.gasprice",
136            Self::BlockHash => "blockhash(...)",
137            Self::BlobHash => "blobhash(...)",
138        }
139    }
140
141    const fn getter(self) -> Option<&'static str> {
142        Some(match self {
143            Self::Number => "vm.getBlockNumber()",
144            Self::Timestamp => "vm.getBlockTimestamp()",
145            Self::ChainId => "vm.getChainId()",
146            Self::BlobBaseFee => "vm.getBlobBaseFee()",
147            Self::BlobHash => "vm.getBlobhashes()",
148            _ => return None,
149        })
150    }
151}
152
153#[derive(Clone, Copy, PartialEq, Eq)]
154struct Read {
155    environment: Environment,
156    span: Span,
157    changed: Option<Mutation>,
158    origin: usize,
159}
160
161#[derive(Clone, Copy, PartialEq, Eq)]
162struct Mutation {
163    span: Span,
164    function: FunctionId,
165}
166
167/// Exact unsigned and boolean locals used to prune exhausted loops and constant branches.
168#[derive(Clone, Copy, PartialEq, Eq)]
169enum Scalar {
170    Uint(U256),
171    Bool(bool),
172}
173
174impl Scalar {
175    const fn as_bool(self) -> Option<bool> {
176        if let Self::Bool(value) = self { Some(value) } else { None }
177    }
178
179    fn binary(self, op: BinOpKind, rhs: Self) -> Option<Self> {
180        Some(match (self, rhs) {
181            (Self::Uint(lhs), Self::Uint(rhs)) => match op {
182                BinOpKind::Lt => Self::Bool(lhs < rhs),
183                BinOpKind::Le => Self::Bool(lhs <= rhs),
184                BinOpKind::Gt => Self::Bool(lhs > rhs),
185                BinOpKind::Ge => Self::Bool(lhs >= rhs),
186                BinOpKind::Eq => Self::Bool(lhs == rhs),
187                BinOpKind::Ne => Self::Bool(lhs != rhs),
188                _ => return None,
189            },
190            (Self::Bool(lhs), Self::Bool(rhs)) => Self::Bool(match op {
191                BinOpKind::And => lhs && rhs,
192                BinOpKind::Or => lhs || rhs,
193                BinOpKind::Eq => lhs == rhs,
194                BinOpKind::Ne => lhs != rhs,
195                _ => return None,
196            }),
197            _ => return None,
198        })
199    }
200}
201
202#[derive(Clone, Default, PartialEq, Eq)]
203struct Value {
204    reads: Vec<Read>,
205    cheatcode: bool,
206    tuple: Vec<Self>,
207    scalar: Option<Scalar>,
208}
209
210impl Value {
211    fn merge(&mut self, other: &Self) {
212        if self.scalar != other.scalar {
213            self.scalar = None;
214        }
215        for read in &other.reads {
216            if !self.reads.contains(read) {
217                self.reads.push(*read);
218            }
219        }
220        self.cheatcode |= other.cheatcode;
221        self.tuple.resize_with(self.tuple.len().max(other.tuple.len()), Self::default);
222        for (a, b) in self.tuple.iter_mut().zip(&other.tuple) {
223            a.merge(b);
224        }
225    }
226
227    fn change(&mut self, environment: Environment, mutation: Mutation) {
228        for read in &mut self.reads {
229            if read.environment == environment {
230                // Keep the first matching call on this path, not an unrelated or later setter.
231                read.changed.get_or_insert(mutation);
232            }
233        }
234        for part in &mut self.tuple {
235            part.change(environment, mutation);
236        }
237    }
238
239    /// Refresh origins held outside locals while sibling expressions execute.
240    fn refresh(&mut self, state: &State) {
241        for read in &mut self.reads {
242            read.changed = read.changed.or_else(|| state.changed.get(&read.origin).copied());
243        }
244        for part in &mut self.tuple {
245            part.refresh(state);
246        }
247    }
248
249    fn part(&self, index: usize) -> Self {
250        self.tuple.get(index).cloned().unwrap_or_default()
251    }
252}
253
254#[derive(Clone, Copy, Default, PartialEq, Eq)]
255enum Flow {
256    #[default]
257    Next,
258    Return,
259    Break,
260    Continue,
261    Halt,
262}
263
264#[derive(Clone, Default)]
265struct State {
266    locals: HashMap<VariableId, Value>,
267    seen: Value,
268    changed: HashMap<usize, Mutation>,
269    return_parameters: Vec<VariableId>,
270    flow: Flow,
271    unchecked: bool,
272}
273
274impl State {
275    fn change(&mut self, environment: Environment, mutation: Mutation) {
276        for read in self.seen.reads.iter().filter(|read| read.environment == environment) {
277            self.changed.entry(read.origin).or_insert(mutation);
278        }
279        self.seen.change(environment, mutation);
280        for value in self.locals.values_mut() {
281            value.change(environment, mutation);
282        }
283    }
284
285    fn merge(&mut self, other: &Self) {
286        for (&origin, &mutation) in &other.changed {
287            self.changed.entry(origin).or_insert(mutation);
288        }
289        self.seen.merge(&other.seen);
290        for (var, value) in &other.locals {
291            self.locals.entry(*var).or_default().merge(value);
292        }
293    }
294}
295
296// Project sources expose the span-owner policies needed to emit a multi-span diagnostic.
297// The pinned Solar late-pass context only supports single-span messages and suggestions.
298impl<'ast> ProjectLintPass<'ast> for CheatcodeEnvironment {
299    fn check_project(&mut self, ctx: &ProjectLintEmitter<'_, '_>, sources: &[ProjectSource<'ast>]) {
300        if !ctx.is_lint_enabled(ENVIRONMENT_READ_ACROSS_MUTATION.id) {
301            return;
302        }
303        let gcx = ctx.gcx();
304        let input_sources = gcx
305            .hir
306            .sources_enumerated()
307            .filter_map(|(id, source)| {
308                let FileName::Real(path) = &source.file.name else { return None };
309                Some((id, sources.iter().find(|source| &source.path == path)?))
310            })
311            .collect::<HashMap<_, _>>();
312        for func in gcx.hir.functions() {
313            if func.body.is_none()
314                || func.kind == FunctionKind::Modifier
315                || !input_sources.contains_key(&func.source)
316            {
317                continue;
318            }
319            let mut checker = Checker {
320                sources,
321                gcx,
322                contract: func.contract,
323                stack: vec![func.span],
324                remaining: MAX_STEPS,
325            };
326            let initial = State { return_parameters: func.returns.to_vec(), ..State::default() };
327            for state in checker.layer(func, 0, initial) {
328                if matches!(state.flow, Flow::Next | Flow::Return) {
329                    checker.use_value(&checker.return_values(func, &state));
330                }
331            }
332        }
333    }
334}
335
336struct Checker<'a, 'ast, 'gcx> {
337    sources: &'a [ProjectSource<'ast>],
338    gcx: Gcx<'gcx>,
339    contract: Option<hir::ContractId>,
340    stack: Vec<Span>,
341    remaining: usize,
342}
343
344impl<'gcx> Checker<'_, '_, 'gcx> {
345    const fn step(&mut self) -> bool {
346        if self.remaining == 0 {
347            return false;
348        }
349        self.remaining -= 1;
350        true
351    }
352
353    fn use_value(&self, value: &Value) {
354        for read in &value.reads {
355            if read.changed.is_some() {
356                self.emit(*read);
357            }
358        }
359        for part in &value.tuple {
360            self.use_value(part);
361        }
362    }
363
364    fn emit(&self, read: Read) {
365        let Some(mutation) = read.changed else { return };
366        let lint = &ENVIRONMENT_READ_ACROSS_MUTATION;
367        // The primary read can belong to an inherited helper in another source. Use its
368        // policy, not the mutation's, and never emit diagnostics for dependency-only files.
369        let Some(source) = self.sources.iter().find(|source| source.file.contains(read.span.lo()))
370        else {
371            return;
372        };
373        if !source.policy.is_lint_enabled(lint.id)
374            || source.policy.is_lint_suppressed(lint.id, read.span)
375        {
376            return;
377        }
378        let name = read.environment.name();
379        let setter = self.gcx.hir.function(mutation.function).name.unwrap();
380        let advice = read.environment.getter().map_or_else(
381            || "capture it through an external helper call instead".to_string(),
382            |getter| format!("capture it with `{getter}` instead"),
383        );
384        self.gcx
385            .sess
386            .dcx
387            .diag::<()>(lint.level(), format!("`{name}` may be reused across `vm.{setter}`"))
388            .code(DiagId::new_str(lint.id))
389            .span(read.span)
390            .span_label(mutation.span, format!("`vm.{setter}` changes this environment here"))
391            .help(advice)
392            .help(lint.help)
393            .emit();
394    }
395
396    fn read(&mut self, environment: Environment, span: Span, state: &mut State) -> Value {
397        if !self.step() || state.flow == Flow::Halt {
398            return Value::default();
399        }
400        for read in &state.seen.reads {
401            if read.environment == environment && read.changed.is_some() {
402                self.emit(*read);
403            }
404        }
405        let value = Value {
406            reads: vec![Read {
407                environment,
408                span,
409                changed: None,
410                // The decreasing step budget gives repeated evaluations distinct identities.
411                origin: self.remaining,
412            }],
413            ..Value::default()
414        };
415        state.seen.merge(&value);
416        value
417    }
418
419    /// Runs the next modifier, substituting the remaining function at each placeholder.
420    fn layer(&mut self, func: &'gcx Function<'gcx>, index: usize, mut state: State) -> Vec<State> {
421        if !self.step() || state.flow == Flow::Halt {
422            return Vec::new();
423        }
424        if let Some(modifier) = func.modifiers.get(index) {
425            let Some(id) = self.contract.map_or_else(
426                || modifier.id.as_function(),
427                |contract| self.gcx.resolve_modifier_target(contract, modifier),
428            ) else {
429                return Vec::new();
430            };
431            let definition = self.gcx.hir.function(id);
432            let Some(body) = definition.body else { return Vec::new() };
433            let values: Vec<_> = definition
434                .parameters
435                .iter()
436                .map(|&param| {
437                    let value = arg_for_param(self.gcx, id, param, &modifier.args)
438                        .map(|arg| self.expr(arg, &mut state))
439                        .unwrap_or_default();
440                    (param, value)
441                })
442                .collect();
443            state.locals.extend(values);
444            self.block(body.stmts, vec![state], Some((func, index + 1)))
445        } else if let Some(body) = func.body {
446            self.block(body.stmts, vec![state], None)
447        } else {
448            Vec::new()
449        }
450    }
451
452    fn return_values(&self, func: &Function<'_>, state: &State) -> Value {
453        let values: Vec<_> = func
454            .returns
455            .iter()
456            .map(|var| state.locals.get(var).cloned().unwrap_or_default())
457            .collect();
458        if values.len() == 1 {
459            values.into_iter().next().unwrap_or_default()
460        } else {
461            Value { tuple: values, ..Value::default() }
462        }
463    }
464
465    fn block(
466        &mut self,
467        stmts: &'gcx [Stmt<'gcx>],
468        mut states: Vec<State>,
469        continuation: Option<(&'gcx Function<'gcx>, usize)>,
470    ) -> Vec<State> {
471        for stmt in stmts {
472            let mut next = Vec::new();
473            for state in states {
474                if state.flow == Flow::Next {
475                    next.extend(self.stmt(stmt, state, continuation));
476                } else {
477                    next.push(state);
478                }
479                if next.len() >= MAX_PATHS {
480                    break;
481                }
482            }
483            next.truncate(MAX_PATHS);
484            states = next;
485        }
486        states
487    }
488
489    fn stmt(
490        &mut self,
491        stmt: &'gcx Stmt<'gcx>,
492        mut state: State,
493        continuation: Option<(&'gcx Function<'gcx>, usize)>,
494    ) -> Vec<State> {
495        if !self.step() || state.flow == Flow::Halt {
496            return Vec::new();
497        }
498        match &stmt.kind {
499            StmtKind::Block(block) => {
500                return self.block(block.stmts, vec![state], continuation);
501            }
502            StmtKind::UncheckedBlock(block) => {
503                let previous = std::mem::replace(&mut state.unchecked, true);
504                let mut states = self.block(block.stmts, vec![state], continuation);
505                for state in &mut states {
506                    state.unchecked = previous;
507                }
508                return states;
509            }
510            StmtKind::DeclSingle(var) => {
511                let value = self
512                    .gcx
513                    .hir
514                    .variable(*var)
515                    .initializer
516                    .map(|expr| self.expr(expr, &mut state))
517                    .unwrap_or_else(|| self.default_value(*var));
518                state.locals.insert(*var, value);
519            }
520            StmtKind::DeclMulti(vars, expr) => {
521                let value = self.expr(expr, &mut state);
522                for (index, var) in vars.iter().enumerate() {
523                    if let Some(var) = var {
524                        state.locals.insert(*var, value.part(index));
525                    }
526                }
527            }
528            StmtKind::If(cond, then, otherwise) => {
529                let known = self.expr(cond, &mut state).scalar.and_then(Scalar::as_bool);
530                let mut states = Vec::new();
531                if known != Some(false) {
532                    states.extend(self.stmt(then, state.clone(), continuation));
533                }
534                if known != Some(true) {
535                    states.extend(otherwise.map_or_else(
536                        || vec![state.clone()],
537                        |stmt| self.stmt(stmt, state.clone(), continuation),
538                    ));
539                }
540                return states;
541            }
542            StmtKind::Loop(body, source) => {
543                // HIR includes the loop condition and its break edge in the body.
544                let mut active = vec![state];
545                let mut exits = Vec::new();
546                for _ in 0..MAX_LOOP_ITERATIONS {
547                    let iteration = self.block(body.stmts, active, continuation);
548                    active = Vec::new();
549                    for mut state in iteration {
550                        match state.flow {
551                            Flow::Break => {
552                                state.flow = Flow::Next;
553                                exits.push(state);
554                            }
555                            Flow::Next | Flow::Continue => {
556                                state.flow = Flow::Next;
557                                if let Some(update) = loop_update(*source) {
558                                    active.extend(self.stmt(update, state, continuation));
559                                } else {
560                                    active.push(state);
561                                }
562                            }
563                            _ => exits.push(state),
564                        }
565                    }
566                    active.truncate(MAX_PATHS);
567                    exits.truncate(MAX_PATHS);
568                }
569                // Check the final for/while condition without executing another body. A loop
570                // exhausted exactly at the iteration budget still reaches following statements.
571                if !matches!(source, hir::LoopSource::DoWhile)
572                    && let [stmt] = body.stmts
573                    && let StmtKind::If(cond, _, Some(otherwise)) = &stmt.kind
574                    && matches!(otherwise.kind, StmtKind::Break)
575                {
576                    for mut state in active {
577                        let known = self.expr(cond, &mut state).scalar.and_then(Scalar::as_bool);
578                        if known != Some(true) && state.flow == Flow::Next {
579                            exits.push(state);
580                        }
581                    }
582                    exits.truncate(MAX_PATHS);
583                }
584                // Only observed exit edges reach following statements. Do not manufacture an
585                // exit from an infinite loop or treat an unvisited iteration as execution.
586                return exits;
587            }
588            StmtKind::Return(expr) => {
589                if let Some(expr) = expr {
590                    let value = self.expr(expr, &mut state);
591                    if state.flow == Flow::Halt {
592                        return vec![state];
593                    }
594                    for (index, var) in state.return_parameters.iter().enumerate() {
595                        let part = if state.return_parameters.len() == 1 {
596                            value.clone()
597                        } else {
598                            value.part(index)
599                        };
600                        state.locals.insert(*var, part);
601                    }
602                }
603                state.flow = Flow::Return;
604            }
605            StmtKind::Break => state.flow = Flow::Break,
606            StmtKind::Continue => state.flow = Flow::Continue,
607            StmtKind::Revert(expr) => {
608                self.expr(expr, &mut state);
609                state.flow = Flow::Halt;
610            }
611            StmtKind::Expr(expr) | StmtKind::Emit(expr) => {
612                self.expr(expr, &mut state);
613                if is_exit_call(self.gcx, expr) {
614                    state.flow = Flow::Halt;
615                }
616            }
617            StmtKind::Try(try_stmt) => {
618                // Argument evaluation happens in the caller even when the external call fails.
619                // Effects of a reverted cheatcode call itself must not reach a catch clause.
620                let mut failure = state.clone();
621                for_each_child(&try_stmt.expr, &mut |child| {
622                    self.expr(child, &mut failure);
623                });
624                self.expr(&try_stmt.expr, &mut state);
625                return try_stmt
626                    .clauses
627                    .iter()
628                    .enumerate()
629                    .flat_map(|(index, clause)| {
630                        let input = if index == 0 { &state } else { &failure };
631                        self.block(clause.block.stmts, vec![input.clone()], continuation)
632                    })
633                    .take(MAX_PATHS)
634                    .collect();
635            }
636            StmtKind::Placeholder => {
637                if let Some((func, index)) = continuation {
638                    let mut states = self.layer(func, index, state);
639                    for state in &mut states {
640                        if state.flow == Flow::Return {
641                            state.flow = Flow::Next;
642                        }
643                    }
644                    return states;
645                }
646            }
647            // Assembly can both overwrite locals and terminate the frame. Stop this path rather
648            // than carrying facts across unmodeled instructions.
649            StmtKind::AssemblyBlock(_) | StmtKind::Switch(_) | StmtKind::Err(_) => {
650                return Vec::new();
651            }
652        }
653        vec![state]
654    }
655
656    fn bind(&self, lhs: &Expr<'_>, value: Value, state: &mut State) {
657        match &lhs.peel_parens().kind {
658            ExprKind::Tuple(parts) => {
659                for (index, part) in parts.iter().enumerate() {
660                    if let Some(part) = part {
661                        self.bind(part, value.part(index), state);
662                    }
663                }
664            }
665            _ => {
666                if let Some(var) = self.gcx.resolved_variable(lhs)
667                    && !self.gcx.hir.variable(var).is_state_variable()
668                {
669                    state.locals.insert(var, value);
670                }
671            }
672        }
673    }
674
675    /// Evaluate destination components without reading an overwritten local.
676    fn destination(&mut self, expr: &'gcx Expr<'gcx>, state: &mut State) {
677        let expr = expr.peel_parens();
678        if let ExprKind::Tuple(parts) = &expr.kind {
679            for part in parts.iter().flatten() {
680                self.destination(part, state);
681            }
682        } else if self.gcx.resolved_variable(expr).is_none() {
683            for_each_child(expr, &mut |child| {
684                self.expr(child, state);
685            });
686        }
687    }
688
689    fn expr(&mut self, expr: &'gcx Expr<'gcx>, state: &mut State) -> Value {
690        let mut value = self.expr_inner(expr, state);
691        value.refresh(state);
692        self.use_value(&value);
693        value.scalar = value.scalar.or_else(|| {
694            self.gcx.try_eval_const_value(expr).ok().and_then(|value| {
695                value.as_bool().map(Scalar::Bool).or_else(|| value.as_u256().map(Scalar::Uint))
696            })
697        });
698        // Only exact unsigned and boolean values participate in branch pruning.
699        value.scalar =
700            value.scalar.filter(|scalar| match (scalar, self.gcx.type_of_expr(expr.id)) {
701                (Scalar::Uint(value), Some(ty)) => match ty.kind {
702                    TyKind::Elementary(ElementaryType::UInt(size)) => {
703                        value.bit_len() <= size.bits() as usize
704                    }
705                    TyKind::IntLiteral(false, ..) => true,
706                    _ => false,
707                },
708                (Scalar::Bool(_), Some(ty)) => {
709                    matches!(ty.kind, TyKind::Elementary(ElementaryType::Bool))
710                }
711                _ => false,
712            });
713        value
714    }
715
716    fn expr_inner(&mut self, expr: &'gcx Expr<'gcx>, state: &mut State) -> Value {
717        if !self.step() || state.flow == Flow::Halt {
718            return Value::default();
719        }
720        let expr = expr.peel_parens();
721        if let Some(environment) =
722            self.gcx.resolved_builtin(expr).and_then(Environment::from_builtin)
723            && !matches!(environment, Environment::BlockHash | Environment::BlobHash)
724        {
725            return self.read(environment, expr.span, state);
726        }
727        if let Some(var) = self.gcx.resolved_variable(expr)
728            && let Some(value) = state.locals.get(&var)
729        {
730            self.use_value(value);
731            return value.clone();
732        }
733        if self.constant_word(expr, 0) == Some(CHEATCODE_ADDRESS) {
734            return Value { cheatcode: true, ..Value::default() };
735        }
736        match &expr.kind {
737            ExprKind::Assign(lhs, op, rhs) => {
738                let mut value = self.expr(rhs, state);
739                if let Some(op) = op {
740                    let left = self.expr(lhs, state);
741                    let scalar = self.binary_scalar(lhs, op.kind, left.scalar, value.scalar, state);
742                    value.merge(&left);
743                    value.scalar = scalar;
744                    value.cheatcode = false;
745                }
746                if op.is_none() {
747                    self.destination(lhs, state);
748                }
749                value.refresh(state);
750                self.bind(lhs, value.clone(), state);
751                value
752            }
753            ExprKind::Delete(lhs) => {
754                self.destination(lhs, state);
755                let value = self
756                    .gcx
757                    .resolved_variable(lhs)
758                    .map(|var| self.default_value(var))
759                    .unwrap_or_default();
760                self.bind(lhs, value, state);
761                Value::default()
762            }
763            ExprKind::Unary(op, inner) if op.kind.has_side_effects() => {
764                let before = self.expr(inner, state);
765                let mut value = before.clone();
766                let binary = if matches!(op.kind, UnOpKind::PreInc | UnOpKind::PostInc) {
767                    BinOpKind::Add
768                } else {
769                    BinOpKind::Sub
770                };
771                value.scalar = self.binary_scalar(
772                    inner,
773                    binary,
774                    value.scalar,
775                    Some(Scalar::Uint(U256::from(1))),
776                    state,
777                );
778                value.cheatcode = false;
779                self.bind(inner, value.clone(), state);
780                if op.kind.is_prefix() { value } else { before }
781            }
782            ExprKind::Unary(op, inner) => {
783                let mut value = self.expr(inner, state);
784                value.scalar = match (op.kind, value.scalar) {
785                    (UnOpKind::Not, Some(Scalar::Bool(value))) => Some(Scalar::Bool(!value)),
786                    _ => None,
787                };
788                value.cheatcode = false;
789                value
790            }
791            ExprKind::Payable(inner) => self.expr(inner, state),
792            ExprKind::Tuple(parts) => Value {
793                tuple: parts
794                    .iter()
795                    .map(|part| part.map(|part| self.expr(part, state)).unwrap_or_default())
796                    .collect(),
797                ..Value::default()
798            },
799            ExprKind::Ternary(cond, yes, no) => {
800                match self.expr(cond, state).scalar.and_then(Scalar::as_bool) {
801                    Some(true) => self.expr(yes, state),
802                    Some(false) => self.expr(no, state),
803                    None => {
804                        let mut alternate = state.clone();
805                        let mut value = self.expr(yes, state);
806                        let other = self.expr(no, &mut alternate);
807                        if state.flow == Flow::Halt {
808                            *state = alternate;
809                            value = other;
810                        } else if alternate.flow != Flow::Halt {
811                            if value != other {
812                                value = Value::default();
813                            }
814                            state.merge(&alternate);
815                        }
816                        value
817                    }
818                }
819            }
820            ExprKind::Binary(lhs, op, rhs) if matches!(op.kind, BinOpKind::And | BinOpKind::Or) => {
821                let mut value = self.expr(lhs, state);
822                let known = value.scalar.and_then(Scalar::as_bool);
823                let skip = op.kind == BinOpKind::Or;
824                if known == Some(skip) {
825                    return value;
826                }
827                let before = state.clone();
828                let right = self.expr(rhs, state);
829                let scalar = self.binary_scalar(expr, op.kind, value.scalar, right.scalar, state);
830                value.merge(&right);
831                value.scalar = scalar;
832                if known.is_none() {
833                    if state.flow == Flow::Halt {
834                        *state = before;
835                    } else {
836                        state.merge(&before);
837                    }
838                }
839                value.cheatcode = false;
840                value
841            }
842            ExprKind::Binary(lhs, op, rhs) => {
843                let mut value = self.expr(lhs, state);
844                let right = self.expr(rhs, state);
845                let scalar = self.binary_scalar(expr, op.kind, value.scalar, right.scalar, state);
846                value.merge(&right);
847                value.scalar = scalar;
848                value.cheatcode = false;
849                value
850            }
851            ExprKind::Call(callee, args, opts) => {
852                let mut receiver = if let ExprKind::Member(receiver, _) = &callee.peel_parens().kind
853                {
854                    self.expr(receiver, state)
855                } else {
856                    Value::default()
857                };
858                for option in opts.iter().flat_map(|opts| opts.args) {
859                    self.expr(&option.value, state);
860                }
861                let mut arguments: Vec<_> =
862                    args.exprs().map(|arg| (arg.id, self.expr(arg, state))).collect();
863                receiver.refresh(state);
864                self.use_value(&receiver);
865                for (_, value) in &mut arguments {
866                    value.refresh(state);
867                    self.use_value(value);
868                }
869                if let Some(environment) =
870                    self.gcx.resolved_builtin(callee).and_then(Environment::from_builtin)
871                    && matches!(environment, Environment::BlockHash | Environment::BlobHash)
872                {
873                    let mut value = self.read(environment, expr.span, state);
874                    // A hash result also depends on its raw environment-derived index.
875                    for (_, argument) in &arguments {
876                        value.merge(argument);
877                    }
878                    value.cheatcode = false;
879                    return value;
880                }
881                if receiver.cheatcode
882                    && let Some((function, environments)) = self.mutation(callee)
883                {
884                    for &environment in environments {
885                        state.change(environment, Mutation { span: expr.span, function });
886                    }
887                    return Value::default();
888                }
889                if matches!(
890                    self.gcx.type_of_expr(callee.id).map(|ty| ty.kind),
891                    Some(TyKind::Type(_))
892                ) {
893                    let mut value =
894                        arguments.into_iter().next().map(|(_, value)| value).unwrap_or_default();
895                    // Only address-preserving casts keep a proven cheatcode receiver.
896                    value.cheatcode &= self.cast_bits(callee).is_some_and(|bits| bits >= 160);
897                    return value;
898                }
899                let target = if let Some(contract) = self.contract {
900                    dispatched_function(self.gcx, contract, callee)
901                } else if matches!(callee.kind, ExprKind::Ident(_)) {
902                    self.gcx.resolved_function(callee)
903                } else {
904                    None
905                };
906                if let Some(target) = target
907                    && matches!(self.gcx.type_of_expr(callee.id).map(|ty| ty.kind), Some(TyKind::Fn(f)) if f.is_internal())
908                {
909                    let func = self.gcx.hir.function(target);
910                    let bindings = func
911                        .parameters
912                        .iter()
913                        .enumerate()
914                        .map(|(index, &param)| {
915                            let value = self
916                                .gcx
917                                .call_arg(expr, index)
918                                .and_then(|arg| arguments.iter().find(|(id, _)| *id == arg.id))
919                                .map(|(_, value)| value.clone())
920                                .unwrap_or_default();
921                            (param, value)
922                        })
923                        .collect();
924                    return self.call(target, bindings, state);
925                }
926                // External return data and getter results are materialized at the call boundary.
927                Value::default()
928            }
929            _ => {
930                let mut value = Value::default();
931                for_each_child(expr, &mut |child| value.merge(&self.expr(child, state)));
932                value.cheatcode = false;
933                value
934            }
935        }
936    }
937
938    fn default_value(&self, var: VariableId) -> Value {
939        let scalar = match self.gcx.type_of_item(var.into()).kind {
940            TyKind::Elementary(ElementaryType::UInt(_)) => Some(Scalar::Uint(U256::ZERO)),
941            TyKind::Elementary(ElementaryType::Bool) => Some(Scalar::Bool(false)),
942            _ => None,
943        };
944        Value { scalar, ..Value::default() }
945    }
946
947    fn binary_scalar(
948        &self,
949        expr: &Expr<'_>,
950        op: BinOpKind,
951        lhs: Option<Scalar>,
952        rhs: Option<Scalar>,
953        state: &mut State,
954    ) -> Option<Scalar> {
955        let (lhs, rhs) = (lhs?, rhs?);
956        if matches!(op, BinOpKind::Add | BinOpKind::Sub)
957            && let (Scalar::Uint(lhs), Scalar::Uint(rhs)) = (lhs, rhs)
958            && let TyKind::Elementary(ElementaryType::UInt(size)) =
959                self.gcx.type_of_expr(expr.id)?.kind
960        {
961            let (value, overflow) = if op == BinOpKind::Add {
962                lhs.overflowing_add(rhs)
963            } else {
964                lhs.overflowing_sub(rhs)
965            };
966            if !state.unchecked && (overflow || value.bit_len() > size.bits() as usize) {
967                state.flow = Flow::Halt;
968                return None;
969            }
970            return Some(Scalar::Uint(value & (U256::MAX >> (256 - size.bits() as usize))));
971        }
972        lhs.binary(op, rhs)
973    }
974
975    fn mutation(&self, callee: &Expr<'_>) -> Option<(FunctionId, &'static [Environment])> {
976        let function = self.gcx.resolved_function(callee)?;
977        // Match the ABI signature, including overloads, rather than just the method name.
978        let environments: &'static [Environment] = match self.gcx.item_signature(function.into()) {
979            "roll(uint256)" => &[Environment::Number, Environment::BlockHash],
980            "warp(uint256)" => &[Environment::Timestamp],
981            "chainId(uint256)" => &[Environment::ChainId],
982            "coinbase(address)" => &[Environment::Coinbase],
983            "difficulty(uint256)" | "prevrandao(bytes32)" | "prevrandao(uint256)" => {
984                &[Environment::Difficulty, Environment::Prevrandao]
985            }
986            "fee(uint256)" => &[Environment::BaseFee],
987            "blobBaseFee(uint256)" => &[Environment::BlobBaseFee],
988            "txGasPrice(uint256)" => &[Environment::GasPrice],
989            "setBlockhash(uint256,bytes32)" => &[Environment::BlockHash],
990            "blobhashes(bytes32[])" => &[Environment::BlobHash],
991            "selectFork(uint256)"
992            | "createSelectFork(string)"
993            | "createSelectFork(string,uint256)"
994            | "createSelectFork(string,bytes32)" => Environment::ALL,
995            "rollFork(uint256)"
996            | "rollFork(bytes32)"
997            | "rollFork(uint256,uint256)"
998            | "rollFork(uint256,bytes32)" => Environment::BLOCK,
999            "revertTo(uint256)"
1000            | "revertToState(uint256)"
1001            | "revertToAndDelete(uint256)"
1002            | "revertToStateAndDelete(uint256)" => Environment::ALL,
1003            _ => return None,
1004        };
1005        Some((function, environments))
1006    }
1007
1008    /// Evaluates only the constant-address forms used by cheatcode declarations. The pinned
1009    /// compiler's integer evaluator does not yet support general casts or keccak calls.
1010    fn constant_word(&self, expr: &Expr<'_>, depth: usize) -> Option<U256> {
1011        if depth >= 16 {
1012            return None;
1013        }
1014        let expr = expr.peel_parens();
1015        if let Ok(value) = self.gcx.try_eval_const(expr) {
1016            return value.as_u256();
1017        }
1018        if let Some(id) = self.gcx.resolved_variable(expr) {
1019            let var = self.gcx.hir.variable(id);
1020            return var
1021                .is_constant()
1022                .then_some(var.initializer)
1023                .flatten()
1024                .and_then(|init| self.constant_word(init, depth + 1));
1025        }
1026        match &expr.kind {
1027            ExprKind::Call(callee, args, None) if args.exprs().count() == 1 => {
1028                let arg = args.exprs().next()?;
1029                if self.gcx.resolved_builtin(callee) == Some(Builtin::Keccak256) {
1030                    let ConstValue::String(bytes) = self.gcx.try_eval_const_value(arg).ok()? else {
1031                        return None;
1032                    };
1033                    return Some(U256::from_be_bytes(keccak256(bytes.as_byte_str()).0));
1034                }
1035                let bits = self.cast_bits(callee)?;
1036                let value = self.constant_word(arg, depth + 1)?;
1037                Some(value & (U256::MAX >> (256 - bits)))
1038            }
1039            ExprKind::Payable(inner) => self.constant_word(inner, depth + 1),
1040            _ => None,
1041        }
1042    }
1043
1044    fn cast_bits(&self, callee: &Expr<'_>) -> Option<usize> {
1045        let TyKind::Type(ty) = self.gcx.type_of_expr(callee.id)?.kind else { return None };
1046        match ty.kind {
1047            TyKind::Contract(_) | TyKind::Elementary(ElementaryType::Address(_)) => Some(160),
1048            TyKind::Elementary(ElementaryType::UInt(size) | ElementaryType::Int(size)) => {
1049                Some(size.bits() as usize)
1050            }
1051            // A bytesN cast can change alignment. Only bytes32 preserves the complete word.
1052            TyKind::Elementary(ElementaryType::FixedBytes(size)) if size.bits() == 256 => Some(256),
1053            _ => None,
1054        }
1055    }
1056
1057    fn call(
1058        &mut self,
1059        id: FunctionId,
1060        bindings: Vec<(VariableId, Value)>,
1061        state: &mut State,
1062    ) -> Value {
1063        let func = self.gcx.hir.function(id);
1064        if func.body.is_none()
1065            || self.stack.len() >= MAX_CALL_DEPTH
1066            || self.stack.contains(&func.span)
1067        {
1068            return Value::default();
1069        }
1070        self.stack.push(func.span);
1071        let mut input = state.clone();
1072        // An unchecked block does not change arithmetic in a called function.
1073        input.unchecked = false;
1074        let caller_returns = std::mem::replace(&mut input.return_parameters, func.returns.to_vec());
1075        input.locals.extend(bindings);
1076        for var in func.returns {
1077            input.locals.insert(*var, self.default_value(*var));
1078        }
1079        let mut output = self
1080            .layer(func, 0, input)
1081            .into_iter()
1082            .filter(|s| matches!(s.flow, Flow::Next | Flow::Return));
1083        let mut value = Value::default();
1084        if let Some(mut first) = output.next() {
1085            value = self.return_values(func, &first);
1086            self.use_value(&value);
1087            let mut ambiguous = false;
1088            for other in output {
1089                let returned = self.return_values(func, &other);
1090                self.use_value(&returned);
1091                ambiguous |= value != returned;
1092                first.merge(&other);
1093            }
1094            // Component-wise joins can invent tuples that no execution returns, e.g.
1095            // `(block.number, nonVm)` and `(0, vm)`. Keep only an unambiguous return value.
1096            if ambiguous {
1097                value = Value::default();
1098            }
1099            first.flow = Flow::Next;
1100            first.unchecked = state.unchecked;
1101            first.return_parameters = caller_returns;
1102            *state = first;
1103        } else {
1104            state.flow = Flow::Halt;
1105        }
1106        self.stack.pop();
1107        value
1108    }
1109}