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