Skip to main content

tempo_precompiles/tip1060_storage_credits/
gas_state.rs

1//! Backend-agnostic TIP-1060 SSTORE storage credits accounting.
2//!
3//! [`sstore_storage_credits`] implements the storage credits policy that runs
4//! after a storage slot is written. It is driven through the [`StorageCreditsBackend`]
5//! trait so the exact same logic can be reused from two places:
6//!
7//! - the opcode-level SSTORE hook in `tempo-revm` (`TempoGasState`), and
8//! - [`EvmPrecompileStorageProvider`](crate::storage::evm::EvmPrecompileStorageProvider)
9//!   so precompile-driven storage writes honor the same accounting.
10
11use super::{CreditMode, TIP1060StorageCredits, TransientState};
12use crate::storage::FromWord;
13use alloy::primitives::{Address, IntoLogData, LogData, U256};
14use revm::{
15    context_interface::cfg::GasParams,
16    interpreter::{InstructionResult, SStoreResult, StateLoad, gas::GasTracker},
17};
18use tempo_chainspec::constants::gas::STORAGE_CREDIT_VALUE;
19use tempo_contracts::precompiles::{STORAGE_CREDITS_ADDRESS, TIP1060StorageCreditsEvent};
20
21/// Error mapping required by storage credit accounting.
22pub trait StorageCreditsError: Sized {
23    fn out_of_gas() -> Self;
24    fn fatal_external() -> Self;
25}
26
27impl StorageCreditsError for InstructionResult {
28    fn out_of_gas() -> Self {
29        Self::OutOfGas
30    }
31
32    fn fatal_external() -> Self {
33        Self::FatalExternalError
34    }
35}
36
37/// Minimal journal/gas operations required by storage credit accounting.
38pub trait StorageCreditsBackend {
39    type Error: StorageCreditsError;
40
41    /// Gas parameters for the active spec.
42    fn gas_params(&self) -> &GasParams;
43
44    /// Gas tracker for the active execution context.
45    fn gas_tracker(&mut self) -> &mut GasTracker;
46
47    /// Charges `cost` regular gas, returning [`out_of_gas`](StorageCreditsError::out_of_gas) if insufficient.
48    #[inline]
49    fn charge_gas(&mut self, cost: u64) -> Result<(), Self::Error> {
50        self.gas_tracker()
51            .record_regular_cost(cost)
52            .then_some(())
53            .ok_or_else(Self::Error::out_of_gas)
54    }
55
56    /// SLOAD `address[key]`, optionally skipping the cold load.
57    fn sload(
58        &mut self,
59        address: Address,
60        key: U256,
61        skip_cold_load: bool,
62    ) -> Result<StateLoad<U256>, Self::Error>;
63
64    /// SSTORE `address[key]`.
65    fn sstore(
66        &mut self,
67        address: Address,
68        key: U256,
69        value: U256,
70        skip_cold_load: bool,
71    ) -> Result<StateLoad<SStoreResult>, Self::Error>;
72
73    /// TLOAD `address[key]`.
74    fn tload(&mut self, address: Address, key: U256) -> U256;
75
76    /// TSTORE `address[key] = value`.
77    fn tstore(&mut self, address: Address, key: U256, value: U256);
78
79    /// Emits `event` from `address`.
80    fn emit_event(&mut self, address: Address, event: LogData) -> Result<(), Self::Error>;
81}
82
83#[inline]
84fn emit_mode_updated<B: StorageCreditsBackend>(
85    backend: &mut B,
86    account: Address,
87    new_mode: CreditMode,
88) -> Result<(), B::Error> {
89    let event = TIP1060StorageCreditsEvent::mode_updated(account, new_mode.into());
90    backend.emit_event(STORAGE_CREDITS_ADDRESS, event.into_log_data())
91}
92
93#[inline]
94fn store_credit_state<B: StorageCreditsBackend>(
95    backend: &mut B,
96    key: U256,
97    state: TransientState,
98) -> Result<(), B::Error> {
99    backend.tstore(STORAGE_CREDITS_ADDRESS, key, state.into());
100    Ok(())
101}
102
103/// Applies TIP-1060 storage credits after a single SSTORE has been journaled.
104///
105/// Returns whether to skip normal dynamic/state gas and/or refund accounting.
106pub fn sstore_storage_credits<B: StorageCreditsBackend>(
107    backend: &mut B,
108    owner: Address,
109    caller_state_load: &StateLoad<SStoreResult>,
110) -> Result<(), B::Error> {
111    let values = &caller_state_load.data;
112
113    // Only account for storage credits when the slot crosses the zero boundary (x→0 or 0→x).
114    // If both values are zero or non-zero, slot occupancy is unchanged, so skip credits accounting.
115    if values.is_present_zero() == values.is_new_zero() {
116        return Ok(());
117    }
118
119    // Storage-credit precompile state is used for protocol bookkeeping. Because of that,
120    // always skips TIP-1000 + TIP-1060 self-accounting and charge only update gas.
121    if owner == STORAGE_CREDITS_ADDRESS {
122        return Ok(());
123    }
124
125    // Load the persistent storage credit balance for the storage-owning account.
126    let warm_storage_read_cost = backend.gas_params().warm_storage_read_cost();
127    backend.charge_gas(warm_storage_read_cost)?;
128
129    let account_slot = TIP1060StorageCredits::slot(owner);
130    let additional_cold_cost = backend.gas_params().cold_storage_additional_cost();
131    let skip_cold = backend.gas_tracker().remaining() < additional_cold_cost;
132    let storage_credit_state_load =
133        backend.sload(STORAGE_CREDITS_ADDRESS, account_slot, skip_cold)?;
134    if storage_credit_state_load.is_cold {
135        backend.charge_gas(additional_cold_cost)?;
136    }
137
138    let mut credit =
139        u64::from_word(storage_credit_state_load.data).map_err(|_| B::Error::fatal_external())?;
140
141    let mut was_changed = false;
142    if values.is_new_zero() {
143        // x→0: storage deletion always mints a new credit.
144        credit = credit.saturating_add(1);
145        was_changed = true;
146    } else {
147        // 0→x: storage creation.
148        // This hook manages the 245k creditable gas, independent of the original value.
149        // revm's SSTORE function adds the 5k residual for clean writes (`original == present == 0`).
150        let mut transient_state: TransientState = backend
151            .tload(STORAGE_CREDITS_ADDRESS, account_slot)
152            .try_into()
153            .map_err(|_| B::Error::fatal_external())?;
154
155        match transient_state.mode {
156            CreditMode::Direct if credit > 0 && transient_state.budget > 0 => {
157                // Consume one credit to cover the 245k creditable portion.
158                credit -= 1;
159                was_changed = true;
160
161                // An unlimited budget is never decremented.
162                if transient_state.budget != u64::MAX {
163                    transient_state.budget -= 1;
164                    if transient_state.budget == 0 {
165                        // When budget is exhausted, switch to `Preserve` mode.
166                        transient_state.mode = CreditMode::Preserve;
167                        emit_mode_updated(backend, owner, CreditMode::Preserve)?;
168                    }
169                    store_credit_state(backend, account_slot, transient_state)?;
170                }
171            }
172            CreditMode::Direct => {
173                // If no credit available, charge the 245k creditable portion as gas.
174                if transient_state.budget == 0 {
175                    // When budget is exhausted, switch to `Preserve` mode.
176                    transient_state.mode = CreditMode::Preserve;
177                    store_credit_state(backend, account_slot, transient_state)?;
178                    emit_mode_updated(backend, owner, CreditMode::Preserve)?;
179                }
180                backend.charge_gas(STORAGE_CREDIT_VALUE)?;
181            }
182            CreditMode::Preserve => {
183                // Always charge the 245k creditable portion as gas without consuming credits.
184                backend.charge_gas(STORAGE_CREDIT_VALUE)?;
185            }
186            CreditMode::Refund => {
187                // Charge the 245k creditable portion upfront and record a pending refund-eligible
188                // creation, settled at end-of-transaction.
189                backend.charge_gas(STORAGE_CREDIT_VALUE)?;
190                transient_state.pending_refunds = transient_state.pending_refunds.saturating_add(1);
191                store_credit_state(backend, account_slot, transient_state)?;
192            }
193        }
194    }
195
196    if was_changed {
197        // Cold load is already checked above when we loaded the storage credits account.
198        let result = backend
199            .sstore(
200                STORAGE_CREDITS_ADDRESS,
201                account_slot,
202                U256::from(credit),
203                false,
204            )?
205            .data;
206
207        // Only when change happens charge additional gas.
208        if result.new_values_changes_present() && result.is_original_eq_present() {
209            backend.charge_gas(backend.gas_params().sstore_reset_without_cold_load_cost())?;
210        };
211    }
212
213    Ok(())
214}