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