Skip to main content

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::{IntoPrecompileResult, Result};
7
8pub mod storage;
9
10pub(crate) mod ip_validation;
11
12pub mod account_keychain;
13pub mod address_registry;
14pub mod nonce;
15pub mod signature_verifier;
16pub mod stablecoin_dex;
17pub mod tip20;
18pub mod tip20_factory;
19pub mod tip403_registry;
20pub mod tip_fee_manager;
21pub mod validator_config;
22pub mod validator_config_v2;
23
24#[cfg(any(test, feature = "test-utils"))]
25pub mod test_util;
26
27use crate::{
28    account_keychain::AccountKeychain, address_registry::AddressRegistry, nonce::NonceManager,
29    signature_verifier::SignatureVerifier, stablecoin_dex::StablecoinDEX, storage::StorageCtx,
30    tip_fee_manager::TipFeeManager, tip20::TIP20Token, tip20_factory::TIP20Factory,
31    tip403_registry::TIP403Registry, validator_config::ValidatorConfig,
32    validator_config_v2::ValidatorConfigV2,
33};
34use tempo_chainspec::hardfork::TempoHardfork;
35use tempo_primitives::TempoAddressExt;
36
37#[cfg(test)]
38use alloy::sol_types::SolInterface;
39use alloy::{
40    primitives::{Address, Bytes},
41    sol,
42    sol_types::{SolCall, SolError},
43};
44use alloy_evm::precompiles::{DynPrecompile, PrecompilesMap};
45use revm::{
46    context::CfgEnv,
47    handler::EthPrecompiles,
48    precompile::{PrecompileHalt, PrecompileId, PrecompileOutput, PrecompileResult},
49    primitives::hardfork::SpecId,
50};
51
52pub use tempo_contracts::precompiles::{
53    ACCOUNT_KEYCHAIN_ADDRESS, ADDRESS_REGISTRY_ADDRESS, DEFAULT_FEE_TOKEN,
54    NONCE_PRECOMPILE_ADDRESS, PATH_USD_ADDRESS, SIGNATURE_VERIFIER_ADDRESS, STABLECOIN_DEX_ADDRESS,
55    TIP_FEE_MANAGER_ADDRESS, TIP20_FACTORY_ADDRESS, TIP403_REGISTRY_ADDRESS,
56    VALIDATOR_CONFIG_ADDRESS, VALIDATOR_CONFIG_V2_ADDRESS,
57};
58
59// Re-export storage layout helpers for read-only contexts (e.g., pool validation)
60pub use account_keychain::AuthorizedKey;
61
62/// Input per word cost. It covers abi decoding and cloning of input into call data.
63///
64/// Being careful and pricing it twice as COPY_COST to mitigate different abi decodings.
65pub const INPUT_PER_WORD_COST: u64 = 6;
66
67/// Gas cost for `ecrecover` signature verification (used by KeyAuthorization and Permit).
68pub const ECRECOVER_GAS: u64 = 3_000;
69
70/// Returns the gas cost for decoding calldata of the given length, rounded up to word boundaries.
71#[inline]
72pub fn input_cost(calldata_len: usize) -> u64 {
73    calldata_len
74        .div_ceil(32)
75        .saturating_mul(INPUT_PER_WORD_COST as usize) as u64
76}
77
78/// Trait implemented by all Tempo precompile contract types.
79///
80/// Precompiles must provide a dispatcher that decodes the 4-byte function selector from calldata,
81/// ABI-decodes the arguments, and routes to the corresponding method.
82pub trait Precompile {
83    /// Dispatches an EVM call to this precompile.
84    ///
85    /// Implementations should deduct calldata gas upfront via [`input_cost`], then decode the
86    /// 4-byte function selector from `calldata` and route to the matching method using
87    /// `dispatch_call` combined with the `view`, `mutate`, or `mutate_void` helpers.
88    ///
89    /// Business-logic errors are returned as reverted [`PrecompileOutput`]s with ABI-encoded
90    /// error data, while fatal failures (e.g. out-of-gas) are returned as
91    /// [`PrecompileError`](revm::precompile::PrecompileError).
92    fn call(&mut self, calldata: &[u8], msg_sender: Address) -> PrecompileResult;
93}
94
95/// Returns the full Tempo precompiles for the given config.
96///
97/// Pre-T1C hardforks use Prague precompiles, T1C+ uses Osaka precompiles.
98/// Tempo-specific precompiles are also registered via [`extend_tempo_precompiles`].
99pub fn tempo_precompiles(cfg: &CfgEnv<TempoHardfork>) -> PrecompilesMap {
100    let spec = if cfg.spec.is_t1c() {
101        cfg.spec.into()
102    } else {
103        SpecId::PRAGUE
104    };
105    let mut precompiles = PrecompilesMap::from_static(EthPrecompiles::new(spec).precompiles);
106    extend_tempo_precompiles(&mut precompiles, cfg);
107    precompiles
108}
109
110/// Registers Tempo-specific precompiles into an existing [`PrecompilesMap`] by installing a
111/// lookup function that matches addresses to their precompile: TIP-20 tokens (by prefix),
112/// TIP20Factory, TIP403Registry, TipFeeManager, StablecoinDEX, NonceManager, ValidatorConfig,
113/// AccountKeychain, and ValidatorConfigV2. Each precompile is wrapped via the `tempo_precompile!`
114/// macro which enforces direct-call-only (no delegatecall) and sets up the storage context.
115pub fn extend_tempo_precompiles(precompiles: &mut PrecompilesMap, cfg: &CfgEnv<TempoHardfork>) {
116    let cfg = cfg.clone();
117
118    precompiles.set_precompile_lookup(move |address: &Address| {
119        if address.is_tip20() {
120            Some(TIP20Token::create_precompile(*address, &cfg))
121        } else if *address == TIP20_FACTORY_ADDRESS {
122            Some(TIP20Factory::create_precompile(&cfg))
123        } else if *address == ADDRESS_REGISTRY_ADDRESS && cfg.spec.is_t3() {
124            Some(AddressRegistry::create_precompile(&cfg))
125        } else if *address == TIP403_REGISTRY_ADDRESS {
126            Some(TIP403Registry::create_precompile(&cfg))
127        } else if *address == TIP_FEE_MANAGER_ADDRESS {
128            Some(TipFeeManager::create_precompile(&cfg))
129        } else if *address == STABLECOIN_DEX_ADDRESS {
130            Some(StablecoinDEX::create_precompile(&cfg))
131        } else if *address == NONCE_PRECOMPILE_ADDRESS {
132            Some(NonceManager::create_precompile(&cfg))
133        } else if *address == VALIDATOR_CONFIG_ADDRESS {
134            Some(ValidatorConfig::create_precompile(&cfg))
135        } else if *address == ACCOUNT_KEYCHAIN_ADDRESS {
136            Some(AccountKeychain::create_precompile(&cfg))
137        } else if *address == VALIDATOR_CONFIG_V2_ADDRESS {
138            Some(ValidatorConfigV2::create_precompile(&cfg))
139        } else if *address == SIGNATURE_VERIFIER_ADDRESS && cfg.spec.is_t3() {
140            Some(SignatureVerifier::create_precompile(&cfg))
141        } else {
142            None
143        }
144    });
145}
146
147sol! {
148    error DelegateCallNotAllowed();
149    error StaticCallNotAllowed();
150}
151
152macro_rules! tempo_precompile {
153    ($id:expr, $cfg:expr, |$input:ident| $impl:expr) => {{
154        let spec = $cfg.spec;
155        let amsterdam_eip8037_enabled = $cfg.enable_amsterdam_eip8037;
156        let gas_params = $cfg.gas_params.clone();
157        DynPrecompile::new_stateful(PrecompileId::Custom($id.into()), move |$input| {
158            if !$input.is_direct_call() {
159                return Ok(PrecompileOutput::revert(
160                    0,
161                    DelegateCallNotAllowed {}.abi_encode().into(),
162                    $input.reservoir,
163                ));
164            }
165            let mut storage = crate::storage::evm::EvmPrecompileStorageProvider::new(
166                $input.internals,
167                $input.gas,
168                $input.reservoir,
169                spec,
170                amsterdam_eip8037_enabled,
171                $input.is_static,
172                gas_params.clone(),
173            );
174            crate::storage::StorageCtx::enter(&mut storage, || {
175                $impl.call($input.data, $input.caller)
176            })
177        })
178    }};
179}
180
181impl TipFeeManager {
182    /// Creates the EVM precompile for this type.
183    pub fn create_precompile(cfg: &CfgEnv<TempoHardfork>) -> DynPrecompile {
184        tempo_precompile!("TipFeeManager", cfg, |input| { Self::new() })
185    }
186}
187
188impl AddressRegistry {
189    /// Creates the EVM precompile for this type.
190    pub fn create_precompile(cfg: &CfgEnv<TempoHardfork>) -> DynPrecompile {
191        tempo_precompile!("AddressRegistry", cfg, |input| { Self::new() })
192    }
193}
194
195impl TIP403Registry {
196    /// Creates the EVM precompile for this type.
197    pub fn create_precompile(cfg: &CfgEnv<TempoHardfork>) -> DynPrecompile {
198        tempo_precompile!("TIP403Registry", cfg, |input| { Self::new() })
199    }
200}
201
202impl TIP20Factory {
203    /// Creates the EVM precompile for this type.
204    pub fn create_precompile(cfg: &CfgEnv<TempoHardfork>) -> DynPrecompile {
205        tempo_precompile!("TIP20Factory", cfg, |input| { Self::new() })
206    }
207}
208
209impl TIP20Token {
210    /// Creates the EVM precompile for this type.
211    pub fn create_precompile(address: Address, cfg: &CfgEnv<TempoHardfork>) -> DynPrecompile {
212        tempo_precompile!("TIP20Token", cfg, |input| {
213            Self::from_address(address).expect("TIP20 prefix already verified")
214        })
215    }
216}
217
218impl StablecoinDEX {
219    /// Creates the EVM precompile for this type.
220    pub fn create_precompile(cfg: &CfgEnv<TempoHardfork>) -> DynPrecompile {
221        tempo_precompile!("StablecoinDEX", cfg, |input| { Self::new() })
222    }
223}
224
225impl NonceManager {
226    /// Creates the EVM precompile for this type.
227    pub fn create_precompile(cfg: &CfgEnv<TempoHardfork>) -> DynPrecompile {
228        tempo_precompile!("NonceManager", cfg, |input| { Self::new() })
229    }
230}
231
232impl AccountKeychain {
233    /// Creates the EVM precompile for this type.
234    pub fn create_precompile(cfg: &CfgEnv<TempoHardfork>) -> DynPrecompile {
235        tempo_precompile!("AccountKeychain", cfg, |input| { Self::new() })
236    }
237}
238
239impl ValidatorConfig {
240    /// Creates the EVM precompile for this type.
241    pub fn create_precompile(cfg: &CfgEnv<TempoHardfork>) -> DynPrecompile {
242        tempo_precompile!("ValidatorConfig", cfg, |input| { Self::new() })
243    }
244}
245
246impl ValidatorConfigV2 {
247    /// Creates the EVM precompile for this type.
248    pub fn create_precompile(cfg: &CfgEnv<TempoHardfork>) -> DynPrecompile {
249        tempo_precompile!("ValidatorConfigV2", cfg, |input| { Self::new() })
250    }
251}
252
253impl SignatureVerifier {
254    /// Creates the EVM precompile for this type.
255    pub fn create_precompile(cfg: &CfgEnv<TempoHardfork>) -> DynPrecompile {
256        tempo_precompile!("SignatureVerifier", cfg, |input| { Self::new() })
257    }
258}
259
260/// Dispatches a parameterless view call, encoding the return via `T`.
261#[inline]
262fn metadata<T: SolCall>(f: impl FnOnce() -> Result<T::Return>) -> PrecompileResult {
263    f().into_precompile_result(0, 0, |ret| T::abi_encode_returns(&ret).into())
264}
265
266/// Dispatches a read-only call with decoded arguments, encoding the return via `T`.
267#[inline]
268fn view<T: SolCall>(call: T, f: impl FnOnce(T) -> Result<T::Return>) -> PrecompileResult {
269    f(call).into_precompile_result(0, 0, |ret| T::abi_encode_returns(&ret).into())
270}
271
272/// Dispatches a state-mutating call that returns ABI-encoded data.
273///
274/// Rejects static calls with [`StaticCallNotAllowed`].
275#[inline]
276fn mutate<T: SolCall>(
277    call: T,
278    sender: Address,
279    f: impl FnOnce(Address, T) -> Result<T::Return>,
280) -> PrecompileResult {
281    if StorageCtx.is_static() {
282        return Ok(PrecompileOutput::revert(
283            0,
284            StaticCallNotAllowed {}.abi_encode().into(),
285            StorageCtx.reservoir(),
286        ));
287    }
288    f(sender, call).into_precompile_result(0, 0, |ret| T::abi_encode_returns(&ret).into())
289}
290
291/// Dispatches a state-mutating call that returns no data (e.g. `approve`, `transfer`).
292///
293/// Rejects static calls with [`StaticCallNotAllowed`].
294#[inline]
295fn mutate_void<T: SolCall>(
296    call: T,
297    sender: Address,
298    f: impl FnOnce(Address, T) -> Result<()>,
299) -> PrecompileResult {
300    if StorageCtx.is_static() {
301        return Ok(PrecompileOutput::revert(
302            0,
303            StaticCallNotAllowed {}.abi_encode().into(),
304            StorageCtx.reservoir(),
305        ));
306    }
307    f(sender, call).into_precompile_result(0, 0, |()| Bytes::new())
308}
309
310/// Deducts the calldata input cost, returning an OOG halt result if insufficient gas.
311#[inline]
312pub(crate) fn charge_input_cost(
313    storage: &mut StorageCtx,
314    calldata: &[u8],
315) -> Option<PrecompileResult> {
316    if storage.deduct_gas(input_cost(calldata.len())).is_err() {
317        return Some(Ok(storage.halt_output(PrecompileHalt::OutOfGas)));
318    }
319    None
320}
321
322/// Fills state gas accounting on a [`PrecompileOutput`] from the storage context.
323///
324/// State gas / reservoir tracking is only set when TIP-1016 (EIP-8037) is enabled.
325/// When disabled, `state_gas_used` must remain 0 to avoid leaking into revm's reservoir
326/// accounting and corrupting `tx_gas_used()` via `handle_reservoir_remaining_gas`.
327///
328/// SSTORE refund propagation is activated unconditionally at T4 so the
329/// `TempoPrecompileProvider` wrapper can apply refunds with `record_refund`. Pre-T4
330/// blocks were executed without refund propagation, so we cannot change their gas
331/// accounting.
332#[inline]
333fn fill_state_gas(output: &mut PrecompileOutput, storage: &StorageCtx) {
334    if storage.spec().is_t4() && output.is_success() {
335        output.gas_refunded = storage.gas_refunded();
336    }
337
338    if storage.amsterdam_eip8037_enabled() {
339        if output.is_success() {
340            // On success: parent takes the child's final reservoir.
341            output.reservoir = storage.reservoir();
342            output.state_gas_used = storage.state_gas_used();
343        } else {
344            // On revert or halt: state changes are undone, so ALL state gas returns
345            // to the parent's reservoir.
346            output.reservoir = storage.state_gas_used() + storage.reservoir();
347            output.state_gas_used = 0;
348        }
349    }
350}
351
352/// A selector schedule at a given hardfork boundary.
353///
354/// Before the hardfork activates, selectors in `added` are treated as unknown.
355/// After it activates, selectors in `dropped` are treated as unknown.
356#[derive(Clone, Copy, Debug, Default)]
357pub(crate) struct SelectorSchedule<'a> {
358    hardfork: TempoHardfork,
359    added: &'a [[u8; 4]],
360    dropped: &'a [[u8; 4]],
361}
362
363impl<'a> SelectorSchedule<'a> {
364    /// Creates a new schedule anchored at `hardfork` with no selectors registered yet.
365    pub(crate) const fn new(hardfork: TempoHardfork) -> Self {
366        Self {
367            hardfork,
368            added: &[],
369            dropped: &[],
370        }
371    }
372
373    /// Registers selectors that are introduced at this hardfork boundary.
374    ///
375    /// These selectors are treated as unknown BEFORE `hardfork` activates.
376    pub(crate) const fn with_added(mut self, selectors: &'a [[u8; 4]]) -> Self {
377        self.added = selectors;
378        self
379    }
380
381    /// Registers selectors that are removed at this hardfork boundary.
382    ///
383    /// These selectors are treated as unknown ONCE `hardfork` activates.
384    pub(crate) const fn with_dropped(mut self, selectors: &'a [[u8; 4]]) -> Self {
385        self.dropped = selectors;
386        self
387    }
388
389    /// Returns `true` if this schedule gates out `selector` under the `active` hardfork.
390    #[inline]
391    fn rejects(self, selector: [u8; 4], active: TempoHardfork) -> bool {
392        if self.hardfork <= active {
393            self.dropped
394        } else {
395            self.added
396        }
397        .contains(&selector)
398    }
399}
400
401/// Applies hardfork selector schedules, decodes calldata via `decode`, then dispatches to `f`.
402///
403/// Handles missing selectors (revert on T1+, error on earlier forks), hardfork-gated selectors,
404/// unknown selectors (ABI-encoded `UnknownFunctionSelector`), and malformed ABI data (empty
405/// revert).
406#[inline]
407pub(crate) fn dispatch_call<T>(
408    calldata: &[u8],
409    hardforks: &[SelectorSchedule<'_>],
410    decode: impl FnOnce(&[u8]) -> core::result::Result<T, alloy::sol_types::Error>,
411    f: impl FnOnce(T) -> PrecompileResult,
412) -> PrecompileResult {
413    let storage = StorageCtx::default();
414
415    if calldata.len() < 4 {
416        if storage.spec().is_t1() {
417            return Ok(storage.revert_output(Bytes::new()));
418        } else {
419            return Ok(storage.halt_output(PrecompileHalt::Other(
420                "Invalid input: missing function selector".into(),
421            )));
422        }
423    }
424
425    let selector: [u8; 4] = calldata[..4].try_into().expect("calldata len >= 4");
426    if hardforks
427        .iter()
428        .any(|schedule| schedule.rejects(selector, storage.spec()))
429    {
430        return storage.error_result(error::TempoPrecompileError::UnknownFunctionSelector(
431            selector,
432        ));
433    }
434
435    let result = decode(calldata);
436
437    match result {
438        Ok(call) => f(call).map(|mut res| {
439            // TODO: fix this, each precompile handler should either return output with proper gas values or don't return any gas values at all.
440            res.gas_used = storage.gas_used();
441            fill_state_gas(&mut res, &storage);
442            res
443        }),
444        Err(alloy::sol_types::Error::UnknownSelector { selector, .. }) => storage.error_result(
445            error::TempoPrecompileError::UnknownFunctionSelector(*selector),
446        ),
447        Err(_) => Ok(storage.revert_output(Bytes::new())),
448    }
449}
450
451/// Asserts that `result` is a reverted output whose bytes decode to `expected_error`.
452#[cfg(test)]
453pub fn expect_precompile_revert<E>(result: &PrecompileResult, expected_error: E)
454where
455    E: SolInterface + PartialEq + std::fmt::Debug,
456{
457    match result {
458        Ok(result) => {
459            assert!(result.is_revert());
460            let decoded = E::abi_decode(&result.bytes).unwrap();
461            assert_eq!(decoded, expected_error);
462        }
463        Err(other) => {
464            panic!("expected reverted output, got: {other:?}");
465        }
466    }
467}
468
469#[cfg(test)]
470mod tests {
471    use super::*;
472    use crate::{
473        storage::{StorageCtx, hashmap::HashMapStorageProvider},
474        tip20::TIP20Token,
475    };
476    use alloy::primitives::{Address, Bytes, U256, bytes};
477    use alloy_evm::{
478        EthEvmFactory, EvmEnv, EvmFactory, EvmInternals,
479        precompiles::{Precompile as AlloyEvmPrecompile, PrecompileInput},
480    };
481    use revm::{
482        context::{ContextTr, TxEnv},
483        database::{CacheDB, EmptyDB},
484        state::{AccountInfo, Bytecode},
485    };
486    use tempo_contracts::precompiles::{ITIP20, UnknownFunctionSelector};
487
488    #[test]
489    fn test_precompile_delegatecall() {
490        let cfg = CfgEnv::<TempoHardfork>::default();
491        let precompile = tempo_precompile!("TIP20Token", &cfg, |input| {
492            TIP20Token::from_address(PATH_USD_ADDRESS).expect("PATH_USD_ADDRESS is valid")
493        });
494
495        let db = CacheDB::new(EmptyDB::new());
496        let mut evm = EthEvmFactory::default().create_evm(db, EvmEnv::default());
497        let block = evm.block.clone();
498        let tx = TxEnv::default();
499        let evm_internals = EvmInternals::new(evm.journal_mut(), &block, &cfg, &tx);
500
501        let target_address = Address::random();
502        let bytecode_address = Address::random();
503        let input = PrecompileInput {
504            data: &Bytes::new(),
505            caller: Address::ZERO,
506            internals: evm_internals,
507            gas: 0,
508            value: U256::ZERO,
509            is_static: false,
510            target_address,
511            bytecode_address,
512            reservoir: 0,
513        };
514
515        let result = AlloyEvmPrecompile::call(&precompile, input);
516
517        match result {
518            Ok(output) => {
519                assert!(output.is_revert());
520                let decoded = DelegateCallNotAllowed::abi_decode(&output.bytes).unwrap();
521                assert!(matches!(decoded, DelegateCallNotAllowed {}));
522            }
523            Err(_) => panic!("expected reverted output"),
524        }
525    }
526
527    #[test]
528    fn test_precompile_static_call() {
529        let cfg = CfgEnv::<TempoHardfork>::default();
530        let tx = TxEnv::default();
531        let precompile = tempo_precompile!("TIP20Token", &cfg, |input| {
532            TIP20Token::from_address(PATH_USD_ADDRESS).expect("PATH_USD_ADDRESS is valid")
533        });
534
535        let token_address = PATH_USD_ADDRESS;
536
537        let call_static = |calldata: Bytes| {
538            let mut db = CacheDB::new(EmptyDB::new());
539            db.insert_account_info(
540                token_address,
541                AccountInfo {
542                    code: Some(Bytecode::new_raw(bytes!("0xEF"))),
543                    ..Default::default()
544                },
545            );
546            let mut evm = EthEvmFactory::default().create_evm(db, EvmEnv::default());
547            let block = evm.block.clone();
548            let evm_internals = EvmInternals::new(evm.journal_mut(), &block, &cfg, &tx);
549
550            let input = PrecompileInput {
551                data: &calldata,
552                caller: Address::ZERO,
553                internals: evm_internals,
554                gas: 1_000_000,
555                is_static: true,
556                value: U256::ZERO,
557                target_address: token_address,
558                bytecode_address: token_address,
559                reservoir: 0,
560            };
561
562            AlloyEvmPrecompile::call(&precompile, input)
563        };
564
565        // Static calls into mutating functions should fail
566        let result = call_static(Bytes::from(
567            ITIP20::transferCall {
568                to: Address::random(),
569                amount: U256::from(100),
570            }
571            .abi_encode(),
572        ));
573        let output = result.expect("expected Ok");
574        assert!(output.is_revert());
575        assert!(StaticCallNotAllowed::abi_decode(&output.bytes).is_ok());
576
577        // Static calls into mutate void functions should fail
578        let result = call_static(Bytes::from(
579            ITIP20::approveCall {
580                spender: Address::random(),
581                amount: U256::from(100),
582            }
583            .abi_encode(),
584        ));
585        let output = result.expect("expected Ok");
586        assert!(output.is_revert());
587        assert!(StaticCallNotAllowed::abi_decode(&output.bytes).is_ok());
588
589        // Static calls into view functions should succeed
590        let result = call_static(Bytes::from(
591            ITIP20::balanceOfCall {
592                account: Address::random(),
593            }
594            .abi_encode(),
595        ));
596        let output = result.expect("expected Ok");
597        assert!(
598            !output.is_revert(),
599            "view function should not revert in static context"
600        );
601    }
602
603    /// Verifies that early-return revert paths in precompile `call()` methods correctly
604    /// report gas_used. When a TIP-20 precompile reverts before reaching `dispatch_call`
605    /// (e.g., uninitialized token), the gas consumed for input decoding and account info
606    /// checks must still be reported in the `PrecompileOutput.gas_used` field.
607    #[test]
608    fn test_early_return_revert_reports_gas_used() {
609        let mut cfg = CfgEnv::<TempoHardfork>::default();
610        cfg.set_spec_and_mainnet_gas_params(TempoHardfork::T1);
611        let tx = TxEnv::default();
612        let precompile = tempo_precompile!("TIP20Token", &cfg, |input| {
613            TIP20Token::from_address(PATH_USD_ADDRESS).expect("PATH_USD_ADDRESS is valid")
614        });
615
616        let token_address = PATH_USD_ADDRESS;
617
618        // NO bytecode set -- token is uninitialized, early revert before dispatch_call
619        let db = CacheDB::new(EmptyDB::new());
620        let mut evm = EthEvmFactory::default().create_evm(db, EvmEnv::default());
621        let block = evm.block.clone();
622        let evm_internals = EvmInternals::new(evm.journal_mut(), &block, &cfg, &tx);
623
624        let calldata = Bytes::from(
625            ITIP20::transferCall {
626                to: Address::random(),
627                amount: U256::from(100),
628            }
629            .abi_encode(),
630        );
631
632        let input = PrecompileInput {
633            data: &calldata,
634            caller: Address::ZERO,
635            internals: evm_internals,
636            gas: 1_000_000,
637            is_static: false,
638            value: U256::ZERO,
639            target_address: token_address,
640            bytecode_address: token_address,
641            reservoir: 0,
642        };
643
644        let result = AlloyEvmPrecompile::call(&precompile, input);
645        let output = result.expect("expected Ok");
646        assert!(
647            output.status.is_revert(),
648            "uninitialized token should revert"
649        );
650        // Gas used should include input_cost(68) = 18 + with_account_info cost
651        assert!(
652            output.gas_used > 0,
653            "early-return revert should report non-zero gas_used, got {}",
654            output.gas_used
655        );
656    }
657
658    #[test]
659    fn test_invalid_calldata_hardfork_behavior() {
660        let call_with_spec = |calldata: Bytes, spec: TempoHardfork| {
661            let mut cfg = CfgEnv::<TempoHardfork>::default();
662            cfg.set_spec_and_mainnet_gas_params(spec);
663            let tx = TxEnv::default();
664            let precompile = tempo_precompile!("TIP20Token", &cfg, |input| {
665                TIP20Token::from_address(PATH_USD_ADDRESS).expect("PATH_USD_ADDRESS is valid")
666            });
667
668            let mut db = CacheDB::new(EmptyDB::new());
669            db.insert_account_info(
670                PATH_USD_ADDRESS,
671                AccountInfo {
672                    code: Some(Bytecode::new_raw(bytes!("0xEF"))),
673                    ..Default::default()
674                },
675            );
676            let mut evm = EthEvmFactory::default().create_evm(db, EvmEnv::default());
677            let block = evm.block.clone();
678            let evm_internals = EvmInternals::new(evm.journal_mut(), &block, &cfg, &tx);
679
680            let input = PrecompileInput {
681                data: &calldata,
682                caller: Address::ZERO,
683                internals: evm_internals,
684                gas: 1_000_000,
685                is_static: false,
686                value: U256::ZERO,
687                target_address: PATH_USD_ADDRESS,
688                bytecode_address: PATH_USD_ADDRESS,
689                reservoir: 0,
690            };
691
692            AlloyEvmPrecompile::call(&precompile, input)
693        };
694
695        // T1: empty calldata (missing selector) should return a reverted output
696        let empty = call_with_spec(Bytes::new(), TempoHardfork::T1)
697            .expect("T1: expected Ok with reverted output");
698        assert!(empty.is_revert(), "T1: expected reverted output");
699        assert!(empty.bytes.is_empty());
700        // Gas was consumed
701        assert!(empty.gas_used > 0);
702
703        // T1: unknown selector should return a reverted output with UnknownFunctionSelector error
704        let unknown = call_with_spec(Bytes::from([0xAA; 4]), TempoHardfork::T1)
705            .expect("T1: expected Ok with reverted output");
706        assert!(unknown.is_revert(), "T1: expected reverted output");
707
708        // Verify it's an UnknownFunctionSelector error with the correct selector
709        let decoded =
710            tempo_contracts::precompiles::UnknownFunctionSelector::abi_decode(&unknown.bytes)
711                .expect("T1: expected UnknownFunctionSelector error");
712        assert_eq!(decoded.selector.as_slice(), &[0xAA, 0xAA, 0xAA, 0xAA]);
713
714        // Verify gas is tracked for both cases (unknown selector may cost slightly more due `INPUT_PER_WORD_COST`)
715        assert!(unknown.gas_used >= empty.gas_used);
716
717        // Pre-T1 (T0): invalid calldata should return a halted output
718        let result = call_with_spec(Bytes::new(), TempoHardfork::T0);
719        let output = result.expect("T0: expected Ok(halt) for invalid calldata");
720        assert!(
721            output.is_halt(),
722            "T0: expected halted output for invalid calldata"
723        );
724    }
725
726    /// Pre-T4 precompile calls must not report state_gas_used, because the new revm's
727    /// reservoir model propagates it via `handle_reservoir_remaining_gas` on revert/halt,
728    /// corrupting `tx_gas_used()`.
729    #[test]
730    fn test_precompile_state_gas_zero_pre_t4() {
731        let call_with_spec = |calldata: Bytes, spec: TempoHardfork| {
732            let mut cfg = CfgEnv::<TempoHardfork>::default();
733            cfg.set_spec_and_mainnet_gas_params(spec);
734            let tx = TxEnv::default();
735            let precompile = tempo_precompile!("TIP20Token", &cfg, |input| {
736                TIP20Token::from_address(PATH_USD_ADDRESS).expect("PATH_USD_ADDRESS is valid")
737            });
738
739            let mut db = CacheDB::new(EmptyDB::new());
740            db.insert_account_info(
741                PATH_USD_ADDRESS,
742                AccountInfo {
743                    code: Some(Bytecode::new_raw(bytes!("0xEF"))),
744                    ..Default::default()
745                },
746            );
747            let mut evm = EthEvmFactory::default().create_evm(db, EvmEnv::default());
748            let block = evm.block.clone();
749            let evm_internals = EvmInternals::new(evm.journal_mut(), &block, &cfg, &tx);
750
751            let input = PrecompileInput {
752                data: &calldata,
753                caller: Address::ZERO,
754                internals: evm_internals,
755                gas: 1_000_000,
756                is_static: false,
757                value: U256::ZERO,
758                target_address: PATH_USD_ADDRESS,
759                bytecode_address: PATH_USD_ADDRESS,
760                reservoir: 0,
761            };
762
763            AlloyEvmPrecompile::call(&precompile, input)
764        };
765
766        // Pre-T4 (T2): state_gas_used must be 0
767        let result = call_with_spec(
768            ITIP20::balanceOfCall::new((Address::ZERO,))
769                .abi_encode()
770                .into(),
771            TempoHardfork::T2,
772        )
773        .expect("T2 balanceOf should succeed");
774        assert!(result.gas_used > 0, "precompile should consume gas");
775        assert_eq!(
776            result.state_gas_used, 0,
777            "pre-T4 precompile must not report state_gas_used, got {}",
778            result.state_gas_used
779        );
780
781        // Pre-T4 (T1): reverted call should also have state_gas_used == 0
782        let reverted =
783            call_with_spec(Bytes::new(), TempoHardfork::T1).expect("T1 empty should revert");
784        assert!(reverted.status.is_revert());
785        assert_eq!(
786            reverted.state_gas_used, 0,
787            "pre-T4 reverted precompile must not report state_gas_used"
788        );
789    }
790
791    /// T4+ precompile `state_gas_used` must only include state-creating gas (cold SSTORE
792    /// zero->non-zero), not all gas consumed. A read-only operation like `balanceOf` must
793    /// have `state_gas_used == 0` even though `gas_used > 0`.
794    #[test]
795    fn test_t4_state_gas_only_includes_state_creating_ops() {
796        let mut cfg = CfgEnv::<TempoHardfork>::default();
797        cfg.set_spec_and_mainnet_gas_params(TempoHardfork::T4);
798
799        let sender = Address::repeat_byte(0x01);
800        let recipient = Address::repeat_byte(0x02);
801
802        let precompile = tempo_precompile!("TIP20Token", &cfg, |input| {
803            TIP20Token::from_address(PATH_USD_ADDRESS).expect("PATH_USD_ADDRESS is valid")
804        });
805
806        let db = CacheDB::new(EmptyDB::new());
807        let mut evm = EthEvmFactory::default().create_evm(db, EvmEnv::default());
808
809        // Set up TIP20 token state: initialize pathUSD and mint tokens to sender
810        {
811            let block = evm.block.clone();
812            let tx = TxEnv::default();
813            let internals = EvmInternals::new(evm.journal_mut(), &block, &cfg, &tx);
814            let mut provider =
815                crate::storage::evm::EvmPrecompileStorageProvider::new_max_gas(internals, &cfg);
816            crate::storage::StorageCtx::enter(&mut provider, || {
817                crate::test_util::TIP20Setup::path_usd(sender)
818                    .with_issuer(sender)
819                    .with_mint(sender, U256::from(1000))
820                    .apply()
821            })
822            .expect("TIP20 setup should succeed");
823        }
824
825        // 1) Read-only: balanceOf must have state_gas_used == 0
826        let calldata: Bytes = ITIP20::balanceOfCall { account: sender }
827            .abi_encode()
828            .into();
829        let block = evm.block.clone();
830        let tx = TxEnv::default();
831        let evm_internals = EvmInternals::new(evm.journal_mut(), &block, &cfg, &tx);
832        let input = PrecompileInput {
833            data: &calldata,
834            caller: sender,
835            internals: evm_internals,
836            gas: 1_000_000,
837            is_static: false,
838            value: U256::ZERO,
839            target_address: PATH_USD_ADDRESS,
840            bytecode_address: PATH_USD_ADDRESS,
841            reservoir: 0,
842        };
843        let output =
844            AlloyEvmPrecompile::call(&precompile, input).expect("balanceOf should succeed");
845        assert!(output.is_success());
846        assert!(output.gas_used > 0, "balanceOf should consume gas");
847        assert_eq!(
848            output.state_gas_used, 0,
849            "read-only balanceOf must have state_gas_used == 0, got {}",
850            output.state_gas_used
851        );
852
853        // 2) Transfer to existing account (warm SSTORE, not zero->non-zero for recipient
854        //    since we pre-fund recipient): state_gas_used must be less than gas_used
855        {
856            // Pre-fund recipient so the transfer is warm SSTORE (nonzero->nonzero)
857            let block = evm.block.clone();
858            let tx = TxEnv::default();
859            let internals = EvmInternals::new(evm.journal_mut(), &block, &cfg, &tx);
860            let mut provider =
861                crate::storage::evm::EvmPrecompileStorageProvider::new_max_gas(internals, &cfg);
862            crate::storage::StorageCtx::enter(&mut provider, || {
863                crate::test_util::TIP20Setup::path_usd(sender)
864                    .with_mint(recipient, U256::from(1))
865                    .apply()
866            })
867            .expect("TIP20 setup should succeed");
868        }
869        let calldata: Bytes = ITIP20::transferCall {
870            to: recipient,
871            amount: U256::from(100),
872        }
873        .abi_encode()
874        .into();
875        let block = evm.block.clone();
876        let tx = TxEnv::default();
877        let evm_internals = EvmInternals::new(evm.journal_mut(), &block, &cfg, &tx);
878        let input = PrecompileInput {
879            data: &calldata,
880            caller: sender,
881            internals: evm_internals,
882            gas: 1_000_000,
883            is_static: false,
884            value: U256::ZERO,
885            target_address: PATH_USD_ADDRESS,
886            bytecode_address: PATH_USD_ADDRESS,
887            reservoir: 0,
888        };
889        let output = AlloyEvmPrecompile::call(&precompile, input).expect("transfer should succeed");
890        assert!(output.is_success());
891        assert!(output.gas_used > 0, "transfer should consume gas");
892        assert_eq!(
893            output.state_gas_used, 0,
894            "transfer to existing account (nonzero->nonzero SSTORE) must have state_gas_used == 0, got {}",
895            output.state_gas_used
896        );
897    }
898
899    /// T4+ precompile calls that trigger SSTORE refunds must encode the refund
900    /// in the `reservoir` field of `PrecompileOutput`, so the wrapper
901    /// `PrecompileProvider` can extract and apply it via `record_refund`.
902    /// Pre-T4 blocks were executed without refund propagation, so they must NOT
903    /// encode refunds.
904    #[test]
905    fn test_precompile_gas_refund_in_reservoir_t4() {
906        let mut cfg = CfgEnv::<TempoHardfork>::default();
907        cfg.set_spec_and_mainnet_gas_params(TempoHardfork::T4);
908        // TIP-1016 gates state-gas refund propagation on `enable_amsterdam_eip8037`.
909        cfg.enable_amsterdam_eip8037 = true;
910
911        let sender = Address::repeat_byte(0x01);
912        let recipient = Address::repeat_byte(0x02);
913
914        let precompile = tempo_precompile!("TIP20Token", &cfg, |input| {
915            TIP20Token::from_address(PATH_USD_ADDRESS).expect("PATH_USD_ADDRESS is valid")
916        });
917
918        let db = CacheDB::new(EmptyDB::new());
919        let mut evm = EthEvmFactory::default().create_evm(db, EvmEnv::default());
920
921        // Set up TIP20 token state: initialize pathUSD and mint tokens to sender
922        {
923            let block = evm.block.clone();
924            let tx = TxEnv::default();
925            let internals = EvmInternals::new(evm.journal_mut(), &block, &cfg, &tx);
926            let mut provider =
927                crate::storage::evm::EvmPrecompileStorageProvider::new_max_gas(internals, &cfg);
928            crate::storage::StorageCtx::enter(&mut provider, || {
929                crate::test_util::TIP20Setup::path_usd(sender)
930                    .with_issuer(sender)
931                    .with_mint(sender, U256::from(1000))
932                    .apply()
933            })
934            .expect("TIP20 setup should succeed");
935        }
936
937        // Transfer ALL tokens from sender to recipient (sender balance: 1000 → 0)
938        // This triggers SSTORE refund because the balance slot goes from nonzero to zero.
939        let calldata: Bytes = ITIP20::transferCall {
940            to: recipient,
941            amount: U256::from(1000),
942        }
943        .abi_encode()
944        .into();
945
946        let block = evm.block.clone();
947        let tx = TxEnv::default();
948        let evm_internals = EvmInternals::new(evm.journal_mut(), &block, &cfg, &tx);
949
950        let input = PrecompileInput {
951            data: &calldata,
952            caller: sender,
953            internals: evm_internals,
954            gas: 1_000_000,
955            is_static: false,
956            value: U256::ZERO,
957            target_address: PATH_USD_ADDRESS,
958            bytecode_address: PATH_USD_ADDRESS,
959            reservoir: 0,
960        };
961
962        let output = AlloyEvmPrecompile::call(&precompile, input).expect("transfer should succeed");
963        assert!(output.is_success(), "transfer should be successful");
964
965        // T4+: gas refund must be encoded in the gas_refunded field
966        assert!(
967            output.gas_refunded != 0,
968            "T4+ successful precompile with SSTORE refund must encode refund in gas_refunded, got 0"
969        );
970    }
971
972    #[test]
973    fn test_dispatch_call_applies_hardfork_selector_gates() -> eyre::Result<()> {
974        alloy::sol! {
975            interface ISelectorGatedTest {
976                function stable() external;
977                function t2Added(uint256 value) external;
978                function t3Removed() external;
979            }
980        }
981
982        const SELECTOR_SCHEDULE: &[SelectorSchedule<'static>] = &[
983            SelectorSchedule::new(TempoHardfork::T2)
984                .with_added(&[ISelectorGatedTest::t2AddedCall::SELECTOR]),
985            SelectorSchedule::new(TempoHardfork::T3)
986                .with_dropped(&[ISelectorGatedTest::t3RemovedCall::SELECTOR]),
987        ];
988
989        let call_with_spec = |spec: TempoHardfork, calldata: &[u8]| {
990            let mut storage = HashMapStorageProvider::new_with_spec(1, spec);
991            StorageCtx::enter(&mut storage, || {
992                dispatch_call(
993                    calldata,
994                    SELECTOR_SCHEDULE,
995                    ISelectorGatedTest::ISelectorGatedTestCalls::abi_decode,
996                    |call| match call {
997                        ISelectorGatedTest::ISelectorGatedTestCalls::stable(_) => {
998                            Ok(PrecompileOutput::new(0, Bytes::from_static(b"stable"), 0))
999                        }
1000                        ISelectorGatedTest::ISelectorGatedTestCalls::t2Added(_) => {
1001                            Ok(PrecompileOutput::new(0, Bytes::from_static(b"added"), 0))
1002                        }
1003                        ISelectorGatedTest::ISelectorGatedTestCalls::t3Removed(_) => {
1004                            Ok(PrecompileOutput::new(0, Bytes::from_static(b"removed"), 0))
1005                        }
1006                    },
1007                )
1008            })
1009        };
1010
1011        let t2_added_calldata = ISelectorGatedTest::t2AddedCall { value: U256::ZERO }.abi_encode();
1012        let t3_removed_calldata = ISelectorGatedTest::t3RemovedCall {}.abi_encode();
1013
1014        // pre-T2: selectors introduced at T2 must still look unknown.
1015        let pre_t2_added = call_with_spec(TempoHardfork::T1, &t2_added_calldata)?;
1016        assert!(pre_t2_added.is_revert());
1017        let decoded = UnknownFunctionSelector::abi_decode(&pre_t2_added.bytes)?;
1018        assert_eq!(
1019            decoded.selector.as_slice(),
1020            &ISelectorGatedTest::t2AddedCall::SELECTOR
1021        );
1022
1023        // T2+: that selector becomes available and dispatches normally.
1024        let post_t2_added = call_with_spec(TempoHardfork::T2, &t2_added_calldata)?;
1025        assert!(!post_t2_added.is_revert());
1026        assert_eq!(post_t2_added.bytes.as_ref(), b"added");
1027
1028        // pre-T3: selectors removed at T3 still dispatch normally.
1029        let pre_t3_removed = call_with_spec(TempoHardfork::T2, &t3_removed_calldata)?;
1030        assert!(!pre_t3_removed.is_revert());
1031        assert_eq!(pre_t3_removed.bytes.as_ref(), b"removed");
1032
1033        // T3+: the removed selector must now revert as unknown.
1034        let post_t3_removed = call_with_spec(TempoHardfork::T3, &t3_removed_calldata)?;
1035        assert!(post_t3_removed.is_revert());
1036        let decoded = UnknownFunctionSelector::abi_decode(&post_t3_removed.bytes)?;
1037        assert_eq!(
1038            decoded.selector.as_slice(),
1039            &ISelectorGatedTest::t3RemovedCall::SELECTOR
1040        );
1041
1042        // preT2: gated selectors must return `UnknownFunctionSelector` even for selector-only calldata.
1043        let malformed_added = call_with_spec(
1044            TempoHardfork::T1,
1045            &ISelectorGatedTest::t2AddedCall::SELECTOR,
1046        )?;
1047        assert!(malformed_added.is_revert());
1048        let decoded = UnknownFunctionSelector::abi_decode(&malformed_added.bytes)?;
1049        assert_eq!(
1050            decoded.selector.as_slice(),
1051            &ISelectorGatedTest::t2AddedCall::SELECTOR
1052        );
1053
1054        Ok(())
1055    }
1056
1057    #[test]
1058    fn test_input_cost_returns_non_zero_for_input() {
1059        // Empty input should cost 0
1060        assert_eq!(input_cost(0), 0);
1061
1062        // 1 byte should cost INPUT_PER_WORD_COST (rounds up to 1 word)
1063        assert_eq!(input_cost(1), INPUT_PER_WORD_COST);
1064
1065        // 32 bytes (1 word) should cost INPUT_PER_WORD_COST
1066        assert_eq!(input_cost(32), INPUT_PER_WORD_COST);
1067
1068        // 33 bytes (2 words) should cost 2 * INPUT_PER_WORD_COST
1069        assert_eq!(input_cost(33), INPUT_PER_WORD_COST * 2);
1070    }
1071
1072    #[test]
1073    fn test_extend_tempo_precompiles_registers_precompiles() {
1074        let mut cfg = CfgEnv::<TempoHardfork>::default();
1075        cfg.set_spec_and_mainnet_gas_params(TempoHardfork::T3);
1076        let precompiles = tempo_precompiles(&cfg);
1077
1078        // TIP20Factory should be registered
1079        let factory_precompile = precompiles.get(&TIP20_FACTORY_ADDRESS);
1080        assert!(
1081            factory_precompile.is_some(),
1082            "TIP20Factory should be registered"
1083        );
1084
1085        // TIP403Registry should be registered
1086        let registry_precompile = precompiles.get(&TIP403_REGISTRY_ADDRESS);
1087        assert!(
1088            registry_precompile.is_some(),
1089            "TIP403Registry should be registered"
1090        );
1091
1092        // TipFeeManager should be registered
1093        let fee_manager_precompile = precompiles.get(&TIP_FEE_MANAGER_ADDRESS);
1094        assert!(
1095            fee_manager_precompile.is_some(),
1096            "TipFeeManager should be registered"
1097        );
1098
1099        // StablecoinDEX should be registered
1100        let dex_precompile = precompiles.get(&STABLECOIN_DEX_ADDRESS);
1101        assert!(
1102            dex_precompile.is_some(),
1103            "StablecoinDEX should be registered"
1104        );
1105
1106        // NonceManager should be registered
1107        let nonce_precompile = precompiles.get(&NONCE_PRECOMPILE_ADDRESS);
1108        assert!(
1109            nonce_precompile.is_some(),
1110            "NonceManager should be registered"
1111        );
1112
1113        // ValidatorConfig should be registered
1114        let validator_precompile = precompiles.get(&VALIDATOR_CONFIG_ADDRESS);
1115        assert!(
1116            validator_precompile.is_some(),
1117            "ValidatorConfig should be registered"
1118        );
1119
1120        // ValidatorConfigV2 should be registered
1121        let validator_v2_precompile = precompiles.get(&VALIDATOR_CONFIG_V2_ADDRESS);
1122        assert!(
1123            validator_v2_precompile.is_some(),
1124            "ValidatorConfigV2 should be registered"
1125        );
1126
1127        // AccountKeychain should be registered
1128        let keychain_precompile = precompiles.get(&ACCOUNT_KEYCHAIN_ADDRESS);
1129        assert!(
1130            keychain_precompile.is_some(),
1131            "AccountKeychain should be registered"
1132        );
1133
1134        // SignatureVerifier should be registered at T3
1135        let sig_verifier_precompile = precompiles.get(&SIGNATURE_VERIFIER_ADDRESS);
1136        assert!(
1137            sig_verifier_precompile.is_some(),
1138            "SignatureVerifier should be registered at T3"
1139        );
1140
1141        // TIP20 tokens with prefix should be registered
1142        let tip20_precompile = precompiles.get(&PATH_USD_ADDRESS);
1143        assert!(
1144            tip20_precompile.is_some(),
1145            "TIP20 tokens should be registered"
1146        );
1147
1148        // Random address without TIP20 prefix should NOT be registered
1149        let random_address = Address::random();
1150        let random_precompile = precompiles.get(&random_address);
1151        assert!(
1152            random_precompile.is_none(),
1153            "Random address should not be a precompile"
1154        );
1155    }
1156
1157    #[test]
1158    fn test_signature_verifier_not_registered_pre_t3() {
1159        let cfg = CfgEnv::<TempoHardfork>::default();
1160        let precompiles = tempo_precompiles(&cfg);
1161
1162        assert!(
1163            precompiles.get(&SIGNATURE_VERIFIER_ADDRESS).is_none(),
1164            "SignatureVerifier should NOT be registered before T3"
1165        );
1166    }
1167
1168    #[test]
1169    fn test_p256verify_availability_across_t1c_boundary() {
1170        let has_p256 = |spec: TempoHardfork| -> bool {
1171            // P256VERIFY lives at address 0x100 (256), added in Osaka
1172            let p256_addr = Address::from_word(U256::from(256).into());
1173
1174            let mut cfg = CfgEnv::<TempoHardfork>::default();
1175            cfg.set_spec_and_mainnet_gas_params(spec);
1176            tempo_precompiles(&cfg).get(&p256_addr).is_some()
1177        };
1178
1179        // Pre-T1C hardforks should use Prague precompiles (no P256VERIFY)
1180        for spec in [
1181            TempoHardfork::Genesis,
1182            TempoHardfork::T0,
1183            TempoHardfork::T1,
1184            TempoHardfork::T1A,
1185            TempoHardfork::T1B,
1186        ] {
1187            assert!(
1188                !has_p256(spec),
1189                "P256VERIFY should NOT be available at {spec:?} (pre-T1C)"
1190            );
1191        }
1192
1193        // T1C+ hardforks should use Osaka precompiles (P256VERIFY available)
1194        for spec in [TempoHardfork::T1C, TempoHardfork::T2] {
1195            assert!(
1196                has_p256(spec),
1197                "P256VERIFY should be available at {spec:?} (T1C+)"
1198            );
1199        }
1200    }
1201}