Skip to main content

foundry_evm_symbolic/executor/
opcodes.rs

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(&current) {
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}