Skip to main content

tempo_precompiles/tip20_factory/
mod.rs

1//! [TIP-20] token factory precompile — deploys new [TIP-20] tokens at deterministic addresses.
2//!
3//! [TIP-20]: <https://docs.tempo.xyz/protocol/tip20>
4
5pub mod dispatch;
6
7pub use tempo_contracts::precompiles::{ITIP20Factory, TIP20FactoryError, TIP20FactoryEvent};
8use tempo_precompiles_macros::contract;
9
10use crate::{
11    PATH_USD_ADDRESS, TIP20_FACTORY_ADDRESS,
12    error::{Result, TempoPrecompileError},
13    tip20::{TIP20Error, TIP20Token, USD_CURRENCY, is_tip20_prefix},
14};
15use alloy::{
16    primitives::{Address, B256, keccak256},
17    sol_types::SolValue,
18};
19use tracing::trace;
20
21/// Number of reserved addresses (0 to RESERVED_SIZE-1) that cannot be deployed via factory
22const RESERVED_SIZE: u64 = 1024;
23
24/// TIP20 token address prefix (12 bytes): 0x20C000000000000000000000
25const TIP20_PREFIX_BYTES: [u8; 12] = [
26    0x20, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
27];
28
29/// Factory contract for deploying new TIP-20 tokens at deterministic addresses.
30///
31/// Tokens are deployed at `TIP20_PREFIX || keccak256(sender, salt)[..8]`.
32/// The first 1024 addresses are reserved for protocol-deployed tokens.
33///
34/// The struct fields define the on-chain storage layout; the `#[contract]` macro generates the
35/// storage handlers which provide an ergonomic way to interact with the EVM state.
36#[contract(addr = TIP20_FACTORY_ADDRESS)]
37pub struct TIP20Factory {}
38
39/// Computes the deterministic TIP20 address from sender and salt.
40/// Returns the address and the lower bytes used for derivation.
41#[cfg_attr(test, allow(dead_code))]
42pub(crate) fn compute_tip20_address(sender: Address, salt: B256) -> (Address, u64) {
43    let hash = keccak256((sender, salt).abi_encode());
44
45    // Take first 8 bytes of hash as lower bytes
46    let mut padded = [0u8; 8];
47    padded.copy_from_slice(&hash[..8]);
48    let lower_bytes = u64::from_be_bytes(padded);
49
50    // Construct the address: TIP20_PREFIX (12 bytes) || hash[..8] (8 bytes)
51    let mut address_bytes = [0u8; 20];
52    address_bytes[..12].copy_from_slice(&TIP20_PREFIX_BYTES);
53    address_bytes[12..].copy_from_slice(&hash[..8]);
54
55    (Address::from(address_bytes), lower_bytes)
56}
57
58// Precompile functions
59impl TIP20Factory {
60    /// Initializes the TIP-20 factory precompile.
61    pub fn initialize(&mut self) -> Result<()> {
62        self.__initialize()
63    }
64
65    /// Computes the deterministic address for a token given `sender` and `salt`. Reverts if the
66    /// derived address falls within the reserved range (lower 8 bytes < `RESERVED_SIZE`).
67    ///
68    /// # Errors
69    /// - `AddressReserved` — the derived address is in the reserved range
70    pub fn get_token_address(&self, call: ITIP20Factory::getTokenAddressCall) -> Result<Address> {
71        let (address, lower_bytes) = compute_tip20_address(call.sender, call.salt);
72
73        // Check if address would be in reserved range
74        if lower_bytes < RESERVED_SIZE {
75            return Err(TempoPrecompileError::TIP20Factory(
76                TIP20FactoryError::address_reserved(),
77            ));
78        }
79
80        Ok(address)
81    }
82
83    /// Returns `true` if `token` has the correct TIP-20 prefix and has code deployed.
84    pub fn is_tip20(&self, token: Address) -> Result<bool> {
85        if !is_tip20_prefix(token) {
86            return Ok(false);
87        }
88        // Check if the token has code deployed (non-empty code hash)
89        self.storage
90            .with_account_info(token, |info| Ok(!info.is_empty_code_hash()))
91    }
92
93    /// Deploys a new TIP-20 token at a deterministic address derived from `sender` and `salt`.
94    ///
95    /// Validates that the token does not already exist, the quote token is a deployed TIP-20 of
96    /// a compatible currency, and the derived address is outside the reserved range. Initializes
97    /// the token via [`TIP20Token::initialize`].
98    ///
99    /// # Errors
100    /// - `TokenAlreadyExists` — a TIP-20 is already deployed at the derived address
101    /// - `InvalidQuoteToken` — quote token is not a deployed TIP-20 or has incompatible currency
102    /// - `AddressReserved` — the derived address is in the reserved range
103    pub fn create_token(
104        &mut self,
105        sender: Address,
106        call: ITIP20Factory::createTokenCall,
107    ) -> Result<Address> {
108        trace!(%sender, ?call, "Create token");
109
110        // Compute the deterministic address from sender and salt
111        let (token_address, lower_bytes) = compute_tip20_address(sender, call.salt);
112
113        if self.is_tip20(token_address)? {
114            return Err(TempoPrecompileError::TIP20Factory(
115                TIP20FactoryError::token_already_exists(token_address),
116            ));
117        }
118
119        // Ensure that the quote token is a valid TIP20 that is currently deployed.
120        if !self.is_tip20(call.quoteToken)? {
121            return Err(TIP20Error::invalid_quote_token().into());
122        }
123
124        // If token is USD, its quote token must also be USD
125        if call.currency == USD_CURRENCY
126            && TIP20Token::from_address(call.quoteToken)?.currency()? != USD_CURRENCY
127        {
128            return Err(TIP20Error::invalid_quote_token().into());
129        }
130
131        // Check if address is in reserved range
132        if lower_bytes < RESERVED_SIZE {
133            return Err(TempoPrecompileError::TIP20Factory(
134                TIP20FactoryError::address_reserved(),
135            ));
136        }
137
138        TIP20Token::from_address(token_address)?.initialize(
139            sender,
140            &call.name,
141            &call.symbol,
142            &call.currency,
143            call.quoteToken,
144            call.admin,
145        )?;
146
147        self.emit_event(TIP20FactoryEvent::TokenCreated(
148            ITIP20Factory::TokenCreated {
149                token: token_address,
150                name: call.name,
151                symbol: call.symbol,
152                currency: call.currency,
153                quoteToken: call.quoteToken,
154                admin: call.admin,
155                salt: call.salt,
156            },
157        ))?;
158
159        Ok(token_address)
160    }
161
162    /// Deploys a TIP-20 token at a reserved address (lower 8 bytes < `RESERVED_SIZE`). Used
163    /// during genesis or hardforks to bootstrap protocol tokens like pathUSD.
164    ///
165    /// # Errors
166    /// - `InvalidToken` — `address` does not have the TIP-20 prefix
167    /// - `TokenAlreadyExists` — a TIP-20 is already deployed at `address`
168    /// - `InvalidQuoteToken` — quote token is invalid, not deployed, or has incompatible
169    ///   currency; pathUSD must use `Address::ZERO` as quote token
170    /// - `AddressNotReserved` — the address is outside the reserved range
171    pub fn create_token_reserved_address(
172        &mut self,
173        address: Address,
174        name: &str,
175        symbol: &str,
176        currency: &str,
177        quote_token: Address,
178        admin: Address,
179    ) -> Result<Address> {
180        // Validate that the address has a TIP20 prefix
181        if !is_tip20_prefix(address) {
182            return Err(TIP20Error::invalid_token().into());
183        }
184
185        // Validate that the address is not already deployed
186        if self.is_tip20(address)? {
187            return Err(TempoPrecompileError::TIP20Factory(
188                TIP20FactoryError::token_already_exists(address),
189            ));
190        }
191
192        // quote_token must be address(0) or a valid TIP20
193        if !quote_token.is_zero() {
194            // pathUSD must set address(0) as the quote token
195            // or the tip20 must be a valid deployed token
196            if address == PATH_USD_ADDRESS || !self.is_tip20(quote_token)? {
197                return Err(TIP20Error::invalid_quote_token().into());
198            }
199            // If token is USD, its quote token must also be USD
200            if currency == USD_CURRENCY
201                && TIP20Token::from_address(quote_token)?.currency()? != USD_CURRENCY
202            {
203                return Err(TIP20Error::invalid_quote_token().into());
204            }
205        }
206
207        // Validate that the address is within the reserved range
208        // Reserved addresses have their last 8 bytes represent a value < RESERVED_SIZE
209        let mut padded = [0u8; 8];
210        padded.copy_from_slice(&address.as_slice()[12..]);
211        let lower_bytes = u64::from_be_bytes(padded);
212        if lower_bytes >= RESERVED_SIZE {
213            return Err(TempoPrecompileError::TIP20Factory(
214                TIP20FactoryError::address_not_reserved(),
215            ));
216        }
217
218        let mut token = TIP20Token::from_address(address)?;
219        token.initialize(admin, name, symbol, currency, quote_token, admin)?;
220
221        self.emit_event(TIP20FactoryEvent::TokenCreated(
222            ITIP20Factory::TokenCreated {
223                token: address,
224                name: name.into(),
225                symbol: symbol.into(),
226                currency: currency.into(),
227                quoteToken: quote_token,
228                admin,
229                salt: B256::ZERO,
230            },
231        ))?;
232
233        Ok(address)
234    }
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240    use crate::{
241        PATH_USD_ADDRESS,
242        error::TempoPrecompileError,
243        storage::{ContractStorage, StorageCtx, hashmap::HashMapStorageProvider},
244        test_util::TIP20Setup,
245    };
246    use alloy::primitives::{Address, address};
247
248    #[test]
249    fn test_is_initialized() -> eyre::Result<()> {
250        let mut storage = HashMapStorageProvider::new(1);
251
252        StorageCtx::enter(&mut storage, || {
253            let mut factory = TIP20Factory::new();
254
255            // Factory should not be initialized before initialize() call
256            assert!(!factory.is_initialized()?);
257
258            // After initialize(), factory should be initialized
259            factory.initialize()?;
260            assert!(factory.is_initialized()?);
261
262            // Creating a new handle should still see initialized state
263            let factory2 = TIP20Factory::new();
264            assert!(factory2.is_initialized()?);
265
266            Ok(())
267        })
268    }
269
270    #[test]
271    fn test_is_tip20_prefix() -> eyre::Result<()> {
272        let mut storage = HashMapStorageProvider::new(1);
273
274        StorageCtx::enter(&mut storage, || {
275            // PATH_USD has correct prefix
276            assert!(is_tip20_prefix(PATH_USD_ADDRESS));
277
278            // Address with TIP20 prefix (0x20C0...)
279            let tip20_addr = Address::from(alloy::hex!("20C0000000000000000000000000000000001234"));
280            assert!(is_tip20_prefix(tip20_addr));
281
282            // Random address does not have TIP20 prefix
283            let random = Address::random();
284            assert!(!is_tip20_prefix(random));
285
286            Ok(())
287        })
288    }
289
290    #[test]
291    fn test_is_tip20() -> eyre::Result<()> {
292        let mut storage = HashMapStorageProvider::new(1);
293        let sender = Address::random();
294
295        StorageCtx::enter(&mut storage, || {
296            // Initialize pathUSD
297            let _path_usd = TIP20Setup::path_usd(sender).apply()?;
298
299            let factory = TIP20Factory::new();
300
301            // PATH_USD should be valid (has code deployed)
302            assert!(factory.is_tip20(PATH_USD_ADDRESS)?);
303
304            // Address with TIP20 prefix but no code should be invalid
305            let no_code_tip20 = address!("20C0000000000000000000000000000000000002");
306            assert!(!factory.is_tip20(no_code_tip20)?);
307
308            // Random address (wrong prefix) should be invalid
309            assert!(!factory.is_tip20(Address::random())?);
310
311            // Create a token via factory and verify it's valid
312            let token = TIP20Setup::create("Test", "TST", sender).apply()?;
313            assert!(factory.is_tip20(token.address())?);
314
315            Ok(())
316        })
317    }
318
319    #[test]
320    fn test_get_token_address() -> eyre::Result<()> {
321        let mut storage = HashMapStorageProvider::new(1);
322
323        StorageCtx::enter(&mut storage, || {
324            let factory = TIP20Factory::new();
325            let sender = Address::random();
326            let salt = B256::random();
327
328            // get_token_address should return same address as compute_tip20_address
329            let call = ITIP20Factory::getTokenAddressCall { sender, salt };
330            let address = factory.get_token_address(call)?;
331            let (expected, _) = compute_tip20_address(sender, salt);
332            assert_eq!(address, expected);
333
334            // Calling with same params should be deterministic
335            let call2 = ITIP20Factory::getTokenAddressCall { sender, salt };
336            assert_eq!(factory.get_token_address(call2)?, address);
337
338            Ok(())
339        })
340    }
341
342    #[test]
343    fn test_compute_tip20_address_deterministic() {
344        let sender1 = Address::random();
345        let sender2 = Address::random();
346        let salt1 = B256::random();
347        let salt2 = B256::random();
348
349        let (addr0, lower0) = compute_tip20_address(sender1, salt1);
350        let (addr1, lower1) = compute_tip20_address(sender1, salt1);
351        assert_eq!(addr0, addr1);
352        assert_eq!(lower0, lower1);
353
354        // Same salt with different senders should produce different addresses
355        let (addr2, lower2) = compute_tip20_address(sender1, salt1);
356        let (addr3, lower3) = compute_tip20_address(sender2, salt1);
357        assert_ne!(addr2, addr3);
358        assert_ne!(lower2, lower3);
359
360        // Same sender with different salts should produce different addresses
361        let (addr4, lower4) = compute_tip20_address(sender1, salt1);
362        let (addr5, lower5) = compute_tip20_address(sender1, salt2);
363        assert_ne!(addr4, addr5);
364        assert_ne!(lower4, lower5);
365
366        // All addresses should have TIP20 prefix
367        assert!(is_tip20_prefix(addr1));
368        assert!(is_tip20_prefix(addr2));
369        assert!(is_tip20_prefix(addr3));
370        assert!(is_tip20_prefix(addr4));
371        assert!(is_tip20_prefix(addr5));
372    }
373
374    #[test]
375    fn test_create_token() -> eyre::Result<()> {
376        let mut storage = HashMapStorageProvider::new(1);
377        let sender = Address::random();
378        StorageCtx::enter(&mut storage, || {
379            let mut factory = TIP20Setup::factory()?;
380            let path_usd = TIP20Setup::path_usd(sender).apply()?;
381            factory.clear_emitted_events();
382
383            let salt1 = B256::random();
384            let salt2 = B256::random();
385            let call1 = ITIP20Factory::createTokenCall {
386                name: "Test Token 1".to_string(),
387                symbol: "TEST1".to_string(),
388                currency: "USD".to_string(),
389                quoteToken: path_usd.address(),
390                admin: sender,
391                salt: salt1,
392            };
393            let call2 = ITIP20Factory::createTokenCall {
394                name: "Test Token 2".to_string(),
395                symbol: "TEST2".to_string(),
396                currency: "USD".to_string(),
397                quoteToken: path_usd.address(),
398                admin: sender,
399                salt: salt2,
400            };
401
402            let token_addr_1 = factory.create_token(sender, call1.clone())?;
403            let token_addr_2 = factory.create_token(sender, call2.clone())?;
404
405            // Verify addresses are different
406            assert_ne!(token_addr_1, token_addr_2);
407
408            // Verify addresses have TIP20 prefix
409            assert!(is_tip20_prefix(token_addr_1));
410            assert!(is_tip20_prefix(token_addr_2));
411
412            // Verify tokens are valid TIP20s
413            assert!(factory.is_tip20(token_addr_1)?);
414            assert!(factory.is_tip20(token_addr_2)?);
415
416            // Verify event emission
417            factory.assert_emitted_events(vec![
418                TIP20FactoryEvent::TokenCreated(ITIP20Factory::TokenCreated {
419                    token: token_addr_1,
420                    name: call1.name,
421                    symbol: call1.symbol,
422                    currency: call1.currency,
423                    quoteToken: call1.quoteToken,
424                    admin: call1.admin,
425                    salt: call1.salt,
426                }),
427                TIP20FactoryEvent::TokenCreated(ITIP20Factory::TokenCreated {
428                    token: token_addr_2,
429                    name: call2.name,
430                    symbol: call2.symbol,
431                    currency: call2.currency,
432                    quoteToken: call2.quoteToken,
433                    admin: call2.admin,
434                    salt: call2.salt,
435                }),
436            ]);
437
438            Ok(())
439        })
440    }
441
442    #[test]
443    fn test_create_token_invalid_quote_token() -> eyre::Result<()> {
444        let mut storage = HashMapStorageProvider::new(1);
445        let sender = Address::random();
446        StorageCtx::enter(&mut storage, || {
447            let mut factory = TIP20Setup::factory()?;
448            TIP20Setup::path_usd(sender).apply()?;
449
450            let invalid_call = ITIP20Factory::createTokenCall {
451                name: "Test Token".to_string(),
452                symbol: "TEST".to_string(),
453                currency: "USD".to_string(),
454                quoteToken: Address::random(),
455                admin: sender,
456                salt: B256::random(),
457            };
458
459            let result = factory.create_token(sender, invalid_call);
460            assert_eq!(
461                result.unwrap_err(),
462                TempoPrecompileError::TIP20(TIP20Error::invalid_quote_token())
463            );
464            Ok(())
465        })
466    }
467
468    #[test]
469    fn test_create_token_usd_with_non_usd_quote() -> eyre::Result<()> {
470        let mut storage = HashMapStorageProvider::new(1);
471        let sender = Address::random();
472        StorageCtx::enter(&mut storage, || {
473            let mut factory = TIP20Setup::factory()?;
474            let _path_usd = TIP20Setup::path_usd(sender).apply()?;
475            let eur_token = TIP20Setup::create("EUR Token", "EUR", sender)
476                .currency("EUR")
477                .apply()?;
478
479            let invalid_call = ITIP20Factory::createTokenCall {
480                name: "USD Token".to_string(),
481                symbol: "USDT".to_string(),
482                currency: "USD".to_string(),
483                quoteToken: eur_token.address(),
484                admin: sender,
485                salt: B256::random(),
486            };
487
488            let result = factory.create_token(sender, invalid_call);
489            assert_eq!(
490                result.unwrap_err(),
491                TempoPrecompileError::TIP20(TIP20Error::invalid_quote_token())
492            );
493            Ok(())
494        })
495    }
496
497    #[test]
498    fn test_create_token_quote_token_not_deployed() -> eyre::Result<()> {
499        let mut storage = HashMapStorageProvider::new(1);
500        let sender = Address::random();
501        StorageCtx::enter(&mut storage, || {
502            let mut factory = TIP20Setup::factory()?;
503            TIP20Setup::path_usd(sender).apply()?;
504
505            // Create an address with TIP20 prefix but no code
506            let non_existent_tip20 =
507                Address::from(alloy::hex!("20C0000000000000000000000000000000009999"));
508            let invalid_call = ITIP20Factory::createTokenCall {
509                name: "Test Token".to_string(),
510                symbol: "TEST".to_string(),
511                currency: "USD".to_string(),
512                quoteToken: non_existent_tip20,
513                admin: sender,
514                salt: B256::random(),
515            };
516
517            let result = factory.create_token(sender, invalid_call);
518            assert_eq!(
519                result.unwrap_err(),
520                TempoPrecompileError::TIP20(TIP20Error::invalid_quote_token())
521            );
522            Ok(())
523        })
524    }
525
526    #[test]
527    fn test_create_token_already_deployed() -> eyre::Result<()> {
528        let mut storage = HashMapStorageProvider::new(1);
529        let sender = Address::random();
530        StorageCtx::enter(&mut storage, || {
531            let mut factory = TIP20Setup::factory()?;
532            TIP20Setup::path_usd(sender).apply()?;
533
534            let salt = B256::random();
535            let create_token_call = ITIP20Factory::createTokenCall {
536                name: "Test Token".to_string(),
537                symbol: "TEST".to_string(),
538                currency: "USD".to_string(),
539                quoteToken: PATH_USD_ADDRESS,
540                admin: sender,
541                salt,
542            };
543
544            let token = factory.create_token(sender, create_token_call.clone())?;
545            let result = factory.create_token(sender, create_token_call);
546            assert_eq!(
547                result.unwrap_err(),
548                TempoPrecompileError::TIP20Factory(TIP20FactoryError::TokenAlreadyExists(
549                    ITIP20Factory::TokenAlreadyExists { token }
550                ))
551            );
552
553            Ok(())
554        })
555    }
556
557    #[test]
558    fn test_create_token_reserved_address_rejects_invalid_prefix() -> eyre::Result<()> {
559        let mut storage = HashMapStorageProvider::new(1);
560        let admin = Address::random();
561
562        StorageCtx::enter(&mut storage, || {
563            let mut factory = TIP20Factory::new();
564            factory.initialize()?;
565
566            let result = factory.create_token_reserved_address(
567                Address::random(), // No TIP20 prefix
568                "Test",
569                "TST",
570                "USD",
571                Address::ZERO,
572                admin,
573            );
574
575            assert_eq!(
576                result.unwrap_err(),
577                TempoPrecompileError::TIP20(TIP20Error::invalid_token())
578            );
579
580            Ok(())
581        })
582    }
583
584    #[test]
585    fn test_create_token_reserved_address_rejects_already_deployed() -> eyre::Result<()> {
586        let mut storage = HashMapStorageProvider::new(1);
587        let admin = Address::random();
588
589        StorageCtx::enter(&mut storage, || {
590            let mut factory = TIP20Factory::new();
591            factory.initialize()?;
592
593            factory.create_token_reserved_address(
594                PATH_USD_ADDRESS,
595                "pathUSD",
596                "pathUSD",
597                "USD",
598                Address::ZERO,
599                admin,
600            )?;
601
602            let result = factory.create_token_reserved_address(
603                PATH_USD_ADDRESS,
604                "pathUSD",
605                "pathUSD",
606                "USD",
607                Address::ZERO,
608                admin,
609            );
610
611            assert_eq!(
612                result.unwrap_err(),
613                TempoPrecompileError::TIP20Factory(TIP20FactoryError::token_already_exists(
614                    PATH_USD_ADDRESS
615                ))
616            );
617
618            Ok(())
619        })
620    }
621
622    #[test]
623    fn test_create_token_reserved_address_rejects_non_usd_quote_for_usd_token() -> eyre::Result<()>
624    {
625        let mut storage = HashMapStorageProvider::new(1);
626        let admin = Address::random();
627
628        StorageCtx::enter(&mut storage, || {
629            let eur_token = TIP20Setup::create("EUR Token", "EUR", admin)
630                .currency("EUR")
631                .apply()?;
632
633            let mut factory = TIP20Factory::new();
634
635            let result = factory.create_token_reserved_address(
636                address!("20C0000000000000000000000000000000000001"), // reserved address
637                "Test USD",
638                "TUSD",
639                "USD",
640                eur_token.address(),
641                admin,
642            );
643
644            assert_eq!(
645                result.unwrap_err(),
646                TempoPrecompileError::TIP20(TIP20Error::invalid_quote_token())
647            );
648
649            Ok(())
650        })
651    }
652
653    #[test]
654    fn test_create_token_reserved_address_rejects_non_reserved_address() -> eyre::Result<()> {
655        let mut storage = HashMapStorageProvider::new(1);
656        let admin = Address::random();
657
658        StorageCtx::enter(&mut storage, || {
659            let _path_usd = TIP20Setup::path_usd(admin).apply()?;
660            let mut factory = TIP20Factory::new();
661
662            // 0x9999 = 39321 > 1024 (RESERVED_SIZE)
663            let non_reserved = address!("20C0000000000000000000000000000000009999");
664
665            let result = factory.create_token_reserved_address(
666                non_reserved,
667                "Test",
668                "TST",
669                "USD",
670                PATH_USD_ADDRESS,
671                admin,
672            );
673
674            assert_eq!(
675                result.unwrap_err(),
676                TempoPrecompileError::TIP20Factory(TIP20FactoryError::address_not_reserved())
677            );
678
679            Ok(())
680        })
681    }
682
683    #[test]
684    fn test_create_token_reserved_address_requires_zero_addr_as_first_quote() -> eyre::Result<()> {
685        let mut storage = HashMapStorageProvider::new(1);
686        let admin = Address::random();
687
688        StorageCtx::enter(&mut storage, || {
689            let mut factory = TIP20Factory::new();
690            factory.initialize()?;
691
692            // Try to create PATH_USD with a non-deployed TIP20 as quote_token
693            let result = factory.create_token_reserved_address(
694                PATH_USD_ADDRESS,
695                "pathUSD",
696                "pathUSD",
697                "USD",
698                address!("20C0000000000000000000000000000000000001"),
699                admin,
700            );
701            assert!(matches!(
702                result,
703                Err(TempoPrecompileError::TIP20(TIP20Error::InvalidQuoteToken(
704                    _
705                )))
706            ));
707
708            // Only possible to deploy PATH_USD (the first token) without a quote token
709            factory.create_token_reserved_address(
710                PATH_USD_ADDRESS,
711                "pathUSD",
712                "pathUSD",
713                "USD",
714                Address::ZERO,
715                admin,
716            )?;
717
718            Ok(())
719        })
720    }
721
722    #[test]
723    fn test_path_usd_requires_zero_quote_token() -> eyre::Result<()> {
724        let mut storage = HashMapStorageProvider::new(1);
725        let admin = Address::random();
726
727        StorageCtx::enter(&mut storage, || {
728            let mut factory = TIP20Factory::new();
729            factory.initialize()?;
730
731            let other_usd = factory.create_token_reserved_address(
732                address!("20C0000000000000000000000000000000000001"),
733                "testUSD",
734                "testUSD",
735                "USD",
736                Address::ZERO,
737                admin,
738            )?;
739
740            let result = factory.create_token_reserved_address(
741                PATH_USD_ADDRESS,
742                "pathUSD",
743                "pathUSD",
744                "USD",
745                other_usd,
746                admin,
747            );
748            assert!(matches!(
749                result,
750                Err(TempoPrecompileError::TIP20(TIP20Error::InvalidQuoteToken(
751                    _
752                )))
753            ));
754
755            factory.create_token_reserved_address(
756                PATH_USD_ADDRESS,
757                "pathUSD",
758                "pathUSD",
759                "USD",
760                Address::ZERO,
761                admin,
762            )?;
763
764            assert!(TIP20Token::from_address(PATH_USD_ADDRESS)?.is_initialized()?);
765
766            Ok(())
767        })
768    }
769
770    #[test]
771    fn test_compute_tip20_address_returns_non_default() {
772        let sender = Address::random();
773        let salt = B256::random();
774
775        let (address, lower_bytes) = compute_tip20_address(sender, salt);
776
777        // Address should NOT be default
778        assert_ne!(address, Address::ZERO);
779
780        // Address should have TIP20 prefix
781        assert!(is_tip20_prefix(address));
782
783        // Same inputs should produce same outputs (deterministic)
784        let (address2, lower_bytes2) = compute_tip20_address(sender, salt);
785        assert_eq!(address, address2);
786        assert_eq!(lower_bytes, lower_bytes2);
787
788        // Different sender should produce different outputs
789        let (address3, _) = compute_tip20_address(Address::random(), salt);
790        assert_ne!(address, address3);
791
792        // Different salt should produce different outputs
793        let (address4, _) = compute_tip20_address(sender, B256::random());
794        assert_ne!(address, address4);
795    }
796
797    #[test]
798    fn test_get_token_address_returns_correct_address() -> eyre::Result<()> {
799        let mut storage = HashMapStorageProvider::new(1);
800        let sender = Address::random();
801
802        StorageCtx::enter(&mut storage, || {
803            let factory = TIP20Factory::new();
804
805            // Use a salt that produces non-reserved address
806            let salt = B256::repeat_byte(0xFF);
807
808            let address =
809                factory.get_token_address(ITIP20Factory::getTokenAddressCall { sender, salt })?;
810
811            // Address should NOT be default
812            assert_ne!(address, Address::ZERO);
813
814            // Should have TIP20 prefix
815            assert!(is_tip20_prefix(address));
816
817            // Should be deterministic
818            let address2 =
819                factory.get_token_address(ITIP20Factory::getTokenAddressCall { sender, salt })?;
820            assert_eq!(address, address2);
821
822            Ok(())
823        })
824    }
825
826    #[test]
827    fn test_is_tip20_returns_correct_boolean() -> eyre::Result<()> {
828        let mut storage = HashMapStorageProvider::new(1);
829        let admin = Address::random();
830
831        StorageCtx::enter(&mut storage, || {
832            let factory = TIP20Factory::new();
833
834            // Non-TIP20 address should return false
835            let non_tip20 = Address::random();
836            assert!(
837                !factory.is_tip20(non_tip20)?,
838                "Non-TIP20 address should return false"
839            );
840
841            // PATH_USD before deployment should return false (no code)
842            assert!(
843                !factory.is_tip20(PATH_USD_ADDRESS)?,
844                "Undeployed TIP20 should return false"
845            );
846
847            // Deploy pathUSD
848            TIP20Setup::path_usd(admin).apply()?;
849
850            // Now PATH_USD should return true
851            assert!(
852                factory.is_tip20(PATH_USD_ADDRESS)?,
853                "Deployed TIP20 should return true"
854            );
855
856            Ok(())
857        })
858    }
859
860    #[test]
861    fn test_get_token_address_reserved_boundary() {
862        let sender = Address::ZERO;
863        let salt = B256::repeat_byte(0xAB);
864        let (_, lower_bytes) = compute_tip20_address(sender, salt);
865        assert!(
866            lower_bytes >= RESERVED_SIZE,
867            "compute_tip20_address should produce non-reserved addresses for typical salts"
868        );
869    }
870}