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        // EIP-8037 (Amsterdam+): charge state gas for new account creation AFTER
886        // initcode size validation, so oversized CREATE doesn't burn state gas.
887        if self.env.config.fork >= Fork::Amsterdam {
888            self.increase_state_gas(self.state_gas_new_account)?;
889        }
890
891        let current_call_frame = &mut self.current_call_frame;
892
893        // Pre-Amsterdam: is_static check happens here, before gas reservation
894        if self.env.config.fork < Fork::Amsterdam && current_call_frame.is_static {
895            return Err(ExceptionalHalt::OpcodeNotAllowedInStaticContext.into());
896        }
897
898        // Clear callframe subreturn data
899        current_call_frame.sub_return_data = Bytes::new();
900
901        // Reserve gas for subcall
902        let gas_limit = gas_cost::max_message_call_gas(current_call_frame)?;
903        current_call_frame.increase_consumed_gas(gas_limit)?;
904
905        // Load code from memory
906        let code = self
907            .current_call_frame
908            .memory
909            .load_range(code_offset_in_memory, code_size_in_memory)?;
910
911        // Get account info of deployer
912        let deployer = self.current_call_frame.to;
913        let (deployer_balance, deployer_nonce) = {
914            let deployer_account = self.db.get_account(deployer)?;
915            (deployer_account.info.balance, deployer_account.info.nonce)
916        };
917
918        // Calculate create address
919        let new_address = match salt {
920            Some(salt) => calculate_create2_address(deployer, &code, salt)?,
921            None => calculate_create_address(deployer, deployer_nonce),
922        };
923
924        // Log CREATE in tracer
925        let call_type = match salt {
926            Some(_) => CallType::CREATE2,
927            None => CallType::CREATE,
928        };
929        self.tracer
930            .enter(call_type, deployer, new_address, value, gas_limit, &code);
931
932        let new_depth = self
933            .current_call_frame
934            .depth
935            .checked_add(1)
936            .ok_or(InternalError::Overflow)?;
937
938        // Validations that push 0 (FAIL) to the stack and return reserved gas to deployer
939        // Per reference: these checks happen BEFORE the new address is tracked for BAL.
940        // 1. Sender doesn't have enough balance to send value.
941        // 2. Depth limit has been reached
942        // 3. Sender nonce is max.
943        let checks = [
944            (deployer_balance < value, "OutOfFund"),
945            (new_depth > 1024, "MaxDepth"),
946            (deployer_nonce == u64::MAX, "MaxNonce"),
947        ];
948        for (condition, reason) in checks {
949            if condition {
950                // EIP-8037: no account created on early failure — refund the CREATE
951                // account state gas charged at the top of this function, per EELS
952                // `credit_state_gas_refund(evm, create_account_state_gas)`.
953                if self.env.config.fork >= Fork::Amsterdam {
954                    self.credit_state_gas_refund(self.state_gas_new_account)?;
955                }
956                self.early_revert_message_call(gas_limit, reason.to_string())?;
957                return Ok(OpcodeResult::Continue);
958            }
959        }
960
961        // Add new contract to accessed addresses (after early checks pass, per reference)
962        self.substate.add_accessed_address(new_address);
963
964        // Record address touch for BAL (after early checks pass per EIP-7928 reference)
965        if let Some(recorder) = self.db.bal_recorder.as_mut() {
966            recorder.record_touched_address(new_address);
967        }
968
969        // Increment sender nonce (irreversible change)
970        self.increment_account_nonce(deployer)?;
971
972        // Deployment will fail (consuming all gas) if the contract already exists.
973        let new_account = self.get_account_mut(new_address)?;
974        if new_account.create_would_collide() {
975            // Per EELS: on collision, regular gas stays consumed (not returned)
976            // but the CREATE account state gas IS refunded — no account was created.
977            if self.env.config.fork >= Fork::Amsterdam {
978                self.credit_state_gas_refund(self.state_gas_new_account)?;
979            }
980            self.current_call_frame.stack.push(FAIL)?;
981            self.tracer
982                .exit_early(gas_limit, Some("CreateAccExists".to_string()))?;
983            return Ok(OpcodeResult::Continue);
984        }
985        // EIP-8037 (#3002): capture whether the create target is already alive,
986        // AFTER the collision guard and BEFORE any nonce increment / state
987        // mutation, mirroring EELS `target_alive = is_account_alive(...)`.
988        // `create_would_collide()` already excluded any account with code, nonce,
989        // or storage, so a surviving non-empty target can differ only by
990        // balance > 0; hence `!is_empty()` is exactly `is_account_alive` for
991        // create targets (nonexistent or empty -> not alive). Used on the success
992        // path to refund the unconditionally-charged new-account state gas (no new
993        // account leaf created).
994        let target_alive = !self.get_account_mut(new_address)?.is_empty();
995
996        // Create BAL checkpoint before entering create call for potential revert per EIP-7928
997        let bal_checkpoint = self.db.bal_recorder.as_ref().map(|r| r.checkpoint());
998
999        let mut stack = self.stack_pool.pop().unwrap_or_default();
1000        stack.clear();
1001
1002        let next_memory = self.current_call_frame.memory.next_memory();
1003
1004        let mut new_call_frame = CallFrame::new(
1005            deployer,
1006            new_address,
1007            new_address,
1008            // SAFETY: init code hash is never used
1009            Code::from_bytecode_unchecked(code, H256::zero()),
1010            value,
1011            Bytes::new(),
1012            false,
1013            gas_limit,
1014            new_depth,
1015            true,
1016            true,
1017            0,
1018            0,
1019            stack,
1020            next_memory,
1021        );
1022        // Store BAL checkpoint in the call frame's backup for restoration on revert
1023        new_call_frame.call_frame_backup.bal_checkpoint = bal_checkpoint;
1024        // Snapshot AFTER the CREATE account state-gas charge has landed in
1025        // `vm.state_gas_used`, so the revert restore in `handle_return_create`
1026        // keeps the parent's pre-CREATE intrinsic without re-refunding it.
1027        new_call_frame.state_gas_used_at_entry = self.state_gas_used;
1028        // EIP-8037 (#3002): thread the pre-mutation target-alive flag to the
1029        // success arm of `handle_return_create`.
1030        new_call_frame.target_alive = target_alive;
1031
1032        self.add_callframe(new_call_frame);
1033
1034        // Changes that revert in case the Create fails.
1035        self.increment_account_nonce(new_address)?; // 0 -> 1
1036        self.transfer(deployer, new_address, value)?;
1037
1038        self.substate.push_backup();
1039        self.substate.add_created_account(new_address); // Mostly for SELFDESTRUCT during initcode.
1040
1041        // EIP-7708: Emit transfer log for nonzero-value CREATE/CREATE2
1042        // Must be after push_backup() so the log reverts if the child context reverts
1043        if self.env.config.fork >= Fork::Amsterdam && !value.is_zero() {
1044            let log = create_eth_transfer_log(deployer, new_address, value);
1045            self.substate.add_log(log);
1046        }
1047
1048        Ok(OpcodeResult::Continue)
1049    }
1050
1051    /// Static gas prelude for CALL/CALLCODE/DELEGATECALL/STATICCALL: compute
1052    /// `(new_memory_size, address_was_cold, static_cost)` and `check_gas` it
1053    /// before any state read, mirroring EELS' `# check static gas before state
1054    /// access`. `value_cost` is the per-opcode positive-value cost (0 when
1055    /// none).
1056    fn check_call_static_gas(
1057        &mut self,
1058        args_offset: usize,
1059        args_len: usize,
1060        return_offset: usize,
1061        return_len: usize,
1062        address: Address,
1063        value_cost: u64,
1064    ) -> Result<(usize, bool, u64), VMError> {
1065        let new_memory_size = calculate_memory_size(args_offset, args_len)?
1066            .max(calculate_memory_size(return_offset, return_len)?);
1067        let address_was_cold = !self.substate.is_address_accessed(&address);
1068        let memory_expansion_cost =
1069            memory::expansion_cost(new_memory_size, self.current_call_frame.memory.len())?;
1070        let access_gas_cost = if address_was_cold {
1071            gas_cost::cold_account_access_cost(self.env.config.fork)
1072        } else {
1073            gas_cost::WARM_ADDRESS_ACCESS_COST
1074        };
1075        let static_cost = memory_expansion_cost
1076            .checked_add(access_gas_cost)
1077            .ok_or(ExceptionalHalt::OutOfGas)?
1078            .checked_add(value_cost)
1079            .ok_or(ExceptionalHalt::OutOfGas)?;
1080        self.current_call_frame.check_gas(static_cost)?;
1081        Ok((new_memory_size, address_was_cold, static_cost))
1082    }
1083
1084    /// Record BAL touched addresses for CALL-family opcodes per EIP-7928.
1085    /// Gated on intermediate gas checks matching the EELS reference.
1086    #[expect(
1087        clippy::too_many_arguments,
1088        reason = "matches EIP-7928 EELS reference parameters"
1089    )]
1090    fn record_bal_call_touch(
1091        &mut self,
1092        target: Address,
1093        code_address: Address,
1094        is_delegation_7702: bool,
1095        eip7702_gas_consumed: u64,
1096        new_memory_size: usize,
1097        current_memory_size: usize,
1098        address_was_cold: bool,
1099        value_cost: u64,
1100        create_cost: u64,
1101    ) {
1102        let Some(recorder) = self.db.bal_recorder.as_mut() else {
1103            return;
1104        };
1105        // Safe: expansion_cost only fails on usize→u64 overflow, which is infallible
1106        // (usize ≤ 64 bits). If it somehow did, u64::MAX makes the gas check fail
1107        // conservatively, skipping the BAL touch — a non-consensus recording path.
1108        let mem_cost =
1109            memory::expansion_cost(new_memory_size, current_memory_size).unwrap_or(u64::MAX);
1110        let access_cost = if address_was_cold {
1111            gas_cost::cold_account_access_cost(self.env.config.fork)
1112        } else {
1113            gas_cost::WARM_ADDRESS_ACCESS_COST
1114        };
1115        let basic_cost = mem_cost
1116            .saturating_add(access_cost)
1117            .saturating_add(value_cost);
1118        let gas_remaining = self.current_call_frame.gas_remaining;
1119
1120        if gas_remaining >= i64::try_from(basic_cost).unwrap_or(i64::MAX) {
1121            recorder.record_touched_address(target);
1122
1123            if is_delegation_7702 {
1124                let delegation_check = basic_cost
1125                    .saturating_add(create_cost)
1126                    .saturating_add(eip7702_gas_consumed);
1127                if gas_remaining >= i64::try_from(delegation_check).unwrap_or(i64::MAX) {
1128                    recorder.record_touched_address(code_address);
1129                }
1130            }
1131        }
1132    }
1133
1134    /// This (should) be the only function where gas is used as a
1135    /// U256. This is because we have to use the values that are
1136    /// pushed to the stack.
1137    ///
1138    // Force inline, due to lot of arguments, inlining must be forced, and it is actually beneficial
1139    // because passing so much data is costly. Verified with samply.
1140    #[expect(
1141        clippy::too_many_arguments,
1142        reason = "inlined for performance, many args needed"
1143    )]
1144    #[inline(always)]
1145    pub fn generic_call(
1146        &mut self,
1147        gas_limit: u64,
1148        value: U256,
1149        msg_sender: Address,
1150        to: Address,
1151        code_address: Address,
1152        should_transfer_value: bool,
1153        is_static: bool,
1154        calldata: Bytes,
1155        ret_offset: usize,
1156        ret_size: usize,
1157        bytecode: Code,
1158        is_delegation_7702: bool,
1159        new_account_charged: bool,
1160    ) -> Result<OpcodeResult, VMError> {
1161        // Clear callframe subreturn data
1162        self.current_call_frame.sub_return_data.clear();
1163
1164        // Validate sender has enough value
1165        if should_transfer_value && !value.is_zero() {
1166            let sender_balance = self.db.get_account(msg_sender)?.info.balance;
1167            if sender_balance < value {
1168                // EIP-8037: no account is created, refund the new-account state gas.
1169                self.refund_new_account_state_gas(new_account_charged)?;
1170                self.early_revert_message_call(gas_limit, "OutOfFund".to_string())?;
1171                return Ok(OpcodeResult::Continue);
1172            }
1173        }
1174
1175        // Validate max depth has not been reached yet.
1176        let new_depth = self
1177            .current_call_frame
1178            .depth
1179            .checked_add(1)
1180            .ok_or(InternalError::Overflow)?;
1181        if new_depth > 1024 {
1182            self.refund_new_account_state_gas(new_account_charged)?;
1183            self.early_revert_message_call(gas_limit, "MaxDepth".to_string())?;
1184            return Ok(OpcodeResult::Continue);
1185        }
1186
1187        if precompiles::is_precompile(&code_address, self.env.config.fork, self.vm_type)
1188            && !is_delegation_7702
1189        {
1190            // Record precompile address touch for BAL per EIP-7928
1191            if let Some(recorder) = self.db.bal_recorder.as_mut() {
1192                recorder.record_touched_address(code_address);
1193            }
1194
1195            let mut gas_remaining = gas_limit;
1196            let ctx_result = Self::execute_precompile(
1197                code_address,
1198                &calldata,
1199                gas_limit,
1200                &mut gas_remaining,
1201                self.env.config.fork,
1202                self.db.store.precompile_cache(),
1203                self.crypto,
1204            )?;
1205
1206            let call_frame = &mut self.current_call_frame;
1207
1208            // Return gas left from subcontext
1209            #[expect(clippy::as_conversions, reason = "remaining gas conversion")]
1210            if ctx_result.is_success() {
1211                call_frame.gas_remaining = (call_frame.gas_remaining as u64)
1212                    .checked_add(
1213                        gas_limit
1214                            .checked_sub(ctx_result.gas_used)
1215                            .ok_or(InternalError::Underflow)?,
1216                    )
1217                    .ok_or(InternalError::Overflow)?
1218                    as i64;
1219            }
1220
1221            // Store return data of sub-context
1222            call_frame.memory.store_data(
1223                ret_offset,
1224                if ctx_result.output.len() >= ret_size {
1225                    ctx_result
1226                        .output
1227                        .get(..ret_size)
1228                        .ok_or(ExceptionalHalt::OutOfBounds)?
1229                } else {
1230                    &ctx_result.output
1231                },
1232            )?;
1233            call_frame.sub_return_data = ctx_result.output.clone();
1234
1235            // What to do, depending on TxResult
1236            call_frame.stack.push(match &ctx_result.result {
1237                TxResult::Success => SUCCESS,
1238                TxResult::Revert(_) => FAIL,
1239            })?;
1240
1241            // EIP-8037: a failed precompile call transfers no value, so no account is
1242            // created — refund the new-account state gas (EELS `generic_call`
1243            // `credit_state_gas_refund(NEW_ACCOUNT)` on child error).
1244            self.refund_new_account_state_gas(new_account_charged && !ctx_result.is_success())?;
1245
1246            // Transfer value from caller to callee.
1247            if should_transfer_value && ctx_result.is_success() {
1248                self.transfer(msg_sender, to, value)?;
1249
1250                // EIP-7708: Emit transfer log for nonzero-value CALL/CALLCODE
1251                // Self-transfers (msg_sender == to) do NOT emit a log (includes CALLCODE)
1252                if self.env.config.fork >= Fork::Amsterdam && !value.is_zero() && msg_sender != to {
1253                    let log = create_eth_transfer_log(msg_sender, to, value);
1254                    self.substate.add_log(log);
1255                }
1256            }
1257
1258            self.tracer.exit_context(&ctx_result, false)?;
1259        } else {
1260            // Create BAL checkpoint before entering nested call for potential revert per EIP-7928
1261            let bal_checkpoint = self.db.bal_recorder.as_ref().map(|r| r.checkpoint());
1262
1263            let mut stack = self.stack_pool.pop().unwrap_or_default();
1264            stack.clear();
1265
1266            let next_memory = self.current_call_frame.memory.next_memory();
1267
1268            let mut new_call_frame = CallFrame::new(
1269                msg_sender,
1270                to,
1271                code_address,
1272                bytecode,
1273                value,
1274                calldata,
1275                is_static,
1276                gas_limit,
1277                new_depth,
1278                should_transfer_value,
1279                false,
1280                ret_offset,
1281                ret_size,
1282                stack,
1283                next_memory,
1284            );
1285            // Store BAL checkpoint in the call frame's backup for restoration on revert
1286            new_call_frame.call_frame_backup.bal_checkpoint = bal_checkpoint;
1287            new_call_frame.state_gas_used_at_entry = self.state_gas_used;
1288            new_call_frame.new_account_state_gas_charged = new_account_charged;
1289
1290            self.add_callframe(new_call_frame);
1291
1292            // Transfer value from caller to callee.
1293            if should_transfer_value {
1294                self.transfer(msg_sender, to, value)?;
1295            }
1296
1297            self.substate.push_backup();
1298
1299            // EIP-7708: Emit transfer log for nonzero-value CALL/CALLCODE
1300            // Must be after push_backup() so the log reverts if the child context reverts
1301            // Self-transfers (msg_sender == to) do NOT emit a log (includes CALLCODE)
1302            if should_transfer_value
1303                && self.env.config.fork >= Fork::Amsterdam
1304                && !value.is_zero()
1305                && msg_sender != to
1306            {
1307                let log = create_eth_transfer_log(msg_sender, to, value);
1308                self.substate.add_log(log);
1309            }
1310        }
1311
1312        Ok(OpcodeResult::Continue)
1313    }
1314
1315    /// Pop backup from stack and restore substate and cache if transaction reverted.
1316    ///
1317    /// `consume_backup` lets the caller move the frame's backup out (no clone) on the
1318    /// revert path when nothing reads it afterward; see [`VM::restore_cache_state_consuming`].
1319    /// The top-level call passes `true` for normal L1 execution and `false` when a
1320    /// `BackupHook` is installed (L2 / stateless), since that hook reads the backup in
1321    /// `finalize_execution` (gated on `VM::preserve_top_level_backup`).
1322    pub fn handle_state_backup(
1323        &mut self,
1324        ctx_result: &ContextResult,
1325        consume_backup: bool,
1326    ) -> Result<(), VMError> {
1327        if ctx_result.is_success() {
1328            self.substate.commit_backup();
1329        } else {
1330            self.substate.revert_backup();
1331            if consume_backup {
1332                self.restore_cache_state_consuming()?;
1333            } else {
1334                self.restore_cache_state()?;
1335            }
1336        }
1337
1338        Ok(())
1339    }
1340
1341    /// Handles case in which callframe was initiated by another callframe (with CALL or CREATE family opcodes)
1342    ///
1343    /// Returns the pc increment.
1344    pub fn handle_return(&mut self, ctx_result: &ContextResult) -> Result<(), VMError> {
1345        // The frame is popped immediately below and its backup is not read again on
1346        // the revert path, so move it out instead of cloning.
1347        self.handle_state_backup(ctx_result, true)?;
1348        let executed_call_frame = self.pop_call_frame()?;
1349
1350        // Here happens the interaction between child (executed) and parent (caller) callframe.
1351        if executed_call_frame.is_create {
1352            self.handle_return_create(executed_call_frame, ctx_result)?;
1353        } else {
1354            self.handle_return_call(executed_call_frame, ctx_result)?;
1355        }
1356
1357        Ok(())
1358    }
1359
1360    #[expect(clippy::as_conversions, reason = "remaining gas conversion")]
1361    pub fn handle_return_call(
1362        &mut self,
1363        executed_call_frame: CallFrame,
1364        ctx_result: &ContextResult,
1365    ) -> Result<(), VMError> {
1366        let CallFrame {
1367            gas_limit,
1368            ret_offset,
1369            ret_size,
1370            memory: old_callframe_memory,
1371            frame_state_gas_spilled: child_frame_state_gas_spilled,
1372            call_frame_backup,
1373            stack,
1374            new_account_state_gas_charged,
1375            ..
1376        } = executed_call_frame;
1377
1378        #[cfg(not(target_arch = "riscv64"))]
1379        old_callframe_memory.clean_from_base();
1380
1381        #[cfg(target_arch = "riscv64")]
1382        old_callframe_memory.truncate_to_base();
1383
1384        let parent_call_frame = &mut self.current_call_frame;
1385
1386        // Return gas left from subcontext
1387        let child_unused_gas = gas_limit
1388            .checked_sub(ctx_result.gas_used)
1389            .ok_or(InternalError::Underflow)?;
1390        parent_call_frame.gas_remaining = parent_call_frame
1391            .gas_remaining
1392            .checked_add(child_unused_gas as i64)
1393            .ok_or(InternalError::Overflow)?;
1394
1395        // Store return data of sub-context
1396        parent_call_frame.memory.store_data(
1397            ret_offset,
1398            if ctx_result.output.len() >= ret_size {
1399                ctx_result
1400                    .output
1401                    .get(..ret_size)
1402                    .ok_or(ExceptionalHalt::OutOfBounds)?
1403            } else {
1404                &ctx_result.output
1405            },
1406        )?;
1407
1408        parent_call_frame.sub_return_data = ctx_result.output.clone();
1409
1410        // What to do, depending on TxResult
1411        match &ctx_result.result {
1412            TxResult::Success => {
1413                self.current_call_frame.stack.push(SUCCESS)?;
1414                self.merge_call_frame_backup_with_parent(&call_frame_backup)?;
1415                // EIP-8037: on success, child's state_gas_used is already
1416                // accumulated into the VM-level field (signed sum handles refunds).
1417                // No pending flush needed — credits were applied inline.
1418                // Propagate the child's per-frame spill to the parent so a later
1419                // parent revert/halt refills it LIFO (EELS `incorporate_child_on_success`).
1420                self.current_call_frame.frame_state_gas_spilled = self
1421                    .current_call_frame
1422                    .frame_state_gas_spilled
1423                    .checked_add(child_frame_state_gas_spilled)
1424                    .ok_or(InternalError::Overflow)?;
1425            }
1426            TxResult::Revert(_) => {
1427                // EIP-8037: the child already self-refilled its execution state gas via
1428                // `refill_frame_state_gas` in `handle_opcode_error`. The parent-charged
1429                // new-account state gas (value transfer to an empty account) is separate
1430                // and refunded here on child failure, mirroring EELS `generic_call`
1431                // `credit_state_gas_refund(NEW_ACCOUNT)`.
1432                self.refund_new_account_state_gas(new_account_state_gas_charged)?;
1433                self.current_call_frame.stack.push(FAIL)?;
1434            }
1435        };
1436
1437        self.tracer.exit_context(ctx_result, false)?;
1438
1439        let mut stack = stack;
1440        stack.clear();
1441        self.stack_pool.push(stack);
1442
1443        Ok(())
1444    }
1445
1446    #[expect(clippy::as_conversions, reason = "remaining gas conversion")]
1447    pub fn handle_return_create(
1448        &mut self,
1449        executed_call_frame: CallFrame,
1450        ctx_result: &ContextResult,
1451    ) -> Result<(), VMError> {
1452        let CallFrame {
1453            gas_limit,
1454            to,
1455            call_frame_backup,
1456            memory: old_callframe_memory,
1457            frame_state_gas_spilled: child_frame_state_gas_spilled,
1458            target_alive,
1459            stack,
1460            ..
1461        } = executed_call_frame;
1462
1463        #[cfg(not(target_arch = "riscv64"))]
1464        old_callframe_memory.clean_from_base();
1465
1466        #[cfg(target_arch = "riscv64")]
1467        old_callframe_memory.truncate_to_base();
1468
1469        // Return unused gas
1470        let unused_gas = gas_limit
1471            .checked_sub(ctx_result.gas_used)
1472            .ok_or(InternalError::Underflow)?;
1473        self.current_call_frame.gas_remaining = self
1474            .current_call_frame
1475            .gas_remaining
1476            .checked_add(unused_gas as i64)
1477            .ok_or(InternalError::Overflow)?;
1478
1479        // What to do, depending on TxResult
1480        match ctx_result.result.clone() {
1481            TxResult::Success => {
1482                self.current_call_frame.stack.push(address_to_word(to))?;
1483                self.merge_call_frame_backup_with_parent(&call_frame_backup)?;
1484                // EIP-8037: on success, child's state_gas_used is already
1485                // accumulated into the VM-level field (signed sum handles refunds).
1486                // No pending flush needed — credits were applied inline.
1487                // Propagate the child's per-frame spill to the parent so a later
1488                // parent revert/halt refills it LIFO (EELS `incorporate_child_on_success`).
1489                self.current_call_frame.frame_state_gas_spilled = self
1490                    .current_call_frame
1491                    .frame_state_gas_spilled
1492                    .checked_add(child_frame_state_gas_spilled)
1493                    .ok_or(InternalError::Overflow)?;
1494                // EIP-8037 (#3002): the parent charged the new-account state gas
1495                // unconditionally before the child ran. On success, if the target was
1496                // already alive (existed and non-empty), no new account leaf is created,
1497                // so refund the new-account portion — EELS `generic_create`:
1498                //   if target_alive: credit_state_gas_refund(evm, StateGasCosts.NEW_ACCOUNT)
1499                // LIFO via `credit_state_gas_refund` (drains the parent frame spill first,
1500                // then the reservoir). Disjoint from the failure/collision refunds, which
1501                // only fire on the Revert arm / early-return paths.
1502                if self.env.config.fork >= Fork::Amsterdam && target_alive {
1503                    self.credit_state_gas_refund(self.state_gas_new_account)?;
1504                }
1505            }
1506            TxResult::Revert(err) => {
1507                // EIP-8037: the child already self-refilled its state gas via
1508                // `refill_frame_state_gas` in `handle_opcode_error`, so no parent-side
1509                // state-gas reabsorption is needed here.
1510
1511                // EIP-8037: CREATE's account state gas was charged in the parent before
1512                // the child frame began; no account was created, so refund it per EELS
1513                // `credit_state_gas_refund(evm, create_account_state_gas)`.
1514                if self.env.config.fork >= Fork::Amsterdam {
1515                    self.credit_state_gas_refund(self.state_gas_new_account)?;
1516                }
1517
1518                // Return data is only propagated on REVERT opcode, not on ExceptionalHalt.
1519                if err.is_revert_opcode() {
1520                    self.current_call_frame.sub_return_data = ctx_result.output.clone();
1521                }
1522
1523                self.current_call_frame.stack.push(FAIL)?;
1524            }
1525        };
1526
1527        self.tracer.exit_context(ctx_result, false)?;
1528
1529        let mut stack = stack;
1530        stack.clear();
1531        self.stack_pool.push(stack);
1532
1533        Ok(())
1534    }
1535
1536    fn get_calldata(&mut self, offset: usize, size: usize) -> Result<Bytes, VMError> {
1537        self.current_call_frame.memory.load_range(offset, size)
1538    }
1539
1540    #[expect(clippy::as_conversions, reason = "remaining gas conversion")]
1541    fn early_revert_message_call(&mut self, gas_limit: u64, reason: String) -> Result<(), VMError> {
1542        let callframe = &mut self.current_call_frame;
1543
1544        // Return gas_limit to callframe.
1545        callframe.gas_remaining = callframe
1546            .gas_remaining
1547            .checked_add(gas_limit as i64)
1548            .ok_or(InternalError::Overflow)?;
1549        callframe.stack.push(FAIL)?; // It's the same as revert for CREATE
1550
1551        self.tracer.exit_early(0, Some(reason))?;
1552        Ok(())
1553    }
1554}