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 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 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 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}