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