Skip to main content

tempo_revm/
gas_credits.rs

1//! TIP-1060 specific implementations.
2
3use crate::{
4    TempoInvalidTransaction,
5    evm::{TempoContext, TempoEvm},
6};
7use alloy_evm::Database;
8use alloy_primitives::{Address, U256};
9use revm::{
10    context::{Host as _, JournalTr, result::EVMError},
11    context_interface::cfg::GasParams,
12    interpreter::{
13        Gas, InstructionContext, InstructionResult, SStoreResult, StateLoad,
14        gas::GasTracker,
15        instructions::host::{sstore_default_gas_accounting, sstore_with_gas_accounting},
16        interpreter::EthInterpreter,
17    },
18};
19use tempo_chainspec::constants::gas::STORAGE_CREDIT_VALUE;
20use tempo_precompiles::{
21    STORAGE_CREDITS_ADDRESS,
22    storage::FromWord,
23    storage_credits::{StorageCreditsBackend, TransientState, sstore_storage_credits},
24};
25
26/// Applies storage-credit settlement at the end of a transaction.
27///
28/// During execution, each account's transaction-local mode and pending `Refund` creations are
29/// stored in one transient word at the same key as its persistent balance. At end-of-transaction,
30/// entries with non-zero pending creations are settled against the same account's persistent
31/// storage credit balance, consuming up to `min(pending, balance)` credits and refunding one fixed
32/// storage credit value per credit. Mode-only transient entries are ignored.
33pub fn apply_refund<DB: Database, I>(
34    evm: &mut TempoEvm<DB, I>,
35    gas: &mut Gas,
36) -> Result<(), EVMError<DB::Error, TempoInvalidTransaction>> {
37    if !evm.cfg.spec.is_t7() {
38        return Ok(());
39    }
40
41    let journal = &mut evm.inner.ctx.journaled_state;
42
43    // Take the tx-local storage-credit slots so we can settle them while mutating the journal.
44    // This is safe cause refunds are applied in post-execution.
45    let Some(slots) = journal.transient_storage.remove(&STORAGE_CREDITS_ADDRESS) else {
46        return Ok(());
47    };
48
49    let mut refunds = 0i64;
50    for (key, word) in slots {
51        let transient_state =
52            TransientState::try_from(word).map_err(|err| EVMError::Custom(err.to_string()))?;
53        let pending = transient_state.pending_refunds;
54        if pending == 0 {
55            continue;
56        }
57
58        // SLOAD the current persistent balance and settle pending refund-eligible creations against it.
59        let old_word = journal.sload(STORAGE_CREDITS_ADDRESS, key)?.data;
60        let mut balance =
61            u64::from_word(old_word).map_err(|err| EVMError::Custom(err.to_string()))?;
62        let settled = pending.min(balance);
63
64        if settled == 0 {
65            continue;
66        }
67
68        // SSTORE the post-settlement balance back into persistent storage.
69        balance -= settled;
70        refunds += settled as i64;
71
72        let new_word = U256::from(balance);
73        debug_assert_ne!(new_word, old_word);
74
75        journal.sstore(STORAGE_CREDITS_ADDRESS, key, new_word)?;
76    }
77
78    // Refund storage credit value per settled credit.
79    gas.record_refund(refunds.saturating_mul(STORAGE_CREDIT_VALUE as i64));
80
81    Ok(())
82}
83
84/// Opcode-level [`StorageCreditsBackend`] adapter over an [`InstructionContext`].
85///
86/// Bridges the revm host/interpreter to the backend-agnostic [`sstore_storage_credits`] so the
87/// SSTORE opcode runs the same TIP-1060 storage credits policy as precompile storage writes.
88struct StorageCreditsContext<'a, DB: Database> {
89    context: &'a mut TempoContext<DB>,
90    gas_tracker: &'a mut GasTracker,
91}
92
93impl<DB: Database> StorageCreditsBackend for StorageCreditsContext<'_, DB> {
94    type Error = InstructionResult;
95
96    #[inline]
97    fn gas_params(&self) -> &GasParams {
98        self.context.gas_params()
99    }
100
101    #[inline]
102    fn gas_tracker(&mut self) -> &mut GasTracker {
103        self.gas_tracker
104    }
105
106    #[inline]
107    fn sload(
108        &mut self,
109        address: Address,
110        key: U256,
111        skip_cold_load: bool,
112    ) -> Result<StateLoad<U256>, Self::Error> {
113        self.context
114            .load_account_info_skip_cold_load(address, false, false)?;
115        Ok(self
116            .context
117            .sload_skip_cold_load(address, key, skip_cold_load)?)
118    }
119
120    #[inline]
121    fn sstore(
122        &mut self,
123        address: Address,
124        key: U256,
125        value: U256,
126        skip_cold_load: bool,
127    ) -> Result<StateLoad<SStoreResult>, Self::Error> {
128        Ok(self
129            .context
130            .sstore_skip_cold_load(address, key, value, skip_cold_load)?)
131    }
132
133    #[inline]
134    fn tload(&mut self, address: Address, key: U256) -> U256 {
135        self.context.tload(address, key)
136    }
137
138    #[inline]
139    fn tstore(&mut self, address: Address, key: U256, value: U256) {
140        self.context.tstore(address, key, value);
141    }
142}
143
144/// Tempo SSTORE instruction with TIP-1060 storage-credit accounting.
145pub(crate) fn sstore<DB: Database>(
146    context: InstructionContext<'_, TempoContext<DB>, EthInterpreter>,
147) -> Result<(), InstructionResult> {
148    sstore_with_gas_accounting(context, |context, owner, values| {
149        {
150            let InstructionContext { interpreter, host } = context;
151            sstore_storage_credits(
152                &mut StorageCreditsContext {
153                    context: host,
154                    gas_tracker: interpreter.gas.tracker_mut(),
155                },
156                owner,
157                values,
158            )?;
159        }
160
161        // Storage-credit hook only handles TIP-1060 bookkeeping + state gas. Keep default
162        // gas/refunds for cold, update, and residual costs. T7 gas table ensures no double-charge.
163        sstore_default_gas_accounting(context, owner, values)
164    })
165}