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