Skip to main content

tempo_precompiles/storage/
mod.rs

1//! EVM storage abstraction layer for Tempo precompile contracts.
2//!
3//! Provides traits and types for reading/writing contract state from EVM storage,
4//! including persistent (SLOAD/SSTORE) and transient (TLOAD/TSTORE) operations.
5
6pub mod actions;
7pub use actions::{StorageAction, StorageActions};
8
9pub mod evm;
10pub mod hashmap;
11
12pub mod thread_local;
13use alloy::primitives::keccak256;
14pub use thread_local::{CheckpointGuard, StorageCtx};
15
16mod types;
17pub use types::*;
18
19pub mod packing;
20pub use packing::FieldLocation;
21pub use types::mapping as slots;
22
23use alloy::primitives::{Address, B256, LogData, Signature, U256};
24use revm::{
25    context::journaled_state::JournalCheckpoint,
26    interpreter::gas::{KECCAK256, KECCAK256WORD},
27    state::{AccountInfo, Bytecode},
28};
29use tempo_chainspec::hardfork::TempoHardfork;
30
31use crate::error::{Result, TempoPrecompileError};
32
33/// Low-level storage provider for interacting with the EVM.
34///
35/// # Implementations
36///
37/// - `EvmPrecompileStorageProvider` - Production EVM storage
38/// - `HashMapStorageProvider` - Test storage
39///
40/// # Sync with `[StorageCtx]`
41///
42/// `StorageCtx` mirrors these methods with split mutability for read (staticcall) vs write (call).
43/// When adding new methods here, remember to add corresponding methods to `StorageCtx`.
44pub trait PrecompileStorageProvider {
45    /// Returns the chain ID.
46    fn chain_id(&self) -> u64;
47
48    /// Returns the current block timestamp.
49    fn timestamp(&self) -> U256;
50
51    /// Returns the current block beneficiary (coinbase).
52    fn beneficiary(&self) -> Address;
53
54    /// Returns the current block number.
55    fn block_number(&self) -> u64;
56
57    /// Sets the bytecode at the given address.
58    fn set_code(&mut self, address: Address, code: Bytecode) -> Result<()>;
59
60    /// Executes a closure with access to the account info for the given address.
61    fn with_account_info(
62        &mut self,
63        address: Address,
64        f: &mut dyn FnMut(&AccountInfo),
65    ) -> Result<()>;
66
67    /// Performs an SLOAD operation (persistent storage read).
68    fn sload(&mut self, address: Address, key: U256) -> Result<U256>;
69
70    /// Performs a TLOAD operation (transient storage read).
71    fn tload(&mut self, address: Address, key: U256) -> Result<U256>;
72
73    /// Performs an SSTORE operation (persistent storage write).
74    fn sstore(&mut self, address: Address, key: U256, value: U256) -> Result<()>;
75
76    /// Increments a persistent storage slot by `delta`.
77    ///
78    /// Intentionally returns no post-increment value, preserving `sinc` as a semantic
79    /// storage delta rather than an observation point that callers can branch on.
80    fn sinc(&mut self, address: Address, key: U256, delta: U256) -> Result<()> {
81        let value = self
82            .sload(address, key)?
83            .checked_add(delta)
84            .ok_or_else(TempoPrecompileError::under_overflow)?;
85        self.sstore(address, key, value)
86    }
87
88    /// Decrements a persistent storage slot by `delta`.
89    ///
90    /// Intentionally returns no post-decrement value, preserving `sdec` as a semantic
91    /// storage delta rather than an observation point that callers can branch on.
92    fn sdec(&mut self, address: Address, key: U256, delta: U256) -> Result<()> {
93        let current = self.sload(address, key)?;
94        let value = current
95            .checked_sub(delta)
96            .ok_or_else(|| TempoPrecompileError::storage_delta_underflow(current))?;
97        self.sstore(address, key, value)
98    }
99
100    /// Performs a TSTORE operation (transient storage write).
101    fn tstore(&mut self, address: Address, key: U256, value: U256) -> Result<()>;
102
103    /// Emits an event from the given contract address.
104    fn emit_event(&mut self, address: Address, event: LogData) -> Result<()>;
105
106    /// Deducts gas from the remaining gas and returns an error if insufficient.
107    fn deduct_gas(&mut self, gas: u64) -> Result<()>;
108
109    /// Add refund to the refund gas counter.
110    fn refund_gas(&mut self, gas: i64);
111
112    /// Returns the gas limit for this precompile call.
113    fn gas_limit(&self) -> u64;
114
115    /// Returns the gas used so far.
116    fn gas_used(&self) -> u64;
117
118    /// Returns the state-creating gas used so far (cold SSTORE zero->non-zero, code deposit).
119    fn state_gas_used(&self) -> u64;
120
121    /// Returns the gas refunded so far.
122    fn gas_refunded(&self) -> i64;
123
124    /// Returns the state gas reservoir.
125    fn reservoir(&self) -> u64;
126
127    /// Returns the currently active hardfork.
128    fn spec(&self) -> TempoHardfork;
129
130    /// Mirrors `CfgEnv::enable_amsterdam_eip8037`. Used by precompiles to gate the TIP-1016
131    /// regular/state gas split independently of the active hardfork.
132    fn amsterdam_eip8037_enabled(&self) -> bool;
133
134    /// Returns whether the current call context is static.
135    fn is_static(&self) -> bool;
136
137    /// Creates a new journal checkpoint so that all subsequent state-changing
138    /// operations can be atomically committed ([`checkpoint_commit`](Self::checkpoint_commit))
139    /// or reverted ([`checkpoint_revert`](Self::checkpoint_revert)).
140    ///
141    /// Prefer [`StorageCtx::checkpoint`] which returns a [`CheckpointGuard`] that
142    /// auto-reverts on drop and is hardfork-aware (no-op pre-T1C).
143    fn checkpoint(&mut self) -> JournalCheckpoint;
144
145    /// Commits all state changes since the given checkpoint.
146    ///
147    /// Prefer [`CheckpointGuard::commit`].
148    fn checkpoint_commit(&mut self, checkpoint: JournalCheckpoint);
149
150    /// Reverts all state changes back to the given checkpoint.
151    ///
152    /// Prefer [`CheckpointGuard`] (auto-reverts on drop).
153    fn checkpoint_revert(&mut self, checkpoint: JournalCheckpoint);
154
155    /// Enables or disables TIP-1060 storage-credit accounting for subsequent storage writes.
156    ///
157    /// Implementations that do not run TIP-1060 accounting may treat this as a no-op. Production
158    /// providers must still hardfork-gate enabling so calling this with `true` before T7 does not
159    /// activate storage credits early.
160    fn set_tip1060_storage_credits(&mut self, enabled: bool);
161
162    /// Enables or disables minting new TIP-1060 storage credits for subsequent storage clears.
163    ///
164    /// This leaves storage-credit accounting active for storage creation charges, redemptions, and
165    /// refund-mode settlement. Implementations that do not run TIP-1060 accounting may treat this
166    /// as a no-op.
167    fn set_tip1060_storage_credit_minting(&mut self, _enabled: bool) {}
168
169    /// Computes keccak256 and charges the appropriate gas.
170    ///
171    /// Implementations should use this over naked `keccak256` call to ensure gas is accounted for.
172    fn keccak256(&mut self, data: &[u8]) -> Result<B256> {
173        let num_words =
174            u64::try_from(data.len().div_ceil(32)).map_err(|_| TempoPrecompileError::OutOfGas)?;
175        let price = KECCAK256WORD
176            .checked_mul(num_words)
177            .and_then(|w| w.checked_add(KECCAK256))
178            .ok_or(TempoPrecompileError::OutOfGas)?;
179        self.deduct_gas(price)?;
180        Ok(keccak256(data))
181    }
182
183    /// Recovers the signer address from an ECDSA signature and charges ecrecover gas.
184    /// As per [TIP-1004], it only accepts `v` values of `27` or `28` (no `0`/`1` normalization).
185    ///
186    /// Returns `Ok(None)` on invalid signatures; callers map to domain-specific errors.
187    ///
188    /// [TIP-1004]: <https://github.com/tempoxyz/tempo/blob/main/tips/tip-1004.md#signature-validation>
189    fn recover_signer(&mut self, digest: B256, v: u8, r: B256, s: B256) -> Result<Option<Address>> {
190        self.deduct_gas(crate::ECRECOVER_GAS)?;
191
192        if v != 27 && v != 28 {
193            return Ok(None);
194        }
195
196        let parity = v == 28;
197        let sig = Signature::from_scalars_and_parity(r, s, parity);
198        let recovered = alloy::consensus::crypto::secp256k1::recover_signer(&sig, digest);
199
200        Ok(recovered.ok().filter(|addr| !addr.is_zero()))
201    }
202}
203
204/// Storage operations for a given (contract) address.
205///
206/// Abstracts over persistent storage (SLOAD/SSTORE) and transient storage (TLOAD/TSTORE).
207/// Implementors must route to the appropriate opcode.
208pub trait StorageOps {
209    /// Stores a value at the provided slot.
210    fn store(&mut self, slot: U256, value: U256) -> Result<()>;
211    /// Loads a value from the provided slot.
212    fn load(&self, slot: U256) -> Result<U256>;
213
214    /// Increments a value at the provided slot by `delta`.
215    ///
216    /// Intentionally returns no post-increment value, preserving `sinc` as a semantic
217    /// storage delta rather than an observation point that callers can branch on.
218    fn sinc(&mut self, slot: U256, delta: U256) -> Result<()> {
219        let value = self
220            .load(slot)?
221            .checked_add(delta)
222            .ok_or_else(TempoPrecompileError::under_overflow)?;
223        self.store(slot, value)
224    }
225
226    /// Decrements a value at the provided slot by `delta`.
227    ///
228    /// Intentionally returns no post-decrement value, preserving `sdec` as a semantic
229    /// storage delta rather than an observation point that callers can branch on.
230    fn sdec(&mut self, slot: U256, delta: U256) -> Result<()> {
231        let current = self.load(slot)?;
232        let value = current
233            .checked_sub(delta)
234            .ok_or_else(|| TempoPrecompileError::storage_delta_underflow(current))?;
235        self.store(slot, value)
236    }
237}
238
239/// Trait providing access to a contract's address.
240///
241/// Automatically implemented by the `#[contract]` macro.
242pub trait ContractStorage {
243    /// Contract address.
244    fn address(&self) -> Address;
245
246    /// Contract storage accessor.
247    fn storage(&self) -> &StorageCtx;
248
249    /// Contract storage mutable accessor.
250    fn storage_mut(&mut self) -> &mut StorageCtx;
251
252    /// Returns true if the contract has been initialized (has bytecode deployed).
253    fn is_initialized(&self) -> Result<bool> {
254        self.storage()
255            .with_account_info(self.address(), |info| Ok(!info.is_empty_code_hash()))
256    }
257}