Skip to main content

foundry_evm_symbolic/executor/
calls.rs

1use super::*;
2
3impl SymbolicExecutor {
4    pub(super) fn call(
5        &mut self,
6        executor: &ExecutorView<'_>,
7        state: &mut PathState,
8        worklist: &mut VecDeque<PathState>,
9        completed_paths: &mut usize,
10        kind: CallKind,
11    ) -> Result<StepOutcome, SymbolicError> {
12        let pre_call_state = (!state.function_mocks.is_empty()
13            || !state.expected_calls.is_empty()
14            || !state.call_mocks.is_empty()
15            || (state.is_static && matches!(kind, CallKind::Call)))
16        .then(|| state.clone());
17        let call_pc = state.pc.saturating_sub(1);
18
19        let has_value = matches!(kind, CallKind::Call | CallKind::CallCode);
20        let in_offset_idx = if has_value { 3 } else { 2 };
21        let in_offset = state.stack.peek(in_offset_idx)?.clone();
22        let in_size = state.stack.peek(in_offset_idx + 1)?.clone();
23        let out_offset = state.stack.peek(in_offset_idx + 2)?.clone();
24        let out_size = state.stack.peek(in_offset_idx + 3)?.clone();
25        if let Some(outcome) =
26            self.guard_memory_range(executor, state, worklist, &in_offset, &in_size)?
27        {
28            return Ok(outcome);
29        }
30        if let Some(outcome) =
31            self.guard_memory_range(executor, state, worklist, &out_offset, &out_size)?
32        {
33            return Ok(outcome);
34        }
35
36        let gas = state.stack.pop()?;
37        if !gas.is_raw_gasleft() {
38            return Err(SymbolicError::Unsupported("explicit CALL gas limit not modeled"));
39        }
40        let target = state.stack.pop()?;
41        ensure_expr_not_gasleft(&target)?;
42        let target_address = state.world.resolve_address(&target);
43        let value = match (kind, target_address) {
44            (CallKind::Call, Some(to))
45                if to == CHEATCODE_ADDRESS || to == SYMBOLIC_VM_COMPAT_ADDRESS =>
46            {
47                let value = state.stack.pop()?;
48                let value =
49                    state.expect_constrained_word(&mut self.cx, value, "symbolic CALL value")?;
50                SymExpr::constant(&mut self.cx, value)
51            }
52            (CallKind::Call, _) => state.stack.pop()?,
53            (CallKind::CallCode, _) => state.stack.pop()?,
54            (CallKind::StaticCall | CallKind::DelegateCall, _) => SymExpr::zero(&mut self.cx),
55        };
56        ensure_expr_not_gasleft(&value)?;
57        let in_offset = state.stack.pop()?;
58        ensure_expr_not_gasleft(&in_offset)?;
59        let in_size = state.stack.pop()?;
60        ensure_expr_not_gasleft(&in_size)?;
61        let in_size = match state.constrained_usize_checked(&mut self.cx, &in_size) {
62            Some(Ok(size)) => BoundedCopySize::Concrete(size),
63            Some(Err(_)) => {
64                return Ok(StepOutcome::Revert);
65            }
66            None => {
67                let max_limit = self.config.max_calldata_bytes as usize;
68                let max_size = self.solver_upper_bound_usize(
69                    state,
70                    &in_size,
71                    max_limit,
72                    "symbolic CALL input size",
73                )?;
74                BoundedCopySize::Symbolic { size: in_size, max_size }
75            }
76        };
77        let out_offset = state.stack.pop()?;
78        ensure_expr_not_gasleft(&out_offset)?;
79        let out_size = state.stack.pop()?;
80        ensure_expr_not_gasleft(&out_size)?;
81        let out_size = match state.constrained_usize_checked(&mut self.cx, &out_size) {
82            Some(Ok(size)) => BoundedCopySize::Concrete(size),
83            Some(Err(_)) => {
84                return Ok(StepOutcome::Revert);
85            }
86            None => {
87                let max_limit = self.config.max_calldata_bytes as usize;
88                let max_size = self.solver_upper_bound_usize(
89                    state,
90                    &out_size,
91                    max_limit,
92                    "symbolic CALL output size",
93                )?;
94                BoundedCopySize::Symbolic { size: out_size, max_size }
95            }
96        };
97
98        in_size.expand_memory(&mut self.cx, &mut state.memory, in_offset.clone());
99        out_size.expand_memory(&mut self.cx, &mut state.memory, out_offset.clone());
100
101        if state.is_static && matches!(kind, CallKind::Call) {
102            match state.constrained_word(&mut self.cx, &value) {
103                Some(value) if value.is_zero() => {}
104                Some(_) => {
105                    state.return_data = SymReturnData::empty(&mut self.cx);
106                    return Ok(StepOutcome::ExceptionalHalt);
107                }
108                None => {
109                    let zero = SymBoolExpr::eq_word_const(&mut self.cx, &value, U256::ZERO);
110                    let (zero_constraints, zero_sat) =
111                        self.constraints_with_condition(state, zero.clone())?;
112                    let nonzero = zero.not(&mut self.cx);
113                    let (nonzero_constraints, nonzero_sat) =
114                        self.constraints_with_condition(state, nonzero)?;
115                    match (zero_sat, nonzero_sat) {
116                        (true, true) => {
117                            let mut zero_state = pre_call_state
118                                .as_ref()
119                                .expect("static calls preserve pre-call state")
120                                .clone();
121                            zero_state.pc = call_pc;
122                            zero_state.constraints = zero_constraints;
123                            worklist.push_back(zero_state);
124                            state.constraints = nonzero_constraints;
125                            state.return_data = SymReturnData::empty(&mut self.cx);
126                            return Ok(StepOutcome::ExceptionalHalt);
127                        }
128                        (true, false) => state.constraints = zero_constraints,
129                        (false, true) => {
130                            state.constraints = nonzero_constraints;
131                            state.return_data = SymReturnData::empty(&mut self.cx);
132                            return Ok(StepOutcome::ExceptionalHalt);
133                        }
134                        (false, false) => return Ok(StepOutcome::AssumeRejected),
135                    }
136                }
137            }
138        }
139
140        let call_input = in_size.read_from_memory(&mut self.cx, &state.memory, in_offset.clone());
141        // Gas is not modeled, so calldata derived from `GAS` / `gasleft()` must fail closed instead
142        // of handing the callee a fabricated gas value.
143        if call_input.contains_gasleft(&mut self.cx) {
144            return Err(SymbolicError::Unsupported("GAS/gasleft() not modeled"));
145        }
146
147        if let Some(to) = target_address {
148            if !state.function_mocks.is_empty() {
149                let pre_call_state =
150                    pre_call_state.as_ref().expect("function mocks require pre-call state");
151                if self.branch_symbolic_function_mock_if_needed(
152                    state,
153                    worklist,
154                    pre_call_state,
155                    call_pc,
156                    to,
157                    &call_input,
158                )? {
159                    return Ok(StepOutcome::Forked);
160                }
161            }
162            let code_address = if state.function_mocks.is_empty() {
163                to
164            } else {
165                self.function_mock_target(state, to, &call_input)?.unwrap_or(to)
166            };
167            if !state.expected_calls.is_empty() || !state.call_mocks.is_empty() {
168                let pre_call_state =
169                    pre_call_state.as_ref().expect("call mocks require pre-call state");
170                if self.branch_symbolic_call_value_if_needed(
171                    state,
172                    worklist,
173                    pre_call_state,
174                    call_pc,
175                    code_address,
176                    &value,
177                    &gas,
178                    &call_input,
179                )? {
180                    return Ok(StepOutcome::Forked);
181                }
182            }
183            let concrete_value = state.constrained_word(&mut self.cx, &value);
184            if !state.expected_calls.is_empty() || !state.call_mocks.is_empty() {
185                let pre_call_state =
186                    pre_call_state.as_ref().expect("call mocks require pre-call state");
187                if self.branch_symbolic_call_match_if_needed(
188                    state,
189                    worklist,
190                    pre_call_state,
191                    call_pc,
192                    code_address,
193                    concrete_value,
194                    &gas,
195                    &call_input,
196                )? {
197                    return Ok(StepOutcome::Forked);
198                }
199            }
200            return self.call_concrete_target(
201                executor,
202                state,
203                worklist,
204                completed_paths,
205                kind,
206                to,
207                Some(target),
208                value,
209                gas,
210                in_offset,
211                in_size,
212                out_offset,
213                out_size,
214            );
215        }
216
217        self.call_symbolic_target(
218            executor,
219            state,
220            worklist,
221            completed_paths,
222            kind,
223            target,
224            value,
225            gas,
226            in_offset,
227            in_size,
228            out_offset,
229            out_size,
230        )
231    }
232
233    #[expect(clippy::too_many_arguments)]
234    pub(super) fn branch_symbolic_call_value_if_needed(
235        &mut self,
236        state: &mut PathState,
237        worklist: &mut VecDeque<PathState>,
238        pre_call_state: &PathState,
239        call_pc: usize,
240        code_address: Address,
241        value: &SymExpr,
242        gas: &SymExpr,
243        call_input: &SymBytes,
244    ) -> Result<bool, SymbolicError> {
245        if state.constrained_word(&mut self.cx, value).is_some() {
246            return Ok(false);
247        }
248
249        for candidate in self.call_value_candidates(state, code_address, gas, call_input)? {
250            let eq = SymBoolExpr::eq_word_const(&mut self.cx, value, candidate);
251            let (eq_constraints, eq_sat) = self.constraints_with_condition(state, eq.clone())?;
252            let eq_not = eq.not(&mut self.cx);
253            let (neq_constraints, neq_sat) = self.constraints_with_condition(state, eq_not)?;
254
255            match (eq_sat, neq_sat) {
256                (true, true) => {
257                    let mut eq_state = pre_call_state.clone();
258                    eq_state.pc = call_pc;
259                    eq_state.constraints = eq_constraints;
260                    worklist.push_back(eq_state);
261
262                    let mut neq_state = pre_call_state.clone();
263                    neq_state.pc = call_pc;
264                    neq_state.constraints = neq_constraints;
265                    worklist.push_back(neq_state);
266                    return Ok(true);
267                }
268                (true, false) => {
269                    state.constraints = eq_constraints;
270                    return Ok(false);
271                }
272                (false, true) => {
273                    state.constraints = neq_constraints;
274                }
275                (false, false) => return Ok(false),
276            }
277        }
278
279        Ok(false)
280    }
281
282    pub(super) fn branch_symbolic_function_mock_if_needed(
283        &mut self,
284        state: &mut PathState,
285        worklist: &mut VecDeque<PathState>,
286        pre_call_state: &PathState,
287        call_pc: usize,
288        callee: Address,
289        calldata: &SymBytes,
290    ) -> Result<bool, SymbolicError> {
291        for condition in self.function_mock_conditions(state, callee, calldata) {
292            if self.branch_symbolic_match_condition_if_needed(
293                state,
294                worklist,
295                pre_call_state,
296                call_pc,
297                condition,
298            )? {
299                return Ok(true);
300            }
301        }
302
303        Ok(false)
304    }
305
306    pub(super) fn observe_expected_call(
307        &mut self,
308        state: &mut PathState,
309        callee: Address,
310        value: Option<U256>,
311        gas: &SymExpr,
312        calldata: &SymBytes,
313    ) -> Result<bool, SymbolicError> {
314        if state.expected_calls.is_empty() {
315            return Ok(true);
316        }
317        for idx in 0..state.expected_calls.len() {
318            if let Some(constraints) = self.expected_call_match_constraints(
319                state,
320                &state.expected_calls[idx],
321                callee,
322                value,
323                gas,
324                calldata,
325            )? {
326                state.constraints = constraints;
327                return Ok(state.expected_calls[idx].observe());
328            }
329        }
330        Ok(true)
331    }
332
333    #[expect(clippy::too_many_arguments)]
334    pub(super) fn branch_symbolic_call_match_if_needed(
335        &mut self,
336        state: &mut PathState,
337        worklist: &mut VecDeque<PathState>,
338        pre_call_state: &PathState,
339        call_pc: usize,
340        code_address: Address,
341        value: Option<U256>,
342        gas: &SymExpr,
343        calldata: &SymBytes,
344    ) -> Result<bool, SymbolicError> {
345        for condition in self.call_match_conditions(state, code_address, value, gas, calldata)? {
346            if self.branch_symbolic_match_condition_if_needed(
347                state,
348                worklist,
349                pre_call_state,
350                call_pc,
351                condition,
352            )? {
353                return Ok(true);
354            }
355        }
356
357        Ok(false)
358    }
359
360    pub(super) fn take_call_mock(
361        &mut self,
362        state: &mut PathState,
363        callee: Address,
364        value: Option<U256>,
365        calldata: &SymBytes,
366    ) -> Result<Option<CallMockOutcome>, SymbolicError> {
367        if state.call_mocks.is_empty() {
368            return Ok(None);
369        }
370        let mut best = None;
371        for idx in 0..state.call_mocks.len() {
372            let Some(constraints) = self.call_mock_match_constraints(
373                state,
374                &state.call_mocks[idx],
375                callee,
376                value,
377                calldata,
378            )?
379            else {
380                continue;
381            };
382            let specificity = state.call_mocks[idx].specificity();
383            if best.as_ref().is_none_or(
384                |(_, best_specificity, _): &(usize, (usize, bool), Vec<SymBoolExpr>)| {
385                    specificity > *best_specificity
386                },
387            ) {
388                best = Some((idx, specificity, constraints));
389            }
390        }
391        let Some((idx, _, constraints)) = best else {
392            return Ok(None);
393        };
394        state.constraints = constraints;
395        Ok(Some(state.call_mocks[idx].next_outcome(&mut self.cx)))
396    }
397
398    /// Returns whether a `vm.mockCall` answers this call.
399    fn has_call_mock(
400        &mut self,
401        state: &PathState,
402        callee: Address,
403        value: &SymExpr,
404        calldata: &SymBytes,
405    ) -> Result<bool, SymbolicError> {
406        if state.call_mocks.is_empty() {
407            return Ok(false);
408        }
409        let value = state.constrained_word(&mut self.cx, value);
410        for mock in &state.call_mocks {
411            if self.call_mock_match_constraints(state, mock, callee, value, calldata)?.is_some() {
412                return Ok(true);
413            }
414        }
415        Ok(false)
416    }
417
418    pub(super) fn branch_symbolic_match_condition_if_needed(
419        &mut self,
420        state: &mut PathState,
421        worklist: &mut VecDeque<PathState>,
422        pre_call_state: &PathState,
423        call_pc: usize,
424        condition: SymBoolExpr,
425    ) -> Result<bool, SymbolicError> {
426        let (match_constraints, match_sat) =
427            self.constraints_with_condition(state, condition.clone())?;
428        let mismatch_condition = condition.not(&mut self.cx);
429        let (mismatch_constraints, mismatch_sat) =
430            self.constraints_with_condition(state, mismatch_condition)?;
431
432        match (match_sat, mismatch_sat) {
433            (true, true) => {
434                let mut match_state = pre_call_state.clone();
435                match_state.pc = call_pc;
436                match_state.constraints = match_constraints;
437                worklist.push_back(match_state);
438
439                let mut mismatch_state = pre_call_state.clone();
440                mismatch_state.pc = call_pc;
441                mismatch_state.constraints = mismatch_constraints;
442                worklist.push_back(mismatch_state);
443                Ok(true)
444            }
445            (true, false) => {
446                state.constraints = match_constraints;
447                Ok(false)
448            }
449            (false, true) => {
450                state.constraints = mismatch_constraints;
451                Ok(false)
452            }
453            (false, false) => Ok(false),
454        }
455    }
456
457    pub(super) fn function_mock_target(
458        &mut self,
459        state: &mut PathState,
460        callee: Address,
461        calldata: &SymBytes,
462    ) -> Result<Option<Address>, SymbolicError> {
463        for idx in (0..state.function_mocks.len()).rev() {
464            if state.function_mocks[idx].calldata_len() != calldata.len() {
465                continue;
466            }
467            let Some(condition) =
468                state.function_mocks[idx].match_condition(&mut self.cx, callee, calldata)
469            else {
470                continue;
471            };
472            if let Some(constraints) = self.constraints_for_condition(state, condition)? {
473                state.constraints = constraints;
474                return Ok(Some(state.function_mocks[idx].target()));
475            }
476        }
477        for idx in (0..state.function_mocks.len()).rev() {
478            if state.function_mocks[idx].calldata_len() != 4 {
479                continue;
480            }
481            let Some(condition) =
482                state.function_mocks[idx].match_condition(&mut self.cx, callee, calldata)
483            else {
484                continue;
485            };
486            if let Some(constraints) = self.constraints_for_condition(state, condition)? {
487                state.constraints = constraints;
488                return Ok(Some(state.function_mocks[idx].target()));
489            }
490        }
491        Ok(None)
492    }
493
494    pub(super) fn expected_call_match_constraints(
495        &mut self,
496        state: &PathState,
497        expected: &ExpectedCall,
498        callee: Address,
499        value: Option<U256>,
500        gas: &SymExpr,
501        calldata: &SymBytes,
502    ) -> Result<Option<Vec<SymBoolExpr>>, SymbolicError> {
503        let Some(condition) =
504            expected.match_condition(&mut self.cx, callee, value, gas, calldata)?
505        else {
506            return Ok(None);
507        };
508        self.constraints_for_condition(state, condition)
509    }
510
511    pub(super) fn call_mock_match_constraints(
512        &mut self,
513        state: &PathState,
514        mock: &CallMock,
515        callee: Address,
516        value: Option<U256>,
517        calldata: &SymBytes,
518    ) -> Result<Option<Vec<SymBoolExpr>>, SymbolicError> {
519        let Some(condition) = mock.match_condition(&mut self.cx, callee, value, calldata) else {
520            return Ok(None);
521        };
522        self.constraints_for_condition(state, condition)
523    }
524
525    /// Returns whether `expected_revert_matches` holds.
526    pub(super) fn expected_revert_matches(
527        &mut self,
528        state: &mut PathState,
529        expected: &ExpectedRevert,
530        reverter: Address,
531        return_data: &SymReturnData,
532    ) -> Result<bool, SymbolicError> {
533        let Some(condition) = expected.match_condition(&mut self.cx, reverter, return_data) else {
534            return Ok(false);
535        };
536
537        let (match_constraints, match_sat) =
538            self.constraints_with_condition(state, condition.clone())?;
539        if !match_sat {
540            return Ok(false);
541        }
542
543        let mismatch_condition = condition.not(&mut self.cx);
544        let (mismatch_constraints, mismatch_sat) =
545            self.constraints_with_condition(state, mismatch_condition)?;
546        if mismatch_sat {
547            state.constraints = mismatch_constraints;
548            return Ok(false);
549        }
550
551        state.constraints = match_constraints;
552        Ok(true)
553    }
554
555    pub(super) fn assume_no_revert_rejects(
556        &mut self,
557        state: &mut PathState,
558        assumption: &AssumeNoRevert,
559        reverter: Address,
560        return_data: &SymReturnData,
561    ) -> Result<bool, SymbolicError> {
562        let AssumeNoRevert::Filtered(filters) = assumption else {
563            return Ok(true);
564        };
565
566        let conditions = filters
567            .iter()
568            .filter_map(|filter| filter.match_condition(&mut self.cx, reverter, return_data))
569            .collect::<Vec<_>>();
570        if conditions.is_empty() {
571            return Ok(false);
572        }
573
574        let condition = SymBoolExpr::or(&mut self.cx, conditions);
575        let (_match_constraints, match_sat) =
576            self.constraints_with_condition(state, condition.clone())?;
577        if !match_sat {
578            return Ok(false);
579        }
580
581        let mismatch_condition = condition.not(&mut self.cx);
582        let (mismatch_constraints, mismatch_sat) =
583            self.constraints_with_condition(state, mismatch_condition)?;
584        if mismatch_sat {
585            state.constraints = mismatch_constraints;
586            return Ok(false);
587        }
588
589        Ok(true)
590    }
591
592    pub(super) fn constraints_for_condition(
593        &mut self,
594        state: &PathState,
595        condition: SymBoolExpr,
596    ) -> Result<Option<Vec<SymBoolExpr>>, SymbolicError> {
597        let (constraints, sat) = self.constraints_with_condition(state, condition)?;
598        Ok(sat.then_some(constraints))
599    }
600
601    pub(super) fn constraints_with_condition(
602        &mut self,
603        state: &PathState,
604        condition: SymBoolExpr,
605    ) -> Result<(Vec<SymBoolExpr>, bool), SymbolicError> {
606        match condition.as_const() {
607            Some(true) => Ok((state.constraints.clone(), true)),
608            Some(false) => Ok((state.constraints.clone(), false)),
609            None => {
610                let mut constraints = state.constraints.clone();
611                constraints.push(condition);
612                let sat = self.is_sat_with_state(state, &constraints)?;
613                Ok((constraints, sat))
614            }
615        }
616    }
617
618    pub(super) fn take_loop_jump(
619        &self,
620        state: &mut PathState,
621        source_pc: usize,
622        dest: usize,
623    ) -> bool {
624        let Some(bound) = self.config.loop_bound else {
625            return true;
626        };
627        if dest >= source_pc {
628            return true;
629        }
630        let count = state.loop_jumps.entry(dest).or_default();
631        if *count >= bound {
632            return false;
633        }
634        *count += 1;
635        true
636    }
637
638    pub(super) fn handle_log(
639        &mut self,
640        state: &mut PathState,
641        log: SymbolicLog,
642    ) -> Result<StepOutcome, SymbolicError> {
643        let Some(mut expected) = state.expected_emit.take() else {
644            state.record_log(log);
645            return Ok(StepOutcome::Continue);
646        };
647
648        if let Some(template) = expected.template().cloned() {
649            if !self.expected_emit_matches(state, &expected, &template, &log)? {
650                state.expected_emit = Some(expected);
651                state.record_log(log);
652                return Ok(StepOutcome::Failure);
653            }
654            expected.consume_one();
655            if !expected.is_satisfied() {
656                state.expected_emit = Some(expected);
657            }
658        } else {
659            expected.set_template(log.clone());
660            state.expected_emit = Some(expected);
661        }
662
663        state.record_log(log);
664        Ok(StepOutcome::Continue)
665    }
666
667    /// Returns whether `expected_emit_matches` holds.
668    pub(super) fn expected_emit_matches(
669        &mut self,
670        state: &mut PathState,
671        expected: &ExpectedEmit,
672        template: &SymbolicLog,
673        actual: &SymbolicLog,
674    ) -> Result<bool, SymbolicError> {
675        let Some(condition) = expected.match_condition(&mut self.cx, template, actual) else {
676            return Ok(false);
677        };
678        let (match_constraints, match_sat) =
679            self.constraints_with_condition(state, condition.clone())?;
680        if !match_sat {
681            return Ok(false);
682        }
683
684        let mismatch_condition = condition.not(&mut self.cx);
685        let (mismatch_constraints, mismatch_sat) =
686            self.constraints_with_condition(state, mismatch_condition)?;
687        if mismatch_sat {
688            state.constraints = mismatch_constraints;
689            return Ok(false);
690        }
691
692        state.constraints = match_constraints;
693        Ok(true)
694    }
695
696    #[expect(clippy::too_many_arguments)]
697    pub(super) fn call_concrete_target(
698        &mut self,
699        executor: &ExecutorView<'_>,
700        state: &mut PathState,
701        worklist: &mut VecDeque<PathState>,
702        completed_paths: &mut usize,
703        kind: CallKind,
704        to: Address,
705        target_word: Option<SymExpr>,
706        value: SymExpr,
707        gas: SymExpr,
708        in_offset: SymExpr,
709        in_size: BoundedCopySize,
710        out_offset: SymExpr,
711        out_size: BoundedCopySize,
712    ) -> Result<StepOutcome, SymbolicError> {
713        if to == CHEATCODE_ADDRESS || to == SYMBOLIC_VM_COMPAT_ADDRESS {
714            if !state.constrained_word(&mut self.cx, &value).is_some_and(|value| value.is_zero()) {
715                return Err(SymbolicError::Unsupported("value-bearing cheatcode CALL"));
716            }
717            let (in_size_word, in_size, has_symbolic_in_size) = in_size.parts(&mut self.cx);
718            if in_size < 4 {
719                return Err(SymbolicError::Unsupported("short cheatcode CALL"));
720            }
721
722            let has_symbolic_input_offset = in_offset.as_const().is_none();
723            let concrete_in_offset = if has_symbolic_input_offset {
724                None
725            } else {
726                Some(in_offset.as_usize_or("symbolic cheatcode CALL input offset")?)
727            };
728            let selector = if has_symbolic_input_offset {
729                let minimum_offset = state.lower_bound_usize(&in_offset);
730                let maximum_offset = state.upper_bound_usize(&mut self.cx, &in_offset);
731                let selector = state
732                    .memory
733                    .read_bytes_offset_with_bounds(
734                        &mut self.cx,
735                        in_offset.clone(),
736                        4,
737                        minimum_offset,
738                        maximum_offset,
739                    )
740                    .right_aligned_word(&mut self.cx, 0, 4);
741                self.constrained_word_with_solver(state, &selector)?
742                    .map(|selector| selector.to_be_bytes::<32>()[28..].try_into().unwrap())
743                    .ok_or(SymbolicError::Unsupported("symbolic cheatcode selector"))?
744            } else {
745                state
746                    .memory
747                    .read_concrete(
748                        &mut self.cx,
749                        concrete_in_offset.expect("ordinary cheatcode input offset is concrete"),
750                        4,
751                    )?
752                    .try_into()
753                    .map_err(|_| SymbolicError::Unsupported("symbolic cheatcode selector"))?
754            };
755            let full_word_array_assertion =
756                to == CHEATCODE_ADDRESS && is_full_word_array_assertion(selector);
757            if has_symbolic_input_offset && !full_word_array_assertion {
758                return Err(SymbolicError::Unsupported("symbolic cheatcode CALL input offset"));
759            }
760            if has_symbolic_in_size {
761                let min_size = if to == CHEATCODE_ADDRESS {
762                    foundry_cheatcode_min_input_size(selector)
763                } else if to == SYMBOLIC_VM_COMPAT_ADDRESS {
764                    symbolic_vm_cheatcode_min_input_size(selector)
765                } else {
766                    None
767                }
768                .ok_or(SymbolicError::Unsupported("symbolic cheatcode CALL input size"))?;
769                if min_size > in_size {
770                    return Err(SymbolicError::Unsupported("symbolic cheatcode CALL input size"));
771                }
772                if !full_word_array_assertion
773                    && state.lower_bound_usize(&in_size_word) < min_size
774                    && !self.assume_expr_at_least(state, &in_size_word, min_size)?
775                {
776                    return Ok(StepOutcome::AssumeRejected);
777                }
778            }
779
780            if to == CHEATCODE_ADDRESS
781                && let Some(concrete_in_offset) = concrete_in_offset
782                && let Some(outcome) = self.branch_accesses_cheatcode_if_needed(
783                    state,
784                    worklist,
785                    selector,
786                    concrete_in_offset,
787                    out_offset.clone(),
788                    &out_size,
789                )?
790            {
791                return Ok(outcome);
792            }
793
794            if to == CHEATCODE_ADDRESS
795                && let Some(concrete_in_offset) = concrete_in_offset
796                && let Some(outcome) = self.deploy_code_cheatcode_if_needed(
797                    executor,
798                    state,
799                    worklist,
800                    completed_paths,
801                    selector,
802                    concrete_in_offset,
803                    out_offset.clone(),
804                    &out_size,
805                )?
806            {
807                return Ok(outcome);
808            }
809
810            let return_data = if to == CHEATCODE_ADDRESS {
811                let outcome = self.handle_foundry_cheatcode(
812                    executor,
813                    state,
814                    selector,
815                    &in_offset,
816                    &in_size_word,
817                    in_size,
818                )?;
819                match outcome {
820                    CheatcodeOutcome::Continue(ret) => SymReturnData::from_words(&mut self.cx, ret),
821                    CheatcodeOutcome::ContinueData(ret) => ret,
822                    CheatcodeOutcome::Revert(ret) => {
823                        state.return_data = ret;
824                        state.copy_call_output_offset(&mut self.cx, out_offset, &out_size)?;
825                        state.stack.push(SymExpr::zero(&mut self.cx))?;
826                        return Ok(StepOutcome::Continue);
827                    }
828                    CheatcodeOutcome::AssumeRejected => return Ok(StepOutcome::AssumeRejected),
829                    CheatcodeOutcome::Failure => return Ok(StepOutcome::Failure),
830                }
831            } else if to == SYMBOLIC_VM_COMPAT_ADDRESS {
832                self.handle_symbolic_vm_cheatcode(
833                    state,
834                    selector,
835                    concrete_in_offset.expect("symbolic vm input offset is concrete"),
836                )?
837            } else {
838                return Err(SymbolicError::Unsupported("symbolic cheatcode address"));
839            };
840
841            state.return_data = return_data;
842            state.copy_call_output_offset(&mut self.cx, out_offset, &out_size)?;
843            state.stack.push(SymExpr::one(&mut self.cx))?;
844            return Ok(StepOutcome::Continue);
845        }
846
847        if to == HARDHAT_CONSOLE_ADDRESS {
848            state.return_data = SymReturnData::empty(&mut self.cx);
849            state.copy_call_output_offset(&mut self.cx, out_offset, &out_size)?;
850            state.stack.push(SymExpr::one(&mut self.cx))?;
851            return Ok(StepOutcome::Continue);
852        }
853
854        let call_input = in_size.read_from_memory(&mut self.cx, &state.memory, in_offset.clone());
855        // `vm.mockFunction` swaps the code that runs, and the concrete inspector matches
856        // `vm.expectCall` against that address, so resolve the redirect before observing.
857        let code_address = self.function_mock_target(state, to, &call_input)?.unwrap_or(to);
858        if !state.expected_calls.is_empty() {
859            let concrete_value = state.constrained_word(&mut self.cx, &value);
860            if !self.observe_expected_call(
861                state,
862                code_address,
863                concrete_value,
864                &gas,
865                &call_input,
866            )? {
867                return Ok(StepOutcome::Failure);
868            }
869        }
870        let call_context =
871            (!matches!(kind, CallKind::DelegateCall)).then(|| state.prank_for_next_call());
872        // An isolated CALL is its own transaction, so it bumps the caller's nonce even when the
873        // call fails. Concrete Forge answers a mocked call before isolating it.
874        let isolated =
875            matches!(kind, CallKind::Call) && state.starts_isolated_transaction(executor);
876        let transfer_to = if matches!(kind, CallKind::Call) { to } else { state.address };
877        if matches!(kind, CallKind::Call | CallKind::CallCode) {
878            let call_caller = call_context.as_ref().expect("value calls have a call context").0;
879            if isolated && !self.has_call_mock(state, code_address, &value, &call_input)? {
880                state.world.increment_nonce(executor, call_caller)?;
881            }
882            if !self.prepare_value_transfer(
883                executor,
884                state,
885                worklist,
886                call_caller,
887                transfer_to,
888                value.clone(),
889                out_offset.clone(),
890                &out_size,
891            )? {
892                return Ok(StepOutcome::Continue);
893            }
894        }
895        if !state.call_mocks.is_empty() {
896            let concrete_value = state.constrained_word(&mut self.cx, &value);
897            if let Some(mock) =
898                self.take_call_mock(state, code_address, concrete_value, &call_input)?
899            {
900                let (return_data, reverts) = mock.into_parts();
901                state.return_data = return_data;
902                if !reverts && let Some((call_caller, _, _)) = call_context {
903                    self.apply_call_value_transfer(executor, state, kind, to, call_caller, value);
904                }
905                state.copy_call_output_offset(&mut self.cx, out_offset, &out_size)?;
906                let success = SymExpr::constant(&mut self.cx, U256::from(!reverts));
907                state.stack.push(success)?;
908                return Ok(StepOutcome::Continue);
909            }
910        }
911        if matches!(kind, CallKind::DelegateCall) && state.prank.has_active() {
912            return Err(SymbolicError::Unsupported("symbolic prank delegatecall"));
913        }
914        let (call_caller, call_caller_word, pranked_origin) =
915            call_context.unwrap_or_else(|| state.prank_for_next_call());
916
917        let spec_id = executor.spec_id;
918        if is_precompile_for_spec(code_address, spec_id) {
919            let input_len = in_size.size_word(&mut self.cx);
920            let input = in_size.read_from_memory(&mut self.cx, &state.memory, in_offset);
921            if precompile_number_for_spec(code_address, spec_id) == Some(10) {
922                let input_bytes = input.materialize(&mut self.cx);
923                return self.execute_kzg_precompile_call(
924                    executor,
925                    state,
926                    worklist,
927                    kind,
928                    to,
929                    call_caller,
930                    value,
931                    out_offset,
932                    &out_size,
933                    input_bytes,
934                    input_len,
935                );
936            }
937            match execute_symbolic_precompile(
938                &mut self.cx,
939                code_address,
940                input,
941                input_len,
942                spec_id,
943            )? {
944                Some(return_data) => {
945                    state.return_data = return_data;
946                    self.apply_call_value_transfer(executor, state, kind, to, call_caller, value);
947                    state.copy_call_output_offset(&mut self.cx, out_offset, &out_size)?;
948                    state.stack.push(SymExpr::one(&mut self.cx))?;
949                }
950                None => {
951                    state.return_data = SymReturnData::empty(&mut self.cx);
952                    state.copy_call_output_offset(&mut self.cx, out_offset, &out_size)?;
953                    state.stack.push(SymExpr::zero(&mut self.cx))?;
954                }
955            }
956            return Ok(StepOutcome::Continue);
957        }
958
959        let child_code = state.world.extcode(&mut self.cx, executor, code_address)?;
960        if child_code.is_empty() {
961            self.apply_call_value_transfer(executor, state, kind, to, call_caller, value);
962            state.return_data = SymReturnData::empty(&mut self.cx);
963            state.copy_call_output_offset(&mut self.cx, out_offset, &out_size)?;
964            state.stack.push(SymExpr::one(&mut self.cx))?;
965            return Ok(StepOutcome::Continue);
966        }
967
968        let calldata = in_size.calldata(&mut self.cx, call_input);
969        let callee_address_word = state
970            .world
971            .symbolic_word_for_address(to)
972            .or_else(|| {
973                target_word
974                    .as_ref()
975                    .filter(|expr| state.world.resolve_address(expr) == Some(to))
976                    .cloned()
977            })
978            .unwrap_or_else(|| SymExpr::constant(&mut self.cx, address_word(to)));
979        let frame = match kind {
980            CallKind::Call => {
981                let mut frame = CallFrame::new(
982                    &mut self.cx,
983                    to,
984                    to,
985                    call_caller,
986                    value.clone(),
987                    state.is_static,
988                    calldata,
989                );
990                frame.address_word = callee_address_word;
991                frame.caller_word = call_caller_word;
992                frame
993            }
994            CallKind::StaticCall => {
995                let value = SymExpr::zero(&mut self.cx);
996                let mut frame =
997                    CallFrame::new(&mut self.cx, to, to, call_caller, value, true, calldata);
998                frame.address_word = callee_address_word;
999                frame.caller_word = call_caller_word;
1000                frame
1001            }
1002            CallKind::DelegateCall => {
1003                let mut frame = CallFrame::new(
1004                    &mut self.cx,
1005                    state.address,
1006                    state.storage_address,
1007                    state.caller,
1008                    state.callvalue.clone(),
1009                    state.is_static,
1010                    calldata,
1011                );
1012                frame.address_word = state.address_word.clone();
1013                frame.caller_word = state.caller_word.clone();
1014                frame
1015            }
1016            CallKind::CallCode => {
1017                let mut frame = CallFrame::new(
1018                    &mut self.cx,
1019                    state.address,
1020                    state.storage_address,
1021                    call_caller,
1022                    value.clone(),
1023                    state.is_static,
1024                    calldata,
1025                );
1026                frame.address_word = state.address_word.clone();
1027                frame.caller_word = call_caller_word;
1028                frame
1029            }
1030        };
1031
1032        let original_world = state.world.clone();
1033        let mut child = state.child(frame);
1034        if isolated {
1035            child.world.clear_transient_storage();
1036        }
1037        if let Some((origin, origin_word)) = pranked_origin {
1038            child.origin = origin;
1039            child.origin_word = origin_word;
1040        }
1041        self.apply_call_value_transfer(executor, &mut child, kind, to, call_caller, value);
1042        let outcomes = self.execute_external_call(executor, child, &child_code, completed_paths)?;
1043        if outcomes.is_empty() {
1044            return Ok(StepOutcome::AssumeRejected);
1045        }
1046
1047        let mut parents = VecDeque::with_capacity(outcomes.len());
1048        for outcome in outcomes {
1049            match self.join_call_outcome(executor, state, outcome, to)? {
1050                JoinedCallOutcome::Rejected => {}
1051                JoinedCallOutcome::Failure(parent) => {
1052                    *state = parent;
1053                    return Ok(StepOutcome::Failure);
1054                }
1055                JoinedCallOutcome::ExceptionalHalt(mut parent) => {
1056                    parent.world = original_world.clone();
1057                    parent.return_data = SymReturnData::empty(&mut self.cx);
1058                    parent.copy_call_output_offset(&mut self.cx, out_offset.clone(), &out_size)?;
1059                    parent.stack.push(SymExpr::zero(&mut self.cx))?;
1060                    parents.push_back(parent);
1061                }
1062                JoinedCallOutcome::ExpectedRevert { mut parent, child } => {
1063                    parent.expected_creates = child.expected_creates;
1064                    parent.world = original_world.clone();
1065                    parent.return_data = SymReturnData::empty(&mut self.cx);
1066                    parent.copy_call_output_offset(&mut self.cx, out_offset.clone(), &out_size)?;
1067                    parent.stack.push(SymExpr::one(&mut self.cx))?;
1068                    parents.push_back(parent);
1069                }
1070                JoinedCallOutcome::Success { mut parent, child, unmet_expected_revert } => {
1071                    parent.world = child.world;
1072                    if isolated {
1073                        parent.world.restore_transient_storage(&original_world);
1074                    }
1075                    parent.expected_emit = child.expected_emit;
1076                    parent.expected_creates = child.expected_creates;
1077                    parent.return_data = if unmet_expected_revert {
1078                        error_string_return_data(
1079                            &mut self.cx,
1080                            "next call did not revert as expected",
1081                        )
1082                    } else {
1083                        child.frame.return_data
1084                    };
1085                    parent.copy_call_output_offset(&mut self.cx, out_offset.clone(), &out_size)?;
1086                    let success =
1087                        SymExpr::constant(&mut self.cx, U256::from(!unmet_expected_revert));
1088                    parent.stack.push(success)?;
1089                    parents.push_back(parent);
1090                }
1091                JoinedCallOutcome::Revert { mut parent, child } => {
1092                    parent.expected_creates = child.expected_creates;
1093                    parent.world = original_world.clone();
1094                    parent.return_data = child.frame.return_data;
1095                    parent.copy_call_output_offset(&mut self.cx, out_offset.clone(), &out_size)?;
1096                    parent.stack.push(SymExpr::zero(&mut self.cx))?;
1097                    parents.push_back(parent);
1098                }
1099            }
1100        }
1101
1102        Ok(self.resume_parent_paths(state, worklist, parents))
1103    }
1104
1105    #[expect(clippy::too_many_arguments)]
1106    fn execute_kzg_precompile_call(
1107        &mut self,
1108        executor: &ExecutorView<'_>,
1109        state: &mut PathState,
1110        worklist: &mut VecDeque<PathState>,
1111        kind: CallKind,
1112        to: Address,
1113        call_caller: Address,
1114        value: SymExpr,
1115        out_offset: SymExpr,
1116        out_size: &BoundedCopySize,
1117        input: Vec<SymExpr>,
1118        input_len: SymExpr,
1119    ) -> Result<StepOutcome, SymbolicError> {
1120        if let Some(outcome) = kzg_constrained_outcome(&mut self.cx, state, &input, &input_len)? {
1121            self.apply_precompile_outcome(
1122                executor,
1123                state,
1124                kind,
1125                to,
1126                call_caller,
1127                value,
1128                out_offset,
1129                out_size,
1130                outcome,
1131            )?;
1132            return Ok(StepOutcome::Continue);
1133        }
1134
1135        let success_condition = kzg_success_witness_condition(&mut self.cx, &input, &input_len);
1136        let failure_condition =
1137            kzg_failure_witness_condition(&mut self.cx, state, &input, &input_len);
1138        let modeled_condition = SymBoolExpr::or(
1139            &mut self.cx,
1140            vec![success_condition.clone(), failure_condition.clone()],
1141        );
1142        let modeled_condition = modeled_condition.not(&mut self.cx);
1143        let (_, residual_sat) = self.constraints_with_condition(state, modeled_condition)?;
1144        if residual_sat {
1145            self.defer_incomplete(KZG_RESIDUAL_REASON);
1146        }
1147
1148        let (success_constraints, success_sat) =
1149            self.constraints_with_condition(state, success_condition)?;
1150
1151        let (failure_constraints, failure_sat) =
1152            self.constraints_with_condition(state, failure_condition)?;
1153
1154        match (success_sat, failure_sat) {
1155            (true, true) => {
1156                let mut failure = state.clone();
1157                failure.constraints = failure_constraints;
1158                self.apply_precompile_outcome(
1159                    executor,
1160                    &mut failure,
1161                    kind,
1162                    to,
1163                    call_caller,
1164                    value.clone(),
1165                    out_offset.clone(),
1166                    out_size,
1167                    None,
1168                )?;
1169                worklist.push_back(failure);
1170
1171                state.constraints = success_constraints;
1172                let return_data = kzg_success_return_data(&mut self.cx);
1173                self.apply_precompile_outcome(
1174                    executor,
1175                    state,
1176                    kind,
1177                    to,
1178                    call_caller,
1179                    value,
1180                    out_offset,
1181                    out_size,
1182                    Some(return_data),
1183                )?;
1184                Ok(StepOutcome::Continue)
1185            }
1186            (true, false) => {
1187                state.constraints = success_constraints;
1188                let return_data = kzg_success_return_data(&mut self.cx);
1189                self.apply_precompile_outcome(
1190                    executor,
1191                    state,
1192                    kind,
1193                    to,
1194                    call_caller,
1195                    value,
1196                    out_offset,
1197                    out_size,
1198                    Some(return_data),
1199                )?;
1200                Ok(StepOutcome::Continue)
1201            }
1202            (false, true) => {
1203                state.constraints = failure_constraints;
1204                self.apply_precompile_outcome(
1205                    executor,
1206                    state,
1207                    kind,
1208                    to,
1209                    call_caller,
1210                    value,
1211                    out_offset,
1212                    out_size,
1213                    None,
1214                )?;
1215                Ok(StepOutcome::Continue)
1216            }
1217            (false, false) => Err(SymbolicError::Unsupported(KZG_RESIDUAL_REASON)),
1218        }
1219    }
1220
1221    #[expect(clippy::too_many_arguments)]
1222    /// Applies a precompile call result to the current symbolic state.
1223    fn apply_precompile_outcome(
1224        &mut self,
1225        executor: &ExecutorView<'_>,
1226        state: &mut PathState,
1227        kind: CallKind,
1228        to: Address,
1229        call_caller: Address,
1230        value: SymExpr,
1231        out_offset: SymExpr,
1232        out_size: &BoundedCopySize,
1233        outcome: Option<SymReturnData>,
1234    ) -> Result<(), SymbolicError> {
1235        match outcome {
1236            Some(return_data) => {
1237                state.return_data = return_data;
1238                self.apply_call_value_transfer(executor, state, kind, to, call_caller, value);
1239                state.copy_call_output_offset(&mut self.cx, out_offset, out_size)?;
1240                state.stack.push(SymExpr::one(&mut self.cx))?;
1241            }
1242            None => {
1243                state.return_data = SymReturnData::empty(&mut self.cx);
1244                state.copy_call_output_offset(&mut self.cx, out_offset, out_size)?;
1245                state.stack.push(SymExpr::zero(&mut self.cx))?;
1246            }
1247        }
1248        Ok(())
1249    }
1250
1251    fn apply_call_value_transfer(
1252        &mut self,
1253        executor: &ExecutorView<'_>,
1254        state: &mut PathState,
1255        kind: CallKind,
1256        to: Address,
1257        from: Address,
1258        value: SymExpr,
1259    ) {
1260        let to = match kind {
1261            CallKind::Call => to,
1262            CallKind::CallCode => state.address,
1263            CallKind::DelegateCall | CallKind::StaticCall => return,
1264        };
1265        state.world.transfer(&mut self.cx, executor, from, to, value);
1266    }
1267
1268    #[expect(clippy::too_many_arguments)]
1269    pub(super) fn prepare_value_transfer(
1270        &mut self,
1271        executor: &ExecutorView<'_>,
1272        state: &mut PathState,
1273        worklist: &mut VecDeque<PathState>,
1274        from: Address,
1275        to: Address,
1276        value: SymExpr,
1277        out_offset: SymExpr,
1278        out_size: &BoundedCopySize,
1279    ) -> Result<bool, SymbolicError> {
1280        if state.constrained_word(&mut self.cx, &value).is_some_and(|value| value.is_zero()) {
1281            return Ok(true);
1282        }
1283
1284        let balance = state.balance(&mut self.cx, executor, from);
1285        let can_pay = SymBoolExpr::cmp(&mut self.cx, SymCmpOp::Uge, balance, value.clone());
1286        let can_transfer = if from == to {
1287            can_pay
1288        } else {
1289            let balance = state.balance(&mut self.cx, executor, to);
1290            let sum = SymExpr::binop(&mut self.cx, SymBinOp::Add, balance.clone(), value);
1291            let no_overflow = SymBoolExpr::cmp(&mut self.cx, SymCmpOp::Uge, sum, balance);
1292            SymBoolExpr::and(&mut self.cx, vec![can_pay, no_overflow])
1293        };
1294        match can_transfer.as_const() {
1295            Some(true) => Ok(true),
1296            Some(false) => {
1297                state.return_data = SymReturnData::empty(&mut self.cx);
1298                state.copy_call_output_offset(&mut self.cx, out_offset, out_size)?;
1299                state.stack.push(SymExpr::zero(&mut self.cx))?;
1300                Ok(false)
1301            }
1302            None => {
1303                let mut success_constraints = state.constraints.clone();
1304                success_constraints.push(can_transfer.clone());
1305                let success_sat = self.is_sat_with_state(state, &success_constraints)?;
1306
1307                let mut failure_constraints = state.constraints.clone();
1308                failure_constraints.push(can_transfer.not(&mut self.cx));
1309                let failure_sat = self.is_sat_with_state(state, &failure_constraints)?;
1310
1311                match (success_sat, failure_sat) {
1312                    (true, true) => {
1313                        let mut failure = state.clone();
1314                        failure.constraints = failure_constraints;
1315                        failure.return_data = SymReturnData::empty(&mut self.cx);
1316                        failure.copy_call_output_offset(&mut self.cx, out_offset, out_size)?;
1317                        failure.stack.push(SymExpr::zero(&mut self.cx))?;
1318                        worklist.push_back(failure);
1319
1320                        state.constraints = success_constraints;
1321                        Ok(true)
1322                    }
1323                    (true, false) => {
1324                        state.constraints = success_constraints;
1325                        Ok(true)
1326                    }
1327                    (false, true) => {
1328                        state.constraints = failure_constraints;
1329                        state.return_data = SymReturnData::empty(&mut self.cx);
1330                        state.copy_call_output_offset(&mut self.cx, out_offset, out_size)?;
1331                        state.stack.push(SymExpr::zero(&mut self.cx))?;
1332                        Ok(false)
1333                    }
1334                    (false, false) => Ok(false),
1335                }
1336            }
1337        }
1338    }
1339
1340    pub(super) fn prepare_create_value_transfer(
1341        &mut self,
1342        executor: &ExecutorView<'_>,
1343        state: &mut PathState,
1344        worklist: &mut VecDeque<PathState>,
1345        value: SymExpr,
1346    ) -> Result<bool, SymbolicError> {
1347        if state.constrained_word(&mut self.cx, &value).is_some_and(|value| value.is_zero()) {
1348            return Ok(true);
1349        }
1350
1351        let balance = state.balance(&mut self.cx, executor, state.address);
1352        let can_pay = SymBoolExpr::cmp(&mut self.cx, SymCmpOp::Uge, balance, value);
1353        match can_pay.as_const() {
1354            Some(true) => Ok(true),
1355            Some(false) => {
1356                state.return_data = SymReturnData::empty(&mut self.cx);
1357                state.stack.push(SymExpr::zero(&mut self.cx))?;
1358                Ok(false)
1359            }
1360            None => {
1361                let mut success_constraints = state.constraints.clone();
1362                success_constraints.push(can_pay.clone());
1363                let success_sat = self.is_sat_with_state(state, &success_constraints)?;
1364
1365                let mut failure_constraints = state.constraints.clone();
1366                failure_constraints.push(can_pay.not(&mut self.cx));
1367                let failure_sat = self.is_sat_with_state(state, &failure_constraints)?;
1368
1369                match (success_sat, failure_sat) {
1370                    (true, true) => {
1371                        let mut failure = state.clone();
1372                        failure.constraints = failure_constraints;
1373                        failure.return_data = SymReturnData::empty(&mut self.cx);
1374                        failure.stack.push(SymExpr::zero(&mut self.cx))?;
1375                        worklist.push_back(failure);
1376
1377                        state.constraints = success_constraints;
1378                        Ok(true)
1379                    }
1380                    (true, false) => {
1381                        state.constraints = success_constraints;
1382                        Ok(true)
1383                    }
1384                    (false, true) => {
1385                        state.constraints = failure_constraints;
1386                        state.return_data = SymReturnData::empty(&mut self.cx);
1387                        state.stack.push(SymExpr::zero(&mut self.cx))?;
1388                        Ok(false)
1389                    }
1390                    (false, false) => Ok(false),
1391                }
1392            }
1393        }
1394    }
1395
1396    #[expect(clippy::too_many_arguments)]
1397    pub(super) fn call_symbolic_target(
1398        &mut self,
1399        executor: &ExecutorView<'_>,
1400        state: &mut PathState,
1401        worklist: &mut VecDeque<PathState>,
1402        completed_paths: &mut usize,
1403        kind: CallKind,
1404        target: SymExpr,
1405        value: SymExpr,
1406        gas: SymExpr,
1407        in_offset: SymExpr,
1408        in_size: BoundedCopySize,
1409        out_offset: SymExpr,
1410        out_size: BoundedCopySize,
1411    ) -> Result<StepOutcome, SymbolicError> {
1412        let mut candidates = state.world.symbolic_call_targets(&mut self.cx, executor)?;
1413        candidates.extend(active_precompiles(executor.spec_id).addresses().copied());
1414        candidates.sort();
1415        candidates.dedup();
1416        if candidates.is_empty() {
1417            return Err(SymbolicError::Unsupported(
1418                "symbolic CALL target has no known contract candidates",
1419            ));
1420        }
1421
1422        let candidate_constraints = candidates
1423            .iter()
1424            .map(|address| {
1425                let address = SymExpr::constant(&mut self.cx, address_word(*address));
1426                SymBoolExpr::eq(&mut self.cx, target.clone(), address)
1427            })
1428            .collect::<Vec<_>>();
1429        let mut outside_constraints = state.constraints.clone();
1430        outside_constraints.extend(
1431            candidate_constraints.iter().cloned().map(|condition| condition.not(&mut self.cx)),
1432        );
1433        let outside_sat = self.is_sat_with_state(state, &outside_constraints)?;
1434
1435        if !self.config.symbolic_call_targets && outside_sat {
1436            return Err(SymbolicError::Unsupported("symbolic CALL target"));
1437        }
1438
1439        let mut parents = VecDeque::new();
1440        if outside_sat {
1441            let mut branch = state.clone();
1442            branch.constraints = outside_constraints;
1443
1444            if matches!(kind, CallKind::DelegateCall) && branch.prank.has_active() {
1445                return Err(SymbolicError::Unsupported("symbolic prank delegatecall"));
1446            }
1447            let (call_caller, _, _) = branch.prank_for_next_call();
1448            if matches!(kind, CallKind::Call) && branch.starts_isolated_transaction(executor) {
1449                branch.world.increment_nonce(executor, call_caller)?;
1450            }
1451            if matches!(kind, CallKind::Call | CallKind::CallCode) {
1452                let transfer_to = if matches!(kind, CallKind::Call) {
1453                    branch.world.symbolic_address_slot(target)
1454                } else {
1455                    branch.address
1456                };
1457                if self.prepare_value_transfer(
1458                    executor,
1459                    &mut branch,
1460                    &mut parents,
1461                    call_caller,
1462                    transfer_to,
1463                    value.clone(),
1464                    out_offset.clone(),
1465                    &out_size,
1466                )? {
1467                    branch.world.transfer(
1468                        &mut self.cx,
1469                        executor,
1470                        call_caller,
1471                        transfer_to,
1472                        value.clone(),
1473                    );
1474                    branch.return_data = SymReturnData::empty(&mut self.cx);
1475                    branch.copy_call_output_offset(&mut self.cx, out_offset.clone(), &out_size)?;
1476                    branch.stack.push(SymExpr::one(&mut self.cx))?;
1477                }
1478            } else {
1479                branch.return_data = SymReturnData::empty(&mut self.cx);
1480                branch.copy_call_output_offset(&mut self.cx, out_offset.clone(), &out_size)?;
1481                branch.stack.push(SymExpr::one(&mut self.cx))?;
1482            }
1483            parents.push_back(branch);
1484        }
1485
1486        let call_input = in_size.read_from_memory(&mut self.cx, &state.memory, in_offset.clone());
1487        for (to, constraint) in candidates.into_iter().zip(candidate_constraints) {
1488            let mut branch = state.clone();
1489            branch.constraints.push(constraint);
1490            if !self.is_sat_with_state(&branch, &branch.constraints)? {
1491                continue;
1492            }
1493
1494            // Decide mock matches before executing either the mocked or real call.
1495            for mut branch in
1496                self.split_symbolic_target_candidate(branch, to, &value, &gas, &call_input)?
1497            {
1498                let mut branch_worklist = VecDeque::new();
1499                match self.call_concrete_target(
1500                    executor,
1501                    &mut branch,
1502                    &mut branch_worklist,
1503                    completed_paths,
1504                    kind,
1505                    to,
1506                    None,
1507                    value.clone(),
1508                    gas.clone(),
1509                    in_offset.clone(),
1510                    in_size.clone(),
1511                    out_offset.clone(),
1512                    out_size.clone(),
1513                )? {
1514                    StepOutcome::Continue => {
1515                        parents.push_back(branch);
1516                        parents.extend(branch_worklist);
1517                    }
1518                    StepOutcome::AssumeRejected => {}
1519                    outcome => {
1520                        *state = branch;
1521                        return Ok(outcome);
1522                    }
1523                }
1524            }
1525        }
1526
1527        let Some(first) = self.pop_next_path(&mut parents) else {
1528            return Ok(StepOutcome::AssumeRejected);
1529        };
1530        *state = first;
1531        worklist.extend(parents);
1532        Ok(StepOutcome::Continue)
1533    }
1534
1535    /// Decides mock and expectation matches before executing a symbolic-target candidate.
1536    fn split_symbolic_target_candidate(
1537        &mut self,
1538        branch: PathState,
1539        to: Address,
1540        value: &SymExpr,
1541        gas: &SymExpr,
1542        call_input: &SymBytes,
1543    ) -> Result<Vec<PathState>, SymbolicError> {
1544        let mut branches = vec![branch];
1545        for condition in self.function_mock_conditions(&branches[0], to, call_input) {
1546            branches = self.split_branches_on(branches, condition)?;
1547        }
1548
1549        let mut out = Vec::new();
1550        for mut branch in branches {
1551            let code_address = if branch.function_mocks.is_empty() {
1552                to
1553            } else {
1554                self.function_mock_target(&mut branch, to, call_input)?.unwrap_or(to)
1555            };
1556
1557            let mut value_branches = vec![branch];
1558            if value_branches[0].constrained_word(&mut self.cx, value).is_none() {
1559                let candidates =
1560                    self.call_value_candidates(&value_branches[0], code_address, gas, call_input)?;
1561                for candidate in candidates {
1562                    let eq = SymBoolExpr::eq_word_const(&mut self.cx, value, candidate);
1563                    value_branches = self.split_branches_on(value_branches, eq)?;
1564                }
1565            }
1566
1567            for branch in value_branches {
1568                let concrete_value = branch.constrained_word(&mut self.cx, value);
1569                let mut match_branches = vec![branch];
1570                let conditions = self.call_match_conditions(
1571                    &match_branches[0],
1572                    code_address,
1573                    concrete_value,
1574                    gas,
1575                    call_input,
1576                )?;
1577                for condition in conditions {
1578                    match_branches = self.split_branches_on(match_branches, condition)?;
1579                }
1580                if out.len() + match_branches.len() > self.config.path_width() as usize {
1581                    return Err(SymbolicError::Unsupported("symbolic path limit exceeded"));
1582                }
1583                out.extend(match_branches);
1584            }
1585        }
1586        Ok(out)
1587    }
1588
1589    /// Splits branches on a match condition, retaining feasible outcomes.
1590    fn split_branches_on(
1591        &mut self,
1592        branches: Vec<PathState>,
1593        condition: SymBoolExpr,
1594    ) -> Result<Vec<PathState>, SymbolicError> {
1595        let mismatch_condition = condition.clone().not(&mut self.cx);
1596        let mut next = Vec::with_capacity(branches.len());
1597        for branch in branches {
1598            let (match_constraints, match_sat) =
1599                self.constraints_with_condition(&branch, condition.clone())?;
1600            let (mismatch_constraints, mismatch_sat) =
1601                self.constraints_with_condition(&branch, mismatch_condition.clone())?;
1602            match (match_sat, mismatch_sat) {
1603                (true, true) => {
1604                    let mut match_branch = branch.clone();
1605                    match_branch.constraints = match_constraints;
1606                    next.push(match_branch);
1607                    let mut mismatch_branch = branch;
1608                    mismatch_branch.constraints = mismatch_constraints;
1609                    next.push(mismatch_branch);
1610                }
1611                (true, false) => {
1612                    let mut branch = branch;
1613                    branch.constraints = match_constraints;
1614                    next.push(branch);
1615                }
1616                (false, true) => {
1617                    let mut branch = branch;
1618                    branch.constraints = mismatch_constraints;
1619                    next.push(branch);
1620                }
1621                (false, false) => {}
1622            }
1623            if next.len() > self.config.path_width() as usize {
1624                return Err(SymbolicError::Unsupported("symbolic path limit exceeded"));
1625            }
1626        }
1627        Ok(next)
1628    }
1629
1630    /// Function mock conditions in match-precedence order.
1631    fn function_mock_conditions(
1632        &mut self,
1633        state: &PathState,
1634        callee: Address,
1635        calldata: &SymBytes,
1636    ) -> Vec<SymBoolExpr> {
1637        let mut conditions = Vec::new();
1638        for calldata_len in [calldata.len(), 4] {
1639            for idx in (0..state.function_mocks.len()).rev() {
1640                if state.function_mocks[idx].calldata_len() != calldata_len {
1641                    continue;
1642                }
1643                if let Some(condition) =
1644                    state.function_mocks[idx].match_condition(&mut self.cx, callee, calldata)
1645                {
1646                    conditions.push(condition);
1647                }
1648            }
1649        }
1650        conditions
1651    }
1652
1653    /// Values that can satisfy a call expectation or mock.
1654    fn call_value_candidates(
1655        &mut self,
1656        state: &PathState,
1657        code_address: Address,
1658        gas: &SymExpr,
1659        call_input: &SymBytes,
1660    ) -> Result<Vec<U256>, SymbolicError> {
1661        let mut candidates = HashSet::<U256>::default();
1662        for expected in &state.expected_calls {
1663            let Some(expected_value) = expected.value() else { continue };
1664            if self
1665                .expected_call_match_constraints(
1666                    state,
1667                    expected,
1668                    code_address,
1669                    Some(expected_value),
1670                    gas,
1671                    call_input,
1672                )?
1673                .is_some()
1674            {
1675                candidates.insert(expected_value);
1676            }
1677        }
1678        for mock in &state.call_mocks {
1679            let Some(mock_value) = mock.value() else { continue };
1680            if self
1681                .call_mock_match_constraints(
1682                    state,
1683                    mock,
1684                    code_address,
1685                    Some(mock_value),
1686                    call_input,
1687                )?
1688                .is_some()
1689            {
1690                candidates.insert(mock_value);
1691            }
1692        }
1693        let mut candidates = candidates.into_iter().collect::<Vec<_>>();
1694        candidates.sort_unstable();
1695        Ok(candidates)
1696    }
1697
1698    /// Expected call and call mock conditions in match-precedence order.
1699    fn call_match_conditions(
1700        &mut self,
1701        state: &PathState,
1702        code_address: Address,
1703        value: Option<U256>,
1704        gas: &SymExpr,
1705        calldata: &SymBytes,
1706    ) -> Result<Vec<SymBoolExpr>, SymbolicError> {
1707        let mut conditions = Vec::new();
1708        for expected in &state.expected_calls {
1709            if let Some(condition) =
1710                expected.match_condition(&mut self.cx, code_address, value, gas, calldata)?
1711            {
1712                conditions.push(condition);
1713            }
1714        }
1715        let mut mocks = (0..state.call_mocks.len()).collect::<Vec<_>>();
1716        mocks.sort_by_key(|idx| {
1717            let (len, has_value) = state.call_mocks[*idx].specificity();
1718            (std::cmp::Reverse(len), std::cmp::Reverse(has_value), *idx)
1719        });
1720        for idx in mocks {
1721            if let Some(condition) =
1722                state.call_mocks[idx].match_condition(&mut self.cx, code_address, value, calldata)
1723            {
1724                conditions.push(condition);
1725            }
1726        }
1727        Ok(conditions)
1728    }
1729}
1730
1731const KZG_POINT_EVALUATION_INPUT_LEN: usize = 192;
1732const KZG_VERSIONED_HASH_OFFSET: usize = 0;
1733const KZG_Z_OFFSET: usize = 32;
1734const KZG_Y_OFFSET: usize = 64;
1735const KZG_COMMITMENT_OFFSET: usize = 96;
1736const KZG_PROOF_OFFSET: usize = 144;
1737
1738const KZG_BLS_MODULUS: [u8; 32] =
1739    hex!("73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001");
1740
1741const KZG_SUCCESS_INPUT: [u8; KZG_POINT_EVALUATION_INPUT_LEN] = hex!(
1742    "01e798154708fe7789429634053cbf9f99b619f9f084048927333fce637f549b"
1743    "73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000000"
1744    "1522a4a7f34e1ea350ae07c29c96c7e79655aa926122e95fe69fcbd932ca49e9"
1745    "8f59a8d2a1a625a17f3fea0fe5eb8c896db3764f3185481bc22f91b4aaffcca25f26936857bc3a7c2539ea8ec3a952b7"
1746    "a62ad71d14c5719385c0686f1871430475bf3a00f0aa3f7b8dd99a9abc2160744faf0070725e00b60ad9a026a15b1a8c"
1747);
1748
1749const KZG_INVALID_PROOF: [u8; 48] = [0xff; 48];
1750const KZG_ZERO_COMMITMENT: [u8; 48] = [0x00; 48];
1751const KZG_ONE_COMMITMENT: [u8; 48] = [0x01; 48];
1752const KZG_RESIDUAL_REASON: &str = "symbolic KZG point-evaluation precompile residual not modeled";
1753
1754fn kzg_success_return_data(cx: &mut SymCx) -> SymReturnData {
1755    SymReturnData::from_concrete_bytes(cx, kzg_point_evaluation::RETURN_VALUE.to_vec())
1756}
1757
1758fn kzg_constrained_outcome(
1759    cx: &mut SymCx,
1760    state: &PathState,
1761    input: &[SymExpr],
1762    input_len: &SymExpr,
1763) -> Result<Option<Option<SymReturnData>>, SymbolicError> {
1764    let Some(input_len) = state.constrained_usize(cx, input_len) else {
1765        return Ok(None);
1766    };
1767    if input_len != KZG_POINT_EVALUATION_INPUT_LEN {
1768        return Ok(Some(None));
1769    }
1770    if input_len > input.len() {
1771        return Err(SymbolicError::Unsupported("out-of-bounds symbolic precompile input"));
1772    }
1773
1774    if let Some(input) = constrained_bytes_at(cx, state, input, 0, input_len) {
1775        return execute_precompile(cx, kzg_point_evaluation::ADDRESS, &input, SpecId::CANCUN)
1776            .map(Some);
1777    }
1778
1779    if constrained_byte(cx, state, &input[0])
1780        .is_some_and(|version| version != kzg_point_evaluation::VERSIONED_HASH_VERSION_KZG)
1781    {
1782        return Ok(Some(None));
1783    }
1784
1785    if constrained_bytes_at(cx, state, input, KZG_Z_OFFSET, KZG_BLS_MODULUS.len())
1786        .is_some_and(|z| z == KZG_BLS_MODULUS)
1787        || constrained_bytes_at(cx, state, input, KZG_Y_OFFSET, KZG_BLS_MODULUS.len())
1788            .is_some_and(|y| y == KZG_BLS_MODULUS)
1789        || constrained_bytes_at(cx, state, input, KZG_PROOF_OFFSET, KZG_INVALID_PROOF.len())
1790            .is_some_and(|proof| proof == KZG_INVALID_PROOF)
1791    {
1792        return Ok(Some(None));
1793    }
1794
1795    if let Some(commitment) = constrained_bytes_at(cx, state, input, KZG_COMMITMENT_OFFSET, 48) {
1796        let expected_hash = kzg_point_evaluation::kzg_to_versioned_hash(&commitment);
1797        for (idx, expected) in expected_hash.into_iter().enumerate() {
1798            if constrained_byte(cx, state, &input[idx]).is_some_and(|actual| actual != expected) {
1799                return Ok(Some(None));
1800            }
1801        }
1802    }
1803
1804    Ok(None)
1805}
1806
1807fn kzg_success_witness_condition(
1808    cx: &mut SymCx,
1809    input: &[SymExpr],
1810    input_len: &SymExpr,
1811) -> SymBoolExpr {
1812    let len = expr_eq_condition(cx, input_len, KZG_POINT_EVALUATION_INPUT_LEN);
1813    let bytes = bytes_eq_condition(cx, input, KZG_VERSIONED_HASH_OFFSET, &KZG_SUCCESS_INPUT);
1814    SymBoolExpr::and(cx, vec![len, bytes])
1815}
1816
1817fn kzg_failure_witness_condition(
1818    cx: &mut SymCx,
1819    state: &PathState,
1820    input: &[SymExpr],
1821    input_len: &SymExpr,
1822) -> SymBoolExpr {
1823    let len_192 = expr_eq_condition(cx, input_len, KZG_POINT_EVALUATION_INPUT_LEN);
1824    let len_ne_192 = expr_ne_condition(cx, input_len, KZG_POINT_EVALUATION_INPUT_LEN);
1825    let bad_version =
1826        byte_ne_condition(cx, input, 0, kzg_point_evaluation::VERSIONED_HASH_VERSION_KZG);
1827    let bad_z = bytes_eq_condition(cx, input, KZG_Z_OFFSET, &KZG_BLS_MODULUS);
1828    let bad_y = bytes_eq_condition(cx, input, KZG_Y_OFFSET, &KZG_BLS_MODULUS);
1829    let bad_proof = bytes_eq_condition(cx, input, KZG_PROOF_OFFSET, &KZG_INVALID_PROOF);
1830    let mut conditions = vec![
1831        len_ne_192,
1832        SymBoolExpr::and(cx, vec![len_192.clone(), bad_version]),
1833        SymBoolExpr::and(cx, vec![len_192.clone(), bad_z]),
1834        SymBoolExpr::and(cx, vec![len_192.clone(), bad_y]),
1835        SymBoolExpr::and(cx, vec![len_192.clone(), bad_proof]),
1836    ];
1837
1838    if let Some(commitment) = constrained_bytes_at(cx, state, input, KZG_COMMITMENT_OFFSET, 48) {
1839        let expected_hash = kzg_point_evaluation::kzg_to_versioned_hash(&commitment);
1840        let mismatch = kzg_versioned_hash_mismatch_condition(cx, input, &expected_hash);
1841        conditions.push(SymBoolExpr::and(cx, vec![len_192.clone(), mismatch]));
1842    }
1843
1844    let expected_hash = &KZG_SUCCESS_INPUT[KZG_VERSIONED_HASH_OFFSET..KZG_Z_OFFSET];
1845    let commitment = &KZG_SUCCESS_INPUT[KZG_COMMITMENT_OFFSET..KZG_PROOF_OFFSET];
1846    let commitment_eq = bytes_eq_condition(cx, input, KZG_COMMITMENT_OFFSET, commitment);
1847    let hash_byte_mismatch = byte_eq_condition(cx, input, 1, expected_hash[1] ^ 1);
1848    conditions.push(SymBoolExpr::and(cx, vec![len_192.clone(), commitment_eq, hash_byte_mismatch]));
1849
1850    for commitment in [&KZG_ZERO_COMMITMENT, &KZG_ONE_COMMITMENT] {
1851        let expected_hash = kzg_point_evaluation::kzg_to_versioned_hash(commitment);
1852        let commitment_eq = bytes_eq_condition(cx, input, KZG_COMMITMENT_OFFSET, commitment);
1853        let mismatch = kzg_versioned_hash_mismatch_condition(cx, input, &expected_hash);
1854        conditions.push(SymBoolExpr::and(cx, vec![len_192.clone(), commitment_eq, mismatch]));
1855    }
1856
1857    SymBoolExpr::or(cx, conditions)
1858}
1859
1860fn kzg_versioned_hash_mismatch_condition(
1861    cx: &mut SymCx,
1862    input: &[SymExpr],
1863    expected_hash: &[u8; 32],
1864) -> SymBoolExpr {
1865    bytes_ne_condition(cx, input, KZG_VERSIONED_HASH_OFFSET, expected_hash)
1866}
1867
1868fn expr_eq_condition(cx: &mut SymCx, expr: &SymExpr, value: usize) -> SymBoolExpr {
1869    SymBoolExpr::eq_word_const(cx, expr, U256::from(value))
1870}
1871
1872fn expr_ne_condition(cx: &mut SymCx, expr: &SymExpr, value: usize) -> SymBoolExpr {
1873    let condition = expr_eq_condition(cx, expr, value);
1874    condition.not(cx)
1875}
1876
1877fn byte_eq_condition(cx: &mut SymCx, input: &[SymExpr], offset: usize, value: u8) -> SymBoolExpr {
1878    match input.get(offset) {
1879        Some(expr) => expr_eq_condition(cx, expr, value as usize),
1880        None => SymBoolExpr::constant(cx, false),
1881    }
1882}
1883
1884fn byte_ne_condition(cx: &mut SymCx, input: &[SymExpr], offset: usize, value: u8) -> SymBoolExpr {
1885    match input.get(offset) {
1886        Some(expr) => expr_ne_condition(cx, expr, value as usize),
1887        None => SymBoolExpr::constant(cx, false),
1888    }
1889}
1890
1891fn bytes_eq_condition(
1892    cx: &mut SymCx,
1893    input: &[SymExpr],
1894    offset: usize,
1895    bytes: &[u8],
1896) -> SymBoolExpr {
1897    let Some(end) = offset.checked_add(bytes.len()) else {
1898        return SymBoolExpr::constant(cx, false);
1899    };
1900    if end > input.len() {
1901        return SymBoolExpr::constant(cx, false);
1902    }
1903    let conditions = input[offset..end]
1904        .iter()
1905        .zip(bytes)
1906        .map(|(expr, byte)| expr_eq_condition(cx, expr, *byte as usize))
1907        .collect();
1908    SymBoolExpr::and(cx, conditions)
1909}
1910
1911fn bytes_ne_condition(
1912    cx: &mut SymCx,
1913    input: &[SymExpr],
1914    offset: usize,
1915    bytes: &[u8],
1916) -> SymBoolExpr {
1917    let Some(end) = offset.checked_add(bytes.len()) else {
1918        return SymBoolExpr::constant(cx, false);
1919    };
1920    if end > input.len() {
1921        return SymBoolExpr::constant(cx, false);
1922    }
1923    let conditions = input[offset..end]
1924        .iter()
1925        .zip(bytes)
1926        .map(|(expr, byte)| expr_ne_condition(cx, expr, *byte as usize))
1927        .collect();
1928    SymBoolExpr::or(cx, conditions)
1929}
1930
1931fn constrained_bytes_at(
1932    cx: &mut SymCx,
1933    state: &PathState,
1934    input: &[SymExpr],
1935    offset: usize,
1936    len: usize,
1937) -> Option<Vec<u8>> {
1938    let end = offset.checked_add(len)?;
1939    let bytes = input.get(offset..end)?;
1940    bytes.iter().map(|byte| constrained_byte(cx, state, byte)).collect()
1941}
1942
1943fn constrained_byte(cx: &mut SymCx, state: &PathState, byte: &SymExpr) -> Option<u8> {
1944    state.constrained_word(cx, byte).and_then(|byte| u8::try_from(byte).ok())
1945}
1946
1947fn ensure_expr_not_gasleft(expr: &SymExpr) -> Result<(), SymbolicError> {
1948    if expr.contains_gasleft() {
1949        Err(SymbolicError::Unsupported("GAS/gasleft() not modeled"))
1950    } else {
1951        Ok(())
1952    }
1953}