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