Skip to main content

ethrex_levm/
execution_handlers.rs

1use crate::{
2    constants::*,
3    errors::{ContextResult, ExceptionalHalt, InternalError, TxResult, VMError},
4    gas_cost::{CODE_DEPOSIT_COST, CODE_DEPOSIT_REGULAR_COST_PER_WORD},
5    utils::create_eth_transfer_log,
6    vm::VM,
7};
8
9use bytes::Bytes;
10use ethrex_common::types::{Code, Fork};
11
12impl<'a> VM<'a> {
13    pub fn handle_precompile_result(
14        precompile_result: Result<Bytes, VMError>,
15        gas_limit: u64,
16        gas_remaining: u64,
17    ) -> Result<ContextResult, VMError> {
18        match precompile_result {
19            Ok(output) => {
20                let gas_used = gas_limit
21                    .checked_sub(gas_remaining)
22                    .ok_or(InternalError::Underflow)?;
23                Ok(ContextResult {
24                    result: TxResult::Success,
25                    gas_used,
26                    gas_spent: gas_used, // Will be updated in finalize_execution
27                    output,
28                })
29            }
30            Err(error) => {
31                if error.should_propagate() {
32                    return Err(error);
33                }
34
35                Ok(ContextResult {
36                    result: TxResult::Revert(error),
37                    gas_used: gas_limit,
38                    gas_spent: gas_limit, // Will be updated in finalize_execution
39                    output: Bytes::new(),
40                })
41            }
42        }
43    }
44
45    #[cold] // used in the hot path loop, called only really once.
46    pub fn handle_opcode_result(&mut self) -> Result<ContextResult, VMError> {
47        // On successful create check output validity
48        if self.is_create()? {
49            let validate_create = self.validate_contract_creation();
50
51            if let Err(error) = validate_create {
52                if error.should_propagate() {
53                    return Err(error);
54                }
55
56                // EIP-8037 (Amsterdam+): roll back this frame's state gas in LIFO order
57                // BEFORE zeroing gas. Mirrors EELS `process_create_message`'s
58                // `refill_frame_state_gas` on the code-deposit ExceptionalHalt path.
59                // Must run before the `&mut self.current_call_frame` borrow below.
60                if self.env.config.fork >= Fork::Amsterdam {
61                    let entry = self.current_call_frame.state_gas_used_at_entry;
62                    self.refill_frame_state_gas(entry)?;
63                }
64
65                // Consume all gas because error was exceptional.
66                let callframe = &mut self.current_call_frame;
67                callframe.gas_remaining = 0;
68
69                #[expect(clippy::as_conversions, reason = "remaining gas conversion")]
70                let gas_used = callframe
71                    .gas_limit
72                    .checked_sub(callframe.gas_remaining as u64)
73                    .ok_or(InternalError::Underflow)?;
74                return Ok(ContextResult {
75                    result: TxResult::Revert(error),
76                    gas_used,
77                    gas_spent: gas_used, // Will be updated in finalize_execution
78                    output: Bytes::new(),
79                });
80            }
81
82            // Set bytecode to the newly created contract.
83            let contract_address = self.current_call_frame.to;
84            let code = self.current_call_frame.output.clone();
85            self.update_account_bytecode(contract_address, Code::from_bytecode(code, self.crypto))?;
86        }
87
88        #[expect(clippy::as_conversions, reason = "remaining gas conversion")]
89        let gas_used = {
90            let callframe = &mut self.current_call_frame;
91            callframe
92                .gas_limit
93                .checked_sub(callframe.gas_remaining as u64)
94                .ok_or(InternalError::Underflow)?
95        };
96        Ok(ContextResult {
97            result: TxResult::Success,
98            gas_used,
99            gas_spent: gas_used, // Will be updated in finalize_execution
100            output: std::mem::take(&mut self.current_call_frame.output),
101        })
102    }
103
104    #[cold] // used in the hot path loop, called only really once.
105    pub fn handle_opcode_error(&mut self, error: VMError) -> Result<ContextResult, VMError> {
106        if error.should_propagate() {
107            return Err(error);
108        }
109
110        // EIP-8037 (Amsterdam+): roll back this frame's state gas in LIFO order BEFORE
111        // zeroing gas on exceptional halt. Covers both revert and exceptional-halt paths
112        // (mirrors EELS `process_message`'s `refill_frame_state_gas` on Revert/ExceptionalHalt).
113        // Must run before the `&mut self.current_call_frame` borrow below since refill needs `&mut self`.
114        if self.env.config.fork >= Fork::Amsterdam {
115            let entry = self.current_call_frame.state_gas_used_at_entry;
116            self.refill_frame_state_gas(entry)?;
117        }
118
119        let callframe = &mut self.current_call_frame;
120
121        // Unless error is caused by Revert Opcode, consume all gas left.
122        if !error.is_revert_opcode() {
123            callframe.gas_remaining = 0;
124        }
125
126        #[expect(clippy::as_conversions, reason = "remaining gas conversion")]
127        let gas_used = callframe
128            .gas_limit
129            .checked_sub(callframe.gas_remaining as u64)
130            .ok_or(InternalError::Underflow)?;
131        Ok(ContextResult {
132            result: TxResult::Revert(error),
133            gas_used,
134            gas_spent: gas_used, // Will be updated in finalize_execution
135            output: std::mem::take(&mut callframe.output),
136        })
137    }
138
139    /// Handles external create transaction.
140    pub fn handle_create_transaction(&mut self) -> Result<Option<ContextResult>, VMError> {
141        let new_contract_address = self.current_call_frame.to;
142
143        // EIP-7928: Record contract address in BAL before collision check.
144        // Per EELS reference, the address is tracked even when the create collides.
145        if let Some(recorder) = self.db.bal_recorder.as_mut() {
146            recorder.record_touched_address(new_contract_address);
147        }
148
149        let new_account = self.get_account_mut(new_contract_address)?;
150
151        if new_account.create_would_collide() {
152            // EIP-8037: a collision returns before any opcode executes, so no execution
153            // state gas was charged via `increase_state_gas` (the only writer of
154            // `state_gas_spill` / `frame_state_gas_spilled`). Intrinsic state gas was added
155            // directly to `state_gas_used` in `add_intrinsic_gas` and never spills. There is
156            // therefore nothing for `refill_frame_state_gas` to roll back; the retained
157            // create-tx NEW_ACCOUNT refund in `finalize_execution` covers the account charge.
158            debug_assert_eq!(
159                self.state_gas_spill, 0,
160                "create collision must occur before any execution state gas spills"
161            );
162            debug_assert_eq!(
163                self.current_call_frame.frame_state_gas_spilled, 0,
164                "create collision must occur before any per-frame state gas spills"
165            );
166
167            // Per EIP-684: a tx-level CREATE collision burns the
168            // full forwarded execution gas as `regular_gas_used`. Zero `gas_remaining`
169            // so `raw_consumed = gas_limit` for the downstream regular-gas formula in
170            // `default_hook::refund_sender`; otherwise the post-intrinsic leftover
171            // leaks back to the sender and never reaches the regular dimension.
172            self.current_call_frame.gas_remaining = 0;
173            return Ok(Some(ContextResult {
174                result: TxResult::Revert(ExceptionalHalt::AddressAlreadyOccupied.into()),
175                gas_used: self.env.gas_limit,
176                gas_spent: self.env.gas_limit, // Will be updated in finalize_execution
177                output: Bytes::new(),
178            }));
179        }
180
181        // EIP-8037 (#3002): capture whether the create-tx target is already alive
182        // (exists and non-empty) BEFORE balance/nonce mutation, mirroring EELS
183        // `target_alive = is_account_alive(message.current_target)` (set in
184        // `process_message_call` only for the non-colliding deployable path).
185        // A non-colliding alive target must have balance > 0 (collision rules forbid
186        // code/nonce/storage), so `!is_empty()` matches `is_account_alive` semantics.
187        // Used in `finalize_execution` to refund the unconditional new-account state
188        // gas on a successful create-tx whose target already existed.
189        self.created_target_alive = !new_account.is_empty();
190
191        let value = self.current_call_frame.msg_value;
192        self.increase_account_balance(new_contract_address, value)?;
193
194        // EIP-7708: Emit transfer log for nonzero-value contract creation transactions.
195        // Origin is sender, new_contract_address is the recipient.
196        if self.env.config.fork >= Fork::Amsterdam && !value.is_zero() {
197            let log = create_eth_transfer_log(self.env.origin, new_contract_address, value);
198            self.substate.add_log(log);
199        }
200
201        self.increment_account_nonce(new_contract_address)?;
202
203        Ok(None)
204    }
205
206    /// Validates that the contract creation was successful, otherwise it returns an ExceptionalHalt.
207    fn validate_contract_creation(&mut self) -> Result<(), VMError> {
208        let fork = self.env.config.fork;
209        let code = &self.current_call_frame.output;
210
211        let code_length: u64 = code
212            .len()
213            .try_into()
214            .map_err(|_| InternalError::TypeConversion)?;
215
216        // 1. If the first byte of code is 0xEF
217        if code.first().is_some_and(|v| v == &EOF_PREFIX) {
218            return Err(ExceptionalHalt::InvalidContractPrefix.into());
219        }
220
221        // EIP-8037 (Amsterdam+): Per EELS process_create_message (bal@v5.4.0):
222        // 1. Size check first (reject oversized before any gas charges)
223        // 2. Keccak hash cost (regular gas)
224        // 3. State gas for code deposit
225        if fork >= Fork::Amsterdam {
226            // Size check BEFORE gas charges
227            if code_length > AMSTERDAM_MAX_CODE_SIZE {
228                return Err(ExceptionalHalt::ContractOutputTooBig.into());
229            }
230
231            let words = code_length.div_ceil(32);
232            let regular = words
233                .checked_mul(CODE_DEPOSIT_REGULAR_COST_PER_WORD)
234                .ok_or(InternalError::Overflow)?;
235            let state = code_length
236                .checked_mul(self.cost_per_state_byte)
237                .ok_or(InternalError::Overflow)?;
238
239            // Regular gas (keccak hash cost) before state gas
240            self.current_call_frame.increase_consumed_gas(regular)?;
241            if state > 0 {
242                self.increase_state_gas(state)?;
243            }
244        } else {
245            // Pre-Amsterdam: size check first, then regular gas charge
246            if code_length > MAX_CODE_SIZE {
247                return Err(ExceptionalHalt::ContractOutputTooBig.into());
248            }
249            let regular = code_length
250                .checked_mul(CODE_DEPOSIT_COST)
251                .ok_or(InternalError::Overflow)?;
252            self.current_call_frame.increase_consumed_gas(regular)?;
253        }
254
255        Ok(())
256    }
257}