Skip to main content

tempo_precompiles/tip1060_storage_credits/
mod.rs

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