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