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