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