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},
14};
15use alloy::{
16 primitives::{Address, B256, keccak256},
17 sol_types::SolValue,
18};
19use tempo_primitives::TempoAddressExt;
20use tracing::trace;
21
22const RESERVED_SIZE: u64 = 1024;
24
25const TIP20_PREFIX_BYTES: [u8; 12] = [
27 0x20, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
28];
29
30#[contract(addr = TIP20_FACTORY_ADDRESS)]
38pub struct TIP20Factory {}
39
40#[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 let mut padded = [0u8; 8];
48 padded.copy_from_slice(&hash[..8]);
49 let lower_bytes = u64::from_be_bytes(padded);
50
51 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
59impl TIP20Factory {
61 pub fn initialize(&mut self) -> Result<()> {
63 self.__initialize()
64 }
65
66 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 if lower_bytes < RESERVED_SIZE {
76 return Err(TempoPrecompileError::TIP20Factory(
77 TIP20FactoryError::address_reserved(),
78 ));
79 }
80
81 Ok(address)
82 }
83
84 pub fn is_tip20(&self, token: Address) -> Result<bool> {
86 if !token.is_tip20() {
87 return Ok(false);
88 }
89 self.storage
91 .with_account_info(token, |info| Ok(!info.is_empty_code_hash()))
92 }
93
94 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 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 if !self.is_tip20(call.quoteToken)? {
122 return Err(TIP20Error::invalid_quote_token().into());
123 }
124
125 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 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 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 if !address.is_tip20() {
183 return Err(TIP20Error::invalid_token().into());
184 }
185
186 if self.is_tip20(address)? {
188 return Err(TempoPrecompileError::TIP20Factory(
189 TIP20FactoryError::token_already_exists(address),
190 ));
191 }
192
193 if !quote_token.is_zero() {
195 if address == PATH_USD_ADDRESS || !self.is_tip20(quote_token)? {
198 return Err(TIP20Error::invalid_quote_token().into());
199 }
200 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 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 assert!(!factory.is_initialized()?);
258
259 factory.initialize()?;
261 assert!(factory.is_initialized()?);
262
263 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 let _path_usd = TIP20Setup::path_usd(sender).apply()?;
279
280 let factory = TIP20Factory::new();
281
282 assert!(factory.is_tip20(PATH_USD_ADDRESS)?);
284
285 let no_code_tip20 = address!("20C0000000000000000000000000000000000002");
287 assert!(!factory.is_tip20(no_code_tip20)?);
288
289 assert!(!factory.is_tip20(Address::random())?);
291
292 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 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 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 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 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 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 assert_ne!(token_addr_1, token_addr_2);
388
389 assert!(token_addr_1.is_tip20());
391 assert!(token_addr_2.is_tip20());
392
393 assert!(factory.is_tip20(token_addr_1)?);
395 assert!(factory.is_tip20(token_addr_2)?);
396
397 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 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(), "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"), "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 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 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 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 assert_ne!(address, Address::ZERO);
760
761 assert!(address.is_tip20());
763
764 let (address2, lower_bytes2) = compute_tip20_address(sender, salt);
766 assert_eq!(address, address2);
767 assert_eq!(lower_bytes, lower_bytes2);
768
769 let (address3, _) = compute_tip20_address(Address::random(), salt);
771 assert_ne!(address, address3);
772
773 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 let salt = B256::repeat_byte(0xFF);
788
789 let address =
790 factory.get_token_address(ITIP20Factory::getTokenAddressCall { sender, salt })?;
791
792 assert_ne!(address, Address::ZERO);
794
795 assert!(address.is_tip20());
797
798 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 let non_tip20 = Address::random();
817 assert!(
818 !factory.is_tip20(non_tip20)?,
819 "Non-TIP20 address should return false"
820 );
821
822 assert!(
824 !factory.is_tip20(PATH_USD_ADDRESS)?,
825 "Undeployed TIP20 should return false"
826 );
827
828 TIP20Setup::path_usd(admin).apply()?;
830
831 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}