tempo_precompiles/
lib.rs

1//! Tempo precompile implementations.
2#![cfg_attr(not(test), warn(unused_crate_dependencies))]
3#![cfg_attr(docsrs, feature(doc_cfg))]
4
5pub mod error;
6pub use error::Result;
7use tempo_chainspec::hardfork::TempoHardfork;
8pub mod account_keychain;
9pub mod nonce;
10pub mod path_usd;
11pub mod stablecoin_exchange;
12pub mod storage;
13pub mod tip20;
14pub mod tip20_factory;
15pub mod tip20_rewards_registry;
16pub mod tip403_registry;
17pub mod tip_account_registrar;
18pub mod tip_fee_manager;
19pub mod validator_config;
20
21#[cfg(test)]
22pub mod test_util;
23
24use crate::{
25    account_keychain::AccountKeychain,
26    nonce::NonceManager,
27    path_usd::PathUSD,
28    stablecoin_exchange::StablecoinExchange,
29    storage::{PrecompileStorageProvider, evm::EvmPrecompileStorageProvider},
30    tip_account_registrar::TipAccountRegistrar,
31    tip_fee_manager::TipFeeManager,
32    tip20::{TIP20Token, address_to_token_id_unchecked, is_tip20_prefix},
33    tip20_factory::TIP20Factory,
34    tip20_rewards_registry::TIP20RewardsRegistry,
35    tip403_registry::TIP403Registry,
36    validator_config::ValidatorConfig,
37};
38pub use error::IntoPrecompileResult;
39
40#[cfg(test)]
41use alloy::sol_types::SolInterface;
42use alloy::{
43    primitives::{Address, Bytes},
44    sol,
45    sol_types::{SolCall, SolError},
46};
47use alloy_evm::precompiles::{DynPrecompile, PrecompilesMap};
48use revm::{
49    context::CfgEnv,
50    precompile::{PrecompileError, PrecompileId, PrecompileOutput, PrecompileResult},
51};
52
53pub use tempo_contracts::precompiles::{
54    ACCOUNT_KEYCHAIN_ADDRESS, DEFAULT_FEE_TOKEN_POST_ALLEGRETTO, DEFAULT_FEE_TOKEN_PRE_ALLEGRETTO,
55    NONCE_PRECOMPILE_ADDRESS, PATH_USD_ADDRESS, STABLECOIN_EXCHANGE_ADDRESS, TIP_ACCOUNT_REGISTRAR,
56    TIP_FEE_MANAGER_ADDRESS, TIP20_FACTORY_ADDRESS, TIP20_REWARDS_REGISTRY_ADDRESS,
57    TIP403_REGISTRY_ADDRESS, VALIDATOR_CONFIG_ADDRESS,
58};
59
60// Re-export storage layout helpers for read-only contexts (e.g., pool validation)
61pub use account_keychain::{AuthorizedKey, compute_keys_slot};
62
63/// Input per word cost. It covers abi decoding and cloning of input into call data.
64///
65/// Being careful and pricing it twice as COPY_COST to mitigate different abi decodings.
66pub const INPUT_PER_WORD_COST: u64 = 6;
67
68#[inline]
69pub fn input_cost(calldata_len: usize) -> u64 {
70    revm::interpreter::gas::cost_per_word(calldata_len, INPUT_PER_WORD_COST).unwrap_or(u64::MAX)
71}
72
73pub trait Precompile {
74    fn call(&mut self, calldata: &[u8], msg_sender: Address) -> PrecompileResult;
75}
76
77pub fn extend_tempo_precompiles(precompiles: &mut PrecompilesMap, cfg: &CfgEnv<TempoHardfork>) {
78    let chain_id = cfg.chain_id;
79    let spec = cfg.spec;
80    precompiles.set_precompile_lookup(move |address: &Address| {
81        if is_tip20_prefix(*address) {
82            let token_id = address_to_token_id_unchecked(*address);
83            if token_id == 0 {
84                Some(PathUSDPrecompile::create(chain_id, spec))
85            } else {
86                Some(TIP20Precompile::create(*address, chain_id, spec))
87            }
88        } else if *address == TIP20_FACTORY_ADDRESS {
89            Some(TIP20FactoryPrecompile::create(chain_id, spec))
90        } else if *address == TIP20_REWARDS_REGISTRY_ADDRESS {
91            Some(TIP20RewardsRegistryPrecompile::create(chain_id, spec))
92        } else if *address == TIP403_REGISTRY_ADDRESS {
93            Some(TIP403RegistryPrecompile::create(chain_id, spec))
94        } else if *address == TIP_FEE_MANAGER_ADDRESS {
95            Some(TipFeeManagerPrecompile::create(chain_id, spec))
96        } else if *address == TIP_ACCOUNT_REGISTRAR {
97            Some(TipAccountRegistrarPrecompile::create(chain_id, spec))
98        } else if *address == STABLECOIN_EXCHANGE_ADDRESS {
99            Some(StablecoinExchangePrecompile::create(chain_id, spec))
100        } else if *address == NONCE_PRECOMPILE_ADDRESS {
101            Some(NoncePrecompile::create(chain_id, spec))
102        } else if *address == VALIDATOR_CONFIG_ADDRESS {
103            Some(ValidatorConfigPrecompile::create(chain_id, spec))
104        } else if *address == ACCOUNT_KEYCHAIN_ADDRESS && spec.is_allegretto() {
105            // AccountKeychain is only available after Allegretto hardfork
106            Some(AccountKeychainPrecompile::create(chain_id, spec))
107        } else {
108            None
109        }
110    });
111}
112
113sol! {
114    error DelegateCallNotAllowed();
115}
116
117macro_rules! tempo_precompile {
118    ($id:expr, |$input:ident| $impl:expr) => {
119        DynPrecompile::new_stateful(PrecompileId::Custom($id.into()), move |$input| {
120            if !$input.is_direct_call() {
121                return Ok(PrecompileOutput::new_reverted(
122                    0,
123                    DelegateCallNotAllowed {}.abi_encode().into(),
124                ));
125            }
126            $impl.call($input.data, $input.caller)
127        })
128    };
129}
130
131pub struct TipFeeManagerPrecompile;
132impl TipFeeManagerPrecompile {
133    pub fn create(chain_id: u64, spec: TempoHardfork) -> DynPrecompile {
134        tempo_precompile!("TipFeeManager", |input| TipFeeManager::new(
135            &mut EvmPrecompileStorageProvider::new(input.internals, input.gas, chain_id, spec)
136        ))
137    }
138}
139
140pub struct TipAccountRegistrarPrecompile;
141impl TipAccountRegistrarPrecompile {
142    pub fn create(chain_id: u64, spec: TempoHardfork) -> DynPrecompile {
143        tempo_precompile!("TipAccountRegistrar", |input| TipAccountRegistrar::new(
144            &mut crate::storage::evm::EvmPrecompileStorageProvider::new(
145                input.internals,
146                input.gas,
147                chain_id,
148                spec
149            ),
150        ))
151    }
152}
153
154pub struct TIP20RewardsRegistryPrecompile;
155impl TIP20RewardsRegistryPrecompile {
156    pub fn create(chain_id: u64, spec: TempoHardfork) -> DynPrecompile {
157        tempo_precompile!("TIP20RewardsRegistry", |input| TIP20RewardsRegistry::new(
158            &mut EvmPrecompileStorageProvider::new(input.internals, input.gas, chain_id, spec),
159        ))
160    }
161}
162
163pub struct TIP403RegistryPrecompile;
164impl TIP403RegistryPrecompile {
165    pub fn create(chain_id: u64, spec: TempoHardfork) -> DynPrecompile {
166        tempo_precompile!("TIP403Registry", |input| TIP403Registry::new(
167            &mut crate::storage::evm::EvmPrecompileStorageProvider::new(
168                input.internals,
169                input.gas,
170                chain_id,
171                spec
172            ),
173        ))
174    }
175}
176
177pub struct TIP20FactoryPrecompile;
178impl TIP20FactoryPrecompile {
179    pub fn create(chain_id: u64, spec: TempoHardfork) -> DynPrecompile {
180        tempo_precompile!("TIP20Factory", |input| TIP20Factory::new(
181            &mut EvmPrecompileStorageProvider::new(input.internals, input.gas, chain_id, spec)
182        ))
183    }
184}
185
186pub struct TIP20Precompile;
187impl TIP20Precompile {
188    pub fn create(address: Address, chain_id: u64, spec: TempoHardfork) -> DynPrecompile {
189        let token_id = address_to_token_id_unchecked(address);
190        tempo_precompile!("TIP20Token", |input| TIP20Token::new(
191            token_id,
192            &mut EvmPrecompileStorageProvider::new(input.internals, input.gas, chain_id, spec),
193        ))
194    }
195}
196
197pub struct StablecoinExchangePrecompile;
198impl StablecoinExchangePrecompile {
199    pub fn create(chain_id: u64, spec: TempoHardfork) -> DynPrecompile {
200        tempo_precompile!("StablecoinExchange", |input| StablecoinExchange::new(
201            &mut EvmPrecompileStorageProvider::new(input.internals, input.gas, chain_id, spec)
202        ))
203    }
204}
205
206pub struct NoncePrecompile;
207impl NoncePrecompile {
208    pub fn create(chain_id: u64, spec: TempoHardfork) -> DynPrecompile {
209        tempo_precompile!("NonceManager", |input| NonceManager::new(
210            &mut EvmPrecompileStorageProvider::new(input.internals, input.gas, chain_id, spec)
211        ))
212    }
213}
214
215pub struct AccountKeychainPrecompile;
216impl AccountKeychainPrecompile {
217    pub fn create(chain_id: u64, spec: TempoHardfork) -> DynPrecompile {
218        tempo_precompile!("AccountKeychain", |input| AccountKeychain::new(
219            &mut EvmPrecompileStorageProvider::new(input.internals, input.gas, chain_id, spec)
220        ))
221    }
222}
223
224pub struct PathUSDPrecompile;
225impl PathUSDPrecompile {
226    pub fn create(chain_id: u64, spec: TempoHardfork) -> DynPrecompile {
227        tempo_precompile!("PathUSD", |input| PathUSD::new(
228            &mut EvmPrecompileStorageProvider::new(input.internals, input.gas, chain_id, spec),
229        ))
230    }
231}
232
233pub struct ValidatorConfigPrecompile;
234impl ValidatorConfigPrecompile {
235    pub fn create(chain_id: u64, spec: TempoHardfork) -> DynPrecompile {
236        tempo_precompile!("ValidatorConfig", |input| ValidatorConfig::new(
237            &mut EvmPrecompileStorageProvider::new(input.internals, input.gas, chain_id, spec),
238        ))
239    }
240}
241
242#[inline]
243fn metadata<T: SolCall>(f: impl FnOnce() -> Result<T::Return>) -> PrecompileResult {
244    f().into_precompile_result(0, |ret| T::abi_encode_returns(&ret).into())
245}
246
247#[inline]
248fn view<T: SolCall>(calldata: &[u8], f: impl FnOnce(T) -> Result<T::Return>) -> PrecompileResult {
249    let Ok(call) = T::abi_decode(calldata) else {
250        // TODO refactor
251        return Ok(PrecompileOutput::new_reverted(0, Bytes::new()));
252    };
253    f(call).into_precompile_result(0, |ret| T::abi_encode_returns(&ret).into())
254}
255
256#[inline]
257pub fn mutate<T: SolCall>(
258    calldata: &[u8],
259    sender: Address,
260    f: impl FnOnce(Address, T) -> Result<T::Return>,
261) -> PrecompileResult {
262    let Ok(call) = T::abi_decode(calldata) else {
263        return Ok(PrecompileOutput::new_reverted(0, Bytes::new()));
264    };
265    f(sender, call).into_precompile_result(0, |ret| T::abi_encode_returns(&ret).into())
266}
267
268#[inline]
269fn mutate_void<T: SolCall>(
270    calldata: &[u8],
271    sender: Address,
272    f: impl FnOnce(Address, T) -> Result<()>,
273) -> PrecompileResult {
274    let Ok(call) = T::abi_decode(calldata) else {
275        return Ok(PrecompileOutput::new_reverted(0, Bytes::new()));
276    };
277    f(sender, call).into_precompile_result(0, |()| Bytes::new())
278}
279
280#[inline]
281fn fill_precompile_output(
282    mut output: PrecompileOutput,
283    storage: &mut impl PrecompileStorageProvider,
284) -> PrecompileOutput {
285    output.gas_used = storage.gas_used();
286
287    // add refund only if it is not reverted
288    if !output.reverted && storage.spec().is_allegretto() {
289        output.gas_refunded = storage.gas_refunded();
290    }
291    output
292}
293
294/// Helper function to return an unknown function selector error
295///
296/// Before Moderato: Returns a generic PrecompileError::Other
297/// Moderato onwards: Returns an ABI-encoded UnknownFunctionSelector error with the selector
298#[inline]
299pub fn unknown_selector(selector: [u8; 4], gas: u64, spec: TempoHardfork) -> PrecompileResult {
300    if spec.is_moderato() {
301        error::TempoPrecompileError::UnknownFunctionSelector(selector)
302            .into_precompile_result(gas, |_: ()| Bytes::new())
303    } else {
304        Err(PrecompileError::Other("Unknown function selector".into()))
305    }
306}
307
308#[cfg(test)]
309pub fn expect_precompile_revert<E>(result: &PrecompileResult, expected_error: E)
310where
311    E: SolInterface + PartialEq + std::fmt::Debug,
312{
313    match result {
314        Ok(result) => {
315            assert!(result.reverted);
316            let decoded = E::abi_decode(&result.bytes).unwrap();
317            assert_eq!(decoded, expected_error);
318        }
319        Err(other) => {
320            panic!("expected reverted output, got: {other:?}");
321        }
322    }
323}
324
325#[cfg(test)]
326mod tests {
327    use super::*;
328    use crate::{storage::evm::EvmPrecompileStorageProvider, tip20::TIP20Token};
329    use alloy::primitives::{Address, Bytes, U256};
330    use alloy_evm::{
331        EthEvmFactory, EvmEnv, EvmFactory, EvmInternals,
332        precompiles::{Precompile as AlloyEvmPrecompile, PrecompileInput},
333    };
334    use revm::{
335        context::ContextTr,
336        database::{CacheDB, EmptyDB},
337    };
338
339    #[test]
340    fn test_precompile_delegatecall() {
341        let precompile = tempo_precompile!("TIP20Token", |input| TIP20Token::new(
342            1,
343            &mut EvmPrecompileStorageProvider::new(
344                input.internals,
345                input.gas,
346                1,
347                Default::default()
348            ),
349        ));
350
351        let db = CacheDB::new(EmptyDB::new());
352        let mut evm = EthEvmFactory::default().create_evm(db, EvmEnv::default());
353        let block = evm.block.clone();
354        let evm_internals = EvmInternals::new(evm.journal_mut(), &block);
355
356        let target_address = Address::random();
357        let bytecode_address = Address::random();
358        let input = PrecompileInput {
359            data: &Bytes::new(),
360            caller: Address::ZERO,
361            internals: evm_internals,
362            gas: 0,
363            value: U256::ZERO,
364            target_address,
365            bytecode_address,
366        };
367
368        let result = AlloyEvmPrecompile::call(&precompile, input);
369
370        match result {
371            Ok(output) => {
372                assert!(output.reverted);
373                let decoded = DelegateCallNotAllowed::abi_decode(&output.bytes).unwrap();
374                assert!(matches!(decoded, DelegateCallNotAllowed {}));
375            }
376            Err(_) => panic!("expected reverted output"),
377        }
378    }
379}