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::Revert(ret) => {
814                        state.return_data = ret;
815                        state.copy_call_output_offset(&mut self.cx, out_offset, &out_size)?;
816                        state.stack.push(SymExpr::zero(&mut self.cx))?;
817                        return Ok(StepOutcome::Continue);
818                    }
819                    CheatcodeOutcome::AssumeRejected => return Ok(StepOutcome::AssumeRejected),
820                    CheatcodeOutcome::Failure => return Ok(StepOutcome::Failure),
821                }
822            } else if to == SYMBOLIC_VM_COMPAT_ADDRESS {
823                self.handle_symbolic_vm_cheatcode(state, selector, in_offset)?
824            } else {
825                return Err(SymbolicError::Unsupported("symbolic cheatcode address"));
826            };
827
828            state.return_data = return_data;
829            state.copy_call_output_offset(&mut self.cx, out_offset, &out_size)?;
830            state.stack.push(SymExpr::one(&mut self.cx))?;
831            return Ok(StepOutcome::Continue);
832        }
833
834        if is_console(to) {
835            state.return_data = SymReturnData::empty(&mut self.cx);
836            state.copy_call_output_offset(&mut self.cx, out_offset, &out_size)?;
837            state.stack.push(SymExpr::one(&mut self.cx))?;
838            return Ok(StepOutcome::Continue);
839        }
840
841        let call_input = in_size.read_from_memory(&mut self.cx, &state.memory, in_offset.clone());
842        if !state.expected_calls.is_empty() {
843            let concrete_value = state.constrained_word(&mut self.cx, &value);
844            if !self.observe_expected_call(state, to, concrete_value, &gas, &call_input)? {
845                return Ok(StepOutcome::Failure);
846            }
847        }
848        let code_address = self.function_mock_target(state, to, &call_input)?.unwrap_or(to);
849        if !state.call_mocks.is_empty() {
850            let concrete_value = state.constrained_word(&mut self.cx, &value);
851            if let Some(mock) =
852                self.take_call_mock(state, code_address, concrete_value, &call_input)?
853            {
854                if !matches!(kind, CallKind::DelegateCall) {
855                    let _ = state.prank_for_next_call();
856                }
857                let (return_data, reverts) = mock.into_parts();
858                state.return_data = return_data;
859                state.copy_call_output_offset(&mut self.cx, out_offset, &out_size)?;
860                let success = SymExpr::constant(&mut self.cx, U256::from(!reverts));
861                state.stack.push(success)?;
862                return Ok(StepOutcome::Continue);
863            }
864        }
865
866        if matches!(kind, CallKind::DelegateCall) && state.prank.has_active() {
867            return Err(SymbolicError::Unsupported("symbolic prank delegatecall"));
868        }
869        let (call_caller, call_caller_word, pranked_origin) = state.prank_for_next_call();
870        if matches!(kind, CallKind::Call)
871            && !self.prepare_value_transfer(
872                executor,
873                state,
874                worklist,
875                call_caller,
876                value.clone(),
877                out_offset.clone(),
878                &out_size,
879            )?
880        {
881            return Ok(StepOutcome::Continue);
882        }
883
884        let spec_id: SpecId = executor.spec_id().into();
885        if is_supported_precompile(code_address, spec_id) {
886            let input_len = in_size.size_word(&mut self.cx);
887            let input = in_size.read_from_memory(&mut self.cx, &state.memory, in_offset);
888            if precompile_number_for_spec(code_address, spec_id) == Some(10) {
889                let input_bytes = input.materialize(&mut self.cx);
890                return self.execute_kzg_precompile_call(
891                    executor,
892                    state,
893                    worklist,
894                    kind,
895                    to,
896                    call_caller,
897                    value,
898                    out_offset,
899                    &out_size,
900                    input_bytes,
901                    input_len,
902                );
903            }
904            match execute_symbolic_precompile(
905                &mut self.cx,
906                code_address,
907                input,
908                input_len,
909                spec_id,
910            )? {
911                Some(return_data) => {
912                    state.return_data = return_data;
913                    if matches!(kind, CallKind::Call) {
914                        state.world.transfer(&mut self.cx, executor, call_caller, to, value);
915                    }
916                    state.copy_call_output_offset(&mut self.cx, out_offset, &out_size)?;
917                    state.stack.push(SymExpr::one(&mut self.cx))?;
918                }
919                None => {
920                    state.return_data = SymReturnData::empty(&mut self.cx);
921                    state.copy_call_output_offset(&mut self.cx, out_offset, &out_size)?;
922                    state.stack.push(SymExpr::zero(&mut self.cx))?;
923                }
924            }
925            return Ok(StepOutcome::Continue);
926        }
927
928        let child_code = state.world.extcode(&mut self.cx, executor, code_address)?;
929        if child_code.is_empty() {
930            if matches!(kind, CallKind::Call) {
931                state.world.transfer(&mut self.cx, executor, call_caller, to, value);
932            }
933            state.return_data = SymReturnData::empty(&mut self.cx);
934            state.copy_call_output_offset(&mut self.cx, out_offset, &out_size)?;
935            state.stack.push(SymExpr::one(&mut self.cx))?;
936            return Ok(StepOutcome::Continue);
937        }
938
939        let calldata = in_size.calldata(&mut self.cx, call_input);
940        let callee_address_word = state
941            .world
942            .symbolic_word_for_address(to)
943            .or_else(|| {
944                target_word
945                    .as_ref()
946                    .filter(|expr| state.world.resolve_address(expr) == Some(to))
947                    .cloned()
948            })
949            .unwrap_or_else(|| SymExpr::constant(&mut self.cx, address_word(to)));
950        let frame = match kind {
951            CallKind::Call => {
952                let mut frame = CallFrame::new(
953                    &mut self.cx,
954                    to,
955                    code_address,
956                    to,
957                    call_caller,
958                    value.clone(),
959                    state.is_static,
960                    calldata,
961                );
962                frame.address_word = callee_address_word;
963                frame.caller_word = call_caller_word;
964                frame
965            }
966            CallKind::StaticCall => {
967                let value = SymExpr::zero(&mut self.cx);
968                let mut frame = CallFrame::new(
969                    &mut self.cx,
970                    to,
971                    code_address,
972                    to,
973                    call_caller,
974                    value,
975                    true,
976                    calldata,
977                );
978                frame.address_word = callee_address_word;
979                frame.caller_word = call_caller_word;
980                frame
981            }
982            CallKind::DelegateCall => {
983                let mut frame = CallFrame::new(
984                    &mut self.cx,
985                    state.address,
986                    code_address,
987                    state.storage_address,
988                    state.caller,
989                    state.callvalue.clone(),
990                    state.is_static,
991                    calldata,
992                );
993                frame.address_word = state.address_word.clone();
994                frame.caller_word = state.caller_word.clone();
995                frame
996            }
997            CallKind::CallCode => {
998                let mut frame = CallFrame::new(
999                    &mut self.cx,
1000                    state.address,
1001                    code_address,
1002                    state.storage_address,
1003                    call_caller,
1004                    value.clone(),
1005                    state.is_static,
1006                    calldata,
1007                );
1008                frame.address_word = state.address_word.clone();
1009                frame.caller_word = call_caller_word;
1010                frame
1011            }
1012        };
1013
1014        let original_world = state.world.clone();
1015        let mut child = state.child(frame);
1016        if let Some((origin, origin_word)) = pranked_origin {
1017            child.origin = origin;
1018            child.origin_word = origin_word;
1019        }
1020        if matches!(kind, CallKind::Call) {
1021            child.world.transfer(&mut self.cx, executor, call_caller, to, value);
1022        }
1023        child.expected_revert = None;
1024        child.assume_no_revert_next_call = None;
1025        let outcomes = self.execute_external_call(executor, child, &child_code, completed_paths)?;
1026        let Some((first, rest)) = outcomes.split_first() else {
1027            return Ok(StepOutcome::AssumeRejected);
1028        };
1029
1030        let mut parents = VecDeque::with_capacity(outcomes.len());
1031        for outcome in std::iter::once(first).chain(rest.iter()) {
1032            let mut parent = state.clone();
1033            parent.constraints = outcome.state.constraints.clone();
1034            parent.next_symbol = outcome.state.next_symbol;
1035            parent.inherit_branch_target_progress(&outcome.state);
1036            parent.storage_load_hooks = outcome.state.storage_load_hooks.clone();
1037            parent.storage_store_hooks = outcome.state.storage_store_hooks.clone();
1038            parent.mapping_storage_store_hooks = outcome.state.mapping_storage_store_hooks.clone();
1039            parent.inherit_mapping_hook_provenance(&outcome.state);
1040
1041            if let Some(assumption) = parent.assume_no_revert_next_call.take()
1042                && matches!(outcome.status, TopLevelCallStatus::Revert)
1043                && self.assume_no_revert_rejects(
1044                    &mut parent,
1045                    &assumption,
1046                    to,
1047                    &outcome.return_data,
1048                )?
1049            {
1050                continue;
1051            }
1052
1053            if let Some(mut expected) = parent.expected_revert.clone() {
1054                match outcome.status {
1055                    TopLevelCallStatus::Success => {
1056                        *state = parent;
1057                        return Ok(StepOutcome::Failure);
1058                    }
1059                    TopLevelCallStatus::Revert | TopLevelCallStatus::Failure => {
1060                        if !self.expected_revert_matches(
1061                            &mut parent,
1062                            &expected,
1063                            to,
1064                            &outcome.return_data,
1065                        )? {
1066                            *state = parent;
1067                            return Ok(StepOutcome::Failure);
1068                        }
1069                        if expected.consume_one() {
1070                            parent.expected_revert = None;
1071                        } else {
1072                            parent.expected_revert = Some(expected);
1073                        }
1074                        parent.access_record = outcome.state.access_record.clone();
1075                        parent.expected_calls = outcome.state.expected_calls.clone();
1076                        parent.expected_creates = outcome.state.expected_creates.clone();
1077                        parent.call_mocks = outcome.state.call_mocks.clone();
1078                        parent.function_mocks = outcome.state.function_mocks.clone();
1079                        parent.world = original_world.clone();
1080                        parent.return_data = SymReturnData::empty(&mut self.cx);
1081                        parent.copy_call_output_offset(
1082                            &mut self.cx,
1083                            out_offset.clone(),
1084                            &out_size,
1085                        )?;
1086                        parent.stack.push(SymExpr::one(&mut self.cx))?;
1087                        parents.push_back(parent);
1088                        continue;
1089                    }
1090                }
1091            }
1092
1093            parent.world = if matches!(outcome.status, TopLevelCallStatus::Success) {
1094                outcome.state.world.clone()
1095            } else {
1096                original_world.clone()
1097            };
1098            match outcome.status {
1099                TopLevelCallStatus::Success => {
1100                    parent.block = outcome.state.block.clone();
1101                    parent.recorded_logs = outcome.state.recorded_logs.clone();
1102                    parent.access_record = outcome.state.access_record.clone();
1103                    parent.expected_emit = outcome.state.expected_emit.clone();
1104                    parent.expected_calls = outcome.state.expected_calls.clone();
1105                    parent.expected_creates = outcome.state.expected_creates.clone();
1106                    parent.call_mocks = outcome.state.call_mocks.clone();
1107                    parent.function_mocks = outcome.state.function_mocks.clone();
1108                }
1109                TopLevelCallStatus::Failure => {
1110                    *state = parent;
1111                    return Ok(StepOutcome::Failure);
1112                }
1113                TopLevelCallStatus::Revert => {}
1114            }
1115            parent.return_data = outcome.return_data.clone();
1116            parent.copy_call_output_offset(&mut self.cx, out_offset.clone(), &out_size)?;
1117            let success = SymExpr::constant(
1118                &mut self.cx,
1119                U256::from(matches!(outcome.status, TopLevelCallStatus::Success)),
1120            );
1121            parent.stack.push(success)?;
1122            parents.push_back(parent);
1123        }
1124
1125        let Some(first) = self.pop_next_path(&mut parents) else {
1126            return Ok(StepOutcome::AssumeRejected);
1127        };
1128        *state = first;
1129        worklist.extend(parents);
1130        Ok(StepOutcome::Continue)
1131    }
1132
1133    #[expect(clippy::too_many_arguments)]
1134    fn execute_kzg_precompile_call<FEN: FoundryEvmNetwork>(
1135        &mut self,
1136        executor: &Executor<FEN>,
1137        state: &mut PathState,
1138        worklist: &mut VecDeque<PathState>,
1139        kind: CallKind,
1140        to: Address,
1141        call_caller: Address,
1142        value: SymExpr,
1143        out_offset: SymExpr,
1144        out_size: &BoundedCopySize,
1145        input: Vec<SymExpr>,
1146        input_len: SymExpr,
1147    ) -> Result<StepOutcome, SymbolicError> {
1148        if let Some(outcome) = kzg_constrained_outcome(&mut self.cx, state, &input, &input_len)? {
1149            self.apply_precompile_outcome(
1150                executor,
1151                state,
1152                kind,
1153                to,
1154                call_caller,
1155                value,
1156                out_offset,
1157                out_size,
1158                outcome,
1159            )?;
1160            return Ok(StepOutcome::Continue);
1161        }
1162
1163        let success_condition = kzg_success_witness_condition(&mut self.cx, &input, &input_len);
1164        let failure_condition =
1165            kzg_failure_witness_condition(&mut self.cx, state, &input, &input_len);
1166        let modeled_condition = SymBoolExpr::or(
1167            &mut self.cx,
1168            vec![success_condition.clone(), failure_condition.clone()],
1169        );
1170        let modeled_condition = modeled_condition.not(&mut self.cx);
1171        let (_, residual_sat) = self.constraints_with_condition(state, modeled_condition)?;
1172        if residual_sat {
1173            self.defer_incomplete(KZG_RESIDUAL_REASON);
1174        }
1175
1176        let (success_constraints, success_sat) =
1177            self.constraints_with_condition(state, success_condition)?;
1178
1179        let (failure_constraints, failure_sat) =
1180            self.constraints_with_condition(state, failure_condition)?;
1181
1182        match (success_sat, failure_sat) {
1183            (true, true) => {
1184                let mut failure = state.clone();
1185                failure.constraints = failure_constraints;
1186                self.apply_precompile_outcome(
1187                    executor,
1188                    &mut failure,
1189                    kind,
1190                    to,
1191                    call_caller,
1192                    value.clone(),
1193                    out_offset.clone(),
1194                    out_size,
1195                    None,
1196                )?;
1197                worklist.push_back(failure);
1198
1199                state.constraints = success_constraints;
1200                let return_data = kzg_success_return_data(&mut self.cx);
1201                self.apply_precompile_outcome(
1202                    executor,
1203                    state,
1204                    kind,
1205                    to,
1206                    call_caller,
1207                    value,
1208                    out_offset,
1209                    out_size,
1210                    Some(return_data),
1211                )?;
1212                Ok(StepOutcome::Continue)
1213            }
1214            (true, false) => {
1215                state.constraints = success_constraints;
1216                let return_data = kzg_success_return_data(&mut self.cx);
1217                self.apply_precompile_outcome(
1218                    executor,
1219                    state,
1220                    kind,
1221                    to,
1222                    call_caller,
1223                    value,
1224                    out_offset,
1225                    out_size,
1226                    Some(return_data),
1227                )?;
1228                Ok(StepOutcome::Continue)
1229            }
1230            (false, true) => {
1231                state.constraints = failure_constraints;
1232                self.apply_precompile_outcome(
1233                    executor,
1234                    state,
1235                    kind,
1236                    to,
1237                    call_caller,
1238                    value,
1239                    out_offset,
1240                    out_size,
1241                    None,
1242                )?;
1243                Ok(StepOutcome::Continue)
1244            }
1245            (false, false) => Err(SymbolicError::Unsupported(KZG_RESIDUAL_REASON)),
1246        }
1247    }
1248
1249    #[expect(clippy::too_many_arguments)]
1250    /// Applies a precompile call result to the current symbolic state.
1251    fn apply_precompile_outcome<FEN: FoundryEvmNetwork>(
1252        &mut self,
1253        executor: &Executor<FEN>,
1254        state: &mut PathState,
1255        kind: CallKind,
1256        to: Address,
1257        call_caller: Address,
1258        value: SymExpr,
1259        out_offset: SymExpr,
1260        out_size: &BoundedCopySize,
1261        outcome: Option<SymReturnData>,
1262    ) -> Result<(), SymbolicError> {
1263        match outcome {
1264            Some(return_data) => {
1265                state.return_data = return_data;
1266                if matches!(kind, CallKind::Call) {
1267                    state.world.transfer(&mut self.cx, executor, call_caller, to, value);
1268                }
1269                state.copy_call_output_offset(&mut self.cx, out_offset, out_size)?;
1270                state.stack.push(SymExpr::one(&mut self.cx))?;
1271            }
1272            None => {
1273                state.return_data = SymReturnData::empty(&mut self.cx);
1274                state.copy_call_output_offset(&mut self.cx, out_offset, out_size)?;
1275                state.stack.push(SymExpr::zero(&mut self.cx))?;
1276            }
1277        }
1278        Ok(())
1279    }
1280
1281    #[expect(clippy::too_many_arguments)]
1282    pub(super) fn prepare_value_transfer<FEN: FoundryEvmNetwork>(
1283        &mut self,
1284        executor: &Executor<FEN>,
1285        state: &mut PathState,
1286        worklist: &mut VecDeque<PathState>,
1287        from: Address,
1288        value: SymExpr,
1289        out_offset: SymExpr,
1290        out_size: &BoundedCopySize,
1291    ) -> Result<bool, SymbolicError> {
1292        if state.constrained_word(&mut self.cx, &value).is_some_and(|value| value.is_zero()) {
1293            return Ok(true);
1294        }
1295
1296        let balance = state.world.balance_word_for_address(&mut self.cx, executor, from);
1297        let can_pay = SymBoolExpr::cmp(&mut self.cx, SymCmpOp::Uge, balance, value);
1298        match can_pay.as_const() {
1299            Some(true) => Ok(true),
1300            Some(false) => {
1301                state.return_data = SymReturnData::empty(&mut self.cx);
1302                state.copy_call_output_offset(&mut self.cx, out_offset, out_size)?;
1303                state.stack.push(SymExpr::zero(&mut self.cx))?;
1304                Ok(false)
1305            }
1306            None => {
1307                let mut success_constraints = state.constraints.clone();
1308                success_constraints.push(can_pay.clone());
1309                let success_sat = self.solver.is_sat(&mut self.cx, &success_constraints)?;
1310
1311                let mut failure_constraints = state.constraints.clone();
1312                failure_constraints.push(can_pay.not(&mut self.cx));
1313                let failure_sat = self.solver.is_sat(&mut self.cx, &failure_constraints)?;
1314
1315                match (success_sat, failure_sat) {
1316                    (true, true) => {
1317                        let mut failure = state.clone();
1318                        failure.constraints = failure_constraints;
1319                        failure.return_data = SymReturnData::empty(&mut self.cx);
1320                        failure.copy_call_output_offset(&mut self.cx, out_offset, out_size)?;
1321                        failure.stack.push(SymExpr::zero(&mut self.cx))?;
1322                        worklist.push_back(failure);
1323
1324                        state.constraints = success_constraints;
1325                        Ok(true)
1326                    }
1327                    (true, false) => {
1328                        state.constraints = success_constraints;
1329                        Ok(true)
1330                    }
1331                    (false, true) => {
1332                        state.constraints = failure_constraints;
1333                        state.return_data = SymReturnData::empty(&mut self.cx);
1334                        state.copy_call_output_offset(&mut self.cx, out_offset, out_size)?;
1335                        state.stack.push(SymExpr::zero(&mut self.cx))?;
1336                        Ok(false)
1337                    }
1338                    (false, false) => Ok(false),
1339                }
1340            }
1341        }
1342    }
1343
1344    pub(super) fn prepare_create_value_transfer<FEN: FoundryEvmNetwork>(
1345        &mut self,
1346        executor: &Executor<FEN>,
1347        state: &mut PathState,
1348        worklist: &mut VecDeque<PathState>,
1349        value: SymExpr,
1350    ) -> Result<bool, SymbolicError> {
1351        if state.constrained_word(&mut self.cx, &value).is_some_and(|value| value.is_zero()) {
1352            return Ok(true);
1353        }
1354
1355        let balance = state.world.balance_word_for_address(&mut self.cx, executor, state.address);
1356        let can_pay = SymBoolExpr::cmp(&mut self.cx, SymCmpOp::Uge, balance, value);
1357        match can_pay.as_const() {
1358            Some(true) => Ok(true),
1359            Some(false) => {
1360                state.return_data = SymReturnData::empty(&mut self.cx);
1361                state.stack.push(SymExpr::zero(&mut self.cx))?;
1362                Ok(false)
1363            }
1364            None => {
1365                let mut success_constraints = state.constraints.clone();
1366                success_constraints.push(can_pay.clone());
1367                let success_sat = self.solver.is_sat(&mut self.cx, &success_constraints)?;
1368
1369                let mut failure_constraints = state.constraints.clone();
1370                failure_constraints.push(can_pay.not(&mut self.cx));
1371                let failure_sat = self.solver.is_sat(&mut self.cx, &failure_constraints)?;
1372
1373                match (success_sat, failure_sat) {
1374                    (true, true) => {
1375                        let mut failure = state.clone();
1376                        failure.constraints = failure_constraints;
1377                        failure.return_data = SymReturnData::empty(&mut self.cx);
1378                        failure.stack.push(SymExpr::zero(&mut self.cx))?;
1379                        worklist.push_back(failure);
1380
1381                        state.constraints = success_constraints;
1382                        Ok(true)
1383                    }
1384                    (true, false) => {
1385                        state.constraints = success_constraints;
1386                        Ok(true)
1387                    }
1388                    (false, true) => {
1389                        state.constraints = failure_constraints;
1390                        state.return_data = SymReturnData::empty(&mut self.cx);
1391                        state.stack.push(SymExpr::zero(&mut self.cx))?;
1392                        Ok(false)
1393                    }
1394                    (false, false) => Ok(false),
1395                }
1396            }
1397        }
1398    }
1399
1400    #[expect(clippy::too_many_arguments)]
1401    pub(super) fn call_symbolic_target<FEN: FoundryEvmNetwork>(
1402        &mut self,
1403        executor: &Executor<FEN>,
1404        state: &mut PathState,
1405        worklist: &mut VecDeque<PathState>,
1406        completed_paths: &mut usize,
1407        kind: CallKind,
1408        target: SymExpr,
1409        value: SymExpr,
1410        gas: SymExpr,
1411        in_offset: SymExpr,
1412        in_size: BoundedCopySize,
1413        out_offset: SymExpr,
1414        out_size: BoundedCopySize,
1415    ) -> Result<StepOutcome, SymbolicError> {
1416        let mut candidates = state.world.symbolic_call_targets(&mut self.cx, executor)?;
1417        candidates.extend((1..=10).map(precompile_address));
1418        candidates.sort();
1419        candidates.dedup();
1420        if candidates.is_empty() {
1421            return Err(SymbolicError::Unsupported(
1422                "symbolic CALL target has no known contract candidates",
1423            ));
1424        }
1425
1426        let candidate_constraints = candidates
1427            .iter()
1428            .map(|address| {
1429                let address = SymExpr::constant(&mut self.cx, address_word(*address));
1430                SymBoolExpr::eq(&mut self.cx, target.clone(), address)
1431            })
1432            .collect::<Vec<_>>();
1433        let mut outside_constraints = state.constraints.clone();
1434        outside_constraints.extend(
1435            candidate_constraints.iter().cloned().map(|condition| condition.not(&mut self.cx)),
1436        );
1437        let outside_sat = self.solver.is_sat(&mut self.cx, &outside_constraints)?;
1438
1439        if !self.config.symbolic_call_targets && outside_sat {
1440            return Err(SymbolicError::Unsupported("symbolic CALL target"));
1441        }
1442
1443        let mut parents = VecDeque::new();
1444        if outside_sat {
1445            let mut branch = state.clone();
1446            branch.constraints = outside_constraints;
1447
1448            if matches!(kind, CallKind::DelegateCall) && branch.prank.has_active() {
1449                return Err(SymbolicError::Unsupported("symbolic prank delegatecall"));
1450            }
1451            let (call_caller, _, _) = branch.prank_for_next_call();
1452            if matches!(kind, CallKind::Call) {
1453                if self.prepare_value_transfer(
1454                    executor,
1455                    &mut branch,
1456                    &mut parents,
1457                    call_caller,
1458                    value.clone(),
1459                    out_offset.clone(),
1460                    &out_size,
1461                )? {
1462                    let symbolic_target = target;
1463                    let to = branch.world.symbolic_address_slot(symbolic_target);
1464                    branch.world.transfer(&mut self.cx, executor, call_caller, to, value.clone());
1465                    branch.return_data = SymReturnData::empty(&mut self.cx);
1466                    branch.copy_call_output_offset(&mut self.cx, out_offset.clone(), &out_size)?;
1467                    branch.stack.push(SymExpr::one(&mut self.cx))?;
1468                    parents.push_back(branch);
1469                }
1470            } else {
1471                branch.return_data = SymReturnData::empty(&mut self.cx);
1472                branch.copy_call_output_offset(&mut self.cx, out_offset.clone(), &out_size)?;
1473                branch.stack.push(SymExpr::one(&mut self.cx))?;
1474                parents.push_back(branch);
1475            }
1476        }
1477
1478        for (to, constraint) in candidates.into_iter().zip(candidate_constraints) {
1479            let mut branch = state.clone();
1480            branch.constraints.push(constraint);
1481            if !self.solver.is_sat(&mut self.cx, &branch.constraints)? {
1482                continue;
1483            }
1484
1485            let mut branch_worklist = VecDeque::new();
1486            match self.call_concrete_target(
1487                executor,
1488                &mut branch,
1489                &mut branch_worklist,
1490                completed_paths,
1491                kind,
1492                to,
1493                None,
1494                value.clone(),
1495                gas.clone(),
1496                in_offset.clone(),
1497                in_size.clone(),
1498                out_offset.clone(),
1499                out_size.clone(),
1500            )? {
1501                StepOutcome::Continue => {
1502                    parents.push_back(branch);
1503                    parents.extend(branch_worklist);
1504                }
1505                StepOutcome::AssumeRejected => {}
1506                outcome => return Ok(outcome),
1507            }
1508        }
1509
1510        let Some(first) = self.pop_next_path(&mut parents) else {
1511            return Ok(StepOutcome::AssumeRejected);
1512        };
1513        *state = first;
1514        worklist.extend(parents);
1515        Ok(StepOutcome::Continue)
1516    }
1517}
1518
1519const KZG_POINT_EVALUATION_INPUT_LEN: usize = 192;
1520const KZG_VERSIONED_HASH_OFFSET: usize = 0;
1521const KZG_Z_OFFSET: usize = 32;
1522const KZG_Y_OFFSET: usize = 64;
1523const KZG_COMMITMENT_OFFSET: usize = 96;
1524const KZG_PROOF_OFFSET: usize = 144;
1525
1526const KZG_BLS_MODULUS: [u8; 32] =
1527    hex!("73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001");
1528
1529const KZG_SUCCESS_INPUT: [u8; KZG_POINT_EVALUATION_INPUT_LEN] = hex!(
1530    "01e798154708fe7789429634053cbf9f99b619f9f084048927333fce637f549b"
1531    "73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000000"
1532    "1522a4a7f34e1ea350ae07c29c96c7e79655aa926122e95fe69fcbd932ca49e9"
1533    "8f59a8d2a1a625a17f3fea0fe5eb8c896db3764f3185481bc22f91b4aaffcca25f26936857bc3a7c2539ea8ec3a952b7"
1534    "a62ad71d14c5719385c0686f1871430475bf3a00f0aa3f7b8dd99a9abc2160744faf0070725e00b60ad9a026a15b1a8c"
1535);
1536
1537const KZG_INVALID_PROOF: [u8; 48] = [0xff; 48];
1538const KZG_ZERO_COMMITMENT: [u8; 48] = [0x00; 48];
1539const KZG_ONE_COMMITMENT: [u8; 48] = [0x01; 48];
1540const KZG_RESIDUAL_REASON: &str = "symbolic KZG point-evaluation precompile residual not modeled";
1541
1542fn kzg_success_return_data(cx: &mut SymCx) -> SymReturnData {
1543    SymReturnData::from_concrete_bytes(cx, kzg_point_evaluation::RETURN_VALUE.to_vec())
1544}
1545
1546fn kzg_constrained_outcome(
1547    cx: &mut SymCx,
1548    state: &PathState,
1549    input: &[SymExpr],
1550    input_len: &SymExpr,
1551) -> Result<Option<Option<SymReturnData>>, SymbolicError> {
1552    let Some(input_len) = state.constrained_usize(cx, input_len) else {
1553        return Ok(None);
1554    };
1555    if input_len != KZG_POINT_EVALUATION_INPUT_LEN {
1556        return Ok(Some(None));
1557    }
1558    if input_len > input.len() {
1559        return Err(SymbolicError::Unsupported("out-of-bounds symbolic precompile input"));
1560    }
1561
1562    if let Some(input) = constrained_bytes_at(cx, state, input, 0, input_len) {
1563        return execute_precompile(cx, precompile_address(10), &input, SpecId::CANCUN).map(Some);
1564    }
1565
1566    if constrained_byte(cx, state, &input[0])
1567        .is_some_and(|version| version != kzg_point_evaluation::VERSIONED_HASH_VERSION_KZG)
1568    {
1569        return Ok(Some(None));
1570    }
1571
1572    if constrained_bytes_at(cx, state, input, KZG_Z_OFFSET, KZG_BLS_MODULUS.len())
1573        .is_some_and(|z| z == KZG_BLS_MODULUS)
1574        || constrained_bytes_at(cx, state, input, KZG_Y_OFFSET, KZG_BLS_MODULUS.len())
1575            .is_some_and(|y| y == KZG_BLS_MODULUS)
1576        || constrained_bytes_at(cx, state, input, KZG_PROOF_OFFSET, KZG_INVALID_PROOF.len())
1577            .is_some_and(|proof| proof == KZG_INVALID_PROOF)
1578    {
1579        return Ok(Some(None));
1580    }
1581
1582    if let Some(commitment) = constrained_bytes_at(cx, state, input, KZG_COMMITMENT_OFFSET, 48) {
1583        let expected_hash = kzg_point_evaluation::kzg_to_versioned_hash(&commitment);
1584        for (idx, expected) in expected_hash.into_iter().enumerate() {
1585            if constrained_byte(cx, state, &input[idx]).is_some_and(|actual| actual != expected) {
1586                return Ok(Some(None));
1587            }
1588        }
1589    }
1590
1591    Ok(None)
1592}
1593
1594fn kzg_success_witness_condition(
1595    cx: &mut SymCx,
1596    input: &[SymExpr],
1597    input_len: &SymExpr,
1598) -> SymBoolExpr {
1599    let len = expr_eq_condition(cx, input_len, KZG_POINT_EVALUATION_INPUT_LEN);
1600    let bytes = bytes_eq_condition(cx, input, KZG_VERSIONED_HASH_OFFSET, &KZG_SUCCESS_INPUT);
1601    SymBoolExpr::and(cx, vec![len, bytes])
1602}
1603
1604fn kzg_failure_witness_condition(
1605    cx: &mut SymCx,
1606    state: &PathState,
1607    input: &[SymExpr],
1608    input_len: &SymExpr,
1609) -> SymBoolExpr {
1610    let len_192 = expr_eq_condition(cx, input_len, KZG_POINT_EVALUATION_INPUT_LEN);
1611    let len_ne_192 = expr_ne_condition(cx, input_len, KZG_POINT_EVALUATION_INPUT_LEN);
1612    let bad_version =
1613        byte_ne_condition(cx, input, 0, kzg_point_evaluation::VERSIONED_HASH_VERSION_KZG);
1614    let bad_z = bytes_eq_condition(cx, input, KZG_Z_OFFSET, &KZG_BLS_MODULUS);
1615    let bad_y = bytes_eq_condition(cx, input, KZG_Y_OFFSET, &KZG_BLS_MODULUS);
1616    let bad_proof = bytes_eq_condition(cx, input, KZG_PROOF_OFFSET, &KZG_INVALID_PROOF);
1617    let mut conditions = vec![
1618        len_ne_192,
1619        SymBoolExpr::and(cx, vec![len_192.clone(), bad_version]),
1620        SymBoolExpr::and(cx, vec![len_192.clone(), bad_z]),
1621        SymBoolExpr::and(cx, vec![len_192.clone(), bad_y]),
1622        SymBoolExpr::and(cx, vec![len_192.clone(), bad_proof]),
1623    ];
1624
1625    if let Some(commitment) = constrained_bytes_at(cx, state, input, KZG_COMMITMENT_OFFSET, 48) {
1626        let expected_hash = kzg_point_evaluation::kzg_to_versioned_hash(&commitment);
1627        let mismatch = kzg_versioned_hash_mismatch_condition(cx, input, &expected_hash);
1628        conditions.push(SymBoolExpr::and(cx, vec![len_192.clone(), mismatch]));
1629    }
1630
1631    let expected_hash = &KZG_SUCCESS_INPUT[KZG_VERSIONED_HASH_OFFSET..KZG_Z_OFFSET];
1632    let commitment = &KZG_SUCCESS_INPUT[KZG_COMMITMENT_OFFSET..KZG_PROOF_OFFSET];
1633    let commitment_eq = bytes_eq_condition(cx, input, KZG_COMMITMENT_OFFSET, commitment);
1634    let hash_byte_mismatch = byte_eq_condition(cx, input, 1, expected_hash[1] ^ 1);
1635    conditions.push(SymBoolExpr::and(cx, vec![len_192.clone(), commitment_eq, hash_byte_mismatch]));
1636
1637    for commitment in [&KZG_ZERO_COMMITMENT, &KZG_ONE_COMMITMENT] {
1638        let expected_hash = kzg_point_evaluation::kzg_to_versioned_hash(commitment);
1639        let commitment_eq = bytes_eq_condition(cx, input, KZG_COMMITMENT_OFFSET, commitment);
1640        let mismatch = kzg_versioned_hash_mismatch_condition(cx, input, &expected_hash);
1641        conditions.push(SymBoolExpr::and(cx, vec![len_192.clone(), commitment_eq, mismatch]));
1642    }
1643
1644    SymBoolExpr::or(cx, conditions)
1645}
1646
1647fn kzg_versioned_hash_mismatch_condition(
1648    cx: &mut SymCx,
1649    input: &[SymExpr],
1650    expected_hash: &[u8; 32],
1651) -> SymBoolExpr {
1652    bytes_ne_condition(cx, input, KZG_VERSIONED_HASH_OFFSET, expected_hash)
1653}
1654
1655fn expr_eq_condition(cx: &mut SymCx, expr: &SymExpr, value: usize) -> SymBoolExpr {
1656    SymBoolExpr::eq_word_const(cx, expr, U256::from(value))
1657}
1658
1659fn expr_ne_condition(cx: &mut SymCx, expr: &SymExpr, value: usize) -> SymBoolExpr {
1660    let condition = expr_eq_condition(cx, expr, value);
1661    condition.not(cx)
1662}
1663
1664fn byte_eq_condition(cx: &mut SymCx, input: &[SymExpr], offset: usize, value: u8) -> SymBoolExpr {
1665    match input.get(offset) {
1666        Some(expr) => expr_eq_condition(cx, expr, value as usize),
1667        None => SymBoolExpr::constant(cx, false),
1668    }
1669}
1670
1671fn byte_ne_condition(cx: &mut SymCx, input: &[SymExpr], offset: usize, value: u8) -> SymBoolExpr {
1672    match input.get(offset) {
1673        Some(expr) => expr_ne_condition(cx, expr, value as usize),
1674        None => SymBoolExpr::constant(cx, false),
1675    }
1676}
1677
1678fn bytes_eq_condition(
1679    cx: &mut SymCx,
1680    input: &[SymExpr],
1681    offset: usize,
1682    bytes: &[u8],
1683) -> SymBoolExpr {
1684    let Some(end) = offset.checked_add(bytes.len()) else {
1685        return SymBoolExpr::constant(cx, false);
1686    };
1687    if end > input.len() {
1688        return SymBoolExpr::constant(cx, false);
1689    }
1690    let conditions = input[offset..end]
1691        .iter()
1692        .zip(bytes)
1693        .map(|(expr, byte)| expr_eq_condition(cx, expr, *byte as usize))
1694        .collect();
1695    SymBoolExpr::and(cx, conditions)
1696}
1697
1698fn bytes_ne_condition(
1699    cx: &mut SymCx,
1700    input: &[SymExpr],
1701    offset: usize,
1702    bytes: &[u8],
1703) -> SymBoolExpr {
1704    let Some(end) = offset.checked_add(bytes.len()) else {
1705        return SymBoolExpr::constant(cx, false);
1706    };
1707    if end > input.len() {
1708        return SymBoolExpr::constant(cx, false);
1709    }
1710    let conditions = input[offset..end]
1711        .iter()
1712        .zip(bytes)
1713        .map(|(expr, byte)| expr_ne_condition(cx, expr, *byte as usize))
1714        .collect();
1715    SymBoolExpr::or(cx, conditions)
1716}
1717
1718fn constrained_bytes_at(
1719    cx: &mut SymCx,
1720    state: &PathState,
1721    input: &[SymExpr],
1722    offset: usize,
1723    len: usize,
1724) -> Option<Vec<u8>> {
1725    let end = offset.checked_add(len)?;
1726    let bytes = input.get(offset..end)?;
1727    bytes.iter().map(|byte| constrained_byte(cx, state, byte)).collect()
1728}
1729
1730fn constrained_byte(cx: &mut SymCx, state: &PathState, byte: &SymExpr) -> Option<u8> {
1731    state.constrained_word(cx, byte).and_then(|byte| u8::try_from(byte).ok())
1732}
1733
1734fn ensure_expr_not_gasleft(expr: &SymExpr) -> Result<(), SymbolicError> {
1735    if expr.contains_gasleft() {
1736        Err(SymbolicError::Unsupported("GAS/gasleft() not modeled"))
1737    } else {
1738        Ok(())
1739    }
1740}