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