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                // Consume all gas because error was exceptional.
57                let callframe = &mut self.current_call_frame;
58                callframe.gas_remaining = 0;
59
60                #[expect(clippy::as_conversions, reason = "remaining gas conversion")]
61                let gas_used = callframe
62                    .gas_limit
63                    .checked_sub(callframe.gas_remaining as u64)
64                    .ok_or(InternalError::Underflow)?;
65                return Ok(ContextResult {
66                    result: TxResult::Revert(error),
67                    gas_used,
68                    gas_spent: gas_used, // Will be updated in finalize_execution
69                    output: Bytes::new(),
70                });
71            }
72
73            // Set bytecode to the newly created contract.
74            let contract_address = self.current_call_frame.to;
75            let code = self.current_call_frame.output.clone();
76            self.update_account_bytecode(contract_address, Code::from_bytecode(code, self.crypto))?;
77        }
78
79        #[expect(clippy::as_conversions, reason = "remaining gas conversion")]
80        let gas_used = {
81            let callframe = &mut self.current_call_frame;
82            callframe
83                .gas_limit
84                .checked_sub(callframe.gas_remaining as u64)
85                .ok_or(InternalError::Underflow)?
86        };
87        Ok(ContextResult {
88            result: TxResult::Success,
89            gas_used,
90            gas_spent: gas_used, // Will be updated in finalize_execution
91            output: std::mem::take(&mut self.current_call_frame.output),
92        })
93    }
94
95    #[cold] // used in the hot path loop, called only really once.
96    pub fn handle_opcode_error(&mut self, error: VMError) -> Result<ContextResult, VMError> {
97        if error.should_propagate() {
98            return Err(error);
99        }
100
101        let callframe = &mut self.current_call_frame;
102
103        // Unless error is caused by Revert Opcode, consume all gas left.
104        if !error.is_revert_opcode() {
105            callframe.gas_remaining = 0;
106        }
107
108        #[expect(clippy::as_conversions, reason = "remaining gas conversion")]
109        let gas_used = callframe
110            .gas_limit
111            .checked_sub(callframe.gas_remaining as u64)
112            .ok_or(InternalError::Underflow)?;
113        Ok(ContextResult {
114            result: TxResult::Revert(error),
115            gas_used,
116            gas_spent: gas_used, // Will be updated in finalize_execution
117            output: std::mem::take(&mut callframe.output),
118        })
119    }
120
121    /// Handles external create transaction.
122    pub fn handle_create_transaction(&mut self) -> Result<Option<ContextResult>, VMError> {
123        let new_contract_address = self.current_call_frame.to;
124
125        // EIP-7928: Record contract address in BAL before collision check.
126        // Per EELS reference, the address is tracked even when the create collides.
127        if let Some(recorder) = self.db.bal_recorder.as_mut() {
128            recorder.record_touched_address(new_contract_address);
129        }
130
131        let new_account = self.get_account_mut(new_contract_address)?;
132
133        if new_account.create_would_collide() {
134            // Per EIP-684: a tx-level CREATE collision burns the
135            // full forwarded execution gas as `regular_gas_used`. Zero `gas_remaining`
136            // so `raw_consumed = gas_limit` for the downstream regular-gas formula in
137            // `default_hook::refund_sender`; otherwise the post-intrinsic leftover
138            // leaks back to the sender and never reaches the regular dimension.
139            self.current_call_frame.gas_remaining = 0;
140            return Ok(Some(ContextResult {
141                result: TxResult::Revert(ExceptionalHalt::AddressAlreadyOccupied.into()),
142                gas_used: self.env.gas_limit,
143                gas_spent: self.env.gas_limit, // Will be updated in finalize_execution
144                output: Bytes::new(),
145            }));
146        }
147
148        let value = self.current_call_frame.msg_value;
149        self.increase_account_balance(new_contract_address, value)?;
150
151        // EIP-7708: Emit transfer log for nonzero-value contract creation transactions.
152        // Origin is sender, new_contract_address is the recipient.
153        if self.env.config.fork >= Fork::Amsterdam && !value.is_zero() {
154            let log = create_eth_transfer_log(self.env.origin, new_contract_address, value);
155            self.substate.add_log(log);
156        }
157
158        self.increment_account_nonce(new_contract_address)?;
159
160        Ok(None)
161    }
162
163    /// Validates that the contract creation was successful, otherwise it returns an ExceptionalHalt.
164    fn validate_contract_creation(&mut self) -> Result<(), VMError> {
165        let fork = self.env.config.fork;
166        let code = &self.current_call_frame.output;
167
168        let code_length: u64 = code
169            .len()
170            .try_into()
171            .map_err(|_| InternalError::TypeConversion)?;
172
173        // 1. If the first byte of code is 0xEF
174        if code.first().is_some_and(|v| v == &EOF_PREFIX) {
175            return Err(ExceptionalHalt::InvalidContractPrefix.into());
176        }
177
178        // EIP-8037 (Amsterdam+): Per EELS process_create_message (bal@v5.4.0):
179        // 1. Size check first (reject oversized before any gas charges)
180        // 2. Keccak hash cost (regular gas)
181        // 3. State gas for code deposit
182        if fork >= Fork::Amsterdam {
183            // Size check BEFORE gas charges
184            if code_length > AMSTERDAM_MAX_CODE_SIZE {
185                return Err(ExceptionalHalt::ContractOutputTooBig.into());
186            }
187
188            let words = code_length.div_ceil(32);
189            let regular = words
190                .checked_mul(CODE_DEPOSIT_REGULAR_COST_PER_WORD)
191                .ok_or(InternalError::Overflow)?;
192            let state = code_length
193                .checked_mul(self.cost_per_state_byte)
194                .ok_or(InternalError::Overflow)?;
195
196            // Regular gas (keccak hash cost) before state gas
197            self.current_call_frame.increase_consumed_gas(regular)?;
198            if state > 0 {
199                self.increase_state_gas(state)?;
200            }
201        } else {
202            // Pre-Amsterdam: size check first, then regular gas charge
203            if code_length > MAX_CODE_SIZE {
204                return Err(ExceptionalHalt::ContractOutputTooBig.into());
205            }
206            let regular = code_length
207                .checked_mul(CODE_DEPOSIT_COST)
208                .ok_or(InternalError::Overflow)?;
209            self.current_call_frame.increase_consumed_gas(regular)?;
210        }
211
212        Ok(())
213    }
214}