Skip to main content

ethrex_levm/opcode_handlers/
system.rs

1//! # System operations
2//!
3//! Includes the following opcodes:
4//!   - `CALL`
5//!   - `CALLCODE`
6//!   - `DELEGATECALL`
7//!   - `STATICCALL`
8//!   - `RETURN`
9//!   - `CREATE`
10//!   - `CREATE2`
11//!   - `SELFDESTRUCT`
12//!   - `REVERT`
13
14use crate::{
15    call_frame::CallFrame,
16    constants::{AMSTERDAM_INIT_CODE_MAX_SIZE, FAIL, INIT_CODE_MAX_SIZE, SUCCESS},
17    errors::{ContextResult, ExceptionalHalt, InternalError, OpcodeResult, TxResult, VMError},
18    gas_cost,
19    memory::{self, calculate_memory_size},
20    opcode_handlers::OpcodeHandler,
21    precompiles,
22    utils::{address_to_word, create_eth_transfer_log, word_to_address, *},
23    vm::VM,
24};
25use bytes::Bytes;
26use ethrex_common::{Address, H256, U256, evm::calculate_create_address, types::Fork};
27use ethrex_common::{tracing::CallType, types::Code};
28
29pub struct OpCallHandler;
30impl OpcodeHandler for OpCallHandler {
31    #[inline(always)]
32    fn eval(vm: &mut VM<'_>) -> Result<OpcodeResult, VMError> {
33        let [
34            gas,
35            callee,
36            value,
37            args_offset,
38            args_len,
39            return_offset,
40            return_len,
41        ] = *vm.current_call_frame.stack.pop()?;
42        let callee = word_to_address(callee);
43        let (args_len, args_offset) = size_offset_to_usize(args_len, args_offset)?;
44        let (return_len, return_offset) = size_offset_to_usize(return_len, return_offset)?;
45
46        // Validations.
47        if vm.current_call_frame.is_static && !value.is_zero() {
48            return Err(ExceptionalHalt::OpcodeNotAllowedInStaticContext.into());
49        }
50
51        let value_cost = if !value.is_zero() {
52            gas_cost::call_positive_value_cost(vm.env.config.fork)
53        } else {
54            0
55        };
56        let (new_memory_size, address_was_cold, static_cost) = vm.check_call_static_gas(
57            args_offset,
58            args_len,
59            return_offset,
60            return_len,
61            callee,
62            value_cost,
63        )?;
64
65        vm.substate.add_accessed_address(callee);
66        // `address_is_empty` only feeds gates that also require `value != 0`,
67        // so skip the read entirely when value is zero (matches EELS' gating
68        // of `is_account_alive` on `value != 0`).
69        let address_is_empty = if value.is_zero() {
70            false
71        } else {
72            vm.db.get_account(callee)?.is_empty()
73        };
74        // Detect a 7702 delegation without reading the delegate account: per
75        // EELS the delegate access cost is gas-checked first, so an OOG must
76        // not leak the delegate read into execution witnesses (EIP-8025).
77        let (callee_code, delegation) =
78            eip7702_peek_delegation(vm.db, &vm.substate, callee, vm.env.config.fork)?;
79        let is_delegation_7702 = delegation.is_some();
80        let (eip7702_gas_consumed, code_address) = match delegation {
81            Some((auth_address, access_cost)) => (access_cost, auth_address),
82            None => (0, callee),
83        };
84        let create_cost = if address_is_empty {
85            gas_cost::CALL_TO_EMPTY_ACCOUNT
86        } else {
87            0
88        };
89
90        // BAL touches the target before the delegation gas check, so a failed
91        // delegate-access check still leaves the target recorded.
92        vm.record_bal_call_touch(
93            callee,
94            code_address,
95            is_delegation_7702,
96            eip7702_gas_consumed,
97            new_memory_size,
98            vm.current_call_frame.memory.len(),
99            address_was_cold,
100            value_cost,
101            create_cost,
102        );
103
104        // `create_cost` is EIP-8037 state gas (charged via `increase_state_gas`
105        // below) and must not appear in the regular-gas check.
106        let bytecode = if let Some((auth_address, access_cost)) = delegation {
107            vm.current_call_frame.check_gas(
108                static_cost
109                    .checked_add(access_cost)
110                    .ok_or(ExceptionalHalt::OutOfGas)?,
111            )?;
112            vm.substate.add_accessed_address(auth_address);
113            vm.db.get_account_code(auth_address)?.clone()
114        } else {
115            callee_code
116        };
117
118        let fork = vm.env.config.fork;
119
120        // Compute gas_left after eip7702 consumption (without modifying gas_remaining yet).
121        #[expect(clippy::as_conversions, reason = "safe")]
122        let gas_left = (vm.current_call_frame.gas_remaining as u64)
123            .checked_sub(eip7702_gas_consumed)
124            .ok_or(ExceptionalHalt::OutOfGas)?;
125
126        // EIP-8037 (Amsterdam+): account for state gas spill in child gas computation,
127        // but charge state gas AFTER regular gas per EIPs#11421.
128        // Regular gas OOG must not consume state gas that would inflate the parent's
129        // reservoir on frame failure.
130        let needs_state_gas = fork >= Fork::Amsterdam && address_is_empty;
131        let gas_left = if needs_state_gas {
132            let state_gas_new_account = vm.state_gas_new_account;
133            let from_reservoir = vm.state_gas_reservoir.min(state_gas_new_account);
134            // Safe: from_reservoir = min(reservoir, state_gas_new_account) <= state_gas_new_account
135            #[expect(
136                clippy::arithmetic_side_effects,
137                reason = "from_reservoir <= state_gas_new_account"
138            )]
139            let spill = state_gas_new_account - from_reservoir;
140            gas_left
141                .checked_sub(spill)
142                .ok_or(ExceptionalHalt::OutOfGas)?
143        } else {
144            gas_left
145        };
146
147        let (gas_cost, gas_limit) = gas_cost::call(
148            new_memory_size,
149            vm.current_call_frame.memory.len(),
150            address_was_cold,
151            address_is_empty,
152            value,
153            gas,
154            gas_left,
155            fork,
156        )?;
157
158        // Charge regular gas first (before state gas, per EIPs#11421).
159        vm.current_call_frame.increase_consumed_gas(
160            gas_cost
161                .checked_add(eip7702_gas_consumed)
162                .ok_or(ExceptionalHalt::OutOfGas)?,
163        )?;
164
165        // Then charge state gas for new account creation.
166        if needs_state_gas {
167            vm.increase_state_gas(vm.state_gas_new_account)?;
168        }
169
170        // Struct-log: record the geth-compatible CALL gasCost.
171        // Geth's gasCost for CALL family = intrinsic_overhead + callGasTemp (forwarded gas
172        // WITHOUT stipend). LEVM's `gas_cost` already equals `call_gas_costs + gas_forwarded`,
173        // i.e. `intrinsic + callGasTemp`. Stipend is added later inside the child frame, after
174        // the tracer fires, so it is NOT part of the reported gasCost.
175        if vm.opcode_tracer.active {
176            let geth_cost = gas_cost.saturating_add(eip7702_gas_consumed);
177            vm.opcode_tracer.last_opcode_gas_cost = Some(geth_cost);
178        }
179
180        // Resize memory: this is necessary for multiple reasons:
181        //   - Make sure the memory is expanded.
182        //   - When there is return data, preallocate it because it won't be possible while the next
183        //     call frame is active.
184        vm.current_call_frame.memory.resize(new_memory_size)?;
185
186        // Trace CALL operation.
187        let data = vm.get_calldata(args_offset, args_len)?;
188        vm.tracer.enter(
189            CallType::CALL,
190            vm.current_call_frame.to,
191            callee,
192            value,
193            gas_limit,
194            &data,
195        );
196
197        // Generic call.
198        vm.generic_call(
199            gas_limit,
200            value,
201            vm.current_call_frame.to,
202            callee,
203            code_address,
204            true,
205            vm.current_call_frame.is_static,
206            data,
207            return_offset,
208            return_len,
209            bytecode,
210            is_delegation_7702,
211            needs_state_gas,
212        )
213    }
214}
215
216pub struct OpCallCodeHandler;
217impl OpcodeHandler for OpCallCodeHandler {
218    #[inline(always)]
219    fn eval(vm: &mut VM<'_>) -> Result<OpcodeResult, VMError> {
220        let [
221            gas,
222            address,
223            value,
224            args_offset,
225            args_len,
226            return_offset,
227            return_len,
228        ] = *vm.current_call_frame.stack.pop()?;
229        let address = word_to_address(address);
230        let (args_len, args_offset) = size_offset_to_usize(args_len, args_offset)?;
231        let (return_len, return_offset) = size_offset_to_usize(return_len, return_offset)?;
232
233        let value_cost = if !value.is_zero() {
234            gas_cost::call_positive_value_cost(vm.env.config.fork)
235        } else {
236            0
237        };
238        let (new_memory_size, address_was_cold, static_cost) = vm.check_call_static_gas(
239            args_offset,
240            args_len,
241            return_offset,
242            return_len,
243            address,
244            value_cost,
245        )?;
246
247        vm.substate.add_accessed_address(address);
248        // Detect a 7702 delegation without reading the delegate account: per
249        // EELS the delegate access cost is gas-checked first, so an OOG must
250        // not leak the delegate read into execution witnesses (EIP-8025).
251        let (target_code, delegation) =
252            eip7702_peek_delegation(vm.db, &vm.substate, address, vm.env.config.fork)?;
253        let is_delegation_7702 = delegation.is_some();
254        let (eip7702_gas_consumed, code_address) = match delegation {
255            Some((auth_address, access_cost)) => (access_cost, auth_address),
256            None => (0, address),
257        };
258
259        // BAL touches the target before the delegation gas check.
260        vm.record_bal_call_touch(
261            address,
262            code_address,
263            is_delegation_7702,
264            eip7702_gas_consumed,
265            new_memory_size,
266            vm.current_call_frame.memory.len(),
267            address_was_cold,
268            value_cost,
269            0,
270        );
271
272        let bytecode = if let Some((auth_address, access_cost)) = delegation {
273            vm.current_call_frame.check_gas(
274                static_cost
275                    .checked_add(access_cost)
276                    .ok_or(ExceptionalHalt::OutOfGas)?,
277            )?;
278            vm.substate.add_accessed_address(auth_address);
279            vm.db.get_account_code(auth_address)?.clone()
280        } else {
281            target_code
282        };
283
284        #[expect(clippy::as_conversions, reason = "safe")]
285        let gas_left = (vm.current_call_frame.gas_remaining as u64)
286            .checked_sub(eip7702_gas_consumed)
287            .ok_or(ExceptionalHalt::OutOfGas)?;
288        let (gas_cost, gas_limit) = gas_cost::callcode(
289            new_memory_size,
290            vm.current_call_frame.memory.len(),
291            address_was_cold,
292            value,
293            gas,
294            gas_left,
295            vm.env.config.fork,
296        )?;
297        vm.current_call_frame.increase_consumed_gas(
298            gas_cost
299                .checked_add(eip7702_gas_consumed)
300                .ok_or(ExceptionalHalt::OutOfGas)?,
301        )?;
302
303        // Struct-log: geth-compatible CALLCODE gasCost (intrinsic + forwarded, no stipend).
304        if vm.opcode_tracer.active {
305            let geth_cost = gas_cost.saturating_add(eip7702_gas_consumed);
306            vm.opcode_tracer.last_opcode_gas_cost = Some(geth_cost);
307        }
308
309        // Resize memory: this is necessary for multiple reasons:
310        //   - Make sure the memory is expanded.
311        //   - When there is return data, preallocate it because it won't be possible while the next
312        //     call frame is active.
313        vm.current_call_frame.memory.resize(new_memory_size)?;
314
315        // Trace CALL operation.
316        let data = vm.get_calldata(args_offset, args_len)?;
317        vm.tracer.enter(
318            CallType::CALLCODE,
319            vm.current_call_frame.to,
320            code_address,
321            value,
322            gas_limit,
323            &data,
324        );
325
326        // Generic call.
327        vm.generic_call(
328            gas_limit,
329            value,
330            vm.current_call_frame.to,
331            vm.current_call_frame.to,
332            code_address,
333            true,
334            vm.current_call_frame.is_static,
335            data,
336            return_offset,
337            return_len,
338            bytecode,
339            is_delegation_7702,
340            false,
341        )
342    }
343}
344
345pub struct OpDelegateCallHandler;
346impl OpcodeHandler for OpDelegateCallHandler {
347    #[inline(always)]
348    fn eval(vm: &mut VM<'_>) -> Result<OpcodeResult, VMError> {
349        let [
350            gas,
351            address,
352            args_offset,
353            args_len,
354            return_offset,
355            return_len,
356        ] = *vm.current_call_frame.stack.pop()?;
357        let address = word_to_address(address);
358        let (args_len, args_offset) = size_offset_to_usize(args_len, args_offset)?;
359        let (return_len, return_offset) = size_offset_to_usize(return_len, return_offset)?;
360
361        let (new_memory_size, address_was_cold, static_cost) =
362            vm.check_call_static_gas(args_offset, args_len, return_offset, return_len, address, 0)?;
363
364        vm.substate.add_accessed_address(address);
365        // Detect a 7702 delegation without reading the delegate account: per
366        // EELS the delegate access cost is gas-checked first, so an OOG must
367        // not leak the delegate read into execution witnesses (EIP-8025).
368        let (target_code, delegation) =
369            eip7702_peek_delegation(vm.db, &vm.substate, address, vm.env.config.fork)?;
370        let is_delegation_7702 = delegation.is_some();
371        let (eip7702_gas_consumed, code_address) = match delegation {
372            Some((auth_address, access_cost)) => (access_cost, auth_address),
373            None => (0, address),
374        };
375
376        // BAL touches the target before the delegation gas check.
377        vm.record_bal_call_touch(
378            address,
379            code_address,
380            is_delegation_7702,
381            eip7702_gas_consumed,
382            new_memory_size,
383            vm.current_call_frame.memory.len(),
384            address_was_cold,
385            0,
386            0,
387        );
388
389        let bytecode = if let Some((auth_address, access_cost)) = delegation {
390            vm.current_call_frame.check_gas(
391                static_cost
392                    .checked_add(access_cost)
393                    .ok_or(ExceptionalHalt::OutOfGas)?,
394            )?;
395            vm.substate.add_accessed_address(auth_address);
396            vm.db.get_account_code(auth_address)?.clone()
397        } else {
398            target_code
399        };
400
401        #[expect(clippy::as_conversions, reason = "safe")]
402        let gas_left = (vm.current_call_frame.gas_remaining as u64)
403            .checked_sub(eip7702_gas_consumed)
404            .ok_or(ExceptionalHalt::OutOfGas)?;
405        let (gas_cost, gas_limit) = gas_cost::delegatecall(
406            new_memory_size,
407            vm.current_call_frame.memory.len(),
408            address_was_cold,
409            gas,
410            gas_left,
411            vm.env.config.fork,
412        )?;
413        vm.current_call_frame.increase_consumed_gas(
414            gas_cost
415                .checked_add(eip7702_gas_consumed)
416                .ok_or(ExceptionalHalt::OutOfGas)?,
417        )?;
418
419        // Struct-log: geth-compatible DELEGATECALL gasCost (intrinsic + forwarded).
420        if vm.opcode_tracer.active {
421            let geth_cost = gas_cost.saturating_add(eip7702_gas_consumed);
422            vm.opcode_tracer.last_opcode_gas_cost = Some(geth_cost);
423        }
424
425        // Resize memory: this is necessary for multiple reasons:
426        //   - Make sure the memory is expanded.
427        //   - When there is return data, preallocate it because it won't be possible while the next
428        //     call frame is available.
429        vm.current_call_frame.memory.resize(new_memory_size)?;
430
431        // Trace CALL operation.
432        let data = vm.get_calldata(args_offset, args_len)?;
433        // In this trace the `from` is the current contract, we don't want the `from` to be,
434        // for example, the EOA that sent the transaction.
435        vm.tracer.enter(
436            CallType::DELEGATECALL,
437            vm.current_call_frame.to,
438            code_address,
439            vm.current_call_frame.msg_value,
440            gas_limit,
441            &data,
442        );
443
444        // Generic call.
445        vm.generic_call(
446            gas_limit,
447            vm.current_call_frame.msg_value,
448            vm.current_call_frame.msg_sender,
449            vm.current_call_frame.to,
450            code_address,
451            false,
452            vm.current_call_frame.is_static,
453            data,
454            return_offset,
455            return_len,
456            bytecode,
457            is_delegation_7702,
458            false,
459        )
460    }
461}
462
463pub struct OpStaticCallHandler;
464impl OpcodeHandler for OpStaticCallHandler {
465    #[inline(always)]
466    fn eval(vm: &mut VM<'_>) -> Result<OpcodeResult, VMError> {
467        let [
468            gas,
469            address,
470            args_offset,
471            args_len,
472            return_offset,
473            return_len,
474        ] = *vm.current_call_frame.stack.pop()?;
475        let address = word_to_address(address);
476        let (args_len, args_offset) = size_offset_to_usize(args_len, args_offset)?;
477        let (return_len, return_offset) = size_offset_to_usize(return_len, return_offset)?;
478
479        let (new_memory_size, address_was_cold, static_cost) =
480            vm.check_call_static_gas(args_offset, args_len, return_offset, return_len, address, 0)?;
481
482        vm.substate.add_accessed_address(address);
483        // Detect a 7702 delegation without reading the delegate account: per
484        // EELS the delegate access cost is gas-checked first, so an OOG must
485        // not leak the delegate read into execution witnesses (EIP-8025).
486        let (target_code, delegation) =
487            eip7702_peek_delegation(vm.db, &vm.substate, address, vm.env.config.fork)?;
488        let is_delegation_7702 = delegation.is_some();
489        let (eip7702_gas_consumed, code_address) = match delegation {
490            Some((auth_address, access_cost)) => (access_cost, auth_address),
491            None => (0, address),
492        };
493
494        // BAL touches the target before the delegation gas check.
495        vm.record_bal_call_touch(
496            address,
497            code_address,
498            is_delegation_7702,
499            eip7702_gas_consumed,
500            new_memory_size,
501            vm.current_call_frame.memory.len(),
502            address_was_cold,
503            0,
504            0,
505        );
506
507        let bytecode = if let Some((auth_address, access_cost)) = delegation {
508            vm.current_call_frame.check_gas(
509                static_cost
510                    .checked_add(access_cost)
511                    .ok_or(ExceptionalHalt::OutOfGas)?,
512            )?;
513            vm.substate.add_accessed_address(auth_address);
514            vm.db.get_account_code(auth_address)?.clone()
515        } else {
516            target_code
517        };
518
519        #[expect(clippy::as_conversions, reason = "safe")]
520        let gas_left = (vm.current_call_frame.gas_remaining as u64)
521            .checked_sub(eip7702_gas_consumed)
522            .ok_or(ExceptionalHalt::OutOfGas)?;
523        let (gas_cost, gas_limit) = gas_cost::staticcall(
524            new_memory_size,
525            vm.current_call_frame.memory.len(),
526            address_was_cold,
527            gas,
528            gas_left,
529            vm.env.config.fork,
530        )?;
531        vm.current_call_frame.increase_consumed_gas(
532            gas_cost
533                .checked_add(eip7702_gas_consumed)
534                .ok_or(ExceptionalHalt::OutOfGas)?,
535        )?;
536
537        // Struct-log: geth-compatible STATICCALL gasCost (intrinsic + forwarded).
538        if vm.opcode_tracer.active {
539            let geth_cost = gas_cost.saturating_add(eip7702_gas_consumed);
540            vm.opcode_tracer.last_opcode_gas_cost = Some(geth_cost);
541        }
542
543        // Resize memory: this is necessary for multiple reasons:
544        //   - Make sure the memory is expanded.
545        //   - When there is return data, preallocate it because it won't be possible while the next
546        //     call frame is active.
547        vm.current_call_frame.memory.resize(new_memory_size)?;
548
549        // Trace CALL operation.
550        let data = vm.get_calldata(args_offset, args_len)?;
551        vm.tracer.enter(
552            CallType::STATICCALL,
553            vm.current_call_frame.to,
554            address,
555            U256::zero(),
556            gas_limit,
557            &data,
558        );
559
560        // Generic call.
561        vm.generic_call(
562            gas_limit,
563            U256::zero(),
564            vm.current_call_frame.to,
565            address,
566            address,
567            true,
568            true,
569            data,
570            return_offset,
571            return_len,
572            bytecode,
573            is_delegation_7702,
574            false,
575        )
576    }
577}
578
579pub struct OpReturnHandler;
580impl OpcodeHandler for OpReturnHandler {
581    #[inline(always)]
582    fn eval(vm: &mut VM<'_>) -> Result<OpcodeResult, VMError> {
583        let [offset, len] = *vm.current_call_frame.stack.pop()?;
584        let (len, offset) = size_offset_to_usize(len, offset)?;
585
586        vm.current_call_frame
587            .increase_consumed_gas(gas_cost::exit_opcode(
588                calculate_memory_size(offset, len)?,
589                vm.current_call_frame.memory.len(),
590            )?)?;
591
592        if len != 0 {
593            vm.current_call_frame.output = vm.current_call_frame.memory.load_range(offset, len)?;
594        }
595
596        Ok(OpcodeResult::Halt)
597    }
598}
599
600pub struct OpCreateHandler;
601impl OpcodeHandler for OpCreateHandler {
602    #[inline(always)]
603    fn eval(vm: &mut VM<'_>) -> Result<OpcodeResult, VMError> {
604        // EIP-8037 (Amsterdam+): is_static check before stack pops and gas charging,
605        // consistent with SSTORE, CALL, and SELFDESTRUCT.
606        if vm.env.config.fork >= Fork::Amsterdam && vm.current_call_frame.is_static {
607            return Err(ExceptionalHalt::OpcodeNotAllowedInStaticContext.into());
608        }
609
610        let [value_in_wei, code_offset, code_len] = *vm.current_call_frame.stack.pop()?;
611        let (code_len, code_offset) = size_offset_to_usize(code_len, code_offset)?;
612
613        let create_gas = gas_cost::create(
614            calculate_memory_size(code_offset, code_len)?,
615            vm.current_call_frame.memory.len(),
616            code_len,
617            vm.env.config.fork,
618        )?;
619        vm.current_call_frame.increase_consumed_gas(create_gas)?;
620
621        // Struct-log: record the opcode-level gas before generic_create charges forwarded gas.
622        if vm.opcode_tracer.active {
623            vm.opcode_tracer.last_opcode_gas_cost = Some(create_gas);
624        }
625
626        vm.generic_create(value_in_wei, code_offset, code_len, None)
627    }
628}
629
630pub struct OpCreate2Handler;
631impl OpcodeHandler for OpCreate2Handler {
632    #[inline(always)]
633    fn eval(vm: &mut VM<'_>) -> Result<OpcodeResult, VMError> {
634        // EIP-8037 (Amsterdam+): is_static check before stack pops and gas charging,
635        // consistent with SSTORE, CALL, and SELFDESTRUCT.
636        if vm.env.config.fork >= Fork::Amsterdam && vm.current_call_frame.is_static {
637            return Err(ExceptionalHalt::OpcodeNotAllowedInStaticContext.into());
638        }
639
640        let [value_in_wei, code_offset, code_len, salt] = *vm.current_call_frame.stack.pop()?;
641        let (code_len, code_offset) = size_offset_to_usize(code_len, code_offset)?;
642
643        let create2_gas = gas_cost::create_2(
644            calculate_memory_size(code_offset, code_len)?,
645            vm.current_call_frame.memory.len(),
646            code_len,
647            vm.env.config.fork,
648        )?;
649        vm.current_call_frame.increase_consumed_gas(create2_gas)?;
650
651        // Struct-log: record the opcode-level gas before generic_create charges forwarded gas.
652        if vm.opcode_tracer.active {
653            vm.opcode_tracer.last_opcode_gas_cost = Some(create2_gas);
654        }
655
656        vm.generic_create(value_in_wei, code_offset, code_len, Some(salt))
657    }
658}
659
660pub struct OpSelfDestructHandler;
661impl OpcodeHandler for OpSelfDestructHandler {
662    #[inline(always)]
663    fn eval(vm: &mut VM<'_>) -> Result<OpcodeResult, VMError> {
664        if vm.current_call_frame.is_static {
665            return Err(ExceptionalHalt::OpcodeNotAllowedInStaticContext.into());
666        }
667
668        let beneficiary = word_to_address(vm.current_call_frame.stack.pop1()?);
669        let to = vm.current_call_frame.to;
670
671        let target_account_is_cold = vm.substate.add_accessed_address(beneficiary);
672
673        // EELS (Amsterdam) checks the base cost (SELFDESTRUCT + cold access)
674        // BEFORE the beneficiary/self state reads: an OOG here must not leak
675        // those reads into execution witnesses (EIP-8025).
676        if vm.env.config.fork >= Fork::Amsterdam {
677            let base_cost =
678                gas_cost::selfdestruct_base(target_account_is_cold, vm.env.config.fork)?;
679            // Phase 1: Check base cost is available (without charging)
680            #[expect(clippy::as_conversions, reason = "base_cost fits in i64")]
681            if vm.current_call_frame.gas_remaining < (base_cost as i64) {
682                return Err(ExceptionalHalt::OutOfGas.into());
683            }
684        }
685
686        let target_account_is_empty = vm.db.get_account(beneficiary)?.is_empty();
687        let balance = vm.db.get_account(to)?.info.balance;
688
689        // EIP-7928 (Amsterdam): Two-phase gas check for SELFDESTRUCT.
690        // Base cost was checked above before state access; now record BAL
691        // tracking, then charge the full cost including NEW_ACCOUNT. This
692        // ensures the beneficiary is recorded in BAL even when the full
693        // selfdestruct cost (with NEW_ACCOUNT) would cause OOG.
694        if vm.env.config.fork >= Fork::Amsterdam {
695            // State access: record BAL tracking between the two gas phases
696            let accessed_slots = vm.substate.get_accessed_storage_slots(&to);
697            if let Some(recorder) = vm.db.bal_recorder.as_mut() {
698                recorder.record_touched_address(beneficiary);
699                recorder.record_touched_address(to);
700                if balance > U256::zero() {
701                    recorder.set_initial_balance(to, balance);
702                }
703                for key in &accessed_slots {
704                    let slot = U256::from_big_endian(key.as_bytes());
705                    recorder.record_storage_read(to, slot);
706                }
707            }
708
709            // Phase 2: Charge the full cost (base only for Amsterdam+; NEW_ACCOUNT moved to state gas)
710            vm.current_call_frame
711                .increase_consumed_gas(gas_cost::selfdestruct(
712                    target_account_is_cold,
713                    target_account_is_empty,
714                    balance,
715                    vm.env.config.fork,
716                )?)?;
717
718            // EIP-8037 (Amsterdam+): charge state gas for new account creation via SELFDESTRUCT
719            if target_account_is_empty && balance > U256::zero() {
720                vm.increase_state_gas(vm.state_gas_new_account)?;
721            }
722        } else {
723            vm.current_call_frame
724                .increase_consumed_gas(gas_cost::selfdestruct(
725                    target_account_is_cold,
726                    target_account_is_empty,
727                    balance,
728                    vm.env.config.fork,
729                )?)?;
730
731            // Record beneficiary and destroyed account for BAL per EIP-7928
732            let accessed_slots = vm.substate.get_accessed_storage_slots(&to);
733            if let Some(recorder) = vm.db.bal_recorder.as_mut() {
734                recorder.record_touched_address(beneficiary);
735                recorder.record_touched_address(to);
736                if balance > U256::zero() {
737                    recorder.set_initial_balance(to, balance);
738                }
739                for key in &accessed_slots {
740                    let slot = U256::from_big_endian(key.as_bytes());
741                    recorder.record_storage_read(to, slot);
742                }
743            }
744        }
745
746        // [EIP-6780] - SELFDESTRUCT only in same transaction from CANCUN
747        if vm.env.config.fork >= Fork::Cancun {
748            // [EIP-8246] (Amsterdam+): a selfdestruct-to-self moves no ETH (balance is
749            // preserved at finalization). Skip the self-transfer so it doesn't fire
750            // spurious BAL balance events that overwrite the recorded initial balance.
751            // For `to != beneficiary` the transfer still runs (balance moves out).
752            if !(vm.env.config.fork >= Fork::Amsterdam && to == beneficiary) {
753                vm.transfer(to, beneficiary, balance)?;
754            }
755
756            // Selfdestruct is executed in the same transaction as the contract was created
757            if vm.substate.is_account_created(&to) {
758                // [EIP-8246] (Amsterdam+): balance is NOT burned; nonce/code/storage are cleared
759                // at finalization while balance is preserved. Pre-Amsterdam (EIP-6780): Ether is
760                // burned when to == beneficiary.
761                if vm.env.config.fork < Fork::Amsterdam {
762                    vm.get_account_mut(to)?.info.balance = U256::zero();
763
764                    // Record balance change to zero for destroyed account in BAL
765                    if let Some(recorder) = vm.db.bal_recorder.as_mut() {
766                        recorder.record_balance_change(to, U256::zero());
767                    }
768                }
769
770                vm.substate.add_selfdestruct(to);
771            }
772
773            // EIP-7708: Emit appropriate log for ETH movement (Amsterdam+ only).
774            // EIP-8246 (Amsterdam+): no burn log for same-tx selfdestruct-to-self; no ETH burned.
775            // Cancun/Prague (pre-Amsterdam): no EIP-7708 logs at all.
776            if vm.env.config.fork >= Fork::Amsterdam && !balance.is_zero() && to != beneficiary {
777                let log = create_eth_transfer_log(to, beneficiary, balance);
778                vm.substate.add_log(log);
779                // No burn log under EIP-8246: selfdestruct-to-self preserves balance.
780            }
781        } else {
782            vm.increase_account_balance(beneficiary, balance)?;
783            vm.get_account_mut(to)?.info.balance = U256::zero();
784
785            // Record balance change to zero for destroyed account in BAL
786            if let Some(recorder) = vm.db.bal_recorder.as_mut() {
787                recorder.record_balance_change(to, U256::zero());
788            }
789
790            vm.substate.add_selfdestruct(to);
791        }
792
793        vm.tracer.enter(
794            CallType::SELFDESTRUCT,
795            vm.current_call_frame.to,
796            beneficiary,
797            balance,
798            0,
799            &Default::default(),
800        );
801        vm.tracer.exit_early(0, None)?;
802
803        Ok(OpcodeResult::Halt)
804    }
805}
806
807pub struct OpRevertHandler;
808impl OpcodeHandler for OpRevertHandler {
809    #[inline(always)]
810    fn eval(vm: &mut VM<'_>) -> Result<OpcodeResult, VMError> {
811        let [offset, len] = *vm.current_call_frame.stack.pop()?;
812        let (len, offset) = size_offset_to_usize(len, offset)?;
813
814        vm.current_call_frame
815            .increase_consumed_gas(gas_cost::exit_opcode(
816                calculate_memory_size(offset, len)?,
817                vm.current_call_frame.memory.len(),
818            )?)?;
819
820        if len != 0 {
821            vm.current_call_frame.output = vm.current_call_frame.memory.load_range(offset, len)?;
822        }
823
824        Err(VMError::RevertOpcode)
825    }
826}
827
828impl<'a> VM<'a> {
829    /// Common behavior for CREATE and CREATE2 opcodes
830    pub fn generic_create(
831        &mut self,
832        value: U256,
833        code_offset_in_memory: usize,
834        code_size_in_memory: usize,
835        salt: Option<U256>,
836    ) -> Result<OpcodeResult, VMError> {
837        // [EIP-3860] / [EIP-7954] - Cant exceed init code max size
838        let init_code_max = if self.env.config.fork >= Fork::Amsterdam {
839            AMSTERDAM_INIT_CODE_MAX_SIZE
840        } else {
841            INIT_CODE_MAX_SIZE
842        };
843        if code_size_in_memory > init_code_max && self.env.config.fork >= Fork::Shanghai {
844            return Err(ExceptionalHalt::OutOfGas.into());
845        }
846
847        // EIP-8037 (Amsterdam+): charge state gas for new account creation AFTER
848        // initcode size validation, so oversized CREATE doesn't burn state gas.
849        if self.env.config.fork >= Fork::Amsterdam {
850            self.increase_state_gas(self.state_gas_new_account)?;
851        }
852
853        let current_call_frame = &mut self.current_call_frame;
854
855        // Pre-Amsterdam: is_static check happens here, before gas reservation
856        if self.env.config.fork < Fork::Amsterdam && current_call_frame.is_static {
857            return Err(ExceptionalHalt::OpcodeNotAllowedInStaticContext.into());
858        }
859
860        // Clear callframe subreturn data
861        current_call_frame.sub_return_data = Bytes::new();
862
863        // Reserve gas for subcall
864        let gas_limit = gas_cost::max_message_call_gas(current_call_frame)?;
865        current_call_frame.increase_consumed_gas(gas_limit)?;
866
867        // Load code from memory
868        let code = self
869            .current_call_frame
870            .memory
871            .load_range(code_offset_in_memory, code_size_in_memory)?;
872
873        // Get account info of deployer
874        let deployer = self.current_call_frame.to;
875        let (deployer_balance, deployer_nonce) = {
876            let deployer_account = self.db.get_account(deployer)?;
877            (deployer_account.info.balance, deployer_account.info.nonce)
878        };
879
880        // Calculate create address
881        let new_address = match salt {
882            Some(salt) => calculate_create2_address(deployer, &code, salt)?,
883            None => calculate_create_address(deployer, deployer_nonce),
884        };
885
886        // Log CREATE in tracer
887        let call_type = match salt {
888            Some(_) => CallType::CREATE2,
889            None => CallType::CREATE,
890        };
891        self.tracer
892            .enter(call_type, deployer, new_address, value, gas_limit, &code);
893
894        let new_depth = self
895            .current_call_frame
896            .depth
897            .checked_add(1)
898            .ok_or(InternalError::Overflow)?;
899
900        // Validations that push 0 (FAIL) to the stack and return reserved gas to deployer
901        // Per reference: these checks happen BEFORE the new address is tracked for BAL.
902        // 1. Sender doesn't have enough balance to send value.
903        // 2. Depth limit has been reached
904        // 3. Sender nonce is max.
905        let checks = [
906            (deployer_balance < value, "OutOfFund"),
907            (new_depth > 1024, "MaxDepth"),
908            (deployer_nonce == u64::MAX, "MaxNonce"),
909        ];
910        for (condition, reason) in checks {
911            if condition {
912                // EIP-8037: no account created on early failure — refund the CREATE
913                // account state gas charged at the top of this function, per EELS
914                // `credit_state_gas_refund(evm, create_account_state_gas)`.
915                if self.env.config.fork >= Fork::Amsterdam {
916                    self.credit_state_gas_refund(self.state_gas_new_account)?;
917                }
918                self.early_revert_message_call(gas_limit, reason.to_string())?;
919                return Ok(OpcodeResult::Continue);
920            }
921        }
922
923        // Add new contract to accessed addresses (after early checks pass, per reference)
924        self.substate.add_accessed_address(new_address);
925
926        // Record address touch for BAL (after early checks pass per EIP-7928 reference)
927        if let Some(recorder) = self.db.bal_recorder.as_mut() {
928            recorder.record_touched_address(new_address);
929        }
930
931        // Increment sender nonce (irreversible change)
932        self.increment_account_nonce(deployer)?;
933
934        // Deployment will fail (consuming all gas) if the contract already exists.
935        let new_account = self.get_account_mut(new_address)?;
936        if new_account.create_would_collide() {
937            // Per EELS: on collision, regular gas stays consumed (not returned)
938            // but the CREATE account state gas IS refunded — no account was created.
939            if self.env.config.fork >= Fork::Amsterdam {
940                self.credit_state_gas_refund(self.state_gas_new_account)?;
941            }
942            self.current_call_frame.stack.push(FAIL)?;
943            self.tracer
944                .exit_early(gas_limit, Some("CreateAccExists".to_string()))?;
945            return Ok(OpcodeResult::Continue);
946        }
947        // EIP-8037 (#3002): capture whether the create target is already alive,
948        // AFTER the collision guard and BEFORE any nonce increment / state
949        // mutation, mirroring EELS `target_alive = is_account_alive(...)`.
950        // `create_would_collide()` already excluded any account with code, nonce,
951        // or storage, so a surviving non-empty target can differ only by
952        // balance > 0; hence `!is_empty()` is exactly `is_account_alive` for
953        // create targets (nonexistent or empty -> not alive). Used on the success
954        // path to refund the unconditionally-charged new-account state gas (no new
955        // account leaf created).
956        let target_alive = !self.get_account_mut(new_address)?.is_empty();
957
958        // Create BAL checkpoint before entering create call for potential revert per EIP-7928
959        let bal_checkpoint = self.db.bal_recorder.as_ref().map(|r| r.checkpoint());
960
961        let mut stack = self.stack_pool.pop().unwrap_or_default();
962        stack.clear();
963
964        let next_memory = self.current_call_frame.memory.next_memory();
965
966        let mut new_call_frame = CallFrame::new(
967            deployer,
968            new_address,
969            new_address,
970            // SAFETY: init code hash is never used
971            Code::from_bytecode_unchecked(code, H256::zero()),
972            value,
973            Bytes::new(),
974            false,
975            gas_limit,
976            new_depth,
977            true,
978            true,
979            0,
980            0,
981            stack,
982            next_memory,
983        );
984        // Store BAL checkpoint in the call frame's backup for restoration on revert
985        new_call_frame.call_frame_backup.bal_checkpoint = bal_checkpoint;
986        // Snapshot AFTER the CREATE account state-gas charge has landed in
987        // `vm.state_gas_used`, so the revert restore in `handle_return_create`
988        // keeps the parent's pre-CREATE intrinsic without re-refunding it.
989        new_call_frame.state_gas_used_at_entry = self.state_gas_used;
990        // EIP-8037 (#3002): thread the pre-mutation target-alive flag to the
991        // success arm of `handle_return_create`.
992        new_call_frame.target_alive = target_alive;
993
994        self.add_callframe(new_call_frame);
995
996        // Changes that revert in case the Create fails.
997        self.increment_account_nonce(new_address)?; // 0 -> 1
998        self.transfer(deployer, new_address, value)?;
999
1000        self.substate.push_backup();
1001        self.substate.add_created_account(new_address); // Mostly for SELFDESTRUCT during initcode.
1002
1003        // EIP-7708: Emit transfer log for nonzero-value CREATE/CREATE2
1004        // Must be after push_backup() so the log reverts if the child context reverts
1005        if self.env.config.fork >= Fork::Amsterdam && !value.is_zero() {
1006            let log = create_eth_transfer_log(deployer, new_address, value);
1007            self.substate.add_log(log);
1008        }
1009
1010        Ok(OpcodeResult::Continue)
1011    }
1012
1013    /// Static gas prelude for CALL/CALLCODE/DELEGATECALL/STATICCALL: compute
1014    /// `(new_memory_size, address_was_cold, static_cost)` and `check_gas` it
1015    /// before any state read, mirroring EELS' `# check static gas before state
1016    /// access`. `value_cost` is the per-opcode positive-value cost (0 when
1017    /// none).
1018    fn check_call_static_gas(
1019        &mut self,
1020        args_offset: usize,
1021        args_len: usize,
1022        return_offset: usize,
1023        return_len: usize,
1024        address: Address,
1025        value_cost: u64,
1026    ) -> Result<(usize, bool, u64), VMError> {
1027        let new_memory_size = calculate_memory_size(args_offset, args_len)?
1028            .max(calculate_memory_size(return_offset, return_len)?);
1029        let address_was_cold = !self.substate.is_address_accessed(&address);
1030        let memory_expansion_cost =
1031            memory::expansion_cost(new_memory_size, self.current_call_frame.memory.len())?;
1032        let access_gas_cost = if address_was_cold {
1033            gas_cost::cold_account_access_cost(self.env.config.fork)
1034        } else {
1035            gas_cost::WARM_ADDRESS_ACCESS_COST
1036        };
1037        let static_cost = memory_expansion_cost
1038            .checked_add(access_gas_cost)
1039            .ok_or(ExceptionalHalt::OutOfGas)?
1040            .checked_add(value_cost)
1041            .ok_or(ExceptionalHalt::OutOfGas)?;
1042        self.current_call_frame.check_gas(static_cost)?;
1043        Ok((new_memory_size, address_was_cold, static_cost))
1044    }
1045
1046    /// Record BAL touched addresses for CALL-family opcodes per EIP-7928.
1047    /// Gated on intermediate gas checks matching the EELS reference.
1048    #[expect(
1049        clippy::too_many_arguments,
1050        reason = "matches EIP-7928 EELS reference parameters"
1051    )]
1052    fn record_bal_call_touch(
1053        &mut self,
1054        target: Address,
1055        code_address: Address,
1056        is_delegation_7702: bool,
1057        eip7702_gas_consumed: u64,
1058        new_memory_size: usize,
1059        current_memory_size: usize,
1060        address_was_cold: bool,
1061        value_cost: u64,
1062        create_cost: u64,
1063    ) {
1064        let Some(recorder) = self.db.bal_recorder.as_mut() else {
1065            return;
1066        };
1067        // Safe: expansion_cost only fails on usize→u64 overflow, which is infallible
1068        // (usize ≤ 64 bits). If it somehow did, u64::MAX makes the gas check fail
1069        // conservatively, skipping the BAL touch — a non-consensus recording path.
1070        let mem_cost =
1071            memory::expansion_cost(new_memory_size, current_memory_size).unwrap_or(u64::MAX);
1072        let access_cost = if address_was_cold {
1073            gas_cost::cold_account_access_cost(self.env.config.fork)
1074        } else {
1075            gas_cost::WARM_ADDRESS_ACCESS_COST
1076        };
1077        let basic_cost = mem_cost
1078            .saturating_add(access_cost)
1079            .saturating_add(value_cost);
1080        let gas_remaining = self.current_call_frame.gas_remaining;
1081
1082        if gas_remaining >= i64::try_from(basic_cost).unwrap_or(i64::MAX) {
1083            recorder.record_touched_address(target);
1084
1085            if is_delegation_7702 {
1086                let delegation_check = basic_cost
1087                    .saturating_add(create_cost)
1088                    .saturating_add(eip7702_gas_consumed);
1089                if gas_remaining >= i64::try_from(delegation_check).unwrap_or(i64::MAX) {
1090                    recorder.record_touched_address(code_address);
1091                }
1092            }
1093        }
1094    }
1095
1096    /// This (should) be the only function where gas is used as a
1097    /// U256. This is because we have to use the values that are
1098    /// pushed to the stack.
1099    ///
1100    // Force inline, due to lot of arguments, inlining must be forced, and it is actually beneficial
1101    // because passing so much data is costly. Verified with samply.
1102    #[expect(
1103        clippy::too_many_arguments,
1104        reason = "inlined for performance, many args needed"
1105    )]
1106    #[inline(always)]
1107    pub fn generic_call(
1108        &mut self,
1109        gas_limit: u64,
1110        value: U256,
1111        msg_sender: Address,
1112        to: Address,
1113        code_address: Address,
1114        should_transfer_value: bool,
1115        is_static: bool,
1116        calldata: Bytes,
1117        ret_offset: usize,
1118        ret_size: usize,
1119        bytecode: Code,
1120        is_delegation_7702: bool,
1121        new_account_charged: bool,
1122    ) -> Result<OpcodeResult, VMError> {
1123        // Clear callframe subreturn data
1124        self.current_call_frame.sub_return_data.clear();
1125
1126        // Validate sender has enough value
1127        if should_transfer_value && !value.is_zero() {
1128            let sender_balance = self.db.get_account(msg_sender)?.info.balance;
1129            if sender_balance < value {
1130                // EIP-8037: no account is created, refund the new-account state gas.
1131                self.refund_new_account_state_gas(new_account_charged)?;
1132                self.early_revert_message_call(gas_limit, "OutOfFund".to_string())?;
1133                return Ok(OpcodeResult::Continue);
1134            }
1135        }
1136
1137        // Validate max depth has not been reached yet.
1138        let new_depth = self
1139            .current_call_frame
1140            .depth
1141            .checked_add(1)
1142            .ok_or(InternalError::Overflow)?;
1143        if new_depth > 1024 {
1144            self.refund_new_account_state_gas(new_account_charged)?;
1145            self.early_revert_message_call(gas_limit, "MaxDepth".to_string())?;
1146            return Ok(OpcodeResult::Continue);
1147        }
1148
1149        if precompiles::is_precompile(&code_address, self.env.config.fork, self.vm_type)
1150            && !is_delegation_7702
1151        {
1152            // Record precompile address touch for BAL per EIP-7928
1153            if let Some(recorder) = self.db.bal_recorder.as_mut() {
1154                recorder.record_touched_address(code_address);
1155            }
1156
1157            let mut gas_remaining = gas_limit;
1158            let ctx_result = Self::execute_precompile(
1159                code_address,
1160                &calldata,
1161                gas_limit,
1162                &mut gas_remaining,
1163                self.env.config.fork,
1164                self.db.store.precompile_cache(),
1165                self.crypto,
1166            )?;
1167
1168            let call_frame = &mut self.current_call_frame;
1169
1170            // Return gas left from subcontext
1171            #[expect(clippy::as_conversions, reason = "remaining gas conversion")]
1172            if ctx_result.is_success() {
1173                call_frame.gas_remaining = (call_frame.gas_remaining as u64)
1174                    .checked_add(
1175                        gas_limit
1176                            .checked_sub(ctx_result.gas_used)
1177                            .ok_or(InternalError::Underflow)?,
1178                    )
1179                    .ok_or(InternalError::Overflow)?
1180                    as i64;
1181            }
1182
1183            // Store return data of sub-context
1184            call_frame.memory.store_data(
1185                ret_offset,
1186                if ctx_result.output.len() >= ret_size {
1187                    ctx_result
1188                        .output
1189                        .get(..ret_size)
1190                        .ok_or(ExceptionalHalt::OutOfBounds)?
1191                } else {
1192                    &ctx_result.output
1193                },
1194            )?;
1195            call_frame.sub_return_data = ctx_result.output.clone();
1196
1197            // What to do, depending on TxResult
1198            call_frame.stack.push(match &ctx_result.result {
1199                TxResult::Success => SUCCESS,
1200                TxResult::Revert(_) => FAIL,
1201            })?;
1202
1203            // EIP-8037: a failed precompile call transfers no value, so no account is
1204            // created — refund the new-account state gas (EELS `generic_call`
1205            // `credit_state_gas_refund(NEW_ACCOUNT)` on child error).
1206            self.refund_new_account_state_gas(new_account_charged && !ctx_result.is_success())?;
1207
1208            // Transfer value from caller to callee.
1209            if should_transfer_value && ctx_result.is_success() {
1210                self.transfer(msg_sender, to, value)?;
1211
1212                // EIP-7708: Emit transfer log for nonzero-value CALL/CALLCODE
1213                // Self-transfers (msg_sender == to) do NOT emit a log (includes CALLCODE)
1214                if self.env.config.fork >= Fork::Amsterdam && !value.is_zero() && msg_sender != to {
1215                    let log = create_eth_transfer_log(msg_sender, to, value);
1216                    self.substate.add_log(log);
1217                }
1218            }
1219
1220            self.tracer.exit_context(&ctx_result, false)?;
1221        } else {
1222            // Create BAL checkpoint before entering nested call for potential revert per EIP-7928
1223            let bal_checkpoint = self.db.bal_recorder.as_ref().map(|r| r.checkpoint());
1224
1225            let mut stack = self.stack_pool.pop().unwrap_or_default();
1226            stack.clear();
1227
1228            let next_memory = self.current_call_frame.memory.next_memory();
1229
1230            let mut new_call_frame = CallFrame::new(
1231                msg_sender,
1232                to,
1233                code_address,
1234                bytecode,
1235                value,
1236                calldata,
1237                is_static,
1238                gas_limit,
1239                new_depth,
1240                should_transfer_value,
1241                false,
1242                ret_offset,
1243                ret_size,
1244                stack,
1245                next_memory,
1246            );
1247            // Store BAL checkpoint in the call frame's backup for restoration on revert
1248            new_call_frame.call_frame_backup.bal_checkpoint = bal_checkpoint;
1249            new_call_frame.state_gas_used_at_entry = self.state_gas_used;
1250            new_call_frame.new_account_state_gas_charged = new_account_charged;
1251
1252            self.add_callframe(new_call_frame);
1253
1254            // Transfer value from caller to callee.
1255            if should_transfer_value {
1256                self.transfer(msg_sender, to, value)?;
1257            }
1258
1259            self.substate.push_backup();
1260
1261            // EIP-7708: Emit transfer log for nonzero-value CALL/CALLCODE
1262            // Must be after push_backup() so the log reverts if the child context reverts
1263            // Self-transfers (msg_sender == to) do NOT emit a log (includes CALLCODE)
1264            if should_transfer_value
1265                && self.env.config.fork >= Fork::Amsterdam
1266                && !value.is_zero()
1267                && msg_sender != to
1268            {
1269                let log = create_eth_transfer_log(msg_sender, to, value);
1270                self.substate.add_log(log);
1271            }
1272        }
1273
1274        Ok(OpcodeResult::Continue)
1275    }
1276
1277    /// Pop backup from stack and restore substate and cache if transaction reverted.
1278    ///
1279    /// `consume_backup` lets the caller move the frame's backup out (no clone) on the
1280    /// revert path when nothing reads it afterward; see [`VM::restore_cache_state_consuming`].
1281    /// The top-level call passes `true` for normal L1 execution and `false` when a
1282    /// `BackupHook` is installed (L2 / stateless), since that hook reads the backup in
1283    /// `finalize_execution` (gated on `VM::preserve_top_level_backup`).
1284    pub fn handle_state_backup(
1285        &mut self,
1286        ctx_result: &ContextResult,
1287        consume_backup: bool,
1288    ) -> Result<(), VMError> {
1289        if ctx_result.is_success() {
1290            self.substate.commit_backup();
1291        } else {
1292            self.substate.revert_backup();
1293            if consume_backup {
1294                self.restore_cache_state_consuming()?;
1295            } else {
1296                self.restore_cache_state()?;
1297            }
1298        }
1299
1300        Ok(())
1301    }
1302
1303    /// Handles case in which callframe was initiated by another callframe (with CALL or CREATE family opcodes)
1304    ///
1305    /// Returns the pc increment.
1306    pub fn handle_return(&mut self, ctx_result: &ContextResult) -> Result<(), VMError> {
1307        // The frame is popped immediately below and its backup is not read again on
1308        // the revert path, so move it out instead of cloning.
1309        self.handle_state_backup(ctx_result, true)?;
1310        let executed_call_frame = self.pop_call_frame()?;
1311
1312        // Here happens the interaction between child (executed) and parent (caller) callframe.
1313        if executed_call_frame.is_create {
1314            self.handle_return_create(executed_call_frame, ctx_result)?;
1315        } else {
1316            self.handle_return_call(executed_call_frame, ctx_result)?;
1317        }
1318
1319        Ok(())
1320    }
1321
1322    #[expect(clippy::as_conversions, reason = "remaining gas conversion")]
1323    pub fn handle_return_call(
1324        &mut self,
1325        executed_call_frame: CallFrame,
1326        ctx_result: &ContextResult,
1327    ) -> Result<(), VMError> {
1328        let CallFrame {
1329            gas_limit,
1330            ret_offset,
1331            ret_size,
1332            memory: old_callframe_memory,
1333            frame_state_gas_spilled: child_frame_state_gas_spilled,
1334            call_frame_backup,
1335            stack,
1336            new_account_state_gas_charged,
1337            ..
1338        } = executed_call_frame;
1339
1340        #[cfg(not(target_arch = "riscv64"))]
1341        old_callframe_memory.clean_from_base();
1342
1343        #[cfg(target_arch = "riscv64")]
1344        old_callframe_memory.truncate_to_base();
1345
1346        let parent_call_frame = &mut self.current_call_frame;
1347
1348        // Return gas left from subcontext
1349        let child_unused_gas = gas_limit
1350            .checked_sub(ctx_result.gas_used)
1351            .ok_or(InternalError::Underflow)?;
1352        parent_call_frame.gas_remaining = parent_call_frame
1353            .gas_remaining
1354            .checked_add(child_unused_gas as i64)
1355            .ok_or(InternalError::Overflow)?;
1356
1357        // Store return data of sub-context
1358        parent_call_frame.memory.store_data(
1359            ret_offset,
1360            if ctx_result.output.len() >= ret_size {
1361                ctx_result
1362                    .output
1363                    .get(..ret_size)
1364                    .ok_or(ExceptionalHalt::OutOfBounds)?
1365            } else {
1366                &ctx_result.output
1367            },
1368        )?;
1369
1370        parent_call_frame.sub_return_data = ctx_result.output.clone();
1371
1372        // What to do, depending on TxResult
1373        match &ctx_result.result {
1374            TxResult::Success => {
1375                self.current_call_frame.stack.push(SUCCESS)?;
1376                self.merge_call_frame_backup_with_parent(&call_frame_backup)?;
1377                // EIP-8037: on success, child's state_gas_used is already
1378                // accumulated into the VM-level field (signed sum handles refunds).
1379                // No pending flush needed — credits were applied inline.
1380                // Propagate the child's per-frame spill to the parent so a later
1381                // parent revert/halt refills it LIFO (EELS `incorporate_child_on_success`).
1382                self.current_call_frame.frame_state_gas_spilled = self
1383                    .current_call_frame
1384                    .frame_state_gas_spilled
1385                    .checked_add(child_frame_state_gas_spilled)
1386                    .ok_or(InternalError::Overflow)?;
1387            }
1388            TxResult::Revert(_) => {
1389                // EIP-8037: the child already self-refilled its execution state gas via
1390                // `refill_frame_state_gas` in `handle_opcode_error`. The parent-charged
1391                // new-account state gas (value transfer to an empty account) is separate
1392                // and refunded here on child failure, mirroring EELS `generic_call`
1393                // `credit_state_gas_refund(NEW_ACCOUNT)`.
1394                self.refund_new_account_state_gas(new_account_state_gas_charged)?;
1395                self.current_call_frame.stack.push(FAIL)?;
1396            }
1397        };
1398
1399        self.tracer.exit_context(ctx_result, false)?;
1400
1401        let mut stack = stack;
1402        stack.clear();
1403        self.stack_pool.push(stack);
1404
1405        Ok(())
1406    }
1407
1408    #[expect(clippy::as_conversions, reason = "remaining gas conversion")]
1409    pub fn handle_return_create(
1410        &mut self,
1411        executed_call_frame: CallFrame,
1412        ctx_result: &ContextResult,
1413    ) -> Result<(), VMError> {
1414        let CallFrame {
1415            gas_limit,
1416            to,
1417            call_frame_backup,
1418            memory: old_callframe_memory,
1419            frame_state_gas_spilled: child_frame_state_gas_spilled,
1420            target_alive,
1421            stack,
1422            ..
1423        } = executed_call_frame;
1424
1425        #[cfg(not(target_arch = "riscv64"))]
1426        old_callframe_memory.clean_from_base();
1427
1428        #[cfg(target_arch = "riscv64")]
1429        old_callframe_memory.truncate_to_base();
1430
1431        // Return unused gas
1432        let unused_gas = gas_limit
1433            .checked_sub(ctx_result.gas_used)
1434            .ok_or(InternalError::Underflow)?;
1435        self.current_call_frame.gas_remaining = self
1436            .current_call_frame
1437            .gas_remaining
1438            .checked_add(unused_gas as i64)
1439            .ok_or(InternalError::Overflow)?;
1440
1441        // What to do, depending on TxResult
1442        match ctx_result.result.clone() {
1443            TxResult::Success => {
1444                self.current_call_frame.stack.push(address_to_word(to))?;
1445                self.merge_call_frame_backup_with_parent(&call_frame_backup)?;
1446                // EIP-8037: on success, child's state_gas_used is already
1447                // accumulated into the VM-level field (signed sum handles refunds).
1448                // No pending flush needed — credits were applied inline.
1449                // Propagate the child's per-frame spill to the parent so a later
1450                // parent revert/halt refills it LIFO (EELS `incorporate_child_on_success`).
1451                self.current_call_frame.frame_state_gas_spilled = self
1452                    .current_call_frame
1453                    .frame_state_gas_spilled
1454                    .checked_add(child_frame_state_gas_spilled)
1455                    .ok_or(InternalError::Overflow)?;
1456                // EIP-8037 (#3002): the parent charged the new-account state gas
1457                // unconditionally before the child ran. On success, if the target was
1458                // already alive (existed and non-empty), no new account leaf is created,
1459                // so refund the new-account portion — EELS `generic_create`:
1460                //   if target_alive: credit_state_gas_refund(evm, StateGasCosts.NEW_ACCOUNT)
1461                // LIFO via `credit_state_gas_refund` (drains the parent frame spill first,
1462                // then the reservoir). Disjoint from the failure/collision refunds, which
1463                // only fire on the Revert arm / early-return paths.
1464                if self.env.config.fork >= Fork::Amsterdam && target_alive {
1465                    self.credit_state_gas_refund(self.state_gas_new_account)?;
1466                }
1467            }
1468            TxResult::Revert(err) => {
1469                // EIP-8037: the child already self-refilled its state gas via
1470                // `refill_frame_state_gas` in `handle_opcode_error`, so no parent-side
1471                // state-gas reabsorption is needed here.
1472
1473                // EIP-8037: CREATE's account state gas was charged in the parent before
1474                // the child frame began; no account was created, so refund it per EELS
1475                // `credit_state_gas_refund(evm, create_account_state_gas)`.
1476                if self.env.config.fork >= Fork::Amsterdam {
1477                    self.credit_state_gas_refund(self.state_gas_new_account)?;
1478                }
1479
1480                // Return data is only propagated on REVERT opcode, not on ExceptionalHalt.
1481                if err.is_revert_opcode() {
1482                    self.current_call_frame.sub_return_data = ctx_result.output.clone();
1483                }
1484
1485                self.current_call_frame.stack.push(FAIL)?;
1486            }
1487        };
1488
1489        self.tracer.exit_context(ctx_result, false)?;
1490
1491        let mut stack = stack;
1492        stack.clear();
1493        self.stack_pool.push(stack);
1494
1495        Ok(())
1496    }
1497
1498    fn get_calldata(&mut self, offset: usize, size: usize) -> Result<Bytes, VMError> {
1499        self.current_call_frame.memory.load_range(offset, size)
1500    }
1501
1502    #[expect(clippy::as_conversions, reason = "remaining gas conversion")]
1503    fn early_revert_message_call(&mut self, gas_limit: u64, reason: String) -> Result<(), VMError> {
1504        let callframe = &mut self.current_call_frame;
1505
1506        // Return gas_limit to callframe.
1507        callframe.gas_remaining = callframe
1508            .gas_remaining
1509            .checked_add(gas_limit as i64)
1510            .ok_or(InternalError::Overflow)?;
1511        callframe.stack.push(FAIL)?; // It's the same as revert for CREATE
1512
1513        self.tracer.exit_early(0, Some(reason))?;
1514        Ok(())
1515    }
1516}