1pub 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
21const RESERVED_SIZE: u64 = 1024;
23
24const TIP20_PREFIX_BYTES: [u8; 12] = [
26 0x20, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
27];
28
29#[contract(addr = TIP20_FACTORY_ADDRESS)]
37pub struct TIP20Factory {}
38
39#[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 let mut padded = [0u8; 8];
47 padded.copy_from_slice(&hash[..8]);
48 let lower_bytes = u64::from_be_bytes(padded);
49
50 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
58impl TIP20Factory {
60 pub fn initialize(&mut self) -> Result<()> {
62 self.__initialize()
63 }
64
65 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 if lower_bytes < RESERVED_SIZE {
75 return Err(TempoPrecompileError::TIP20Factory(
76 TIP20FactoryError::address_reserved(),
77 ));
78 }
79
80 Ok(address)
81 }
82
83 pub fn is_tip20(&self, token: Address) -> Result<bool> {
85 if !is_tip20_prefix(token) {
86 return Ok(false);
87 }
88 self.storage
90 .with_account_info(token, |info| Ok(!info.is_empty_code_hash()))
91 }
92
93 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 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 if !self.is_tip20(call.quoteToken)? {
121 return Err(TIP20Error::invalid_quote_token().into());
122 }
123
124 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 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 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 if !is_tip20_prefix(address) {
182 return Err(TIP20Error::invalid_token().into());
183 }
184
185 if self.is_tip20(address)? {
187 return Err(TempoPrecompileError::TIP20Factory(
188 TIP20FactoryError::token_already_exists(address),
189 ));
190 }
191
192 if !quote_token.is_zero() {
194 if address == PATH_USD_ADDRESS || !self.is_tip20(quote_token)? {
197 return Err(TIP20Error::invalid_quote_token().into());
198 }
199 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 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 assert!(!factory.is_initialized()?);
257
258 factory.initialize()?;
260 assert!(factory.is_initialized()?);
261
262 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 assert!(is_tip20_prefix(PATH_USD_ADDRESS));
277
278 let tip20_addr = Address::from(alloy::hex!("20C0000000000000000000000000000000001234"));
280 assert!(is_tip20_prefix(tip20_addr));
281
282 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 let _path_usd = TIP20Setup::path_usd(sender).apply()?;
298
299 let factory = TIP20Factory::new();
300
301 assert!(factory.is_tip20(PATH_USD_ADDRESS)?);
303
304 let no_code_tip20 = address!("20C0000000000000000000000000000000000002");
306 assert!(!factory.is_tip20(no_code_tip20)?);
307
308 assert!(!factory.is_tip20(Address::random())?);
310
311 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 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 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 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 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 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 assert_ne!(token_addr_1, token_addr_2);
407
408 assert!(is_tip20_prefix(token_addr_1));
410 assert!(is_tip20_prefix(token_addr_2));
411
412 assert!(factory.is_tip20(token_addr_1)?);
414 assert!(factory.is_tip20(token_addr_2)?);
415
416 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 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(), "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"), "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 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 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 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 assert_ne!(address, Address::ZERO);
779
780 assert!(is_tip20_prefix(address));
782
783 let (address2, lower_bytes2) = compute_tip20_address(sender, salt);
785 assert_eq!(address, address2);
786 assert_eq!(lower_bytes, lower_bytes2);
787
788 let (address3, _) = compute_tip20_address(Address::random(), salt);
790 assert_ne!(address, address3);
791
792 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 let salt = B256::repeat_byte(0xFF);
807
808 let address =
809 factory.get_token_address(ITIP20Factory::getTokenAddressCall { sender, salt })?;
810
811 assert_ne!(address, Address::ZERO);
813
814 assert!(is_tip20_prefix(address));
816
817 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 let non_tip20 = Address::random();
836 assert!(
837 !factory.is_tip20(non_tip20)?,
838 "Non-TIP20 address should return false"
839 );
840
841 assert!(
843 !factory.is_tip20(PATH_USD_ADDRESS)?,
844 "Undeployed TIP20 should return false"
845 );
846
847 TIP20Setup::path_usd(admin).apply()?;
849
850 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}