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