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