tempo_precompiles/tip20_factory/
mod.rs1pub mod dispatch;
3
4pub use tempo_contracts::precompiles::{ITIP20Factory, TIP20FactoryEvent};
5use tempo_precompiles_macros::contract;
6
7use crate::{
8 TIP20_FACTORY_ADDRESS,
9 error::{Result, TempoPrecompileError},
10 storage::Handler,
11 tip20::{
12 TIP20Error, TIP20Token, address_to_token_id_unchecked, is_tip20_prefix, token_id_to_address,
13 },
14};
15use alloy::primitives::{Address, U256};
16use tracing::trace;
17
18#[contract(addr = TIP20_FACTORY_ADDRESS)]
19pub struct TIP20Factory {
20 token_id_counter: U256,
23}
24
25impl TIP20Factory {
27 pub fn initialize(&mut self) -> Result<()> {
29 self.__initialize()
31 }
32
33 pub fn is_initialized(&self) -> Result<bool> {
35 self.storage
36 .with_account_info(TIP20_FACTORY_ADDRESS, |info| Ok(info.code.is_some()))
37 }
38
39 pub fn is_tip20(&self, token: Address) -> Result<bool> {
47 if !is_tip20_prefix(token) {
48 return Ok(false);
49 }
50 if self.storage.spec().is_allegro_moderato() {
52 let token_id = U256::from(address_to_token_id_unchecked(token));
53 return Ok(token_id < self.token_id_counter()?);
54 }
55 Ok(true)
56 }
57
58 pub fn create_token(
59 &mut self,
60 sender: Address,
61 call: ITIP20Factory::createTokenCall,
62 ) -> Result<Address> {
63 let token_id = self
66 .token_id_counter()?
67 .try_into()
68 .map_err(|_| TempoPrecompileError::under_overflow())?;
69
70 trace!(%sender, %token_id, ?call, "Create token");
71
72 if self.storage.spec().is_allegretto() && token_id == 0 {
77 if !call.quoteToken.is_zero() {
78 return Err(TIP20Error::invalid_quote_token().into());
79 }
80 } else if self.storage.spec().is_moderato() {
81 if !is_tip20_prefix(call.quoteToken)
83 || address_to_token_id_unchecked(call.quoteToken) >= token_id
84 {
85 return Err(TIP20Error::invalid_quote_token().into());
86 }
87 } else {
88 if !is_tip20_prefix(call.quoteToken)
91 || address_to_token_id_unchecked(call.quoteToken) > token_id
92 {
93 return Err(TIP20Error::invalid_quote_token().into());
94 }
95 }
96
97 TIP20Token::new(token_id).initialize(
100 &call.name,
101 &call.symbol,
102 &call.currency,
103 call.quoteToken,
104 call.admin,
105 Address::ZERO,
106 )?;
107
108 let token_address = token_id_to_address(token_id);
109 let token_id = U256::from(token_id);
110 self.emit_event(TIP20FactoryEvent::TokenCreated(
111 ITIP20Factory::TokenCreated {
112 token: token_address,
113 tokenId: token_id,
114 name: call.name,
115 symbol: call.symbol,
116 currency: call.currency,
117 quoteToken: call.quoteToken,
118 admin: call.admin,
119 },
120 ))?;
121
122 self.token_id_counter.write(
124 token_id
125 .checked_add(U256::ONE)
126 .ok_or(TempoPrecompileError::under_overflow())?,
127 )?;
128
129 Ok(token_address)
130 }
131
132 pub fn token_id_counter(&self) -> Result<U256> {
133 let counter = self.token_id_counter.read()?;
134
135 if !self.storage.spec().is_allegretto() && counter.is_zero() {
137 Ok(U256::ONE)
138 } else {
139 Ok(counter)
140 }
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147 use crate::{
148 error::TempoPrecompileError,
149 storage::{ContractStorage, StorageCtx, hashmap::HashMapStorageProvider},
150 test_util::TIP20Setup,
151 tip20::tests::initialize_path_usd,
152 };
153 use alloy::primitives::Address;
154 use tempo_chainspec::hardfork::TempoHardfork;
155
156 #[test]
157 fn test_create_token() -> eyre::Result<()> {
158 let mut storage = HashMapStorageProvider::new(1);
159 let sender = Address::random();
160 StorageCtx::enter(&mut storage, || {
161 let mut factory = TIP20Setup::factory()?;
162 let path_usd = TIP20Setup::path_usd(sender).apply()?;
163
164 let call = ITIP20Factory::createTokenCall {
165 name: "Test Token".to_string(),
166 symbol: "TEST".to_string(),
167 currency: "USD".to_string(),
168 quoteToken: path_usd.address(),
169 admin: sender,
170 };
171
172 let token_addr_0 = factory.create_token(sender, call.clone())?;
173 let token_addr_1 = factory.create_token(sender, call)?;
174
175 let token_id_0 = address_to_token_id_unchecked(token_addr_0);
176 let token_id_1 = address_to_token_id_unchecked(token_addr_1);
177 let expected = vec![
178 TIP20FactoryEvent::TokenCreated(ITIP20Factory::TokenCreated {
179 token: path_usd.address(),
180 tokenId: U256::ZERO,
181 name: "PathUSD".to_string(),
182 symbol: "PUSD".to_string(),
183 currency: "USD".to_string(),
184 quoteToken: Address::ZERO,
185 admin: sender,
186 }),
187 TIP20FactoryEvent::TokenCreated(ITIP20Factory::TokenCreated {
188 token: token_addr_0,
189 tokenId: U256::from(token_id_0),
190 name: "Test Token".to_string(),
191 symbol: "TEST".to_string(),
192 currency: "USD".to_string(),
193 quoteToken: path_usd.address(),
194 admin: sender,
195 }),
196 TIP20FactoryEvent::TokenCreated(ITIP20Factory::TokenCreated {
197 token: token_addr_1,
198 tokenId: U256::from(token_id_1),
199 name: "Test Token".to_string(),
200 symbol: "TEST".to_string(),
201 currency: "USD".to_string(),
202 quoteToken: path_usd.address(),
203 admin: sender,
204 }),
205 ];
206 factory.assert_emitted_events(expected);
207
208 Ok(())
209 })
210 }
211
212 #[test]
213 fn test_create_token_invalid_quote_token_post_moderato() -> eyre::Result<()> {
214 let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Moderato);
215 let sender = Address::random();
216 StorageCtx::enter(&mut storage, || {
217 let mut factory = TIP20Setup::factory()?;
218
219 let invalid_call = ITIP20Factory::createTokenCall {
220 name: "Test Token".to_string(),
221 symbol: "TEST".to_string(),
222 currency: "USD".to_string(),
223 quoteToken: Address::random(),
224 admin: sender,
225 };
226
227 let result = factory.create_token(sender, invalid_call);
228 assert_eq!(
229 result.unwrap_err(),
230 TempoPrecompileError::TIP20(TIP20Error::invalid_quote_token())
231 );
232 Ok(())
233 })
234 }
235
236 #[test]
237 fn test_create_token_quote_token_not_deployed_post_moderato() -> eyre::Result<()> {
238 let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Moderato);
239 let sender = Address::random();
240 StorageCtx::enter(&mut storage, || {
241 let mut factory = TIP20Setup::factory()?;
242
243 let non_existent_tip20 = token_id_to_address(5);
244 let invalid_call = ITIP20Factory::createTokenCall {
245 name: "Test Token".to_string(),
246 symbol: "TEST".to_string(),
247 currency: "USD".to_string(),
248 quoteToken: non_existent_tip20,
249 admin: sender,
250 };
251
252 let result = factory.create_token(sender, invalid_call);
253 assert_eq!(
254 result.unwrap_err(),
255 TempoPrecompileError::TIP20(TIP20Error::invalid_quote_token())
256 );
257 Ok(())
258 })
259 }
260
261 #[test]
262 fn test_create_token_off_by_one_rejected_post_moderato() -> eyre::Result<()> {
263 let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Moderato);
264 let sender = Address::random();
265 StorageCtx::enter(&mut storage, || {
266 let mut factory = TIP20Setup::factory()?;
268
269 let current_token_id = factory.token_id_counter()?;
271 assert_eq!(current_token_id, U256::from(1));
272
273 let same_id_quote_token = token_id_to_address(1);
276 let call = ITIP20Factory::createTokenCall {
277 name: "Test Token".to_string(),
278 symbol: "TEST".to_string(),
279 currency: "USD".to_string(),
280 quoteToken: same_id_quote_token,
281 admin: sender,
282 };
283
284 let result = factory.create_token(sender, call);
285 assert_eq!(
287 result.unwrap_err(),
288 TempoPrecompileError::TIP20(TIP20Error::invalid_quote_token())
289 );
290 Ok(())
291 })
292 }
293
294 #[test]
295 fn test_create_token_future_quote_token_pre_moderato() -> eyre::Result<()> {
296 let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Moderato);
297 let sender = Address::random();
298 StorageCtx::enter(&mut storage, || {
299 let mut factory = TIP20Setup::factory()?;
302
303 assert_eq!(factory.token_id_counter()?, U256::from(1));
305
306 let future_quote_token = token_id_to_address(5);
309 let call = ITIP20Factory::createTokenCall {
310 name: "Test Token".to_string(),
311 symbol: "TEST".to_string(),
312 currency: "EUR".to_string(), quoteToken: future_quote_token,
314 admin: sender,
315 };
316
317 let result = factory.create_token(sender, call);
318
319 assert!(
322 result.is_err(),
323 "Should fail when using a not-yet-created token as quote token"
324 );
325 if let Err(e) = result {
326 assert_eq!(
327 e,
328 TempoPrecompileError::TIP20(TIP20Error::invalid_quote_token()),
329 "Should fail with InvalidQuoteToken from factory validation"
330 );
331 }
332 Ok(())
333 })
334 }
335
336 #[test]
337 fn test_create_token_off_by_one_allowed_pre_moderato() -> eyre::Result<()> {
338 let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Adagio);
339 let sender = Address::random();
340 StorageCtx::enter(&mut storage, || {
341 let mut factory = TIP20Setup::factory()?;
342
343 let current_token_id = factory.token_id_counter()?;
345 assert_eq!(current_token_id, U256::from(1));
346
347 let same_id_quote_token = token_id_to_address(1);
350 let call = ITIP20Factory::createTokenCall {
351 name: "Test Token".to_string(),
352 symbol: "TEST".to_string(),
353 currency: "USD".to_string(),
354 quoteToken: same_id_quote_token,
355 admin: sender,
356 };
357
358 let result = factory.create_token(sender, call);
359
360 match result {
364 Ok(_) => {
365 }
367 Err(e) => {
368 assert!(
370 !matches!(
371 e,
372 TempoPrecompileError::TIP20(TIP20Error::InvalidQuoteToken(_))
373 ),
374 "Pre-Moderato should not reject with InvalidQuoteToken when quote_token_id == token_id (buggy > logic)"
375 );
376 }
377 }
378 Ok(())
379 })
380 }
381
382 #[test]
383 fn test_token_id_post_allegretto() -> eyre::Result<()> {
384 let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Allegretto);
385 StorageCtx::enter(&mut storage, || {
386 let factory = TIP20Setup::factory()?;
387
388 let current_token_id = factory.token_id_counter()?;
389 assert_eq!(current_token_id, U256::ZERO);
390 Ok(())
391 })
392 }
393
394 #[test]
395 fn test_create_token_post_allegretto() -> eyre::Result<()> {
396 let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Allegretto);
397 let sender = Address::random();
398 StorageCtx::enter(&mut storage, || {
399 let mut factory = TIP20Setup::factory()?;
400
401 let call_fail = ITIP20Factory::createTokenCall {
402 name: "Test".to_string(),
403 symbol: "Test".to_string(),
404 currency: "USD".to_string(),
405 quoteToken: token_id_to_address(0),
406 admin: sender,
407 };
408
409 let result = factory.create_token(sender, call_fail);
410 assert_eq!(
411 result.unwrap_err(),
412 TempoPrecompileError::TIP20(TIP20Error::invalid_quote_token())
413 );
414
415 let call = ITIP20Factory::createTokenCall {
416 name: "Test".to_string(),
417 symbol: "Test".to_string(),
418 currency: "USD".to_string(),
419 quoteToken: Address::ZERO,
420 admin: sender,
421 };
422
423 factory.create_token(sender, call)?;
424 Ok(())
425 })
426 }
427
428 #[test]
429 fn test_is_tip20_post_allegro_moderato() -> eyre::Result<()> {
430 let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::AllegroModerato);
431 let sender = Address::random();
432
433 StorageCtx::enter(&mut storage, || {
434 initialize_path_usd(sender)?;
437
438 let mut factory = TIP20Factory::new();
439 factory.initialize()?;
440
441 assert_eq!(factory.token_id_counter()?, U256::from(1));
443
444 assert!(factory.is_tip20(crate::PATH_USD_ADDRESS)?);
446
447 let token_id_counter: u64 = factory.token_id_counter()?.to();
449 let non_existent_tip20 = token_id_to_address(token_id_counter + 100);
450 assert!(!factory.is_tip20(non_existent_tip20)?);
451
452 assert!(!factory.is_tip20(Address::random())?);
454
455 Ok(())
456 })
457 }
458
459 #[test]
460 fn test_is_tip20_pre_allegro_moderato() -> eyre::Result<()> {
461 let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Allegretto);
463 let sender = Address::random();
464
465 StorageCtx::enter(&mut storage, || {
466 initialize_path_usd(sender)?;
467
468 let mut factory = TIP20Factory::new();
469 factory.initialize()?;
470
471 assert!(factory.is_tip20(crate::PATH_USD_ADDRESS)?);
473
474 let token_id_counter: u64 = factory.token_id_counter()?.to();
476 let non_existent_tip20 = token_id_to_address(token_id_counter + 100);
477 assert!(
478 factory.is_tip20(non_existent_tip20)?,
479 "Pre-AllegroModerato: should only check prefix"
480 );
481
482 assert!(!factory.is_tip20(Address::random())?);
484
485 Ok(())
486 })
487 }
488
489 #[test]
490 fn test_is_tip20_prefix() -> eyre::Result<()> {
491 let mut storage = HashMapStorageProvider::new(1);
492
493 StorageCtx::enter(&mut storage, || {
494 let token_id = rand::random::<u64>();
496 let token = token_id_to_address(token_id);
497 assert!(is_tip20_prefix(token));
498
499 let random = Address::random();
501 assert!(!is_tip20_prefix(random));
502
503 Ok(())
504 })
505 }
506}