1pub mod dispatch;
6
7pub use tempo_contracts::precompiles::{
8 ITIP20Factory, TIP20FactoryError, TIP20FactoryEvent, createTokenCall, createTokenWithLogoCall,
9};
10use tempo_precompiles_macros::contract;
11
12use crate::{
13 PATH_USD_ADDRESS, TIP20_FACTORY_ADDRESS,
14 error::{Result, TempoPrecompileError},
15 tip20::{TIP20Error, TIP20Token, USD_CURRENCY},
16};
17use alloy::{
18 primitives::{Address, B256, keccak256},
19 sol_types::SolValue,
20};
21use tempo_primitives::TempoAddressExt;
22use tracing::trace;
23
24const RESERVED_SIZE: u64 = 1024;
26
27const TIP20_PREFIX_BYTES: [u8; 12] = [
29 0x20, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
30];
31
32#[contract(addr = TIP20_FACTORY_ADDRESS)]
40pub struct TIP20Factory {}
41
42#[cfg_attr(test, allow(dead_code))]
45pub(crate) fn compute_tip20_address(sender: Address, salt: B256) -> (Address, u64) {
46 let hash = keccak256((sender, salt).abi_encode());
47
48 let mut padded = [0u8; 8];
50 padded.copy_from_slice(&hash[..8]);
51 let lower_bytes = u64::from_be_bytes(padded);
52
53 let mut address_bytes = [0u8; 20];
55 address_bytes[..12].copy_from_slice(&TIP20_PREFIX_BYTES);
56 address_bytes[12..].copy_from_slice(&hash[..8]);
57
58 (Address::from(address_bytes), lower_bytes)
59}
60
61impl TIP20Factory {
63 pub fn initialize(&mut self) -> Result<()> {
65 self.__initialize()
66 }
67
68 pub fn get_token_address(&self, call: ITIP20Factory::getTokenAddressCall) -> Result<Address> {
74 let (address, lower_bytes) = compute_tip20_address(call.sender, call.salt);
75
76 if lower_bytes < RESERVED_SIZE {
78 return Err(TempoPrecompileError::TIP20Factory(
79 TIP20FactoryError::address_reserved(),
80 ));
81 }
82
83 Ok(address)
84 }
85
86 pub fn is_tip20(&self, token: Address) -> Result<bool> {
88 if !token.is_tip20() {
89 return Ok(false);
90 }
91 self.storage
93 .with_account_info(token, |info| Ok(!info.is_empty_code_hash()))
94 }
95
96 pub fn create_token(&mut self, sender: Address, call: createTokenCall) -> Result<Address> {
107 trace!(%sender, ?call, "Create token");
108
109 let (token_address, lower_bytes) = compute_tip20_address(sender, call.salt);
111
112 if self.is_tip20(token_address)? {
113 return Err(TempoPrecompileError::TIP20Factory(
114 TIP20FactoryError::token_already_exists(token_address),
115 ));
116 }
117
118 if !self.is_tip20(call.quoteToken)? {
120 return Err(TIP20Error::invalid_quote_token().into());
121 }
122
123 if call.currency == USD_CURRENCY
125 && TIP20Token::from_address(call.quoteToken)?.currency()? != USD_CURRENCY
126 {
127 return Err(TIP20Error::invalid_quote_token().into());
128 }
129
130 if lower_bytes < RESERVED_SIZE {
132 return Err(TempoPrecompileError::TIP20Factory(
133 TIP20FactoryError::address_reserved(),
134 ));
135 }
136
137 TIP20Token::from_address(token_address)?.initialize(
138 sender,
139 &call.name,
140 &call.symbol,
141 &call.currency,
142 call.quoteToken,
143 call.admin,
144 )?;
145
146 self.emit_event(TIP20FactoryEvent::token_created(
147 token_address,
148 call.name,
149 call.symbol,
150 call.currency,
151 call.quoteToken,
152 call.admin,
153 call.salt,
154 ))?;
155
156 Ok(token_address)
157 }
158
159 pub fn create_token_with_logo(
170 &mut self,
171 sender: Address,
172 call: createTokenWithLogoCall,
173 ) -> Result<Address> {
174 if !call.logoURI.is_empty() {
176 crate::tip20::TIP20Token::validate_logo_uri(&call.logoURI)?;
177 }
178
179 let token_address = self.create_token(
180 sender,
181 createTokenCall {
182 name: call.name,
183 symbol: call.symbol,
184 currency: call.currency,
185 quoteToken: call.quoteToken,
186 admin: call.admin,
187 salt: call.salt,
188 },
189 )?;
190
191 if !call.logoURI.is_empty() {
192 TIP20Token::from_address(token_address)?.write_logo_uri(sender, call.logoURI)?;
193 }
194
195 Ok(token_address)
196 }
197
198 pub fn create_token_reserved_address(
208 &mut self,
209 address: Address,
210 name: &str,
211 symbol: &str,
212 currency: &str,
213 quote_token: Address,
214 admin: Address,
215 ) -> Result<Address> {
216 if !address.is_tip20() {
218 return Err(TIP20Error::invalid_token().into());
219 }
220
221 if self.is_tip20(address)? {
223 return Err(TempoPrecompileError::TIP20Factory(
224 TIP20FactoryError::token_already_exists(address),
225 ));
226 }
227
228 if !quote_token.is_zero() {
230 if address == PATH_USD_ADDRESS || !self.is_tip20(quote_token)? {
233 return Err(TIP20Error::invalid_quote_token().into());
234 }
235 if currency == USD_CURRENCY
237 && TIP20Token::from_address(quote_token)?.currency()? != USD_CURRENCY
238 {
239 return Err(TIP20Error::invalid_quote_token().into());
240 }
241 }
242
243 let mut padded = [0u8; 8];
246 padded.copy_from_slice(&address.as_slice()[12..]);
247 let lower_bytes = u64::from_be_bytes(padded);
248 if lower_bytes >= RESERVED_SIZE {
249 return Err(TempoPrecompileError::TIP20Factory(
250 TIP20FactoryError::address_not_reserved(),
251 ));
252 }
253
254 let mut token = TIP20Token::from_address(address)?;
255 token.initialize(admin, name, symbol, currency, quote_token, admin)?;
256
257 self.emit_event(TIP20FactoryEvent::token_created(
258 address,
259 name.into(),
260 symbol.into(),
261 currency.into(),
262 quote_token,
263 admin,
264 B256::ZERO,
265 ))?;
266
267 Ok(address)
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274 use crate::{
275 PATH_USD_ADDRESS,
276 error::TempoPrecompileError,
277 storage::{ContractStorage, StorageCtx, hashmap::HashMapStorageProvider},
278 test_util::TIP20Setup,
279 };
280 use alloy::primitives::{Address, address};
281
282 #[test]
283 fn test_is_initialized() -> eyre::Result<()> {
284 let mut storage = HashMapStorageProvider::new(1);
285
286 StorageCtx::enter(&mut storage, || {
287 let mut factory = TIP20Factory::new();
288
289 assert!(!factory.is_initialized()?);
291
292 factory.initialize()?;
294 assert!(factory.is_initialized()?);
295
296 let factory2 = TIP20Factory::new();
298 assert!(factory2.is_initialized()?);
299
300 Ok(())
301 })
302 }
303
304 #[test]
305 fn test_is_tip20() -> eyre::Result<()> {
306 let mut storage = HashMapStorageProvider::new(1);
307 let sender = Address::random();
308
309 StorageCtx::enter(&mut storage, || {
310 let _path_usd = TIP20Setup::path_usd(sender).apply()?;
312
313 let factory = TIP20Factory::new();
314
315 assert!(factory.is_tip20(PATH_USD_ADDRESS)?);
317
318 let no_code_tip20 = address!("20C0000000000000000000000000000000000002");
320 assert!(!factory.is_tip20(no_code_tip20)?);
321
322 assert!(!factory.is_tip20(Address::random())?);
324
325 let token = TIP20Setup::create("Test", "TST", sender).apply()?;
327 assert!(factory.is_tip20(token.address())?);
328
329 Ok(())
330 })
331 }
332
333 #[test]
334 fn test_get_token_address() -> eyre::Result<()> {
335 let mut storage = HashMapStorageProvider::new(1);
336
337 StorageCtx::enter(&mut storage, || {
338 let factory = TIP20Factory::new();
339 let sender = Address::random();
340 let salt = B256::random();
341
342 let call = ITIP20Factory::getTokenAddressCall { sender, salt };
344 let address = factory.get_token_address(call)?;
345 let (expected, _) = compute_tip20_address(sender, salt);
346 assert_eq!(address, expected);
347
348 let call2 = ITIP20Factory::getTokenAddressCall { sender, salt };
350 assert_eq!(factory.get_token_address(call2)?, address);
351
352 Ok(())
353 })
354 }
355
356 #[test]
357 fn test_compute_tip20_address_deterministic() {
358 let sender1 = Address::random();
359 let sender2 = Address::random();
360 let salt1 = B256::random();
361 let salt2 = B256::random();
362
363 let (addr0, lower0) = compute_tip20_address(sender1, salt1);
364 let (addr1, lower1) = compute_tip20_address(sender1, salt1);
365 assert_eq!(addr0, addr1);
366 assert_eq!(lower0, lower1);
367
368 let (addr2, lower2) = compute_tip20_address(sender1, salt1);
370 let (addr3, lower3) = compute_tip20_address(sender2, salt1);
371 assert_ne!(addr2, addr3);
372 assert_ne!(lower2, lower3);
373
374 let (addr4, lower4) = compute_tip20_address(sender1, salt1);
376 let (addr5, lower5) = compute_tip20_address(sender1, salt2);
377 assert_ne!(addr4, addr5);
378 assert_ne!(lower4, lower5);
379
380 assert!(addr1.is_tip20());
382 assert!(addr2.is_tip20());
383 assert!(addr3.is_tip20());
384 assert!(addr4.is_tip20());
385 assert!(addr5.is_tip20());
386 }
387
388 #[test]
389 fn test_create_token() -> eyre::Result<()> {
390 let mut storage = HashMapStorageProvider::new(1);
391 let sender = Address::random();
392 StorageCtx::enter(&mut storage, || {
393 let mut factory = TIP20Setup::factory()?;
394 let path_usd = TIP20Setup::path_usd(sender).apply()?;
395 factory.clear_emitted_events();
396
397 let salt1 = B256::random();
398 let salt2 = B256::random();
399 let call1 = createTokenCall {
400 name: "Test Token 1".to_string(),
401 symbol: "TEST1".to_string(),
402 currency: "USD".to_string(),
403 quoteToken: path_usd.address(),
404 admin: sender,
405 salt: salt1,
406 };
407 let call2 = createTokenCall {
408 name: "Test Token 2".to_string(),
409 symbol: "TEST2".to_string(),
410 currency: "USD".to_string(),
411 quoteToken: path_usd.address(),
412 admin: sender,
413 salt: salt2,
414 };
415
416 let token_addr_1 = factory.create_token(sender, call1.clone())?;
417 let token_addr_2 = factory.create_token(sender, call2.clone())?;
418
419 assert_ne!(token_addr_1, token_addr_2);
421
422 assert!(token_addr_1.is_tip20());
424 assert!(token_addr_2.is_tip20());
425
426 assert!(factory.is_tip20(token_addr_1)?);
428 assert!(factory.is_tip20(token_addr_2)?);
429
430 factory.assert_emitted_events(vec![
432 TIP20FactoryEvent::token_created(
433 token_addr_1,
434 call1.name,
435 call1.symbol,
436 call1.currency,
437 call1.quoteToken,
438 call1.admin,
439 call1.salt,
440 ),
441 TIP20FactoryEvent::token_created(
442 token_addr_2,
443 call2.name,
444 call2.symbol,
445 call2.currency,
446 call2.quoteToken,
447 call2.admin,
448 call2.salt,
449 ),
450 ]);
451
452 Ok(())
453 })
454 }
455
456 #[test]
457 fn test_create_token_selector_and_event_unchanged() {
458 use alloy::sol_types::{SolCall, SolEvent};
459
460 assert_eq!(
461 createTokenCall::SELECTOR,
462 [0x68, 0x13, 0x04, 0x45],
463 "createToken selector must remain 0x68130445"
464 );
465
466 assert_eq!(
467 ITIP20Factory::TokenCreated::SIGNATURE_HASH,
468 alloy::primitives::b256!(
469 "44f7b8011db3e3647a530b4ff635726de5fafc8fa8ad10f0f31c0eb9dd52fc65"
470 ),
471 "TokenCreated topic0 must remain unchanged"
472 );
473 }
474
475 #[test]
476 fn test_create_token_with_logo() -> eyre::Result<()> {
477 use alloy::sol_types::SolEvent;
478 use tempo_contracts::precompiles::ITIP20;
479
480 let mut storage = HashMapStorageProvider::new(1);
481 let sender = Address::random();
482 let admin = Address::random();
486 assert_ne!(sender, admin);
487
488 StorageCtx::enter(&mut storage, || {
489 let mut factory = TIP20Setup::factory()?;
490 let path_usd = TIP20Setup::path_usd(sender).apply()?;
491 factory.clear_emitted_events();
492
493 let salt = B256::random();
494 let logo_uri = "https://example.com/icon.svg".to_string();
495 let call = createTokenWithLogoCall {
496 name: "Logo Token".to_string(),
497 symbol: "LOGO".to_string(),
498 currency: "USD".to_string(),
499 quoteToken: path_usd.address(),
500 admin,
501 salt,
502 logoURI: logo_uri.clone(),
503 };
504
505 let token_addr = factory.create_token_with_logo(sender, call.clone())?;
506
507 assert!(token_addr.is_tip20());
509 assert!(factory.is_tip20(token_addr)?);
510
511 let token = TIP20Token::from_address(token_addr)?;
513 assert_eq!(token.logo_uri()?, logo_uri);
514
515 let logo_topic = ITIP20::LogoURIUpdated::SIGNATURE_HASH;
518 let logo_event = token
519 .emitted_events()
520 .iter()
521 .find(|e| e.topics().first() == Some(&logo_topic))
522 .expect("LogoURIUpdated event missing")
523 .clone();
524 let decoded = ITIP20::LogoURIUpdated::decode_log_data(&logo_event).expect("decode log");
525 assert_eq!(decoded.updater, sender);
526 assert_eq!(decoded.newLogoURI, logo_uri);
527
528 factory.assert_emitted_events(vec![TIP20FactoryEvent::TokenCreated(
530 ITIP20Factory::TokenCreated {
531 token: token_addr,
532 name: call.name,
533 symbol: call.symbol,
534 currency: call.currency,
535 quoteToken: call.quoteToken,
536 admin: call.admin,
537 salt: call.salt,
538 },
539 )]);
540
541 Ok(())
542 })
543 }
544
545 #[test]
546 fn test_create_token_with_logo_empty_uri_skips_event() -> eyre::Result<()> {
547 use alloy::sol_types::SolEvent;
548 use tempo_contracts::precompiles::ITIP20;
549
550 let mut storage = HashMapStorageProvider::new(1);
551 let sender = Address::random();
552
553 StorageCtx::enter(&mut storage, || {
554 let mut factory = TIP20Setup::factory()?;
555 let path_usd = TIP20Setup::path_usd(sender).apply()?;
556 factory.clear_emitted_events();
557
558 let token_addr = factory.create_token_with_logo(
559 sender,
560 createTokenWithLogoCall {
561 name: "Empty Logo".to_string(),
562 symbol: "EMPTY".to_string(),
563 currency: "USD".to_string(),
564 quoteToken: path_usd.address(),
565 admin: sender,
566 salt: B256::random(),
567 logoURI: String::new(),
568 },
569 )?;
570
571 let token = TIP20Token::from_address(token_addr)?;
573 assert_eq!(token.logo_uri()?, "");
574
575 let logo_topic = ITIP20::LogoURIUpdated::SIGNATURE_HASH;
577 assert!(
578 !token
579 .emitted_events()
580 .iter()
581 .any(|e| e.topics().first() == Some(&logo_topic)),
582 "LogoURIUpdated should not be emitted when logoURI is empty"
583 );
584
585 Ok(())
586 })
587 }
588
589 #[test]
590 fn test_create_token_with_logo_rejects_atomically() -> eyre::Result<()> {
591 let mut storage = HashMapStorageProvider::new(1);
592 let sender = Address::random();
593
594 StorageCtx::enter(&mut storage, || {
595 let mut factory = TIP20Setup::factory()?;
596 let path_usd = TIP20Setup::path_usd(sender).apply()?;
597 let salt = B256::random();
598
599 let call = |logo_uri: &str| createTokenWithLogoCall {
600 name: "Tok".to_string(),
601 symbol: "TOK".to_string(),
602 currency: "USD".to_string(),
603 quoteToken: path_usd.address(),
604 admin: sender,
605 salt,
606 logoURI: logo_uri.to_string(),
607 };
608
609 let prefix = "https://example.com/";
612 let too_long = format!("{prefix}{}", "a".repeat(257 - prefix.len()));
613 assert_eq!(too_long.len(), 257);
614 assert!(matches!(
615 factory.create_token_with_logo(sender, call(&too_long)),
616 Err(TempoPrecompileError::TIP20(TIP20Error::LogoURITooLong(_)))
617 ));
618
619 assert!(matches!(
622 factory.create_token_with_logo(sender, call("javascript:alert(1)")),
623 Err(TempoPrecompileError::TIP20(TIP20Error::InvalidLogoURI(_)))
624 ));
625
626 let token =
629 factory.create_token_with_logo(sender, call("https://example.com/icon.svg"))?;
630 assert!(factory.is_tip20(token)?);
631
632 Ok(())
633 })
634 }
635
636 #[test]
637 fn test_create_token_invalid_quote_token() -> eyre::Result<()> {
638 let mut storage = HashMapStorageProvider::new(1);
639 let sender = Address::random();
640 StorageCtx::enter(&mut storage, || {
641 let mut factory = TIP20Setup::factory()?;
642 TIP20Setup::path_usd(sender).apply()?;
643
644 let invalid_call = createTokenCall {
645 name: "Test Token".to_string(),
646 symbol: "TEST".to_string(),
647 currency: "USD".to_string(),
648 quoteToken: Address::random(),
649 admin: sender,
650 salt: B256::random(),
651 };
652
653 let result = factory.create_token(sender, invalid_call);
654 assert_eq!(
655 result.unwrap_err(),
656 TempoPrecompileError::TIP20(TIP20Error::invalid_quote_token())
657 );
658 Ok(())
659 })
660 }
661
662 #[test]
663 fn test_create_token_usd_with_non_usd_quote() -> eyre::Result<()> {
664 let mut storage = HashMapStorageProvider::new(1);
665 let sender = Address::random();
666 StorageCtx::enter(&mut storage, || {
667 let mut factory = TIP20Setup::factory()?;
668 let _path_usd = TIP20Setup::path_usd(sender).apply()?;
669 let eur_token = TIP20Setup::create("EUR Token", "EUR", sender)
670 .currency("EUR")
671 .apply()?;
672
673 let invalid_call = createTokenCall {
674 name: "USD Token".to_string(),
675 symbol: "USDT".to_string(),
676 currency: "USD".to_string(),
677 quoteToken: eur_token.address(),
678 admin: sender,
679 salt: B256::random(),
680 };
681
682 let result = factory.create_token(sender, invalid_call);
683 assert_eq!(
684 result.unwrap_err(),
685 TempoPrecompileError::TIP20(TIP20Error::invalid_quote_token())
686 );
687 Ok(())
688 })
689 }
690
691 #[test]
692 fn test_create_token_quote_token_not_deployed() -> eyre::Result<()> {
693 let mut storage = HashMapStorageProvider::new(1);
694 let sender = Address::random();
695 StorageCtx::enter(&mut storage, || {
696 let mut factory = TIP20Setup::factory()?;
697 TIP20Setup::path_usd(sender).apply()?;
698
699 let non_existent_tip20 =
701 Address::from(alloy::hex!("20C0000000000000000000000000000000009999"));
702 let invalid_call = createTokenCall {
703 name: "Test Token".to_string(),
704 symbol: "TEST".to_string(),
705 currency: "USD".to_string(),
706 quoteToken: non_existent_tip20,
707 admin: sender,
708 salt: B256::random(),
709 };
710
711 let result = factory.create_token(sender, invalid_call);
712 assert_eq!(
713 result.unwrap_err(),
714 TempoPrecompileError::TIP20(TIP20Error::invalid_quote_token())
715 );
716 Ok(())
717 })
718 }
719
720 #[test]
721 fn test_create_token_already_deployed() -> eyre::Result<()> {
722 let mut storage = HashMapStorageProvider::new(1);
723 let sender = Address::random();
724 StorageCtx::enter(&mut storage, || {
725 let mut factory = TIP20Setup::factory()?;
726 TIP20Setup::path_usd(sender).apply()?;
727
728 let salt = B256::random();
729 let create_token_call = createTokenCall {
730 name: "Test Token".to_string(),
731 symbol: "TEST".to_string(),
732 currency: "USD".to_string(),
733 quoteToken: PATH_USD_ADDRESS,
734 admin: sender,
735 salt,
736 };
737
738 let token = factory.create_token(sender, create_token_call.clone())?;
739 let result = factory.create_token(sender, create_token_call);
740 assert_eq!(
741 result.unwrap_err(),
742 TempoPrecompileError::TIP20Factory(TIP20FactoryError::TokenAlreadyExists(
743 ITIP20Factory::TokenAlreadyExists { token }
744 ))
745 );
746
747 Ok(())
748 })
749 }
750
751 #[test]
752 fn test_create_token_reserved_address_rejects_invalid_prefix() -> eyre::Result<()> {
753 let mut storage = HashMapStorageProvider::new(1);
754 let admin = Address::random();
755
756 StorageCtx::enter(&mut storage, || {
757 let mut factory = TIP20Factory::new();
758 factory.initialize()?;
759
760 let result = factory.create_token_reserved_address(
761 Address::random(), "Test",
763 "TST",
764 "USD",
765 Address::ZERO,
766 admin,
767 );
768
769 assert_eq!(
770 result.unwrap_err(),
771 TempoPrecompileError::TIP20(TIP20Error::invalid_token())
772 );
773
774 Ok(())
775 })
776 }
777
778 #[test]
779 fn test_create_token_reserved_address_rejects_already_deployed() -> eyre::Result<()> {
780 let mut storage = HashMapStorageProvider::new(1);
781 let admin = Address::random();
782
783 StorageCtx::enter(&mut storage, || {
784 let mut factory = TIP20Factory::new();
785 factory.initialize()?;
786
787 factory.create_token_reserved_address(
788 PATH_USD_ADDRESS,
789 "pathUSD",
790 "pathUSD",
791 "USD",
792 Address::ZERO,
793 admin,
794 )?;
795
796 let result = factory.create_token_reserved_address(
797 PATH_USD_ADDRESS,
798 "pathUSD",
799 "pathUSD",
800 "USD",
801 Address::ZERO,
802 admin,
803 );
804
805 assert_eq!(
806 result.unwrap_err(),
807 TempoPrecompileError::TIP20Factory(TIP20FactoryError::token_already_exists(
808 PATH_USD_ADDRESS
809 ))
810 );
811
812 Ok(())
813 })
814 }
815
816 #[test]
817 fn test_create_token_reserved_address_rejects_non_usd_quote_for_usd_token() -> eyre::Result<()>
818 {
819 let mut storage = HashMapStorageProvider::new(1);
820 let admin = Address::random();
821
822 StorageCtx::enter(&mut storage, || {
823 let eur_token = TIP20Setup::create("EUR Token", "EUR", admin)
824 .currency("EUR")
825 .apply()?;
826
827 let mut factory = TIP20Factory::new();
828
829 let result = factory.create_token_reserved_address(
830 address!("20C0000000000000000000000000000000000001"), "Test USD",
832 "TUSD",
833 "USD",
834 eur_token.address(),
835 admin,
836 );
837
838 assert_eq!(
839 result.unwrap_err(),
840 TempoPrecompileError::TIP20(TIP20Error::invalid_quote_token())
841 );
842
843 Ok(())
844 })
845 }
846
847 #[test]
848 fn test_create_token_reserved_address_rejects_non_reserved_address() -> eyre::Result<()> {
849 let mut storage = HashMapStorageProvider::new(1);
850 let admin = Address::random();
851
852 StorageCtx::enter(&mut storage, || {
853 let _path_usd = TIP20Setup::path_usd(admin).apply()?;
854 let mut factory = TIP20Factory::new();
855
856 let non_reserved = address!("20C0000000000000000000000000000000009999");
858
859 let result = factory.create_token_reserved_address(
860 non_reserved,
861 "Test",
862 "TST",
863 "USD",
864 PATH_USD_ADDRESS,
865 admin,
866 );
867
868 assert_eq!(
869 result.unwrap_err(),
870 TempoPrecompileError::TIP20Factory(TIP20FactoryError::address_not_reserved())
871 );
872
873 Ok(())
874 })
875 }
876
877 #[test]
878 fn test_create_token_reserved_address_requires_zero_addr_as_first_quote() -> eyre::Result<()> {
879 let mut storage = HashMapStorageProvider::new(1);
880 let admin = Address::random();
881
882 StorageCtx::enter(&mut storage, || {
883 let mut factory = TIP20Factory::new();
884 factory.initialize()?;
885
886 let result = factory.create_token_reserved_address(
888 PATH_USD_ADDRESS,
889 "pathUSD",
890 "pathUSD",
891 "USD",
892 address!("20C0000000000000000000000000000000000001"),
893 admin,
894 );
895 assert!(matches!(
896 result,
897 Err(TempoPrecompileError::TIP20(TIP20Error::InvalidQuoteToken(
898 _
899 )))
900 ));
901
902 factory.create_token_reserved_address(
904 PATH_USD_ADDRESS,
905 "pathUSD",
906 "pathUSD",
907 "USD",
908 Address::ZERO,
909 admin,
910 )?;
911
912 Ok(())
913 })
914 }
915
916 #[test]
917 fn test_path_usd_requires_zero_quote_token() -> eyre::Result<()> {
918 let mut storage = HashMapStorageProvider::new(1);
919 let admin = Address::random();
920
921 StorageCtx::enter(&mut storage, || {
922 let mut factory = TIP20Factory::new();
923 factory.initialize()?;
924
925 let other_usd = factory.create_token_reserved_address(
926 address!("20C0000000000000000000000000000000000001"),
927 "testUSD",
928 "testUSD",
929 "USD",
930 Address::ZERO,
931 admin,
932 )?;
933
934 let result = factory.create_token_reserved_address(
935 PATH_USD_ADDRESS,
936 "pathUSD",
937 "pathUSD",
938 "USD",
939 other_usd,
940 admin,
941 );
942 assert!(matches!(
943 result,
944 Err(TempoPrecompileError::TIP20(TIP20Error::InvalidQuoteToken(
945 _
946 )))
947 ));
948
949 factory.create_token_reserved_address(
950 PATH_USD_ADDRESS,
951 "pathUSD",
952 "pathUSD",
953 "USD",
954 Address::ZERO,
955 admin,
956 )?;
957
958 assert!(TIP20Token::from_address(PATH_USD_ADDRESS)?.is_initialized()?);
959
960 Ok(())
961 })
962 }
963
964 #[test]
965 fn test_compute_tip20_address_returns_non_default() {
966 let sender = Address::random();
967 let salt = B256::random();
968
969 let (address, lower_bytes) = compute_tip20_address(sender, salt);
970
971 assert_ne!(address, Address::ZERO);
973
974 assert!(address.is_tip20());
976
977 let (address2, lower_bytes2) = compute_tip20_address(sender, salt);
979 assert_eq!(address, address2);
980 assert_eq!(lower_bytes, lower_bytes2);
981
982 let (address3, _) = compute_tip20_address(Address::random(), salt);
984 assert_ne!(address, address3);
985
986 let (address4, _) = compute_tip20_address(sender, B256::random());
988 assert_ne!(address, address4);
989 }
990
991 #[test]
992 fn test_get_token_address_returns_correct_address() -> eyre::Result<()> {
993 let mut storage = HashMapStorageProvider::new(1);
994 let sender = Address::random();
995
996 StorageCtx::enter(&mut storage, || {
997 let factory = TIP20Factory::new();
998
999 let salt = B256::repeat_byte(0xFF);
1001
1002 let address =
1003 factory.get_token_address(ITIP20Factory::getTokenAddressCall { sender, salt })?;
1004
1005 assert_ne!(address, Address::ZERO);
1007
1008 assert!(address.is_tip20());
1010
1011 let address2 =
1013 factory.get_token_address(ITIP20Factory::getTokenAddressCall { sender, salt })?;
1014 assert_eq!(address, address2);
1015
1016 Ok(())
1017 })
1018 }
1019
1020 #[test]
1021 fn test_is_tip20_returns_correct_boolean() -> eyre::Result<()> {
1022 let mut storage = HashMapStorageProvider::new(1);
1023 let admin = Address::random();
1024
1025 StorageCtx::enter(&mut storage, || {
1026 let factory = TIP20Factory::new();
1027
1028 let non_tip20 = Address::random();
1030 assert!(
1031 !factory.is_tip20(non_tip20)?,
1032 "Non-TIP20 address should return false"
1033 );
1034
1035 assert!(
1037 !factory.is_tip20(PATH_USD_ADDRESS)?,
1038 "Undeployed TIP20 should return false"
1039 );
1040
1041 TIP20Setup::path_usd(admin).apply()?;
1043
1044 assert!(
1046 factory.is_tip20(PATH_USD_ADDRESS)?,
1047 "Deployed TIP20 should return true"
1048 );
1049
1050 Ok(())
1051 })
1052 }
1053
1054 #[test]
1055 fn test_get_token_address_reserved_boundary() {
1056 let sender = Address::ZERO;
1057 let salt = B256::repeat_byte(0xAB);
1058 let (_, lower_bytes) = compute_tip20_address(sender, salt);
1059 assert!(
1060 lower_bytes >= RESERVED_SIZE,
1061 "compute_tip20_address should produce non-reserved addresses for typical salts"
1062 );
1063 }
1064}