Skip to main content

foundry_evm_symbolic/executor/
calls.rs

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