Skip to main content

tempo_revm/
gas_params.rs

1use revm::{
2    context_interface::cfg::{GasId, GasParams},
3    primitives::OnceLock,
4};
5use tempo_chainspec::{
6    constants::gas::{SSTORE_CREATE_COST, SSTORE_SET_COST},
7    hardfork::TempoHardfork,
8};
9
10// TIP-1000 total gas costs (used by T1)
11const CONTRACT_CREATE_COST: u64 = 500_000;
12const NEW_ACCOUNT_COST: u64 = 250_000;
13const CODE_DEPOSIT_COST_T1: u64 = 1_000;
14const EIP7702_PER_EMPTY_ACCOUNT_COST_T1: u64 = 12_500;
15
16// TIP-1060 (T7): the SSTORE gas function charges only the 5,000-gas residual
17// (`SSTORE_SET_COST`) on a clean creation (`original == present == 0`). The
18// remaining 245,000-gas creditable portion of the TIP-1000 creation cost is
19// governed by the storage-credit hook (see `sstore_storage_credits`), so it is
20// no longer charged through the SSTORE gas function.
21
22// TIP-1016 regular gas (computational overhead) — matches pre-TIP-1000 EVM costs.
23// These values are "at least the pre-TIP-1000 (standard EVM) cost" per spec invariant 15.
24//
25// For SSTORE: revm decomposes the cost as sstore_static(WARM_STORAGE_READ=100) +
26// sstore_set_without_load_cost(20,000), with the cold-slot surcharge applied separately.
27const T4_SSTORE_SET_REGULAR: u64 = 20_000;
28const T4_NEW_ACCOUNT_REGULAR: u64 = 25_000;
29const T4_CREATE_REGULAR: u64 = 32_000;
30const T4_CODE_DEPOSIT_REGULAR: u64 = 200;
31
32// TIP-1016 state gas (permanent storage burden)
33const T4_SSTORE_SET_STATE: u64 = SSTORE_CREATE_COST - T4_SSTORE_SET_REGULAR; // 230,000
34const T4_NEW_ACCOUNT_STATE: u64 = NEW_ACCOUNT_COST - T4_NEW_ACCOUNT_REGULAR; // 225,000
35const T4_CREATE_STATE: u64 = CONTRACT_CREATE_COST - T4_CREATE_REGULAR; // 468,000
36const T4_CODE_DEPOSIT_STATE: u64 = 2_300;
37
38// TIP-1016 SSTORE set refund for 0→X→0 restoration (combined state + regular).
39// Spec: state_gas(230,000) + regular(GAS_STORAGE_UPDATE - GAS_COLD_SLOAD - GAS_WARM_ACCESS)
40//      = 230,000 + (20,000 - 2,100 - 100) = 247,800
41const T4_SSTORE_SET_REFUND: u64 = T4_SSTORE_SET_STATE + 17_800; // 230,000 + 17,800 = 247,800
42
43/// Tempo gas params override.
44///
45/// `amsterdam_eip8037_enabled` mirrors `CfgEnv::enable_amsterdam_eip8037` and gates the
46/// TIP-1016 regular/state gas split. When `false` on T1+, TIP-1000 (T1) costs are used,
47/// so TIP-1016 can be deferred independently of the T4 hardfork activation.
48#[inline]
49pub fn tempo_gas_params_with_amsterdam(
50    spec: TempoHardfork,
51    amsterdam_eip8037_enabled: bool,
52) -> GasParams {
53    if amsterdam_eip8037_enabled {
54        static TABLE: OnceLock<GasParams> = OnceLock::new();
55        return TABLE.get_or_init(amsterdam_gas_params).clone();
56    }
57
58    // TIP-1060 (T7+): the SSTORE creation cost drops to the 5k residual; the
59    // 245k creditable portion is handled by the storage-credit hook.
60    if spec.is_t7() {
61        static TABLE: OnceLock<GasParams> = OnceLock::new();
62        return TABLE.get_or_init(t7_gas_params).clone();
63    }
64
65    if spec.is_t1() {
66        static TABLE: OnceLock<GasParams> = OnceLock::new();
67        return TABLE.get_or_init(t1_gas_params).clone();
68    }
69
70    GasParams::new_spec(spec.into())
71}
72
73/// Builds the T7 gas table: TIP-1000 creation costs, but the SSTORE creation
74/// cost is lowered to the 5k residual (`SSTORE_SET_COST`) per TIP-1060.
75///
76/// revm charges this residual through `sstore_dynamic_gas` under the same
77/// `original == present == 0` condition as the upstream storage-set cost, so a
78/// dirty recreation (`x→0→y`) is charged neither the residual nor the base
79/// set cost. The 245k creditable portion is charged (or covered by a credit) by
80/// the storage-credit hook in `sstore_storage_credits`.
81fn t7_gas_params() -> GasParams {
82    // T7 starts from the TIP-1000 (T1) table so that every creation cost is inherited unchanged.
83    // TIP-1060 only touches the SSTORE creation, clear, and restore-to-original-zero refund
84    // entries overridden below; everything else (tx_create_cost, create, new_account_cost,
85    // code_deposit_cost, eip7702 costs, auth refund) is exactly as in `t1_gas_params`.
86    let mut gas_params = t1_gas_params();
87    gas_params.override_gas([
88        // SSTORE (zero -> non-zero): only the 5k residual; the 245k creditable portion is governed
89        // by the TIP-1060 storage-credit hook (T1 charged the full `SSTORE_CREATE_COST` here).
90        (GasId::sstore_set_without_load_cost(), SSTORE_SET_COST),
91        // Restore (non-zero -> zero) refund must not exceed the T7 residual. Important with
92        // TIP-1060 because the refund cap is removed. Otherwise, 0→x→0 could be refund-positive.
93        (GasId::sstore_set_refund(), SSTORE_SET_COST),
94        // TIP-1060: SSTORE_CLEARS_SCHEDULE = 0. The nonzero-to-zero clear is now handled by storage
95        // credit minting, so the legacy clearing refund is removed. Restore-to-original-nonzero
96        // refunds (sstore_reset_refund) remain at their upstream reset refund.
97        (GasId::sstore_clearing_slot_refund(), 0),
98    ]);
99    gas_params
100}
101
102/// Builds the Amsterdam gas table with the TIP-1016 regular/state split.
103fn amsterdam_gas_params() -> GasParams {
104    let mut gas_params = GasParams::new_spec(TempoHardfork::T4.into());
105    // TIP-1016: Split storage creation costs into regular gas + state gas.
106    // Regular gas (computational overhead) = at least pre-TIP-1000 EVM cost.
107    // State gas (permanent storage burden) = total - regular.
108    gas_params.override_gas([
109        // SSTORE (zero -> non-zero): 20k regular + 230k state
110        (GasId::sstore_set_without_load_cost(), T4_SSTORE_SET_REGULAR),
111        (GasId::sstore_set_state_gas(), T4_SSTORE_SET_STATE),
112        (GasId::sstore_set_refund(), T4_SSTORE_SET_REFUND),
113        // Contract metadata (CREATE base): 32k regular + 468k state
114        (GasId::tx_create_cost(), T4_CREATE_REGULAR),
115        (GasId::create(), T4_CREATE_REGULAR),
116        (GasId::create_state_gas(), T4_CREATE_STATE),
117        // Account creation: 25k regular + 225k state
118        (GasId::new_account_cost(), T4_NEW_ACCOUNT_REGULAR),
119        (GasId::new_account_state_gas(), T4_NEW_ACCOUNT_STATE),
120        (
121            GasId::new_account_cost_for_selfdestruct(),
122            T4_NEW_ACCOUNT_REGULAR,
123        ),
124        // Code deposit: 200 regular + 2,300 state per byte
125        (GasId::code_deposit_cost(), T4_CODE_DEPOSIT_REGULAR),
126        (GasId::code_deposit_state_gas(), T4_CODE_DEPOSIT_STATE),
127        // EIP-7702 delegation: 25k regular + 225k state = 250k per auth
128        (
129            GasId::tx_eip7702_per_empty_account_cost(),
130            T4_NEW_ACCOUNT_REGULAR,
131        ),
132        // Auth refund is disabled post-T1.
133        (GasId::tx_eip7702_auth_refund(), 0),
134        // For each auth revm charges new_account_state_gas + tx_eip7702_state_gas_bytecode state gas
135        //
136        // Per TIP-1016, we only need 225k unconditional state gas charge (another 250k is charged only
137        // if nonce is zero). Thus, we are zeroing the bytecode cost so that only new_account_state_gas (225k) is charged.
138        (GasId::tx_eip7702_state_gas_bytecode(), 0),
139    ]);
140    gas_params
141}
142
143/// Builds the T1+ gas table with TIP-1000 costs and no state gas split.
144fn t1_gas_params() -> GasParams {
145    let mut gas_params = GasParams::new_spec(TempoHardfork::T1.into());
146    // TIP-1000: All storage creation costs in regular gas (no state gas split).
147    gas_params.override_gas([
148        (GasId::sstore_set_without_load_cost(), SSTORE_CREATE_COST),
149        (GasId::tx_create_cost(), CONTRACT_CREATE_COST),
150        (GasId::create(), CONTRACT_CREATE_COST),
151        (GasId::new_account_cost(), NEW_ACCOUNT_COST),
152        (GasId::new_account_cost_for_selfdestruct(), NEW_ACCOUNT_COST),
153        (GasId::code_deposit_cost(), CODE_DEPOSIT_COST_T1),
154        (
155            GasId::tx_eip7702_per_empty_account_cost(),
156            EIP7702_PER_EMPTY_ACCOUNT_COST_T1,
157        ),
158        // Auth refund is disabled post-T1.
159        (GasId::tx_eip7702_auth_refund(), 0),
160    ]);
161    gas_params
162}
163
164/// Backward-compatible alias for [`tempo_gas_params_with_amsterdam`] with TIP-1016 disabled.
165///
166/// External consumers (e.g. foundry) that depend on the single-argument signature continue
167/// to work: TIP-1016 is opt-in via `tempo_gas_params_with_amsterdam(spec, true)`.
168#[inline]
169pub fn tempo_gas_params(spec: TempoHardfork) -> GasParams {
170    tempo_gas_params_with_amsterdam(spec, false)
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    #[test]
178    fn test_tempo_override_gas_params_are_cached() {
179        let t1 = tempo_gas_params_with_amsterdam(TempoHardfork::T1, false);
180        let t5 = tempo_gas_params_with_amsterdam(TempoHardfork::T5, false);
181        assert!(
182            std::ptr::eq(t1.table(), t5.table()),
183            "T1+ TIP-1000 gas params should share the cached table"
184        );
185
186        let amsterdam_t4 = tempo_gas_params_with_amsterdam(TempoHardfork::T4, true);
187        let amsterdam_t5 = tempo_gas_params_with_amsterdam(TempoHardfork::T5, true);
188        assert!(
189            std::ptr::eq(amsterdam_t4.table(), amsterdam_t5.table()),
190            "Amsterdam gas params should share the cached table"
191        );
192    }
193
194    /// TIP-1060 (T7): SSTORE creation charges only the 5k residual through the
195    /// gas function; other TIP-1000 creation costs are unchanged, and there is
196    /// no TIP-1016 state-gas split (production T7 runs with EIP-8037 disabled).
197    #[test]
198    fn test_t7_gas_params_sstore_residual() {
199        let gas_params = tempo_gas_params_with_amsterdam(TempoHardfork::T7, false);
200
201        // SSTORE creation cost drops to the 5k residual; the 245k creditable
202        // portion is charged by the storage-credit hook, not the gas function.
203        assert_eq!(
204            gas_params.get(GasId::sstore_set_without_load_cost()),
205            5_000,
206            "T7 SSTORE creation charges only the 5k residual"
207        );
208        assert!(
209            gas_params.get(GasId::sstore_set_without_load_cost())
210                >= gas_params.get(GasId::sstore_set_refund()),
211            "T7 restore-to-original-zero refund must not exceed the residual set charge"
212        );
213        assert_eq!(
214            gas_params.get(GasId::sstore_clearing_slot_refund()),
215            0,
216            "TIP-1060 removes the legacy SSTORE clearing refund"
217        );
218
219        // Other TIP-1000 creation costs are unchanged by TIP-1060.
220        assert_eq!(gas_params.get(GasId::new_account_cost()), 250_000);
221        assert_eq!(gas_params.get(GasId::tx_create_cost()), 500_000);
222        assert_eq!(gas_params.get(GasId::create()), 500_000);
223        assert_eq!(gas_params.get(GasId::code_deposit_cost()), 1_000);
224
225        // No TIP-1016 state-gas split: state gas params stay at upstream defaults.
226        let upstream = GasParams::new_spec(TempoHardfork::T7.into());
227        assert_eq!(
228            gas_params.get(GasId::sstore_set_state_gas()),
229            upstream.get(GasId::sstore_set_state_gas()),
230            "T7 (EIP-8037 disabled) must not split SSTORE into state gas"
231        );
232
233        // T7+ shares the cached table.
234        let t8 = tempo_gas_params_with_amsterdam(TempoHardfork::T8, false);
235        assert!(
236            std::ptr::eq(gas_params.table(), t8.table()),
237            "T7+ TIP-1060 gas params should share the cached table"
238        );
239    }
240
241    #[test]
242    fn test_t1_gas_params_no_state_gas_split() {
243        let gas_params = tempo_gas_params_with_amsterdam(TempoHardfork::T1, false);
244
245        // T1 has full 250k costs in regular gas, no state gas split
246        assert_eq!(
247            gas_params.get(GasId::sstore_set_without_load_cost()),
248            250_000
249        );
250        assert_eq!(gas_params.get(GasId::new_account_cost()), 250_000);
251        assert_eq!(gas_params.get(GasId::tx_create_cost()), 500_000);
252        assert_eq!(gas_params.get(GasId::create()), 500_000);
253        assert_eq!(gas_params.get(GasId::code_deposit_cost()), 1_000);
254
255        // State gas params should remain at upstream defaults (not Tempo-bumped)
256        let upstream = GasParams::new_spec(TempoHardfork::T1.into());
257        assert_eq!(
258            gas_params.get(GasId::sstore_set_state_gas()),
259            upstream.get(GasId::sstore_set_state_gas()),
260            "T1 should not override state gas params"
261        );
262        assert_eq!(
263            gas_params.get(GasId::new_account_state_gas()),
264            upstream.get(GasId::new_account_state_gas()),
265        );
266        assert_eq!(
267            gas_params.get(GasId::create_state_gas()),
268            upstream.get(GasId::create_state_gas()),
269        );
270    }
271
272    /// TIP-1016 spec table: regular/state gas splits must match the spec exactly.
273    ///
274    /// | Operation                      | Execution Gas | Storage Gas | Total   |
275    /// |--------------------------------|---------------|-------------|---------|
276    /// | Cold SSTORE (zero → non-zero)  | 22,200        | 230,000     | 252,200 |
277    /// | Account creation (nonce 0 → 1) | 25,000        | 225,000     | 250,000 |
278    /// | Contract metadata (CREATE)     | 32,000        | 468,000     | 500,000 |
279    /// | Contract code storage (/byte)  | 200           | 2,300       | 2,500   |
280    /// | EIP-7702 delegation (per auth) | 25,000        | 225,000     | 250,000 |
281    ///
282    /// Note: The cold SSTORE total keeps Berlin's access charging. In revm terms the
283    /// zero->non-zero write path is: warm read (100) + `sstore_set_without_load_cost` (20,000)
284    /// + cold slot surcharge (2,100) + state gas (230,000) = 252,200.
285    #[test]
286    fn test_t4_gas_params_splits_storage_costs() {
287        let gas_params = tempo_gas_params_with_amsterdam(TempoHardfork::T4, true);
288
289        // T4 execution gas (regular/computational overhead)
290        // SSTORE keeps revm's decomposed accounting: static(100) + sstore_set_without_load(20,000),
291        // with cold slot access (2,100) retained separately through `cold_storage_cost`.
292        assert_eq!(
293            gas_params.get(GasId::sstore_set_without_load_cost()),
294            20_000,
295            "SSTORE set_without_load matches the retained zero->non-zero write component"
296        );
297        assert_eq!(
298            gas_params.get(GasId::new_account_cost()),
299            25_000,
300            "Account creation regular gas per spec"
301        );
302        assert_eq!(
303            gas_params.get(GasId::new_account_cost_for_selfdestruct()),
304            25_000
305        );
306        assert_eq!(
307            gas_params.get(GasId::tx_create_cost()),
308            32_000,
309            "CREATE base regular gas per spec"
310        );
311        assert_eq!(
312            gas_params.get(GasId::create()),
313            32_000,
314            "CREATE base regular gas per spec"
315        );
316        assert_eq!(gas_params.get(GasId::code_deposit_cost()), 200);
317
318        // T4 state gas (permanent storage burden)
319        assert_eq!(
320            gas_params.get(GasId::sstore_set_state_gas()),
321            230_000,
322            "SSTORE state gas per spec"
323        );
324        assert_eq!(
325            gas_params.get(GasId::new_account_state_gas()),
326            225_000,
327            "Account creation state gas per spec"
328        );
329        assert_eq!(
330            gas_params.get(GasId::create_state_gas()),
331            468_000,
332            "CREATE base state gas per spec"
333        );
334        assert_eq!(gas_params.get(GasId::code_deposit_state_gas()), 2_300);
335
336        // EIP-7702 delegation: 25,000 regular + 225,000 state per auth
337        assert_eq!(
338            gas_params.get(GasId::tx_eip7702_per_empty_account_cost()),
339            25_000,
340            "EIP-7702 per auth total = 25k regular + 225k state per spec"
341        );
342        assert_eq!(
343            gas_params.tx_eip7702_per_empty_account_cost(),
344            250_000,
345            "EIP-7702 per auth state gas per spec"
346        );
347        assert_eq!(
348            gas_params.new_account_state_gas(),
349            225_000,
350            "EIP-7702 per auth state gas per spec"
351        );
352        assert_eq!(
353            gas_params.tx_eip7702_per_empty_account_cost() - gas_params.new_account_state_gas(),
354            25_000,
355            "EIP-7702 per auth regular gas = total - state = 25k"
356        );
357        assert_eq!(
358            gas_params.tx_eip7702_auth_refund_regular(),
359            0,
360            "TIP-1000: no refund for existing accounts on T1+"
361        );
362
363        // SSTORE set refund for 0→X→0 restoration (combined state + regular)
364        // Spec: state_gas(230,000) + regular(20,000 - 2,100 - 100 = 17,800) = 247,800
365        assert_eq!(
366            gas_params.get(GasId::sstore_set_refund()),
367            247_800,
368            "SSTORE set refund = state(230k) + regular(17.8k) per spec"
369        );
370    }
371
372    /// TIP-1016: Verify totals (regular + state) match the clarified spec table.
373    /// Note: SSTORE total comparison needs to account for revm decomposition and the cold-slot charge.
374    ///
375    /// T1 sstore_set_without_load_cost = 250,000 (full TIP-1000 cost as override).
376    /// T4 warm SSTORE = sstore_set_without_load_cost(20,000) + warm_read(100) + state(230,000) = 250,100.
377    /// T4 cold SSTORE = warm path + cold_slot_access(2,100) = 252,200.
378    #[test]
379    fn test_t4_totals_match_spec() {
380        let t4 = tempo_gas_params_with_amsterdam(TempoHardfork::T4, true);
381
382        // Warm SSTORE total: write component(20,000) + warm read(100) + state(230,000)
383        let warm_sstore_regular =
384            t4.get(GasId::sstore_set_without_load_cost()) + t4.warm_storage_read_cost();
385        assert_eq!(
386            warm_sstore_regular + t4.get(GasId::sstore_set_state_gas()),
387            250_100,
388            "warm SSTORE total must be 250,100"
389        );
390
391        // Cold SSTORE total: warm path + Berlin cold slot access(2,100)
392        let cold_sstore_regular = warm_sstore_regular + t4.cold_storage_cost();
393        assert_eq!(
394            cold_sstore_regular + t4.get(GasId::sstore_set_state_gas()),
395            252_200,
396            "cold SSTORE total must include Berlin cold slot access charging"
397        );
398
399        // New account: 25,000 + 225,000 = 250,000
400        assert_eq!(
401            t4.get(GasId::new_account_cost()) + t4.get(GasId::new_account_state_gas()),
402            250_000,
403            "new_account total must be 250,000"
404        );
405
406        // CREATE: 32,000 + 468,000 = 500,000
407        assert_eq!(
408            t4.get(GasId::create()) + t4.get(GasId::create_state_gas()),
409            500_000,
410            "CREATE total must be 500,000"
411        );
412
413        // Code deposit: 200 + 2,300 = 2,500/byte
414        assert_eq!(
415            t4.get(GasId::code_deposit_cost()) + t4.get(GasId::code_deposit_state_gas()),
416            2_500,
417            "code_deposit total must be 2,500/byte"
418        );
419
420        // EIP-7702: 25,000 regular + 225,000 state = 250,000 per auth
421        assert_eq!(
422            t4.tx_eip7702_per_empty_account_cost(),
423            250_000,
424            "EIP-7702 per auth total must be 250,000"
425        );
426    }
427}