1use super::*;
2
3enum MappingStorageProvenance {
4 None,
5 Exact(SymbolicMappingProvenance),
6 Fork { equality: Vec<SymBoolExpr>, inequality: Vec<SymBoolExpr> },
7}
8
9impl SymbolicExecutor {
10 fn classify_mapping_match(
11 &mut self,
12 state: &PathState,
13 matches: SymBoolExpr,
14 provenance: SymbolicMappingProvenance,
15 ) -> Result<Option<MappingStorageProvenance>, SymbolicError> {
16 let does_not_match = matches.clone().not(&mut self.cx);
17 let (inequality, inequality_is_sat) =
18 self.constraints_with_condition(state, does_not_match)?;
19 if !inequality_is_sat {
20 return Ok(Some(MappingStorageProvenance::Exact(provenance)));
21 }
22 let (equality, equality_is_sat) = self.constraints_with_condition(state, matches)?;
23 Ok(equality_is_sat.then_some(MappingStorageProvenance::Fork { equality, inequality }))
24 }
25
26 fn observed_mapping_chain(
27 &mut self,
28 state: &PathState,
29 hash: &SymExpr,
30 ) -> Option<(SymExpr, Vec<SymExpr>)> {
31 let account = state.storage_address;
32 let mut current = hash.clone();
33 let mut keys = Vec::new();
34 let mut visited = Vec::new();
35 loop {
36 if visited.contains(¤t) {
37 return None;
38 }
39 visited.push(current.clone());
40 let Some(bytes) =
41 state.mapping_hook_keccak_preimages.get(&(account, current.clone())).cloned()
42 else {
43 keys.reverse();
44 return Some((current, keys));
45 };
46 if bytes.len() != 64 {
47 return None;
48 }
49 keys.push(SymExpr::from_bytes(&mut self.cx, bytes[..32].iter().cloned()));
50 current = SymExpr::from_bytes(&mut self.cx, bytes[32..64].iter().cloned());
51 }
52 }
53
54 fn mapping_storage_provenance(
55 &mut self,
56 state: &PathState,
57 key: &SymExpr,
58 ) -> Result<MappingStorageProvenance, SymbolicError> {
59 let account = state.storage_address;
60 let observed = |hash: &SymExpr| {
61 state.mapping_hook_keccak_preimages.get(&(account, hash.clone())).cloned()
62 };
63 if let Some(provenance) =
64 key.storage_mapping_provenance_observed_with(&mut self.cx, observed)
65 {
66 return Ok(MappingStorageProvenance::Exact(provenance));
67 }
68 let mut hashes = state
69 .mapping_hook_keccak_preimages
70 .keys()
71 .filter(|(address, _)| *address == account)
72 .map(|(_, hash)| hash.clone())
73 .collect::<Vec<_>>();
74 let contains_observed_hash =
75 hashes.iter().any(|hash| key.visit_bool(|candidate| candidate == hash));
76 let key_is_const = key.as_const().is_some();
77 let key_contains_keccak = key.contains_keccak();
78 let key_is_storage_mapping_key = key.storage_mapping_key(&mut self.cx).is_some();
79 let roots = state
80 .mapping_storage_store_hooks
81 .keys()
82 .filter(|(address, _)| *address == account)
83 .map(|(_, root)| *root)
84 .collect::<Vec<_>>();
85 hashes.sort_by_key(|hash| hash != key);
86 for hash in hashes {
87 if contains_observed_hash && !key.visit_bool(|candidate| candidate == &hash) {
88 continue;
89 }
90 let use_legacy_match = contains_observed_hash || !key_contains_keccak;
91 if use_legacy_match
92 && let Some(provenance) =
93 hash.storage_mapping_provenance_observed_with(&mut self.cx, |candidate| {
94 state
95 .mapping_hook_keccak_preimages
96 .get(&(account, candidate.clone()))
97 .cloned()
98 })
99 {
100 if !state.mapping_storage_store_hooks.contains_key(&(account, provenance.root_slot))
101 {
102 continue;
103 }
104 let equality = SymBoolExpr::eq(&mut self.cx, key.clone(), hash.clone());
105 if key_is_const {
106 let inequality = equality.not(&mut self.cx);
107 let (_, inequality_is_sat) =
108 self.constraints_with_condition(state, inequality)?;
109 if !inequality_is_sat {
110 return Ok(MappingStorageProvenance::Exact(provenance));
111 }
112 continue;
113 }
114 if let Some(provenance) =
115 self.classify_mapping_match(state, equality, provenance)?
116 {
117 return Ok(provenance);
118 }
119 continue;
120 }
121 if (key_is_const || key_contains_keccak) && !key_is_storage_mapping_key {
122 continue;
123 }
124 let Some((root, keys)) = self.observed_mapping_chain(state, &hash) else {
125 continue;
126 };
127 for &root_slot in &roots {
128 let slot_matches = if key_is_const || key.visit_bool(|candidate| candidate == &hash)
129 {
130 SymBoolExpr::eq(&mut self.cx, key.clone(), hash.clone())
131 } else {
132 key.storage_key_eq(&mut self.cx, &hash)
133 };
134 let matches = if let Some(constrained_root) =
135 state.constrained_word(&mut self.cx, &root)
136 {
137 if constrained_root != root_slot {
138 continue;
139 }
140 slot_matches
141 } else {
142 let root_slot_expr = SymExpr::constant(&mut self.cx, root_slot);
143 let root_matches = SymBoolExpr::eq(&mut self.cx, root.clone(), root_slot_expr);
144 SymBoolExpr::and(&mut self.cx, vec![slot_matches, root_matches])
145 };
146 let provenance = SymbolicMappingProvenance { root_slot, keys: keys.clone() };
147 if let Some(provenance) = self.classify_mapping_match(state, matches, provenance)? {
148 return Ok(provenance);
149 }
150 }
151 }
152 Ok(MappingStorageProvenance::None)
153 }
154
155 fn storage_hook_calldata(
156 &mut self,
157 selector: [u8; 4],
158 words: impl IntoIterator<Item = SymExpr>,
159 ) -> SymCalldata {
160 let selector = SymBytes::concrete(&mut self.cx, selector.to_vec());
161 let words = words.into_iter().map(|word| word.into_bytes(&mut self.cx)).collect::<Vec<_>>();
162 let bytes = SymBytes::concat(&mut self.cx, std::iter::once(selector).chain(words));
163 SymCalldata::from_bytes(&mut self.cx, bytes)
164 }
165
166 fn mapping_storage_hook_calldata(
167 &mut self,
168 selector: [u8; 4],
169 [account, computed_slot, root_slot, old_value, new_value]: [SymExpr; 5],
170 keys: Vec<SymExpr>,
171 ) -> SymCalldata {
172 let selector = SymBytes::concrete(&mut self.cx, selector.to_vec());
173 let keys_offset = SymExpr::constant(&mut self.cx, U256::from(6 * 32));
174 let mut words = vec![account, computed_slot, root_slot, keys_offset, old_value, new_value];
175 words.push(SymExpr::constant(&mut self.cx, U256::from(keys.len())));
176 words.extend(keys);
177 let words = words.into_iter().map(|word| word.into_bytes(&mut self.cx)).collect::<Vec<_>>();
178 let bytes = SymBytes::concat(&mut self.cx, std::iter::once(selector).chain(words));
179 SymCalldata::from_bytes(&mut self.cx, bytes)
180 }
181
182 fn invoke_storage_hook<FEN: FoundryEvmNetwork>(
183 &mut self,
184 executor: &Executor<FEN>,
185 state: &mut PathState,
186 worklist: &mut VecDeque<PathState>,
187 completed_paths: &mut usize,
188 hook: SymbolicStorageHook,
189 calldata: SymCalldata,
190 ) -> Result<StepOutcome, SymbolicError> {
191 let code = state.world.extcode(&mut self.cx, executor, hook.callback_target)?;
192 if code.is_empty() {
193 return Ok(StepOutcome::Continue);
194 }
195
196 let callvalue = SymExpr::zero(&mut self.cx);
197 let frame = CallFrame::new(
198 &mut self.cx,
199 hook.callback_target,
200 hook.callback_target,
201 CHEATCODE_ADDRESS,
202 callvalue,
203 false,
204 calldata,
205 );
206 let child = state.storage_hook_child(frame);
207 let outcomes = self.execute_external_call(executor, child, &code, completed_paths)?;
208 if outcomes.is_empty() {
209 return Ok(StepOutcome::AssumeRejected);
210 }
211
212 let mut parents = VecDeque::with_capacity(outcomes.len());
213 for mut outcome in outcomes {
214 let mut parent = state.clone();
215 parent.constraints = std::mem::take(&mut outcome.state.constraints);
216 parent.next_symbol = outcome.state.next_symbol;
217 parent.storage_load_hooks = std::mem::take(&mut outcome.state.storage_load_hooks);
218 parent.storage_store_hooks = std::mem::take(&mut outcome.state.storage_store_hooks);
219 parent.mapping_storage_store_hooks =
220 std::mem::take(&mut outcome.state.mapping_storage_store_hooks);
221 parent.mapping_hook_keccak_preimages =
222 std::mem::take(&mut outcome.state.mapping_hook_keccak_preimages);
223 parent.storage_hook_active = false;
224
225 match outcome.status {
226 CallStatus::Success => {
227 parent.world = outcome.state.world;
228 parent.block = outcome.state.block;
229 }
230 CallStatus::Revert | CallStatus::ExceptionalHalt | CallStatus::Failure => {
231 parent.return_data = outcome.state.frame.return_data;
232 parent.pending_storage_hook_revert = true;
233 }
234 }
235 parents.push_back(parent);
236 }
237
238 let Some(first) = self.pop_next_path(&mut parents) else {
239 return Ok(StepOutcome::AssumeRejected);
240 };
241 *state = first;
242 worklist.extend(parents);
243 Ok(if std::mem::take(&mut state.pending_storage_hook_revert) {
244 StepOutcome::Revert
245 } else {
246 StepOutcome::Continue
247 })
248 }
249
250 fn push_comparison_result(
251 &mut self,
252 state: &mut PathState,
253 op_pc: usize,
254 opcode: u8,
255 condition: SymBoolExpr,
256 ) -> Result<StepOutcome, SymbolicError> {
257 if !self.apply_branch_target_constraint(state, op_pc, opcode, &condition)? {
258 return Ok(StepOutcome::AssumeRejected);
259 }
260 let value = SymExpr::bool_word(&mut self.cx, condition);
261 state.stack.push(value)?;
262 Ok(StepOutcome::Continue)
263 }
264
265 fn apply_branch_target_constraint(
266 &mut self,
267 state: &mut PathState,
268 op_pc: usize,
269 opcode: u8,
270 condition: &SymBoolExpr,
271 ) -> Result<bool, SymbolicError> {
272 let Some(target) = state.branch_target() else {
273 return Ok(true);
274 };
275 if state.satisfies_branch_target() {
276 return Ok(true);
277 }
278 if !target.matches(state.address, op_pc, opcode) {
279 return Ok(true);
280 }
281
282 let desired =
283 if target.result() { condition.clone().not(&mut self.cx) } else { condition.clone() };
284 let mut constraints = state.constraints.clone();
285 constraints.push(desired);
286 if !self.branch_is_sat_or_defer(state, &constraints)? {
287 return Ok(false);
288 }
289 state.constraints = constraints;
290 state.mark_branch_target_reached();
291 Ok(true)
292 }
293
294 fn guard_fixed_memory_access<FEN: FoundryEvmNetwork>(
295 &mut self,
296 executor: &Executor<FEN>,
297 state: &mut PathState,
298 worklist: &mut VecDeque<PathState>,
299 offset: &SymExpr,
300 size: usize,
301 ) -> Result<Option<StepOutcome>, SymbolicError> {
302 let memory_limit = executor.evm_env().cfg_env.memory_limit();
303 let host_max_offset = (usize::MAX & !31usize).checked_sub(size);
304 let constrained_offset = state.constrained_usize_checked(&mut self.cx, offset);
305 if constrained_offset.as_ref().is_some_and(|offset| match offset {
306 Ok(offset) => host_max_offset.is_none_or(|max| *offset > max),
307 Err(_) => true,
308 }) {
309 state.return_data = SymReturnData::empty(&mut self.cx);
310 return Ok(Some(StepOutcome::Revert));
311 }
312
313 let expanded_size_bound = state
314 .upper_bound_usize(&mut self.cx, offset)
315 .and_then(|offset| offset.checked_add(size))
316 .and_then(|end| end.checked_add(31))
317 .and_then(|end| u64::try_from(end & !31usize).ok());
318 if expanded_size_bound.is_some_and(|size| size <= memory_limit) {
319 return Ok(None);
320 }
321
322 let representable = if let Some(host_max_offset) = host_max_offset {
323 let host_max_offset = SymExpr::constant(&mut self.cx, U256::from(host_max_offset));
324 SymBoolExpr::cmp(&mut self.cx, SymCmpOp::Ule, offset.clone(), host_max_offset)
325 } else {
326 SymBoolExpr::constant(&mut self.cx, false)
327 };
328 let size = SymExpr::constant(&mut self.cx, U256::from(size));
329 let local_size =
330 state.memory.size_after_range_expansion_word(&mut self.cx, offset.clone(), size);
331 if let Some(local_size) = local_size.as_const() {
332 if local_size <= U256::from(memory_limit) {
333 return Ok(None);
334 }
335 state.return_data = SymReturnData::empty(&mut self.cx);
336 return Ok(Some(StepOutcome::Revert));
337 }
338 let memory_limit = SymExpr::constant(&mut self.cx, U256::from(memory_limit));
339 let within_limit = SymBoolExpr::cmp(&mut self.cx, SymCmpOp::Ule, local_size, memory_limit);
340 let valid_access = SymBoolExpr::and(&mut self.cx, vec![representable, within_limit]);
341 self.apply_memory_access_guard(state, worklist, valid_access)
342 }
343
344 fn apply_memory_access_guard(
345 &mut self,
346 state: &mut PathState,
347 worklist: &mut VecDeque<PathState>,
348 valid_access: SymBoolExpr,
349 ) -> Result<Option<StepOutcome>, SymbolicError> {
350 let (valid_constraints, valid_sat) =
351 self.constraints_with_condition(state, valid_access.clone())?;
352 let invalid = valid_access.clone().not(&mut self.cx);
353 let (invalid_constraints, invalid_sat) = self.constraints_with_condition(state, invalid)?;
354 match (valid_sat, invalid_sat) {
355 (true, true) => {
356 let (valid_seed_models, invalid_seed_models) =
357 state.split_corpus_seed_models(&valid_access);
358 let mut valid = state.clone();
359 valid.pc = valid.pc.saturating_sub(1);
360 valid.depth = valid.depth.saturating_sub(1);
361 valid.constraints = valid_constraints;
362 valid.set_corpus_seed_models(valid_seed_models);
363 worklist.push_back(valid);
364 state.constraints = invalid_constraints;
365 state.set_corpus_seed_models(invalid_seed_models);
366 state.return_data = SymReturnData::empty(&mut self.cx);
367 Ok(Some(StepOutcome::Revert))
368 }
369 (true, false) => {
370 state.constraints = valid_constraints;
371 Ok(None)
372 }
373 (false, true) => {
374 state.constraints = invalid_constraints;
375 state.return_data = SymReturnData::empty(&mut self.cx);
376 Ok(Some(StepOutcome::Revert))
377 }
378 (false, false) => Ok(Some(StepOutcome::AssumeRejected)),
379 }
380 }
381
382 pub(super) fn guard_memory_range<FEN: FoundryEvmNetwork>(
383 &mut self,
384 executor: &Executor<FEN>,
385 state: &mut PathState,
386 worklist: &mut VecDeque<PathState>,
387 offset: &SymExpr,
388 size: &SymExpr,
389 ) -> Result<Option<StepOutcome>, SymbolicError> {
390 let memory_limit = executor.evm_env().cfg_env.memory_limit();
391 if let (Some(offset_value), Some(size_value)) = (offset.as_const(), size.as_const()) {
392 let valid = size_value.is_zero()
393 || usize::try_from(offset_value)
394 .ok()
395 .zip(usize::try_from(size_value).ok())
396 .and_then(|(offset, size)| offset.checked_add(size))
397 .and_then(|end| end.checked_add(31))
398 .and_then(|end| u64::try_from(end & !31usize).ok())
399 .is_some_and(|end| end <= memory_limit);
400 if !valid {
401 state.return_data = SymReturnData::empty(&mut self.cx);
402 return Ok(Some(StepOutcome::Revert));
403 }
404 state.memory.expand_range(&mut self.cx, offset.clone(), size.clone());
405 return Ok(None);
406 }
407
408 let offset_bound = state.upper_bound_usize(&mut self.cx, offset);
409 let size_bound = state.upper_bound_usize(&mut self.cx, size);
410 if offset_bound
411 .zip(size_bound)
412 .and_then(|(offset, size)| offset.checked_add(size))
413 .and_then(|end| end.checked_add(31))
414 .and_then(|end| u64::try_from(end & !31usize).ok())
415 .is_some_and(|end| end <= memory_limit)
416 {
417 state.memory.expand_range(&mut self.cx, offset.clone(), size.clone());
418 return Ok(None);
419 }
420
421 let zero_size = SymBoolExpr::eq_word_const(&mut self.cx, size, U256::ZERO);
422 let host_max = SymExpr::constant(&mut self.cx, U256::from(usize::MAX & !31usize));
423 let size_fits =
424 SymBoolExpr::cmp(&mut self.cx, SymCmpOp::Ule, size.clone(), host_max.clone());
425 let max_offset = SymExpr::binop(&mut self.cx, SymBinOp::Sub, host_max, size.clone());
426 let offset_fits = SymBoolExpr::cmp(&mut self.cx, SymCmpOp::Ule, offset.clone(), max_offset);
427
428 let local_size = state.memory.size_after_range_expansion_word(
429 &mut self.cx,
430 offset.clone(),
431 size.clone(),
432 );
433 let memory_limit = SymExpr::constant(&mut self.cx, U256::from(memory_limit));
434 let local_fits = SymBoolExpr::cmp(&mut self.cx, SymCmpOp::Ule, local_size, memory_limit);
435 let nonzero_valid =
436 SymBoolExpr::and(&mut self.cx, vec![size_fits, offset_fits, local_fits]);
437 let valid_access = SymBoolExpr::or(&mut self.cx, vec![zero_size, nonzero_valid]);
438
439 let outcome = self.apply_memory_access_guard(state, worklist, valid_access)?;
440 if outcome.is_none() {
441 state.memory.expand_range(&mut self.cx, offset.clone(), size.clone());
442 }
443 Ok(outcome)
444 }
445
446 #[expect(clippy::too_many_arguments)]
447 pub(super) fn step<FEN: FoundryEvmNetwork>(
448 &mut self,
449 executor: &Executor<FEN>,
450 code: &SymCode,
451 jumpdests: &JumpTable,
452 state: &mut PathState,
453 worklist: &mut VecDeque<PathState>,
454 completed_paths: &mut usize,
455 op: u8,
456 ) -> Result<StepOutcome, SymbolicError> {
457 state.pc += 1;
458
459 match op {
460 opcode::PUSH0 => {
461 state.stack.push(SymExpr::zero(&mut self.cx))?;
462 }
463 opcode::PUSH1..=opcode::PUSH32 => {
464 let n = (op - opcode::PUSH1 + 1) as usize;
465 let end = state.pc.saturating_add(n);
466 if end > code.len() {
467 return Err(SymbolicError::InvalidBytecode("truncated PUSH data"));
468 }
469 let value = code.push_data_word(&mut self.cx, state.pc, n);
470 state.pc = end;
471 state.stack.push(value)?;
472 }
473 opcode::DUP1..=opcode::DUP16 => {
474 let n = (op - opcode::DUP1 + 1) as usize;
475 let value = state.stack.peek(n - 1)?.clone();
476 state.stack.push(value)?;
477 }
478 opcode::SWAP1..=opcode::SWAP16 => {
479 let n = (op - opcode::SWAP1 + 1) as usize;
480 state.stack.swap(n)?;
481 }
482 opcode::STOP => return Ok(StepOutcome::Halt),
483 opcode::ADD => {
484 state.bin_word(&mut self.cx, SymBinOp::Add)?;
485 }
486 opcode::SUB => {
487 state.bin_word(&mut self.cx, SymBinOp::Sub)?;
488 }
489 opcode::MUL => {
490 state.bin_word(&mut self.cx, SymBinOp::Mul)?;
491 }
492 opcode::EXP => {
493 state.exp_word(&mut self.cx)?;
494 }
495 opcode::DIV => {
496 state.bin_word_div_zero_guard(&mut self.cx, SymBinOp::UDiv)?;
497 }
498 opcode::SDIV => {
499 state.bin_word_div_zero_guard(&mut self.cx, SymBinOp::SDiv)?;
500 }
501 opcode::MOD => {
502 state.bin_word_div_zero_guard(&mut self.cx, SymBinOp::URem)?;
503 }
504 opcode::SMOD => {
505 state.bin_word_div_zero_guard(&mut self.cx, SymBinOp::SRem)?;
506 }
507 opcode::ADDMOD => {
508 let a = state.stack.pop()?;
509 let b = state.stack.pop()?;
510 let n = state.stack.pop()?;
511 state.stack.push(SymExpr::ternop(&mut self.cx, SymTernOp::AddMod, a, b, n))?;
512 }
513 opcode::MULMOD => {
514 let a = state.stack.pop()?;
515 let b = state.stack.pop()?;
516 let n = state.stack.pop()?;
517 state.stack.push(SymExpr::ternop(&mut self.cx, SymTernOp::MulMod, a, b, n))?;
518 }
519 opcode::LT => {
520 let op_pc = state.pc - 1;
521 let condition = state.cmp_word_condition(&mut self.cx, SymCmpOp::Ult)?;
522 return self.push_comparison_result(state, op_pc, op, condition);
523 }
524 opcode::GT => {
525 let op_pc = state.pc - 1;
526 let condition = state.cmp_word_condition(&mut self.cx, SymCmpOp::Ugt)?;
527 return self.push_comparison_result(state, op_pc, op, condition);
528 }
529 opcode::SLT => {
530 let op_pc = state.pc - 1;
531 let condition = state.cmp_word_condition(&mut self.cx, SymCmpOp::Slt)?;
532 return self.push_comparison_result(state, op_pc, op, condition);
533 }
534 opcode::SGT => {
535 let op_pc = state.pc - 1;
536 let condition = state.cmp_word_condition(&mut self.cx, SymCmpOp::Sgt)?;
537 return self.push_comparison_result(state, op_pc, op, condition);
538 }
539 opcode::EQ => {
540 let op_pc = state.pc - 1;
541 let a = state.stack.pop()?;
542 let b = state.stack.pop()?;
543 let condition = SymBoolExpr::eq(&mut self.cx, b, a);
544 return self.push_comparison_result(state, op_pc, op, condition);
545 }
546 opcode::ISZERO => {
547 let op_pc = state.pc - 1;
548 let value = state.stack.pop()?;
549 let value = value.into_zero_bool(&mut self.cx);
550 return self.push_comparison_result(state, op_pc, op, value);
551 }
552 opcode::AND => {
553 state.bin_word(&mut self.cx, SymBinOp::And)?;
554 }
555 opcode::OR => {
556 state.bin_word(&mut self.cx, SymBinOp::Or)?;
557 }
558 opcode::XOR => {
559 state.bin_word(&mut self.cx, SymBinOp::Xor)?;
560 }
561 opcode::NOT => {
562 let value = state.stack.pop()?;
563 state.stack.push(SymExpr::not(&mut self.cx, value))?;
564 }
565 opcode::SIGNEXTEND => {
566 let byte_index = state.stack.pop()?;
567 let value = state.stack.pop()?;
568 state.stack.push(signextend_word_dynamic(&mut self.cx, byte_index, value))?;
569 }
570 opcode::BYTE => {
571 let index = state.stack.pop()?;
572 let word = state.stack.pop()?;
573 state.stack.push(byte_word_dynamic(&mut self.cx, index, word))?;
574 }
575 opcode::SHL => {
576 state.shift_word(&mut self.cx, ShiftKind::Shl)?;
577 }
578 opcode::SHR => {
579 state.shift_word(&mut self.cx, ShiftKind::Shr)?;
580 }
581 opcode::SAR => {
582 state.shift_word(&mut self.cx, ShiftKind::Sar)?;
583 }
584 opcode::KECCAK256 => {
585 let offset = state.stack.peek(0)?.clone();
586 let size = state.stack.peek(1)?.clone();
587 if let Some(outcome) =
588 self.guard_memory_range(executor, state, worklist, &offset, &size)?
589 {
590 return Ok(outcome);
591 }
592 let offset = state.stack.pop()?;
593 let size = state.stack.pop()?;
594 match state.constrained_usize_checked(&mut self.cx, &size) {
595 Some(Ok(size)) => {
596 let bytes = state.memory.read_byte_exprs_offset(&mut self.cx, offset, size);
597 let hash = keccak_word(&mut self.cx, bytes.clone());
598 let has_mapping_hook = state
599 .mapping_storage_store_hooks
600 .keys()
601 .any(|(address, _)| *address == state.storage_address);
602 if has_mapping_hook && !state.storage_hook_active && size == 64 {
603 state
604 .mapping_hook_keccak_preimages
605 .entry((state.storage_address, hash.clone()))
606 .or_insert_with(|| bytes.into());
607 }
608 state.stack.push(hash)?;
609 }
610 Some(Err(_)) => {
611 return Ok(StepOutcome::Revert);
612 }
613 None => {
614 let has_mapping_hook = state
615 .mapping_storage_store_hooks
616 .keys()
617 .any(|(address, _)| *address == state.storage_address);
618 if has_mapping_hook && !state.storage_hook_active {
619 let mapping_size = SymExpr::constant(&mut self.cx, U256::from(64));
620 let mapping_size_feasible =
621 SymBoolExpr::eq(&mut self.cx, size.clone(), mapping_size);
622 let (_, mapping_size_feasible) =
623 self.constraints_with_condition(state, mapping_size_feasible)?;
624 if mapping_size_feasible {
625 self.defer_incomplete(
626 "symbolic KECCAK256 size may conceal mapping provenance",
627 );
628 }
629 }
630 let max_limit = self.config.max_calldata_bytes as usize;
631 let max_size = state
632 .upper_bound_usize(&mut self.cx, &size)
633 .filter(|size| *size <= max_limit)
634 .map(Ok)
635 .unwrap_or_else(|| {
636 self.solver_upper_bound_usize(
637 state,
638 &size,
639 max_limit,
640 "symbolic SHA3 size",
641 )
642 })?;
643 let bytes = state.memory.read_byte_exprs_symbolic_size(
644 &mut self.cx,
645 offset,
646 size.clone(),
647 max_size,
648 );
649 state.stack.push(keccak_word_with_len(&mut self.cx, bytes, size))?;
650 }
651 }
652 }
653 opcode::ADDRESS => {
654 let address = state.address_word.clone();
655 state.stack.push(address)?;
656 }
657 opcode::CALLER => {
658 let caller = state.caller_word.clone();
659 state.stack.push(caller)?;
660 }
661 opcode::ORIGIN => {
662 let origin = state.origin_word.clone();
663 state.stack.push(origin)?;
664 }
665 opcode::CALLVALUE => {
666 let callvalue = state.callvalue.clone();
667 state.stack.push(callvalue)?;
668 }
669 opcode::BLOCKHASH => {
670 let number = state.stack.pop()?;
671 let hash = state.block.block_hash_word(&mut self.cx, executor, number)?;
672 state.stack.push(hash)?;
673 }
674 opcode::BALANCE => {
675 let target = state.stack.pop()?;
676 let balance = state.balance_word(&mut self.cx, executor, target)?;
677 state.stack.push(balance)?;
678 }
679 opcode::SELFBALANCE => {
680 let balance = state.balance(&mut self.cx, executor, state.address);
681 state.stack.push(balance)?;
682 }
683 opcode::EXTCODESIZE => {
684 let target = state.stack.pop()?;
685 let size = state.extcode_size_word(&mut self.cx, executor, target)?;
686 state.stack.push(size)?;
687 }
688 opcode::EXTCODEHASH => {
689 let target = state.stack.pop()?;
690 let hash = state.extcode_hash_word(&mut self.cx, executor, target)?;
691 state.stack.push(hash)?;
692 }
693 opcode::EXTCODECOPY => {
694 let dest = state.stack.peek(1)?.clone();
695 let size = state.stack.peek(3)?.clone();
696 if let Some(outcome) =
697 self.guard_memory_range(executor, state, worklist, &dest, &size)?
698 {
699 return Ok(outcome);
700 }
701 let target = state.stack.pop()?;
702 let dest = state.stack.pop()?;
703 let offset = state.stack.pop()?;
704 let size = state.stack.pop()?;
705 match state.constrained_usize_checked(&mut self.cx, &size) {
706 Some(Ok(size)) => {
707 let bytes = state.extcode_bytes_word(
708 &mut self.cx,
709 executor,
710 target,
711 offset,
712 size,
713 )?;
714 state.memory.copy_bytes_offset(&mut self.cx, dest, bytes);
715 }
716 Some(Err(_)) => {
717 return Ok(StepOutcome::Revert);
718 }
719 None => {
720 let max_limit = self.config.max_calldata_bytes as usize;
721 let max_size = state
722 .upper_bound_usize(&mut self.cx, &size)
723 .filter(|size| *size <= max_limit)
724 .map(Ok)
725 .unwrap_or_else(|| {
726 self.solver_upper_bound_usize(
727 state,
728 &size,
729 max_limit,
730 "symbolic EXTCODECOPY size",
731 )
732 })?;
733 if max_size != 0 {
734 let bytes = state.extcode_bytes_word(
735 &mut self.cx,
736 executor,
737 target,
738 offset,
739 max_size,
740 )?;
741 state.memory.copy_bytes_size_offset(&mut self.cx, dest, size, bytes)?;
742 }
743 }
744 }
745 }
746 opcode::CALLDATALOAD => {
747 let offset = state.stack.pop()?;
748 let value = state.calldata.load_word(&mut self.cx, offset)?;
749 state.stack.push(value)?;
750 }
751 opcode::CALLDATASIZE => {
752 let size = state.calldata.size_word();
753 state.stack.push(size)?;
754 }
755 opcode::CALLDATACOPY => {
756 let dest = state.stack.peek(0)?.clone();
757 let size = state.stack.peek(2)?.clone();
758 if let Some(outcome) =
759 self.guard_memory_range(executor, state, worklist, &dest, &size)?
760 {
761 return Ok(outcome);
762 }
763 let dest = state.stack.pop()?;
764 let offset = state.stack.pop()?;
765 let size = state.stack.pop()?;
766 match state.constrained_usize_checked(&mut self.cx, &size) {
767 Some(Ok(size)) => {
768 if size != 0 {
769 state.copy_calldata_to_offset(&mut self.cx, dest, offset, size)?;
770 }
771 }
772 Some(Err(_)) => {
773 return Ok(StepOutcome::Revert);
774 }
775 None => {
776 let max_limit = self.config.max_calldata_bytes as usize;
777 let max_size = state
778 .upper_bound_usize(&mut self.cx, &size)
779 .filter(|size| *size <= max_limit)
780 .map(Ok)
781 .unwrap_or_else(|| {
782 self.solver_upper_bound_usize(
783 state,
784 &size,
785 max_limit,
786 "symbolic CALLDATACOPY size",
787 )
788 })?;
789 if max_size != 0 {
790 state.copy_calldata_symbolic_size(
791 &mut self.cx,
792 dest,
793 offset,
794 size,
795 max_size,
796 )?;
797 }
798 }
799 }
800 }
801 opcode::CODESIZE => {
802 let value = SymExpr::constant(&mut self.cx, U256::from(code.len()));
803 state.stack.push(value)?;
804 }
805 opcode::CODECOPY => {
806 let dest = state.stack.peek(0)?.clone();
807 let size = state.stack.peek(2)?.clone();
808 if let Some(outcome) =
809 self.guard_memory_range(executor, state, worklist, &dest, &size)?
810 {
811 return Ok(outcome);
812 }
813 let dest = state.stack.pop()?;
814 let offset = state.stack.pop()?;
815 let size = state.stack.pop()?;
816 match state.constrained_usize_checked(&mut self.cx, &size) {
817 Some(Ok(size)) => {
818 let bytes = code.read_bytes_offset(&mut self.cx, offset, size);
819 state.memory.copy_bytes_offset(&mut self.cx, dest, bytes);
820 }
821 Some(Err(_)) => {
822 return Ok(StepOutcome::Revert);
823 }
824 None => {
825 let max_limit = self.config.max_calldata_bytes as usize;
826 let max_size = state
827 .upper_bound_usize(&mut self.cx, &size)
828 .filter(|size| *size <= max_limit)
829 .map(Ok)
830 .unwrap_or_else(|| {
831 self.solver_upper_bound_usize(
832 state,
833 &size,
834 max_limit,
835 "symbolic CODECOPY size",
836 )
837 })?;
838 if max_size != 0 {
839 let bytes = code.read_bytes_offset(&mut self.cx, offset, max_size);
840 state.memory.copy_bytes_size_offset(&mut self.cx, dest, size, bytes)?;
841 }
842 }
843 }
844 }
845 opcode::RETURNDATASIZE => {
846 let size = state.return_data.len_word();
847 state.stack.push(size)?;
848 }
849 opcode::RETURNDATACOPY => {
850 let dest = state.stack.peek(0)?.clone();
851 let offset = state.stack.peek(1)?.clone();
852 let size = state.stack.peek(2)?.clone();
853 if let Some(outcome) =
854 self.guard_memory_range(executor, state, worklist, &dest, &size)?
855 {
856 return Ok(outcome);
857 }
858 if let Some(outcome) =
859 self.guard_returndata_copy_range(state, worklist, &offset, &size)?
860 {
861 return Ok(outcome);
862 }
863 let dest = state.stack.pop()?;
864 let offset = state.stack.pop()?;
865 let size = state.stack.pop()?;
866 match state.constrained_usize_checked(&mut self.cx, &size) {
867 Some(Ok(size)) => {
868 state.copy_return_data_to_offset(&mut self.cx, dest, offset, size)?;
869 }
870 Some(Err(_)) => {
871 return Ok(StepOutcome::Revert);
872 }
873 None => {
874 let available = state
875 .constrained_usize(&mut self.cx, &offset)
876 .map(|offset| state.return_data.len().saturating_sub(offset))
877 .unwrap_or(state.return_data.len());
878 let max_limit = available.min(self.config.max_calldata_bytes as usize);
879 let max_size = state
880 .upper_bound_usize(&mut self.cx, &size)
881 .filter(|size| *size <= max_limit)
882 .map(Ok)
883 .unwrap_or_else(|| {
884 self.solver_upper_bound_usize(
885 state,
886 &size,
887 max_limit,
888 "symbolic RETURNDATACOPY size",
889 )
890 })?;
891 state.copy_return_data_symbolic_size(
892 &mut self.cx,
893 dest,
894 offset,
895 size,
896 max_size,
897 )?;
898 }
899 }
900 }
901 opcode::POP => {
902 state.stack.pop()?;
903 }
904 opcode::MLOAD => {
905 let offset = state.stack.peek(0)?.clone();
906 if let Some(outcome) =
907 self.guard_fixed_memory_access(executor, state, worklist, &offset, 32)?
908 {
909 return Ok(outcome);
910 }
911 let offset = state.stack.pop()?;
912 let value = state.memory.load_word_offset(&mut self.cx, offset)?;
913 state.stack.push(value)?;
914 }
915 opcode::MSTORE => {
916 let offset = state.stack.peek(0)?.clone();
917 if let Some(outcome) =
918 self.guard_fixed_memory_access(executor, state, worklist, &offset, 32)?
919 {
920 return Ok(outcome);
921 }
922 let offset = state.stack.pop()?;
923 let value = state.stack.pop()?;
924 let minimum_offset = state.lower_bound_usize(&offset);
925 state.memory.store_word_offset(&mut self.cx, offset, value, minimum_offset);
926 }
927 opcode::MSTORE8 => {
928 let offset = state.stack.peek(0)?.clone();
929 if let Some(outcome) =
930 self.guard_fixed_memory_access(executor, state, worklist, &offset, 1)?
931 {
932 return Ok(outcome);
933 }
934 let offset = state.stack.pop()?;
935 let value = state.stack.pop()?;
936 let minimum_offset = state.lower_bound_usize(&offset);
937 state.memory.store_byte_offset(&mut self.cx, offset, value, minimum_offset);
938 }
939 opcode::SLOAD => {
940 let key = state.stack.pop()?;
941 state.record_sload(state.storage_address, key.clone());
942 let concrete_key = state.constrained_word(&mut self.cx, &key);
943 let value = state.world.sload(
944 &mut self.cx,
945 executor,
946 state.storage_address,
947 key.clone(),
948 concrete_key,
949 )?;
950 state.stack.push(value.clone())?;
951 if !state.storage_hook_active
952 && let Some(hook) =
953 state.storage_load_hooks.get(&state.storage_address).copied()
954 {
955 let account =
956 SymExpr::constant(&mut self.cx, address_word(state.storage_address));
957 let calldata =
958 self.storage_hook_calldata(hook.callback_selector, [account, key, value]);
959 return self.invoke_storage_hook(
960 executor,
961 state,
962 worklist,
963 completed_paths,
964 hook,
965 calldata,
966 );
967 }
968 }
969 opcode::SSTORE => {
970 if state.is_static {
971 state.return_data = SymReturnData::empty(&mut self.cx);
972 return Ok(StepOutcome::Revert);
973 }
974 let key = state.stack.peek(0)?.clone();
975 state.stack.peek(1)?;
976 let hook = (!state.storage_hook_active)
977 .then(|| state.storage_store_hooks.get(&state.storage_address).copied())
978 .flatten();
979 let mapping = if state.storage_hook_active {
980 MappingStorageProvenance::None
981 } else {
982 self.mapping_storage_provenance(state, &key)?
983 };
984 let mapping = match mapping {
985 MappingStorageProvenance::None => None,
986 MappingStorageProvenance::Exact(provenance) => state
987 .mapping_storage_store_hooks
988 .get(&(state.storage_address, provenance.root_slot))
989 .copied()
990 .map(|hook| (hook, provenance)),
991 MappingStorageProvenance::Fork { equality, inequality } => {
992 let store_pc = state.pc - 1;
993 let mut equality_state = state.clone();
994 equality_state.pc = store_pc;
995 equality_state.depth = equality_state.depth.saturating_sub(1);
996 equality_state.constraints = equality;
997 let mut inequality_state = state.clone();
998 inequality_state.pc = store_pc;
999 inequality_state.depth = inequality_state.depth.saturating_sub(1);
1000 inequality_state.constraints = inequality;
1001 worklist.push_back(equality_state);
1002 worklist.push_back(inequality_state);
1003 return Ok(StepOutcome::Forked);
1004 }
1005 };
1006 state.stack.pop()?;
1007 let value = state.stack.pop()?;
1008 state.record_sstore(state.storage_address, key.clone());
1009 let old_value = if hook.is_some() || mapping.is_some() {
1010 let concrete_key = state.constrained_word(&mut self.cx, &key);
1011 Some(state.world.sload(
1012 &mut self.cx,
1013 executor,
1014 state.storage_address,
1015 key.clone(),
1016 concrete_key,
1017 )?)
1018 } else {
1019 None
1020 };
1021 state.world.sstore(state.storage_address, key.clone(), value.clone());
1022 if let Some(hook) = hook {
1023 let account =
1024 SymExpr::constant(&mut self.cx, address_word(state.storage_address));
1025 let calldata = self.storage_hook_calldata(
1026 hook.callback_selector,
1027 [
1028 account,
1029 key,
1030 old_value.expect("old value loaded for storage hook"),
1031 value,
1032 ],
1033 );
1034 return self.invoke_storage_hook(
1035 executor,
1036 state,
1037 worklist,
1038 completed_paths,
1039 hook,
1040 calldata,
1041 );
1042 } else if let Some((hook, provenance)) = mapping {
1043 let account =
1044 SymExpr::constant(&mut self.cx, address_word(state.storage_address));
1045 let root = SymExpr::constant(&mut self.cx, provenance.root_slot);
1046 let calldata = self.mapping_storage_hook_calldata(
1047 hook.callback_selector,
1048 [
1049 account,
1050 key,
1051 root,
1052 old_value.expect("old value loaded for mapping storage hook"),
1053 value,
1054 ],
1055 provenance.keys,
1056 );
1057 return self.invoke_storage_hook(
1058 executor,
1059 state,
1060 worklist,
1061 completed_paths,
1062 hook,
1063 calldata,
1064 );
1065 }
1066 }
1067 opcode::TLOAD => {
1068 let key = state.stack.pop()?;
1069 let value = state.world.tload(&mut self.cx, state.storage_address, key);
1070 state.stack.push(value)?;
1071 }
1072 opcode::TSTORE => {
1073 if state.is_static {
1074 state.return_data = SymReturnData::empty(&mut self.cx);
1075 return Ok(StepOutcome::Revert);
1076 }
1077 let key = state.stack.pop()?;
1078 let value = state.stack.pop()?;
1079 state.world.tstore(state.storage_address, key, value);
1080 }
1081 opcode::JUMP => {
1082 let dest = state.stack.pop()?;
1083 let Some(dest) = self.resolve_jump_destination(
1084 state,
1085 jumpdests,
1086 dest,
1087 "symbolic JUMP destination",
1088 )?
1089 else {
1090 state.return_data = SymReturnData::empty(&mut self.cx);
1091 return Ok(StepOutcome::Revert);
1092 };
1093 if !self.take_loop_jump(state, state.pc, dest) {
1094 return Ok(StepOutcome::AssumeRejected);
1095 }
1096 state.pc = dest;
1097 }
1098 opcode::JUMPI => {
1099 let dest = state.stack.pop()?;
1100 let cond = state.stack.pop()?;
1101 match cond.truth() {
1102 Some(true) => {
1103 let Some(dest) = self.resolve_jump_destination(
1104 state,
1105 jumpdests,
1106 dest,
1107 "symbolic JUMPI destination",
1108 )?
1109 else {
1110 state.return_data = SymReturnData::empty(&mut self.cx);
1111 return Ok(StepOutcome::Revert);
1112 };
1113 if !self.take_loop_jump(state, state.pc, dest) {
1114 return Ok(StepOutcome::AssumeRejected);
1115 }
1116 state.pc = dest;
1117 }
1118 Some(false) => {}
1119 None => {
1120 let true_cond = cond.nonzero_bool(&mut self.cx);
1121 let dest = match self.resolve_jump_destination(
1122 state,
1123 jumpdests,
1124 dest,
1125 "symbolic JUMPI destination",
1126 ) {
1127 Ok(Some(dest)) => dest,
1128 Ok(None) => {
1129 return self.branch_invalid_jumpi(state, worklist, true_cond);
1130 }
1131 Err(err) => {
1132 let (_, taken_sat) =
1133 self.constraints_with_condition(state, true_cond.clone())?;
1134 if taken_sat {
1135 return Err(err);
1136 }
1137 let (_, not_taken_seed_models) =
1138 state.split_corpus_seed_models(&true_cond);
1139 state.constraints.push(true_cond.not(&mut self.cx));
1140 state.set_corpus_seed_models(not_taken_seed_models);
1141 return Ok(StepOutcome::Continue);
1142 }
1143 };
1144 let op_pc = state.pc.saturating_sub(1);
1145 let _branch_span = trace_span!("jumpi_branch", pc = op_pc, dest).entered();
1146 let false_cond = true_cond.clone().not(&mut self.cx);
1147 let fallthrough = state.pc;
1148 let (true_seed_models, false_seed_models) =
1149 state.split_corpus_seed_models(&true_cond);
1150 let mut true_state = state.clone();
1151 true_state.constraints.push(true_cond);
1152 true_state.set_corpus_seed_models(true_seed_models);
1153 true_state.pc = dest;
1154 let mut false_state = state.clone();
1155 false_state.constraints.push(false_cond);
1156 false_state.set_corpus_seed_models(false_seed_models);
1157 false_state.pc = fallthrough;
1158
1159 let true_pending = self.take_loop_jump(&mut true_state, fallthrough, dest);
1160 if true_pending {
1161 true_state.defer_feasibility_check();
1162 }
1163 false_state.defer_feasibility_check();
1164 trace!(true_pending, false_pending = true, "JUMPI symbolic branch");
1165 if true_pending {
1166 let true_seed_count = true_state.corpus_seed_model_count();
1167 let false_seed_count = false_state.corpus_seed_model_count();
1168 match (
1169 false_seed_count.cmp(&true_seed_count),
1170 self.config.exploration_order,
1171 ) {
1172 (std::cmp::Ordering::Greater, SymbolicExplorationOrder::Bfs)
1173 | (std::cmp::Ordering::Less, SymbolicExplorationOrder::Dfs) => {
1174 worklist.push_back(false_state);
1175 worklist.push_back(true_state);
1176 }
1177 (std::cmp::Ordering::Greater, SymbolicExplorationOrder::Dfs)
1178 | (std::cmp::Ordering::Less, SymbolicExplorationOrder::Bfs)
1179 | (std::cmp::Ordering::Equal, _) => {
1180 worklist.push_back(true_state);
1181 worklist.push_back(false_state);
1182 }
1183 }
1184 } else {
1185 worklist.push_back(false_state);
1186 }
1187 return Ok(StepOutcome::Forked);
1188 }
1189 }
1190 }
1191 opcode::PC => {
1192 let pc = state.pc - 1;
1193 let pc = SymExpr::constant(&mut self.cx, U256::from(pc));
1194 state.stack.push(pc)?;
1195 }
1196 opcode::MSIZE => {
1197 let size = state.memory.size_word(&mut self.cx);
1198 state.stack.push(size)?;
1199 }
1200 opcode::GAS => {
1201 let gas = state.fresh_gasleft(&mut self.cx);
1202 state.stack.push(gas)?;
1203 }
1204 opcode::JUMPDEST => {}
1205 opcode::MCOPY => {
1206 let dest = state.stack.peek(0)?.clone();
1207 let src = state.stack.peek(1)?.clone();
1208 let size = state.stack.peek(2)?.clone();
1209 if let Some(outcome) =
1210 self.guard_memory_range(executor, state, worklist, &dest, &size)?
1211 {
1212 return Ok(outcome);
1213 }
1214 if let Some(outcome) =
1215 self.guard_memory_range(executor, state, worklist, &src, &size)?
1216 {
1217 return Ok(outcome);
1218 }
1219 let dest = state.stack.pop()?;
1220 let src = state.stack.pop()?;
1221 let size = state.stack.pop()?;
1222 match state.constrained_usize_checked(&mut self.cx, &size) {
1223 Some(Ok(size)) => {
1224 state.memory.copy_memory_to_offset(&mut self.cx, dest, src, size)?;
1225 }
1226 Some(Err(_)) => {
1227 return Ok(StepOutcome::Revert);
1228 }
1229 None => {
1230 let max_limit = self.config.max_calldata_bytes as usize;
1231 let max_size = state
1232 .upper_bound_usize(&mut self.cx, &size)
1233 .filter(|size| *size <= max_limit)
1234 .map(Ok)
1235 .unwrap_or_else(|| {
1236 self.solver_upper_bound_usize(
1237 state,
1238 &size,
1239 max_limit,
1240 "symbolic MCOPY size",
1241 )
1242 })?;
1243 if max_size != 0 {
1244 state.memory.copy_memory_symbolic_size(
1245 &mut self.cx,
1246 dest,
1247 src,
1248 size,
1249 max_size,
1250 )?;
1251 }
1252 }
1253 }
1254 }
1255 opcode::RETURN | opcode::REVERT => {
1256 let offset = state.stack.peek(0)?.clone();
1257 let size = state.stack.peek(1)?.clone();
1258 if let Some(outcome) =
1259 self.guard_memory_range(executor, state, worklist, &offset, &size)?
1260 {
1261 return Ok(outcome);
1262 }
1263 return self.return_or_revert(state, op == opcode::REVERT);
1264 }
1265 opcode::INVALID => return Ok(StepOutcome::ExceptionalHalt),
1266 opcode::CALL => {
1267 return self.call(executor, state, worklist, completed_paths, CallKind::Call);
1268 }
1269 opcode::CALLCODE => {
1270 return self.call(executor, state, worklist, completed_paths, CallKind::CallCode);
1271 }
1272 opcode::DELEGATECALL => {
1273 return self.call(
1274 executor,
1275 state,
1276 worklist,
1277 completed_paths,
1278 CallKind::DelegateCall,
1279 );
1280 }
1281 opcode::STATICCALL => {
1282 return self.call(executor, state, worklist, completed_paths, CallKind::StaticCall);
1283 }
1284 opcode::CREATE => {
1285 return self.create(executor, state, worklist, completed_paths, CreateKind::Create);
1286 }
1287 opcode::CREATE2 => {
1288 return self.create(
1289 executor,
1290 state,
1291 worklist,
1292 completed_paths,
1293 CreateKind::Create2,
1294 );
1295 }
1296 opcode::SELFDESTRUCT => {
1297 if state.is_static {
1298 state.return_data = SymReturnData::empty(&mut self.cx);
1299 return Ok(StepOutcome::Revert);
1300 }
1301 let spec_id: SpecId = executor.spec_id().into();
1302 let (beneficiary_word, beneficiary) =
1303 state.pop_address_word_or_symbolic_slot(&mut self.cx)?;
1304 if spec_id < SpecId::CANCUN
1305 || state.world.was_created_in_current_transaction(state.address)
1306 {
1307 state.world.selfdestruct_legacy(
1308 &mut self.cx,
1309 executor,
1310 state.address,
1311 beneficiary,
1312 )?;
1313 } else {
1314 if state.constrained_word(&mut self.cx, &beneficiary_word).is_none() {
1315 return Err(SymbolicError::Unsupported(
1316 "symbolic SELFDESTRUCT beneficiary",
1317 ));
1318 }
1319 state.world.selfdestruct_cancun_existing(
1320 &mut self.cx,
1321 executor,
1322 state.address,
1323 beneficiary,
1324 );
1325 }
1326 state.return_data = SymReturnData::empty(&mut self.cx);
1327 return Ok(StepOutcome::Halt);
1328 }
1329 opcode::CHAINID => {
1330 let value = state.block.chain_id.clone();
1331 state.stack.push(value)?;
1332 }
1333 opcode::BASEFEE => {
1334 let value = state.block.basefee.clone();
1335 state.stack.push(value)?;
1336 }
1337 opcode::GASPRICE => {
1338 let gas_price = state.gas_price.clone();
1339 state.stack.push(gas_price)?;
1340 }
1341 opcode::BLOBHASH => {
1342 let index = state.stack.pop()?;
1343 let index = state.expect_constrained_usize(
1344 &mut self.cx,
1345 index,
1346 "symbolic BLOBHASH index",
1347 )?;
1348 let hash = state.block.blob_hash(index);
1349 let hash = SymExpr::constant(&mut self.cx, U256::from_be_slice(hash.as_slice()));
1350 state.stack.push(hash)?;
1351 }
1352 opcode::COINBASE => {
1353 let coinbase = state.block.coinbase;
1354 let coinbase = SymExpr::constant(&mut self.cx, address_word(coinbase));
1355 state.stack.push(coinbase)?;
1356 }
1357 opcode::TIMESTAMP => {
1358 let value = state.block.timestamp.clone();
1359 state.stack.push(value)?;
1360 }
1361 opcode::NUMBER => {
1362 let value = state.block.number.clone();
1363 state.stack.push(value)?;
1364 }
1365 opcode::DIFFICULTY => {
1366 let value = state.block.difficulty.clone();
1367 state.stack.push(value)?;
1368 }
1369 opcode::GASLIMIT => {
1370 let value = state.block.gaslimit.clone();
1371 state.stack.push(value)?;
1372 }
1373 opcode::BLOBBASEFEE => {
1374 let value = state.block.blob_basefee.clone();
1375 state.stack.push(value)?;
1376 }
1377 opcode::LOG0 | opcode::LOG1 | opcode::LOG2 | opcode::LOG3 | opcode::LOG4 => {
1378 if state.is_static {
1379 state.return_data = SymReturnData::empty(&mut self.cx);
1380 return Ok(StepOutcome::Revert);
1381 }
1382 let topics = (op - opcode::LOG0) as usize;
1383 let offset = state.stack.peek(0)?.clone();
1384 let size = state.stack.peek(1)?.clone();
1385 if let Some(outcome) =
1386 self.guard_memory_range(executor, state, worklist, &offset, &size)?
1387 {
1388 return Ok(outcome);
1389 }
1390 let offset = state.stack.pop()?;
1391 if offset.contains_gasleft() {
1392 return Err(SymbolicError::Unsupported("GAS/gasleft() not modeled"));
1393 }
1394 let size = state.stack.pop()?;
1395 if size.contains_gasleft() {
1396 return Err(SymbolicError::Unsupported("GAS/gasleft() not modeled"));
1397 }
1398 let (data_len, data) = match state.constrained_usize_checked(&mut self.cx, &size) {
1399 Some(Ok(size)) => (
1400 SymExpr::constant(&mut self.cx, U256::from(size)),
1401 state.memory.read_bytes_offset(&mut self.cx, offset, size),
1402 ),
1403 Some(Err(_)) => {
1404 return Ok(StepOutcome::Revert);
1405 }
1406 None => {
1407 let max_limit = self.config.max_calldata_bytes as usize;
1408 let max_size = state
1409 .upper_bound_usize(&mut self.cx, &size)
1410 .filter(|size| *size <= max_limit)
1411 .map(Ok)
1412 .unwrap_or_else(|| {
1413 self.solver_upper_bound_usize(
1414 state,
1415 &size,
1416 max_limit,
1417 "symbolic LOG size",
1418 )
1419 })?;
1420 let data = state.memory.read_bytes_symbolic_size(
1421 &mut self.cx,
1422 offset,
1423 size.clone(),
1424 max_size,
1425 );
1426 (size, data)
1427 }
1428 };
1429 let mut log_topics = Vec::with_capacity(topics);
1430 for _ in 0..topics {
1431 let topic = state.stack.pop()?;
1432 if topic.contains_gasleft() {
1433 return Err(SymbolicError::Unsupported("GAS/gasleft() not modeled"));
1434 }
1435 log_topics.push(topic);
1436 }
1437 return self.handle_log(
1438 state,
1439 SymbolicLog::new(log_topics, data_len, data, state.address),
1440 );
1441 }
1442 _ => return Err(SymbolicError::UnsupportedOpcode(op)),
1443 };
1444
1445 Ok(StepOutcome::Continue)
1446 }
1447
1448 fn resolve_jump_destination(
1449 &mut self,
1450 state: &PathState,
1451 jumpdests: &JumpTable,
1452 dest: SymExpr,
1453 unsupported: &'static str,
1454 ) -> Result<Option<usize>, SymbolicError> {
1455 let dest = state.expect_constrained_word(&mut self.cx, dest, unsupported)?;
1456 let Ok(dest) = usize::try_from(dest) else { return Ok(None) };
1457 Ok(jumpdests.is_valid(dest).then_some(dest))
1458 }
1459
1460 fn branch_invalid_jumpi(
1461 &mut self,
1462 state: &mut PathState,
1463 worklist: &mut VecDeque<PathState>,
1464 taken: SymBoolExpr,
1465 ) -> Result<StepOutcome, SymbolicError> {
1466 let (taken_constraints, taken_sat) =
1467 self.constraints_with_condition(state, taken.clone())?;
1468 let not_taken = taken.clone().not(&mut self.cx);
1469 let (taken_seed_models, not_taken_seed_models) = state.split_corpus_seed_models(&taken);
1470 if !taken_sat {
1471 state.constraints.push(not_taken);
1472 state.set_corpus_seed_models(not_taken_seed_models);
1473 return Ok(StepOutcome::Continue);
1474 }
1475
1476 let (not_taken_constraints, not_taken_sat) =
1477 self.constraints_with_condition(state, not_taken)?;
1478 if not_taken_sat {
1479 let mut fallthrough = state.clone();
1480 fallthrough.constraints = not_taken_constraints;
1481 fallthrough.set_corpus_seed_models(not_taken_seed_models);
1482 worklist.push_back(fallthrough);
1483 }
1484 state.constraints = taken_constraints;
1485 state.set_corpus_seed_models(taken_seed_models);
1486 state.return_data = SymReturnData::empty(&mut self.cx);
1487 Ok(StepOutcome::Revert)
1488 }
1489
1490 fn guard_returndata_copy_range(
1491 &mut self,
1492 state: &mut PathState,
1493 worklist: &mut VecDeque<PathState>,
1494 offset: &SymExpr,
1495 size: &SymExpr,
1496 ) -> Result<Option<StepOutcome>, SymbolicError> {
1497 if offset.contains_gasleft() || size.contains_gasleft() {
1498 return Err(SymbolicError::Unsupported("GAS/gasleft() not modeled"));
1499 }
1500 let return_data_len = state.return_data.len_expr();
1501 let offset_in_bounds =
1502 SymBoolExpr::cmp(&mut self.cx, SymCmpOp::Ule, offset.clone(), return_data_len.clone());
1503 let remaining =
1504 SymExpr::binop(&mut self.cx, SymBinOp::Sub, return_data_len, offset.clone());
1505 let size_in_bounds = SymBoolExpr::cmp(&mut self.cx, SymCmpOp::Ule, size.clone(), remaining);
1506 let valid_access = SymBoolExpr::and(&mut self.cx, vec![offset_in_bounds, size_in_bounds]);
1507 self.apply_memory_access_guard(state, worklist, valid_access)
1508 }
1509
1510 pub(super) fn return_or_revert(
1511 &mut self,
1512 state: &mut PathState,
1513 is_revert: bool,
1514 ) -> Result<StepOutcome, SymbolicError> {
1515 let offset = state.stack.pop()?;
1516 let size = state.stack.pop()?;
1517 match state.constrained_usize_checked(&mut self.cx, &size) {
1518 Some(Ok(size)) => {
1519 state.return_data = state.memory.return_data(&mut self.cx, offset.clone(), size)?;
1520 if is_revert {
1521 Ok(self.classify_revert(state, offset, size))
1522 } else {
1523 Ok(StepOutcome::Halt)
1524 }
1525 }
1526 Some(Err(_)) => Ok(StepOutcome::Revert),
1527 None => {
1528 let max_limit = self.config.max_calldata_bytes as usize;
1529 let max_size = state
1530 .upper_bound_usize(&mut self.cx, &size)
1531 .filter(|size| *size <= max_limit)
1532 .map(Ok)
1533 .unwrap_or_else(|| {
1534 self.solver_upper_bound_usize(
1535 state,
1536 &size,
1537 max_limit,
1538 if is_revert { "symbolic REVERT size" } else { "symbolic RETURN size" },
1539 )
1540 })?;
1541 state.return_data =
1542 state.memory.return_data_symbolic_size(&mut self.cx, offset, size, max_size)?;
1543 Ok(if is_revert { StepOutcome::Revert } else { StepOutcome::Halt })
1544 }
1545 }
1546 }
1547
1548 pub(super) fn classify_revert(
1549 &mut self,
1550 state: &PathState,
1551 offset: SymExpr,
1552 size: usize,
1553 ) -> StepOutcome {
1554 if state.call_depth == 0
1555 && let Some(offset) = offset.as_const()
1556 && let Ok(offset) = usize::try_from(offset)
1557 && let Ok(data) = state.memory.read_concrete(&mut self.cx, offset, size)
1558 && is_assertion_revert(&data)
1559 {
1560 StepOutcome::Failure
1561 } else {
1562 StepOutcome::Revert
1563 }
1564 }
1565}
1566
1567#[cfg(test)]
1568mod tests {
1569 use super::*;
1570 use foundry_evm::{
1571 core::{backend::Backend, evm::EthEvmNetwork},
1572 executors::ExecutorBuilder,
1573 };
1574
1575 fn empty_state(executor: &mut SymbolicExecutor) -> PathState {
1576 let calldata =
1577 SymbolicCalldata::selector_only(&mut executor.cx, &Function::parse("empty()").unwrap())
1578 .unwrap();
1579 PathState::new(&mut executor.cx, Address::ZERO, Address::ZERO, U256::ZERO, calldata, false)
1580 }
1581
1582 #[test]
1583 fn branch_target_constraint_is_one_shot_after_target_reached() {
1584 let mut executor = SymbolicExecutor::new(SymbolicConfig::default());
1585 let mut state = empty_state(&mut executor);
1586 state.set_branch_target(Some(SymbolicBranchTarget::new(
1587 Address::ZERO,
1588 0,
1589 opcode::EQ,
1590 false,
1591 )));
1592 state.mark_branch_target_reached();
1593
1594 let condition = SymBoolExpr::constant(&mut executor.cx, false);
1595 let accepted =
1596 executor.apply_branch_target_constraint(&mut state, 0, opcode::EQ, &condition).unwrap();
1597
1598 assert!(accepted);
1599 assert!(state.constraints.is_empty());
1600 assert!(state.satisfies_branch_target());
1601 }
1602
1603 #[test]
1604 fn sstore_mapping_fork_refunds_retry_depth() {
1605 let mut executor = SymbolicExecutor::new(SymbolicConfig::default());
1606 if let Err(err) = executor.solver.check_available() {
1607 let _ = foundry_common::sh_eprintln!(
1608 "skipping sstore_mapping_fork_refunds_retry_depth: {err}"
1609 );
1610 return;
1611 }
1612 let backend = Backend::<EthEvmNetwork>::spawn(None).unwrap();
1613 let concrete = ExecutorBuilder::default().build(
1614 Default::default(),
1615 Default::default(),
1616 backend,
1617 Default::default(),
1618 );
1619 let mut state = empty_state(&mut executor);
1620 let original_depth = 7;
1621 state.depth = original_depth;
1622 state.mapping_storage_store_hooks.insert(
1623 (state.storage_address, U256::ZERO),
1624 SymbolicStorageHook {
1625 callback_target: Address::repeat_byte(0x22),
1626 callback_selector: [0x12, 0x34, 0x56, 0x78],
1627 },
1628 );
1629 let preimage = vec![SymExpr::zero(&mut executor.cx); 64];
1630 let hash = keccak_word(&mut executor.cx, preimage.clone());
1631 state.mapping_hook_keccak_preimages.insert((state.storage_address, hash), preimage.into());
1632 let key = state.fresh_word(&mut executor.cx, "storage_key");
1633 state.stack.push(SymExpr::one(&mut executor.cx)).unwrap();
1634 state.stack.push(key).unwrap();
1635 let code = SymCode::concrete(&mut executor.cx, vec![opcode::SSTORE]);
1636 let mut worklist = VecDeque::new();
1637 let mut completed_paths = 0;
1638
1639 let outcome = executor
1640 .step(
1641 &concrete,
1642 &code,
1643 code.jump_table(),
1644 &mut state,
1645 &mut worklist,
1646 &mut completed_paths,
1647 opcode::SSTORE,
1648 )
1649 .unwrap();
1650
1651 assert!(matches!(outcome, StepOutcome::Forked));
1652 assert_eq!(worklist.len(), 2);
1653 for retry in worklist {
1654 assert_eq!(retry.pc, 0);
1655 assert_eq!(retry.depth, original_depth - 1);
1656 }
1657 }
1658
1659 #[test]
1660 fn returndata_copy_range_preserves_valid_and_invalid_paths() {
1661 let mut executor = SymbolicExecutor::new(SymbolicConfig::default());
1662 if let Err(err) = executor.solver.check_available() {
1663 let _ = foundry_common::sh_eprintln!(
1664 "skipping returndata_copy_range_preserves_valid_and_invalid_paths: {err}"
1665 );
1666 return;
1667 }
1668 let mut state = empty_state(&mut executor);
1669 state.return_data = SymReturnData::from_concrete_bytes(&mut executor.cx, vec![0; 64]);
1670 let offset = state.fresh_word(&mut executor.cx, "offset");
1671 state.constraints.push(SymBoolExpr::cmp_word_const(
1672 &mut executor.cx,
1673 SymCmpOp::Uge,
1674 &offset,
1675 U256::from(64),
1676 ));
1677 state.constraints.push(SymBoolExpr::cmp_word_const(
1678 &mut executor.cx,
1679 SymCmpOp::Ule,
1680 &offset,
1681 U256::from(65),
1682 ));
1683 let size = SymExpr::zero(&mut executor.cx);
1684 let mut worklist = VecDeque::new();
1685
1686 let outcome = executor
1687 .guard_returndata_copy_range(&mut state, &mut worklist, &offset, &size)
1688 .unwrap();
1689
1690 assert!(matches!(outcome, Some(StepOutcome::Revert)));
1691 assert_eq!(state.return_data.len(), 0);
1692 let valid = worklist.pop_back().unwrap();
1693 assert_eq!(valid.return_data.len(), 64);
1694 assert!(worklist.is_empty());
1695
1696 let offset_is_64 = SymBoolExpr::eq_word_const(&mut executor.cx, &offset, U256::from(64));
1697 let offset_is_65 = SymBoolExpr::eq_word_const(&mut executor.cx, &offset, U256::from(65));
1698 assert!(!executor.constraints_with_condition(&state, offset_is_64.clone()).unwrap().1);
1699 assert!(executor.constraints_with_condition(&state, offset_is_65.clone()).unwrap().1);
1700 assert!(executor.constraints_with_condition(&valid, offset_is_64).unwrap().1);
1701 assert!(!executor.constraints_with_condition(&valid, offset_is_65).unwrap().1);
1702 }
1703}