revm_interpreter/gas/
calc.rs

1use super::constants::*;
2use crate::{num_words, tri, SStoreResult, SelfDestructResult, StateLoad};
3use context_interface::{
4    journaled_state::AccountLoad, transaction::AccessListItemTr as _, Transaction,
5};
6use primitives::{eip7702, hardfork::SpecId, U256};
7
8/// `SSTORE` opcode refund calculation.
9#[allow(clippy::collapsible_else_if)]
10#[inline]
11pub fn sstore_refund(spec_id: SpecId, vals: &SStoreResult) -> i64 {
12    if spec_id.is_enabled_in(SpecId::ISTANBUL) {
13        // EIP-3529: Reduction in refunds
14        let sstore_clears_schedule = if spec_id.is_enabled_in(SpecId::LONDON) {
15            (SSTORE_RESET - COLD_SLOAD_COST + ACCESS_LIST_STORAGE_KEY) as i64
16        } else {
17            REFUND_SSTORE_CLEARS
18        };
19        if vals.is_new_eq_present() {
20            0
21        } else {
22            if vals.is_original_eq_present() && vals.is_new_zero() {
23                sstore_clears_schedule
24            } else {
25                let mut refund = 0;
26
27                if !vals.is_original_zero() {
28                    if vals.is_present_zero() {
29                        refund -= sstore_clears_schedule;
30                    } else if vals.is_new_zero() {
31                        refund += sstore_clears_schedule;
32                    }
33                }
34
35                if vals.is_original_eq_new() {
36                    let (gas_sstore_reset, gas_sload) = if spec_id.is_enabled_in(SpecId::BERLIN) {
37                        (SSTORE_RESET - COLD_SLOAD_COST, WARM_STORAGE_READ_COST)
38                    } else {
39                        (SSTORE_RESET, sload_cost(spec_id, false))
40                    };
41                    if vals.is_original_zero() {
42                        refund += (SSTORE_SET - gas_sload) as i64;
43                    } else {
44                        refund += (gas_sstore_reset - gas_sload) as i64;
45                    }
46                }
47
48                refund
49            }
50        }
51    } else {
52        if !vals.is_present_zero() && vals.is_new_zero() {
53            REFUND_SSTORE_CLEARS
54        } else {
55            0
56        }
57    }
58}
59
60/// `CREATE2` opcode cost calculation.
61#[inline]
62pub const fn create2_cost(len: usize) -> Option<u64> {
63    CREATE.checked_add(tri!(cost_per_word(len, KECCAK256WORD)))
64}
65
66#[inline]
67const fn log2floor(value: U256) -> u64 {
68    let mut l: u64 = 256;
69    let mut i = 3;
70    loop {
71        if value.as_limbs()[i] == 0u64 {
72            l -= 64;
73        } else {
74            l -= value.as_limbs()[i].leading_zeros() as u64;
75            if l == 0 {
76                return l;
77            } else {
78                return l - 1;
79            }
80        }
81        if i == 0 {
82            break;
83        }
84        i -= 1;
85    }
86    l
87}
88
89/// `EXP` opcode cost calculation.
90#[inline]
91pub fn exp_cost(spec_id: SpecId, power: U256) -> Option<u64> {
92    if power.is_zero() {
93        Some(EXP)
94    } else {
95        // EIP-160: EXP cost increase
96        let gas_byte = U256::from(if spec_id.is_enabled_in(SpecId::SPURIOUS_DRAGON) {
97            50
98        } else {
99            10
100        });
101        let gas = U256::from(EXP)
102            .checked_add(gas_byte.checked_mul(U256::from(log2floor(power) / 8 + 1))?)?;
103
104        u64::try_from(gas).ok()
105    }
106}
107
108/// `*COPY` opcodes cost calculation.
109#[inline]
110pub const fn copy_cost_verylow(len: usize) -> Option<u64> {
111    copy_cost(VERYLOW, len)
112}
113
114/// `EXTCODECOPY` opcode cost calculation.
115#[inline]
116pub const fn extcodecopy_cost(spec_id: SpecId, len: usize, is_cold: bool) -> Option<u64> {
117    let base_gas = if spec_id.is_enabled_in(SpecId::BERLIN) {
118        warm_cold_cost(is_cold)
119    } else if spec_id.is_enabled_in(SpecId::TANGERINE) {
120        700
121    } else {
122        20
123    };
124    copy_cost(base_gas, len)
125}
126
127#[inline]
128pub const fn copy_cost(base_cost: u64, len: usize) -> Option<u64> {
129    base_cost.checked_add(tri!(cost_per_word(len, COPY)))
130}
131
132/// `LOG` opcode cost calculation.
133#[inline]
134pub const fn log_cost(n: u8, len: u64) -> Option<u64> {
135    tri!(LOG.checked_add(tri!(LOGDATA.checked_mul(len)))).checked_add(LOGTOPIC * n as u64)
136}
137
138/// `KECCAK256` opcode cost calculation.
139#[inline]
140pub const fn keccak256_cost(len: usize) -> Option<u64> {
141    KECCAK256.checked_add(tri!(cost_per_word(len, KECCAK256WORD)))
142}
143
144/// Calculate the cost of buffer per word.
145#[inline]
146pub const fn cost_per_word(len: usize, multiple: u64) -> Option<u64> {
147    multiple.checked_mul(num_words(len) as u64)
148}
149
150/// EIP-3860: Limit and meter initcode
151///
152/// Apply extra gas cost of 2 for every 32-byte chunk of initcode.
153///
154/// This cannot overflow as the initcode length is assumed to be checked.
155#[inline]
156pub const fn initcode_cost(len: usize) -> u64 {
157    let Some(cost) = cost_per_word(len, INITCODE_WORD_COST) else {
158        panic!("initcode cost overflow")
159    };
160    cost
161}
162
163/// `SLOAD` opcode cost calculation.
164#[inline]
165pub const fn sload_cost(spec_id: SpecId, is_cold: bool) -> u64 {
166    if spec_id.is_enabled_in(SpecId::BERLIN) {
167        if is_cold {
168            COLD_SLOAD_COST
169        } else {
170            WARM_STORAGE_READ_COST
171        }
172    } else if spec_id.is_enabled_in(SpecId::ISTANBUL) {
173        // EIP-1884: Repricing for trie-size-dependent opcodes
174        ISTANBUL_SLOAD_GAS
175    } else if spec_id.is_enabled_in(SpecId::TANGERINE) {
176        // EIP-150: Gas cost changes for IO-heavy operations
177        200
178    } else {
179        50
180    }
181}
182
183/// `SSTORE` opcode cost calculation.
184#[inline]
185pub fn sstore_cost(spec_id: SpecId, vals: &SStoreResult, is_cold: bool) -> u64 {
186    if spec_id.is_enabled_in(SpecId::BERLIN) {
187        // Berlin specification logic
188        let mut gas_cost = istanbul_sstore_cost::<WARM_STORAGE_READ_COST, WARM_SSTORE_RESET>(vals);
189
190        if is_cold {
191            gas_cost += COLD_SLOAD_COST;
192        }
193        gas_cost
194    } else if spec_id.is_enabled_in(SpecId::ISTANBUL) {
195        // Istanbul logic
196        istanbul_sstore_cost::<ISTANBUL_SLOAD_GAS, SSTORE_RESET>(vals)
197    } else {
198        // Frontier logic
199        frontier_sstore_cost(vals)
200    }
201}
202
203/// EIP-2200: Structured Definitions for Net Gas Metering
204#[inline]
205fn istanbul_sstore_cost<const SLOAD_GAS: u64, const SSTORE_RESET_GAS: u64>(
206    vals: &SStoreResult,
207) -> u64 {
208    if vals.is_new_eq_present() {
209        SLOAD_GAS
210    } else if vals.is_original_eq_present() && vals.is_original_zero() {
211        SSTORE_SET
212    } else if vals.is_original_eq_present() {
213        SSTORE_RESET_GAS
214    } else {
215        SLOAD_GAS
216    }
217}
218
219/// Frontier sstore cost just had two cases set and reset values.
220#[inline]
221fn frontier_sstore_cost(vals: &SStoreResult) -> u64 {
222    if vals.is_present_zero() && !vals.is_new_zero() {
223        SSTORE_SET
224    } else {
225        SSTORE_RESET
226    }
227}
228
229/// `SELFDESTRUCT` opcode cost calculation.
230#[inline]
231pub const fn selfdestruct_cost(spec_id: SpecId, res: StateLoad<SelfDestructResult>) -> u64 {
232    // EIP-161: State trie clearing (invariant-preserving alternative)
233    let should_charge_topup = if spec_id.is_enabled_in(SpecId::SPURIOUS_DRAGON) {
234        res.data.had_value && !res.data.target_exists
235    } else {
236        !res.data.target_exists
237    };
238
239    // EIP-150: Gas cost changes for IO-heavy operations
240    let selfdestruct_gas_topup = if spec_id.is_enabled_in(SpecId::TANGERINE) && should_charge_topup
241    {
242        25000
243    } else {
244        0
245    };
246
247    // EIP-150: Gas cost changes for IO-heavy operations
248    let selfdestruct_gas = if spec_id.is_enabled_in(SpecId::TANGERINE) {
249        5000
250    } else {
251        0
252    };
253
254    let mut gas = selfdestruct_gas + selfdestruct_gas_topup;
255    if spec_id.is_enabled_in(SpecId::BERLIN) && res.is_cold {
256        gas += COLD_ACCOUNT_ACCESS_COST
257    }
258    gas
259}
260
261/// Calculate call gas cost for the call instruction.
262///
263/// There is three types of gas.
264/// * Account access gas. after berlin it can be cold or warm.
265/// * Transfer value gas. If value is transferred and balance of target account is updated.
266/// * If account is not existing and needs to be created. After Spurious dragon
267///   this is only accounted if value is transferred.
268///
269/// account_load.is_empty will be accounted only if hardfork is SPURIOUS_DRAGON and
270/// there is transfer value.
271///
272/// This means that [`bytecode::opcode::EXTSTATICCALL`],
273/// [`bytecode::opcode::EXTDELEGATECALL`] that dont transfer value will not be
274/// effected by this field.
275///
276/// [`bytecode::opcode::CALL`], [`bytecode::opcode::EXTCALL`] use this field.
277///
278/// While [`bytecode::opcode::STATICCALL`], [`bytecode::opcode::DELEGATECALL`],
279/// [`bytecode::opcode::CALLCODE`] need to have this field hardcoded to false
280/// as they were present before SPURIOUS_DRAGON hardfork.
281#[inline]
282pub const fn call_cost(
283    spec_id: SpecId,
284    transfers_value: bool,
285    account_load: StateLoad<AccountLoad>,
286) -> u64 {
287    let is_empty = account_load.data.is_empty;
288    // Account access.
289    let mut gas = if spec_id.is_enabled_in(SpecId::BERLIN) {
290        warm_cold_cost_with_delegation(account_load)
291    } else if spec_id.is_enabled_in(SpecId::TANGERINE) {
292        // EIP-150: Gas cost changes for IO-heavy operations
293        700
294    } else {
295        40
296    };
297
298    // Transfer value cost
299    if transfers_value {
300        gas += CALLVALUE;
301    }
302
303    // New account cost
304    if is_empty {
305        // EIP-161: State trie clearing (invariant-preserving alternative)
306        if spec_id.is_enabled_in(SpecId::SPURIOUS_DRAGON) {
307            // Account only if there is value transferred.
308            if transfers_value {
309                gas += NEWACCOUNT;
310            }
311        } else {
312            gas += NEWACCOUNT;
313        }
314    }
315
316    gas
317}
318
319/// Berlin warm and cold storage access cost for account access.
320#[inline]
321pub const fn warm_cold_cost(is_cold: bool) -> u64 {
322    if is_cold {
323        COLD_ACCOUNT_ACCESS_COST
324    } else {
325        WARM_STORAGE_READ_COST
326    }
327}
328
329/// Berlin warm and cold storage access cost for account access.
330///
331/// If delegation is Some, add additional cost for delegation account load.
332#[inline]
333pub const fn warm_cold_cost_with_delegation(load: StateLoad<AccountLoad>) -> u64 {
334    let mut gas = warm_cold_cost(load.is_cold);
335    if let Some(is_cold) = load.data.is_delegate_account_cold {
336        gas += warm_cold_cost(is_cold);
337    }
338    gas
339}
340
341/// Memory expansion cost calculation for a given number of words.
342#[inline]
343pub const fn memory_gas(num_words: usize) -> u64 {
344    let num_words = num_words as u64;
345    MEMORY
346        .saturating_mul(num_words)
347        .saturating_add(num_words.saturating_mul(num_words) / 512)
348}
349
350/// Init and floor gas from transaction
351#[derive(Clone, Copy, Debug, Default)]
352pub struct InitialAndFloorGas {
353    /// Initial gas for transaction.
354    pub initial_gas: u64,
355    /// If transaction is a Call and Prague is enabled
356    /// floor_gas is at least amount of gas that is going to be spent.
357    pub floor_gas: u64,
358}
359
360/// Initial gas that is deducted for transaction to be included.
361/// Initial gas contains initial stipend gas, gas for access list and input data.
362///
363/// # Returns
364///
365/// - Intrinsic gas
366/// - Number of tokens in calldata
367pub fn calculate_initial_tx_gas(
368    spec_id: SpecId,
369    input: &[u8],
370    is_create: bool,
371    access_list_accounts: u64,
372    access_list_storages: u64,
373    authorization_list_num: u64,
374) -> InitialAndFloorGas {
375    let mut gas = InitialAndFloorGas::default();
376
377    // Initdate stipend
378    let tokens_in_calldata = get_tokens_in_calldata(input, spec_id.is_enabled_in(SpecId::ISTANBUL));
379    gas.initial_gas += tokens_in_calldata * STANDARD_TOKEN_COST;
380
381    // Get number of access list account and storages.
382    gas.initial_gas += access_list_accounts * ACCESS_LIST_ADDRESS;
383    gas.initial_gas += access_list_storages * ACCESS_LIST_STORAGE_KEY;
384
385    // Base stipend
386    gas.initial_gas += if is_create {
387        if spec_id.is_enabled_in(SpecId::HOMESTEAD) {
388            // EIP-2: Homestead Hard-fork Changes
389            53000
390        } else {
391            21000
392        }
393    } else {
394        21000
395    };
396
397    // EIP-3860: Limit and meter initcode
398    // Init code stipend for bytecode analysis
399    if spec_id.is_enabled_in(SpecId::SHANGHAI) && is_create {
400        gas.initial_gas += initcode_cost(input.len())
401    }
402
403    // EIP-7702
404    if spec_id.is_enabled_in(SpecId::PRAGUE) {
405        gas.initial_gas += authorization_list_num * eip7702::PER_EMPTY_ACCOUNT_COST;
406
407        // Calculate gas floor for EIP-7623
408        gas.floor_gas = calc_tx_floor_cost(tokens_in_calldata);
409    }
410
411    gas
412}
413
414/// Initial gas that is deducted for transaction to be included.
415/// Initial gas contains initial stipend gas, gas for access list and input data.
416///
417/// # Returns
418///
419/// - Intrinsic gas
420/// - Number of tokens in calldata
421pub fn calculate_initial_tx_gas_for_tx(tx: impl Transaction, spec: SpecId) -> InitialAndFloorGas {
422    let (accounts, storages) = tx
423        .access_list()
424        .map(|al| {
425            al.fold((0, 0), |(mut num_accounts, mut num_storage_slots), item| {
426                num_accounts += 1;
427                num_storage_slots += item.storage_slots().count();
428
429                (num_accounts, num_storage_slots)
430            })
431        })
432        .unwrap_or_default();
433
434    calculate_initial_tx_gas(
435        spec,
436        tx.input(),
437        tx.kind().is_create(),
438        accounts as u64,
439        storages as u64,
440        tx.authorization_list_len() as u64,
441    )
442}
443
444/// Retrieve the total number of tokens in calldata.
445#[inline]
446pub fn get_tokens_in_calldata(input: &[u8], is_istanbul: bool) -> u64 {
447    let zero_data_len = input.iter().filter(|v| **v == 0).count() as u64;
448    let non_zero_data_len = input.len() as u64 - zero_data_len;
449    let non_zero_data_multiplier = if is_istanbul {
450        // EIP-2028: Transaction data gas cost reduction
451        NON_ZERO_BYTE_MULTIPLIER_ISTANBUL
452    } else {
453        NON_ZERO_BYTE_MULTIPLIER
454    };
455    zero_data_len + non_zero_data_len * non_zero_data_multiplier
456}
457
458/// Calculate the transaction cost floor as specified in EIP-7623.
459#[inline]
460pub fn calc_tx_floor_cost(tokens_in_calldata: u64) -> u64 {
461    tokens_in_calldata * TOTAL_COST_FLOOR_PER_TOKEN + 21_000
462}