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