Skip to main content

tempo_precompiles/storage_credits/
mod.rs

1//! Storage credits precompile (TIP-1060).
2
3pub mod accounting;
4pub mod dispatch;
5
6pub use accounting::{StorageCreditsBackend, StorageCreditsErr, sstore_storage_credits};
7
8use crate::{
9    STORAGE_CREDITS_ADDRESS,
10    error::{Result, TempoPrecompileError},
11    storage::{Handler, LayoutCtx, StorableType},
12};
13use alloy::primitives::{Address, U256};
14use tempo_contracts::precompiles::{IStorageCredits::Mode, StorageCreditsError};
15use tempo_precompiles_macros::{Storable, contract};
16
17#[repr(u8)]
18#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Storable)]
19pub enum CreditMode {
20    #[default]
21    Refund,
22    Preserve,
23    Direct,
24}
25
26// NOTE: Can't leverage `Storable` because `StorageCtx` only exists during precompile execution.
27#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
28pub struct TransientState {
29    /// Remaining number of credits that may be spent directly in `Direct` mode.
30    pub budget: u64,
31    /// Current storage creation mode for this account within the transaction.
32    pub mode: CreditMode,
33    /// Number of Refund-mode storage creations pending end-of-transaction settlement.
34    pub pending_refunds: u64,
35}
36
37impl TryFrom<U256> for TransientState {
38    type Error = TempoPrecompileError;
39
40    #[inline]
41    fn try_from(value: U256) -> Result<Self> {
42        let limbs = value.as_limbs();
43        Ok(Self {
44            budget: limbs[0],
45            mode: (limbs[1] as u8).try_into()?,
46            pending_refunds: limbs[3],
47        })
48    }
49}
50
51impl From<TransientState> for U256 {
52    #[inline]
53    fn from(value: TransientState) -> Self {
54        Self::from_limbs([value.budget, value.mode as u64, 0, value.pending_refunds])
55    }
56}
57
58/// TIP-1060 storage credits precompile, which tracks per-account storage credit state.
59///
60/// Unlike the Solidity-compatible `Mapping<Address, GasState>` layout, persistent account state is
61/// stored directly at the account-derived slot: the 20-byte address is left-padded to 32 bytes and
62/// used as the storage key, avoiding hashing on the SSTORE gas-state hook hot path.
63///
64/// ```text
65/// storage_credit_slot = uint256(bytes32(account))
66/// solidity_mapping_slot = keccak256(abi.encode(account, base_slot))
67/// ```
68///
69/// Storage creation mode, direct-spend budget, and pending refund counters are transaction-local
70/// transient state at the same account-derived slot.
71#[contract(addr = STORAGE_CREDITS_ADDRESS)]
72pub struct StorageCredits {}
73
74impl StorageCredits {
75    pub fn initialize(&mut self) -> Result<()> {
76        self.__initialize()
77    }
78
79    pub fn balance_of(&self, account: Address) -> Result<u64> {
80        u64::handle(Self::slot(account), LayoutCtx::FULL, self.address).read()
81    }
82
83    pub fn mode_of(&self, account: Address) -> Result<CreditMode> {
84        self.credit_state_of(account).map(|state| state.mode)
85    }
86
87    pub fn budget_of(&self, account: Address) -> Result<u64> {
88        self.credit_state_of(account).map(|state| state.budget)
89    }
90
91    pub fn set_mode(&mut self, msg_sender: Address, mode: Mode) -> Result<()> {
92        let mode = CreditMode::try_from(mode)?;
93        let budget = if matches!(mode, CreditMode::Direct) {
94            u64::MAX
95        } else {
96            0
97        };
98
99        self.write_mode_with_budget(msg_sender, mode, budget)
100    }
101
102    pub fn set_budget(&mut self, msg_sender: Address, credit_budget: u64) -> Result<()> {
103        self.write_mode_with_budget(msg_sender, CreditMode::Direct, credit_budget)
104    }
105
106    fn write_mode_with_budget(
107        &mut self,
108        msg_sender: Address,
109        mode: CreditMode,
110        budget: u64,
111    ) -> Result<()> {
112        let mut state = self.credit_state_of(msg_sender)?;
113        state.mode = mode;
114        state.budget = budget;
115        self.write_credit_state_of(msg_sender, state)
116    }
117
118    #[inline]
119    pub fn slot(account: Address) -> U256 {
120        U256::from_be_bytes(account.into_word().0)
121    }
122
123    #[inline]
124    fn credit_state_of(&self, account: Address) -> Result<TransientState> {
125        U256::handle(Self::slot(account), LayoutCtx::FULL, self.address)
126            .t_read()?
127            .try_into()
128    }
129
130    #[inline]
131    fn write_credit_state_of(&mut self, account: Address, state: TransientState) -> Result<()> {
132        U256::handle(Self::slot(account), LayoutCtx::FULL, self.address).t_write(state.into())
133    }
134}
135
136impl TryFrom<u8> for CreditMode {
137    type Error = TempoPrecompileError;
138
139    fn try_from(value: u8) -> Result<Self> {
140        match value {
141            0 => Ok(Self::Refund),
142            1 => Ok(Self::Preserve),
143            2 => Ok(Self::Direct),
144            _ => Err(StorageCreditsError::invalid_mode().into()),
145        }
146    }
147}
148
149impl TryFrom<Mode> for CreditMode {
150    type Error = TempoPrecompileError;
151
152    fn try_from(mode: Mode) -> Result<Self> {
153        match mode {
154            Mode::Refund => Ok(Self::Refund),
155            Mode::Preserve => Ok(Self::Preserve),
156            Mode::Direct => Ok(Self::Direct),
157            _ => Err(StorageCreditsError::invalid_mode().into()),
158        }
159    }
160}
161
162impl From<CreditMode> for Mode {
163    fn from(mode: CreditMode) -> Self {
164        match mode {
165            CreditMode::Refund => Self::Refund,
166            CreditMode::Preserve => Self::Preserve,
167            CreditMode::Direct => Self::Direct,
168        }
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175    use crate::storage::{StorageCtx, hashmap::HashMapStorageProvider};
176
177    #[test]
178    fn test_set_mode_budget_semantics() -> eyre::Result<()> {
179        let account = Address::repeat_byte(0x11);
180        let mut storage = HashMapStorageProvider::new(1);
181
182        StorageCtx::enter(&mut storage, || {
183            let mut credits = StorageCredits::new();
184
185            assert_eq!(credits.mode_of(account)?, CreditMode::Refund);
186            assert_eq!(credits.budget_of(account)?, 0);
187
188            credits.set_mode(account, Mode::Direct)?;
189            assert_eq!(credits.mode_of(account)?, CreditMode::Direct);
190            assert_eq!(credits.budget_of(account)?, u64::MAX);
191
192            credits.set_mode(account, Mode::Preserve)?;
193            assert_eq!(credits.mode_of(account)?, CreditMode::Preserve);
194            assert_eq!(credits.budget_of(account)?, 0);
195
196            credits.set_mode(account, Mode::Refund)?;
197            assert_eq!(credits.mode_of(account)?, CreditMode::Refund);
198            assert_eq!(credits.budget_of(account)?, 0);
199
200            Ok(())
201        })
202    }
203
204    #[test]
205    fn test_set_budget_zero_stays_direct_with_zero_budget() -> eyre::Result<()> {
206        let account = Address::repeat_byte(0x12);
207        let mut storage = HashMapStorageProvider::new(1);
208
209        StorageCtx::enter(&mut storage, || {
210            let mut credits = StorageCredits::new();
211
212            credits.set_budget(account, 2)?;
213            assert_eq!(credits.mode_of(account)?, CreditMode::Direct);
214            assert_eq!(credits.budget_of(account)?, 2);
215
216            credits.set_budget(account, 0)?;
217            assert_eq!(credits.mode_of(account)?, CreditMode::Direct);
218            assert_eq!(credits.budget_of(account)?, 0);
219
220            Ok(())
221        })
222    }
223}