1use crate::{
4 Precompile, SelectorSchedule, charge_input_cost, dispatch_call, metadata, mutate, mutate_void,
5 storage::ContractStorage,
6 tip20::{ITIP20, TIP20Token},
7 view,
8};
9use alloy::{
10 primitives::Address,
11 sol_types::{SolCall, SolInterface},
12};
13use revm::precompile::PrecompileResult;
14use tempo_chainspec::hardfork::TempoHardfork;
15use tempo_contracts::precompiles::{IRolesAuth::IRolesAuthCalls, ITIP20::ITIP20Calls, TIP20Error};
16
17const T2_ADDED: &[[u8; 4]] = &[
18 ITIP20::permitCall::SELECTOR,
19 ITIP20::noncesCall::SELECTOR,
20 ITIP20::DOMAIN_SEPARATORCall::SELECTOR,
21];
22
23const T5_ADDED: &[[u8; 4]] = &[
25 ITIP20::logoURICall::SELECTOR,
26 ITIP20::setLogoURICall::SELECTOR,
27];
28
29enum TIP20Call {
31 TIP20(ITIP20Calls),
32 RolesAuth(IRolesAuthCalls),
33}
34
35impl TIP20Call {
36 fn decode(calldata: &[u8]) -> Result<Self, alloy::sol_types::Error> {
37 let selector: [u8; 4] = calldata[..4].try_into().expect("calldata len >= 4");
39
40 if IRolesAuthCalls::valid_selector(selector) {
41 IRolesAuthCalls::abi_decode(calldata).map(Self::RolesAuth)
42 } else {
43 ITIP20Calls::abi_decode(calldata).map(Self::TIP20)
44 }
45 }
46}
47
48impl Precompile for TIP20Token {
49 fn call(&mut self, calldata: &[u8], msg_sender: Address) -> PrecompileResult {
50 if let Some(err) = charge_input_cost(&mut self.storage, calldata) {
51 return err;
52 }
53
54 let initialized = match self.is_initialized() {
56 Ok(v) => v,
57 Err(_) if !self.storage.spec().is_t4() => false,
58 Err(e) => return self.storage.error_result(e),
59 };
60 if !initialized {
61 return self.storage.error_result(TIP20Error::uninitialized());
62 }
63
64 dispatch_call(
65 calldata,
66 &[
67 SelectorSchedule::new(TempoHardfork::T2).with_added(T2_ADDED),
68 SelectorSchedule::new(TempoHardfork::T5).with_added(T5_ADDED),
69 ],
70 TIP20Call::decode,
71 |call| match call {
72 TIP20Call::TIP20(ITIP20Calls::name(_)) => {
74 metadata::<ITIP20::nameCall>(|| self.name())
75 }
76 TIP20Call::TIP20(ITIP20Calls::symbol(_)) => {
77 metadata::<ITIP20::symbolCall>(|| self.symbol())
78 }
79 TIP20Call::TIP20(ITIP20Calls::decimals(_)) => {
80 metadata::<ITIP20::decimalsCall>(|| self.decimals())
81 }
82 TIP20Call::TIP20(ITIP20Calls::currency(_)) => {
83 metadata::<ITIP20::currencyCall>(|| self.currency())
84 }
85 TIP20Call::TIP20(ITIP20Calls::totalSupply(_)) => {
86 metadata::<ITIP20::totalSupplyCall>(|| self.total_supply())
87 }
88 TIP20Call::TIP20(ITIP20Calls::supplyCap(_)) => {
89 metadata::<ITIP20::supplyCapCall>(|| self.supply_cap())
90 }
91 TIP20Call::TIP20(ITIP20Calls::transferPolicyId(_)) => {
92 metadata::<ITIP20::transferPolicyIdCall>(|| self.transfer_policy_id())
93 }
94 TIP20Call::TIP20(ITIP20Calls::paused(_)) => {
95 metadata::<ITIP20::pausedCall>(|| self.paused())
96 }
97 TIP20Call::TIP20(ITIP20Calls::logoURI(_)) => {
98 metadata::<ITIP20::logoURICall>(|| self.logo_uri())
99 }
100
101 TIP20Call::TIP20(ITIP20Calls::balanceOf(call)) => {
103 view(call, |c| self.balance_of(c))
104 }
105 TIP20Call::TIP20(ITIP20Calls::allowance(call)) => view(call, |c| self.allowance(c)),
106 TIP20Call::TIP20(ITIP20Calls::quoteToken(call)) => {
107 view(call, |_| self.quote_token())
108 }
109 TIP20Call::TIP20(ITIP20Calls::nextQuoteToken(call)) => {
110 view(call, |_| self.next_quote_token())
111 }
112 TIP20Call::TIP20(ITIP20Calls::PAUSE_ROLE(call)) => {
113 view(call, |_| Ok(Self::pause_role()))
114 }
115 TIP20Call::TIP20(ITIP20Calls::UNPAUSE_ROLE(call)) => {
116 view(call, |_| Ok(Self::unpause_role()))
117 }
118 TIP20Call::TIP20(ITIP20Calls::ISSUER_ROLE(call)) => {
119 view(call, |_| Ok(Self::issuer_role()))
120 }
121 TIP20Call::TIP20(ITIP20Calls::BURN_BLOCKED_ROLE(call)) => {
122 view(call, |_| Ok(Self::burn_blocked_role()))
123 }
124
125 TIP20Call::TIP20(ITIP20Calls::transferFrom(call)) => {
127 mutate(call, msg_sender, |s, c| self.transfer_from(s, c))
128 }
129 TIP20Call::TIP20(ITIP20Calls::transfer(call)) => {
130 mutate(call, msg_sender, |s, c| self.transfer(s, c))
131 }
132 TIP20Call::TIP20(ITIP20Calls::approve(call)) => {
133 mutate(call, msg_sender, |s, c| self.approve(s, c))
134 }
135 TIP20Call::TIP20(ITIP20Calls::changeTransferPolicyId(call)) => {
136 mutate_void(call, msg_sender, |s, c| {
137 self.change_transfer_policy_id(s, c)
138 })
139 }
140 TIP20Call::TIP20(ITIP20Calls::setSupplyCap(call)) => {
141 mutate_void(call, msg_sender, |s, c| self.set_supply_cap(s, c))
142 }
143 TIP20Call::TIP20(ITIP20Calls::setLogoURI(call)) => {
144 mutate_void(call, msg_sender, |s, c| self.set_logo_uri(s, c))
145 }
146 TIP20Call::TIP20(ITIP20Calls::pause(call)) => {
147 mutate_void(call, msg_sender, |s, c| self.pause(s, c))
148 }
149 TIP20Call::TIP20(ITIP20Calls::unpause(call)) => {
150 mutate_void(call, msg_sender, |s, c| self.unpause(s, c))
151 }
152 TIP20Call::TIP20(ITIP20Calls::setNextQuoteToken(call)) => {
153 mutate_void(call, msg_sender, |s, c| self.set_next_quote_token(s, c))
154 }
155 TIP20Call::TIP20(ITIP20Calls::completeQuoteTokenUpdate(call)) => {
156 mutate_void(call, msg_sender, |s, c| {
157 self.complete_quote_token_update(s, c)
158 })
159 }
160 TIP20Call::TIP20(ITIP20Calls::mint(call)) => {
161 mutate_void(call, msg_sender, |s, c| self.mint(s, c))
162 }
163 TIP20Call::TIP20(ITIP20Calls::mintWithMemo(call)) => {
164 mutate_void(call, msg_sender, |s, c| self.mint_with_memo(s, c))
165 }
166 TIP20Call::TIP20(ITIP20Calls::burn(call)) => {
167 mutate_void(call, msg_sender, |s, c| self.burn(s, c))
168 }
169 TIP20Call::TIP20(ITIP20Calls::burnWithMemo(call)) => {
170 mutate_void(call, msg_sender, |s, c| self.burn_with_memo(s, c))
171 }
172 TIP20Call::TIP20(ITIP20Calls::burnBlocked(call)) => {
173 mutate_void(call, msg_sender, |s, c| {
174 self.burn_blocked(s, c.from, c.amount, true)
175 })
176 }
177 TIP20Call::TIP20(ITIP20Calls::transferWithMemo(call)) => {
178 mutate_void(call, msg_sender, |s, c| self.transfer_with_memo(s, c))
179 }
180 TIP20Call::TIP20(ITIP20Calls::transferFromWithMemo(call)) => {
181 mutate(call, msg_sender, |sender, c| {
182 self.transfer_from_with_memo(sender, c)
183 })
184 }
185 TIP20Call::TIP20(ITIP20Calls::distributeReward(call)) => {
186 mutate_void(call, msg_sender, |s, c| self.distribute_reward(s, c))
187 }
188 TIP20Call::TIP20(ITIP20Calls::setRewardRecipient(call)) => {
189 mutate_void(call, msg_sender, |s, c| self.set_reward_recipient(s, c))
190 }
191 TIP20Call::TIP20(ITIP20Calls::claimRewards(call)) => {
192 mutate(call, msg_sender, |_, _| self.claim_rewards(msg_sender))
193 }
194 TIP20Call::TIP20(ITIP20Calls::globalRewardPerToken(call)) => {
195 view(call, |_| self.get_global_reward_per_token())
196 }
197 TIP20Call::TIP20(ITIP20Calls::optedInSupply(call)) => {
198 view(call, |_| self.get_opted_in_supply())
199 }
200 TIP20Call::TIP20(ITIP20Calls::userRewardInfo(call)) => view(call, |c| {
201 self.get_user_reward_info(c.account).map(|info| info.into())
202 }),
203 TIP20Call::TIP20(ITIP20Calls::getPendingRewards(call)) => {
204 view(call, |c| self.get_pending_rewards(c.account))
205 }
206
207 TIP20Call::TIP20(ITIP20Calls::permit(call)) => {
208 mutate_void(call, msg_sender, |_s, c| self.permit(c))
209 }
210 TIP20Call::TIP20(ITIP20Calls::nonces(call)) => view(call, |c| self.nonces(c)),
211 TIP20Call::TIP20(ITIP20Calls::DOMAIN_SEPARATOR(call)) => {
212 view(call, |_| self.domain_separator())
213 }
214
215 TIP20Call::RolesAuth(IRolesAuthCalls::hasRole(call)) => {
217 view(call, |c| self.has_role(c))
218 }
219 TIP20Call::RolesAuth(IRolesAuthCalls::getRoleAdmin(call)) => {
220 view(call, |c| self.get_role_admin(c))
221 }
222 TIP20Call::RolesAuth(IRolesAuthCalls::grantRole(call)) => {
223 mutate_void(call, msg_sender, |s, c| self.grant_role(s, c))
224 }
225 TIP20Call::RolesAuth(IRolesAuthCalls::revokeRole(call)) => {
226 mutate_void(call, msg_sender, |s, c| self.revoke_role(s, c))
227 }
228 TIP20Call::RolesAuth(IRolesAuthCalls::renounceRole(call)) => {
229 mutate_void(call, msg_sender, |s, c| self.renounce_role(s, c))
230 }
231 TIP20Call::RolesAuth(IRolesAuthCalls::setRoleAdmin(call)) => {
232 mutate_void(call, msg_sender, |s, c| self.set_role_admin(s, c))
233 }
234 },
235 )
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242 use crate::{
243 storage::{StorageCtx, hashmap::HashMapStorageProvider},
244 test_util::{TIP20Setup, setup_storage},
245 tip20::{ISSUER_ROLE, PAUSE_ROLE, UNPAUSE_ROLE},
246 tip403_registry::{ITIP403Registry, TIP403Registry},
247 };
248 use alloy::{
249 primitives::{Bytes, U256, address},
250 sol_types::{SolCall, SolError, SolInterface, SolValue},
251 };
252
253 use tempo_chainspec::hardfork::TempoHardfork;
254 use tempo_contracts::precompiles::{
255 IRolesAuth, RolesAuthError, TIP20Error, UnknownFunctionSelector,
256 };
257
258 #[test]
259 fn test_function_selector_dispatch() -> eyre::Result<()> {
260 let (_, sender) = setup_storage();
261
262 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T1);
264 StorageCtx::enter(&mut storage, || -> eyre::Result<()> {
265 let mut token = TIP20Setup::create("Test", "TST", sender).apply()?;
266
267 let result = token.call(&Bytes::from([0x12, 0x34, 0x56, 0x78]), sender)?;
268 assert!(result.is_revert());
269
270 let result = token.call(&Bytes::from([0x12, 0x34]), sender)?;
272 assert!(result.is_revert());
273
274 Ok(())
275 })?;
276
277 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T0);
279 StorageCtx::enter(&mut storage, || {
280 let mut token = TIP20Setup::create("Test", "TST", sender).apply()?;
281
282 let result = token.call(&Bytes::from([0x12, 0x34]), sender);
283 let output = result.expect("expected Ok(halt) for short calldata");
284 assert!(output.is_halt());
285
286 Ok(())
287 })
288 }
289
290 #[test]
291 fn test_balance_of_calldata_handling() -> eyre::Result<()> {
292 let (mut storage, admin) = setup_storage();
293 let sender = Address::random();
294 let account = Address::random();
295 let test_balance = U256::from(1000);
296
297 StorageCtx::enter(&mut storage, || {
298 let mut token = TIP20Setup::create("Test", "TST", admin)
299 .with_issuer(admin)
300 .with_mint(account, test_balance)
301 .apply()?;
302
303 let balance_of_call = ITIP20::balanceOfCall { account };
304 let calldata = balance_of_call.abi_encode();
305
306 let result = token.call(&calldata, sender)?;
307 assert!(result.status.is_success());
308
309 let decoded = U256::abi_decode(&result.bytes)?;
310 assert_eq!(decoded, test_balance);
311
312 Ok(())
313 })
314 }
315
316 #[test]
317 fn test_mint_updates_storage() -> eyre::Result<()> {
318 let (mut storage, admin) = setup_storage();
319 let recipient = Address::random();
320
321 StorageCtx::enter(&mut storage, || {
322 let mut token = TIP20Setup::create("Test", "TST", admin)
323 .with_issuer(admin)
324 .apply()?;
325
326 let initial_balance = token.balance_of(ITIP20::balanceOfCall { account: recipient })?;
327 assert_eq!(initial_balance, U256::ZERO);
328
329 let mint_amount = U256::random().min(U256::from(u128::MAX)) % token.supply_cap()?;
330 let mint_call = ITIP20::mintCall {
331 to: recipient,
332 amount: mint_amount,
333 };
334 let calldata = mint_call.abi_encode();
335
336 let result = token.call(&calldata, admin)?;
337 assert!(result.status.is_success());
338
339 let final_balance = token.balance_of(ITIP20::balanceOfCall { account: recipient })?;
340 assert_eq!(final_balance, mint_amount);
341
342 Ok(())
343 })
344 }
345
346 #[test]
347 fn test_transfer_updates_balances() -> eyre::Result<()> {
348 let (mut storage, admin) = setup_storage();
349 let sender = Address::random();
350 let recipient = Address::random();
351 let transfer_amount = U256::from(300);
352 let initial_sender_balance = U256::from(1000);
353
354 StorageCtx::enter(&mut storage, || {
355 let mut token = TIP20Setup::create("Test", "TST", admin)
356 .with_issuer(admin)
357 .with_mint(sender, initial_sender_balance)
358 .apply()?;
359
360 assert_eq!(
361 token.balance_of(ITIP20::balanceOfCall { account: sender })?,
362 initial_sender_balance
363 );
364 assert_eq!(
365 token.balance_of(ITIP20::balanceOfCall { account: recipient })?,
366 U256::ZERO
367 );
368
369 let transfer_call = ITIP20::transferCall {
370 to: recipient,
371 amount: transfer_amount,
372 };
373 let calldata = transfer_call.abi_encode();
374 let result = token.call(&calldata, sender)?;
375 assert!(result.status.is_success());
376
377 let success = bool::abi_decode(&result.bytes)?;
378 assert!(success);
379
380 let final_sender_balance =
381 token.balance_of(ITIP20::balanceOfCall { account: sender })?;
382 let final_recipient_balance =
383 token.balance_of(ITIP20::balanceOfCall { account: recipient })?;
384
385 assert_eq!(
386 final_sender_balance,
387 initial_sender_balance - transfer_amount
388 );
389 assert_eq!(final_recipient_balance, transfer_amount);
390
391 Ok(())
392 })
393 }
394
395 #[test]
396 fn test_approve_and_transfer_from() -> eyre::Result<()> {
397 let (mut storage, admin) = setup_storage();
398 let owner = Address::random();
399 let spender = Address::random();
400 let recipient = Address::random();
401 let approve_amount = U256::from(500);
402 let transfer_amount = U256::from(300);
403 let initial_owner_balance = U256::from(1000);
404
405 StorageCtx::enter(&mut storage, || {
406 let mut token = TIP20Setup::create("Test", "TST", admin)
407 .with_issuer(admin)
408 .with_mint(owner, initial_owner_balance)
409 .apply()?;
410
411 let approve_call = ITIP20::approveCall {
412 spender,
413 amount: approve_amount,
414 };
415 let calldata = approve_call.abi_encode();
416 let result = token.call(&calldata, owner)?;
417 assert!(result.status.is_success());
418 let success = bool::abi_decode(&result.bytes)?;
419 assert!(success);
420
421 let allowance = token.allowance(ITIP20::allowanceCall { owner, spender })?;
422 assert_eq!(allowance, approve_amount);
423
424 let transfer_from_call = ITIP20::transferFromCall {
425 from: owner,
426 to: recipient,
427 amount: transfer_amount,
428 };
429 let calldata = transfer_from_call.abi_encode();
430 let result = token.call(&calldata, spender)?;
431 assert!(result.status.is_success());
432 let success = bool::abi_decode(&result.bytes)?;
433 assert!(success);
434
435 assert_eq!(
437 token.balance_of(ITIP20::balanceOfCall { account: owner })?,
438 initial_owner_balance - transfer_amount
439 );
440 assert_eq!(
441 token.balance_of(ITIP20::balanceOfCall { account: recipient })?,
442 transfer_amount
443 );
444
445 let remaining_allowance = token.allowance(ITIP20::allowanceCall { owner, spender })?;
447 assert_eq!(remaining_allowance, approve_amount - transfer_amount);
448
449 Ok(())
450 })
451 }
452
453 #[test]
454 fn test_pause_and_unpause() -> eyre::Result<()> {
455 let (mut storage, admin) = setup_storage();
456 let pauser = Address::random();
457 let unpauser = Address::random();
458
459 StorageCtx::enter(&mut storage, || {
460 let mut token = TIP20Setup::create("Test", "TST", admin)
461 .with_role(pauser, *PAUSE_ROLE)
462 .with_role(unpauser, *UNPAUSE_ROLE)
463 .apply()?;
464 assert!(!token.paused()?);
465
466 let pause_call = ITIP20::pauseCall {};
468 let calldata = pause_call.abi_encode();
469 let result = token.call(&calldata, pauser)?;
470 assert!(result.status.is_success());
471 assert!(token.paused()?);
472
473 let unpause_call = ITIP20::unpauseCall {};
475 let calldata = unpause_call.abi_encode();
476 let result = token.call(&calldata, unpauser)?;
477 assert!(result.status.is_success());
478 assert!(!token.paused()?);
479
480 Ok(())
481 })
482 }
483
484 #[test]
485 fn test_burn_functionality() -> eyre::Result<()> {
486 let (mut storage, admin) = setup_storage();
487 let burner = Address::random();
488 let initial_balance = U256::from(1000);
489 let burn_amount = U256::from(300);
490
491 StorageCtx::enter(&mut storage, || {
492 let mut token = TIP20Setup::create("Test", "TST", admin)
493 .with_issuer(admin)
494 .with_role(burner, *ISSUER_ROLE)
495 .with_mint(burner, initial_balance)
496 .apply()?;
497
498 assert_eq!(
500 token.balance_of(ITIP20::balanceOfCall { account: burner })?,
501 initial_balance
502 );
503 assert_eq!(token.total_supply()?, initial_balance);
504
505 let burn_call = ITIP20::burnCall {
507 amount: burn_amount,
508 };
509 let calldata = burn_call.abi_encode();
510 let result = token.call(&calldata, burner)?;
511 assert!(result.status.is_success());
512 assert_eq!(
513 token.balance_of(ITIP20::balanceOfCall { account: burner })?,
514 initial_balance - burn_amount
515 );
516 assert_eq!(token.total_supply()?, initial_balance - burn_amount);
517
518 Ok(())
519 })
520 }
521
522 #[test]
523 fn test_metadata_functions() -> eyre::Result<()> {
524 let (mut storage, admin) = setup_storage();
525 let caller = Address::random();
526
527 StorageCtx::enter(&mut storage, || {
528 let mut token = TIP20Setup::create("Test Token", "TEST", admin).apply()?;
529
530 let name_call = ITIP20::nameCall {};
532 let calldata = name_call.abi_encode();
533 let result = token.call(&calldata, caller)?;
534 assert!(result.status.is_success());
536 let name = String::abi_decode(&result.bytes)?;
537 assert_eq!(name, "Test Token");
538
539 let symbol_call = ITIP20::symbolCall {};
541 let calldata = symbol_call.abi_encode();
542 let result = token.call(&calldata, caller)?;
543 assert!(result.status.is_success());
544 let symbol = String::abi_decode(&result.bytes)?;
545 assert_eq!(symbol, "TEST");
546
547 let decimals_call = ITIP20::decimalsCall {};
549 let calldata = decimals_call.abi_encode();
550 let result = token.call(&calldata, caller)?;
551 assert!(result.status.is_success());
552 let decimals = ITIP20::decimalsCall::abi_decode_returns(&result.bytes)?;
553 assert_eq!(decimals, 6);
554
555 let currency_call = ITIP20::currencyCall {};
557 let calldata = currency_call.abi_encode();
558 let result = token.call(&calldata, caller)?;
559 assert!(result.status.is_success());
560 let currency = String::abi_decode(&result.bytes)?;
561 assert_eq!(currency, "USD");
562
563 let total_supply_call = ITIP20::totalSupplyCall {};
565 let calldata = total_supply_call.abi_encode();
566 let result = token.call(&calldata, caller)?;
567 assert!(result.status.is_success());
569 let total_supply = U256::abi_decode(&result.bytes)?;
570 assert_eq!(total_supply, U256::ZERO);
571
572 Ok(())
573 })
574 }
575
576 #[test]
577 fn test_supply_cap_enforcement() -> eyre::Result<()> {
578 let (mut storage, admin) = setup_storage();
579 let recipient = Address::random();
580 let supply_cap = U256::from(1000);
581 let mint_amount = U256::from(1001);
582
583 StorageCtx::enter(&mut storage, || {
584 let mut token = TIP20Setup::create("Test", "TST", admin)
585 .with_issuer(admin)
586 .apply()?;
587
588 let set_cap_call = ITIP20::setSupplyCapCall {
589 newSupplyCap: supply_cap,
590 };
591 let calldata = set_cap_call.abi_encode();
592 let result = token.call(&calldata, admin)?;
593 assert!(result.status.is_success());
594
595 let mint_call = ITIP20::mintCall {
596 to: recipient,
597 amount: mint_amount,
598 };
599 let calldata = mint_call.abi_encode();
600 let output = token.call(&calldata, admin)?;
601 assert!(output.is_revert());
602
603 let expected: Bytes = TIP20Error::supply_cap_exceeded().selector().into();
604 assert_eq!(output.bytes, expected);
605
606 Ok(())
607 })
608 }
609
610 #[test]
611 fn test_role_based_access_control() -> eyre::Result<()> {
612 let (mut storage, admin) = setup_storage();
613 let user1 = Address::random();
614 let user2 = Address::random();
615 let unauthorized = Address::random();
616
617 StorageCtx::enter(&mut storage, || {
618 let mut token = TIP20Setup::create("Test", "TST", admin)
619 .with_issuer(admin)
620 .with_role(user1, *ISSUER_ROLE)
621 .apply()?;
622
623 let has_role_call = IRolesAuth::hasRoleCall {
624 role: *ISSUER_ROLE,
625 account: user1,
626 };
627 let calldata = has_role_call.abi_encode();
628 let result = token.call(&calldata, admin)?;
629 assert!(result.status.is_success());
630 let has_role = bool::abi_decode(&result.bytes)?;
631 assert!(has_role);
632
633 let has_role_call = IRolesAuth::hasRoleCall {
634 role: *ISSUER_ROLE,
635 account: user2,
636 };
637 let calldata = has_role_call.abi_encode();
638 let result = token.call(&calldata, admin)?;
639 let has_role = bool::abi_decode(&result.bytes)?;
640 assert!(!has_role);
641
642 let mint_call = ITIP20::mintCall {
643 to: user2,
644 amount: U256::from(100),
645 };
646 let calldata = mint_call.abi_encode();
647 let output = token.call(&Bytes::from(calldata.clone()), unauthorized)?;
648 assert!(output.is_revert());
649 let expected: Bytes = RolesAuthError::unauthorized().selector().into();
650 assert_eq!(output.bytes, expected);
651
652 let result = token.call(&calldata, user1)?;
653 assert!(result.status.is_success());
654
655 Ok(())
656 })
657 }
658
659 #[test]
660 fn test_transfer_with_memo() -> eyre::Result<()> {
661 let (mut storage, admin) = setup_storage();
662 let sender = Address::random();
663 let recipient = Address::random();
664 let transfer_amount = U256::from(100);
665 let initial_balance = U256::from(500);
666
667 StorageCtx::enter(&mut storage, || {
668 let mut token = TIP20Setup::create("Test", "TST", admin)
669 .with_issuer(admin)
670 .with_mint(sender, initial_balance)
671 .apply()?;
672
673 let memo = alloy::primitives::B256::from([1u8; 32]);
674 let transfer_call = ITIP20::transferWithMemoCall {
675 to: recipient,
676 amount: transfer_amount,
677 memo,
678 };
679 let calldata = transfer_call.abi_encode();
680 let result = token.call(&calldata, sender)?;
681 assert!(result.status.is_success());
682 assert_eq!(
683 token.balance_of(ITIP20::balanceOfCall { account: sender })?,
684 initial_balance - transfer_amount
685 );
686 assert_eq!(
687 token.balance_of(ITIP20::balanceOfCall { account: recipient })?,
688 transfer_amount
689 );
690
691 Ok(())
692 })
693 }
694
695 #[test]
696 fn test_change_transfer_policy_id() -> eyre::Result<()> {
697 let (mut storage, admin) = setup_storage();
698 let non_admin = Address::random();
699
700 StorageCtx::enter(&mut storage, || {
701 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
702
703 let mut registry = TIP403Registry::new();
705 registry.initialize()?;
706
707 let new_policy_id = registry.create_policy(
709 admin,
710 ITIP403Registry::createPolicyCall {
711 admin,
712 policyType: ITIP403Registry::PolicyType::WHITELIST,
713 },
714 )?;
715
716 let change_policy_call = ITIP20::changeTransferPolicyIdCall {
717 newPolicyId: new_policy_id,
718 };
719 let calldata = change_policy_call.abi_encode();
720 let result = token.call(&calldata, admin)?;
721 assert!(result.status.is_success());
722 assert_eq!(token.transfer_policy_id()?, new_policy_id);
723
724 let another_policy_id = registry.create_policy(
726 admin,
727 ITIP403Registry::createPolicyCall {
728 admin,
729 policyType: ITIP403Registry::PolicyType::BLACKLIST,
730 },
731 )?;
732
733 let change_policy_call = ITIP20::changeTransferPolicyIdCall {
734 newPolicyId: another_policy_id,
735 };
736 let calldata = change_policy_call.abi_encode();
737 let output = token.call(&calldata, non_admin)?;
738 assert!(output.is_revert());
739 let expected: Bytes = RolesAuthError::unauthorized().selector().into();
740 assert_eq!(output.bytes, expected);
741
742 Ok(())
743 })
744 }
745
746 #[test]
747 fn test_call_uninitialized_token_reverts() -> eyre::Result<()> {
748 let (mut storage, _) = setup_storage();
749 let caller = Address::random();
750
751 StorageCtx::enter(&mut storage, || {
752 let uninitialized_addr = address!("20C0000000000000000000000000000000000999");
753 let mut token = TIP20Token::from_address(uninitialized_addr)?;
754
755 let calldata = ITIP20::approveCall {
756 spender: Address::random(),
757 amount: U256::random(),
758 }
759 .abi_encode();
760 let result = token.call(&calldata, caller)?;
761
762 assert!(result.is_revert());
763 let expected: Bytes = TIP20Error::uninitialized().selector().into();
764 assert_eq!(result.bytes, expected);
765
766 Ok(())
767 })
768 }
769
770 #[test]
771 fn tip20_test_selector_coverage() -> eyre::Result<()> {
772 use crate::test_util::{assert_full_coverage, check_selector_coverage};
773 use tempo_contracts::precompiles::{IRolesAuth::IRolesAuthCalls, ITIP20::ITIP20Calls};
774
775 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T5);
777 let admin = Address::random();
778
779 StorageCtx::enter(&mut storage, || {
780 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
781
782 let itip20_unsupported =
783 check_selector_coverage(&mut token, ITIP20Calls::SELECTORS, "ITIP20", |s| {
784 ITIP20Calls::name_by_selector(s)
785 });
786
787 let roles_unsupported = check_selector_coverage(
788 &mut token,
789 IRolesAuthCalls::SELECTORS,
790 "IRolesAuth",
791 IRolesAuthCalls::name_by_selector,
792 );
793
794 assert_full_coverage([itip20_unsupported, roles_unsupported]);
795 Ok(())
796 })
797 }
798
799 #[test]
800 fn test_logo_uri_selectors_gated_behind_t5() -> eyre::Result<()> {
801 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T4);
803 let admin = Address::random();
804
805 StorageCtx::enter(&mut storage, || {
806 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
807
808 let logo_uri_calldata = ITIP20::logoURICall {}.abi_encode();
810 let result = token.call(&logo_uri_calldata, admin)?;
811 assert!(result.is_revert());
812 assert!(UnknownFunctionSelector::abi_decode(&result.bytes).is_ok());
813
814 let set_logo_uri_calldata = ITIP20::setLogoURICall {
816 newLogoURI: "https://example.com/icon.svg".to_string(),
817 }
818 .abi_encode();
819 let result = token.call(&set_logo_uri_calldata, admin)?;
820 assert!(result.is_revert());
821 assert!(UnknownFunctionSelector::abi_decode(&result.bytes).is_ok());
822
823 Ok(())
824 })
825 }
826
827 #[test]
828 fn test_logo_uri_pre_t5_deploy_post_t5_read_returns_empty() -> eyre::Result<()> {
829 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T4);
830 let admin = Address::random();
831 let token_address = StorageCtx::enter(&mut storage, || -> eyre::Result<Address> {
832 let token = TIP20Setup::create("Test", "TST", admin).apply()?;
833 Ok(token.address())
834 })?;
835
836 storage.set_spec(TempoHardfork::T5);
838
839 StorageCtx::enter(&mut storage, || {
840 let mut token = TIP20Token::from_address(token_address)?;
841
842 assert_eq!(token.logo_uri()?, "");
844
845 let calldata = ITIP20::logoURICall {}.abi_encode();
847 let result = token.call(&calldata, admin)?;
848 assert!(!result.is_revert(), "logoURI() must succeed post-T5");
849 let decoded = ITIP20::logoURICall::abi_decode_returns(&result.bytes)?;
850 assert_eq!(decoded, "");
851
852 Ok(())
853 })
854 }
855
856 #[test]
857 fn test_permit_selectors_gated_behind_t2() -> eyre::Result<()> {
858 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T1);
860 let admin = Address::random();
861
862 StorageCtx::enter(&mut storage, || {
863 let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
864
865 let permit_calldata = ITIP20::permitCall {
867 owner: Address::random(),
868 spender: Address::random(),
869 value: U256::ZERO,
870 deadline: U256::MAX,
871 v: 27,
872 r: alloy::primitives::B256::ZERO,
873 s: alloy::primitives::B256::ZERO,
874 }
875 .abi_encode();
876 let result = token.call(&permit_calldata, admin)?;
877 assert!(result.is_revert());
878 assert!(UnknownFunctionSelector::abi_decode(&result.bytes).is_ok());
879
880 let nonces_calldata = ITIP20::noncesCall {
882 owner: Address::random(),
883 }
884 .abi_encode();
885 let result = token.call(&nonces_calldata, admin)?;
886 assert!(result.is_revert());
887 assert!(UnknownFunctionSelector::abi_decode(&result.bytes).is_ok());
888
889 let ds_calldata = ITIP20::DOMAIN_SEPARATORCall {}.abi_encode();
891 let result = token.call(&ds_calldata, admin)?;
892 assert!(result.is_revert());
893 assert!(UnknownFunctionSelector::abi_decode(&result.bytes).is_ok());
894
895 Ok(())
896 })
897 }
898}