Skip to main content

tempo_precompiles/tip20/
dispatch.rs

1//! ABI dispatch for the [`TIP20Token`] precompile.
2
3use 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
18/// Decoded call variant — either a TIP-20 token call or a role-management call.
19enum TIP20Call {
20    TIP20(ITIP20Calls),
21    RolesAuth(IRolesAuthCalls),
22}
23
24impl TIP20Call {
25    fn decode(calldata: &[u8]) -> Result<Self, alloy::sol_types::Error> {
26        // safe to expect as `dispatch_call` pre-validates calldata len
27        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        // Ensure that the token is initialized (has bytecode)
44        // Note that if the initialization check fails, this is treated as uninitialized
45        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            // Metadata functions (no calldata decoding needed)
52            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            // View functions
76            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            // State changing functions
96            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            // RolesAuth functions
195            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        // T1: invalid selector returns reverted output
240        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            // T1: insufficient calldata also returns reverted output
248            let result = token.call(&Bytes::from([0x12, 0x34]), sender)?;
249            assert!(result.reverted);
250
251            Ok(())
252        })?;
253
254        // Pre-T1 (T0): insufficient calldata returns error
255        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            // Verify balances
413            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            // Verify allowance was reduced
423            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            // Pause the token
444            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            // Unpause the token
451            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            // Check initial state
476            assert_eq!(
477                token.balance_of(ITIP20::balanceOfCall { account: burner })?,
478                initial_balance
479            );
480            assert_eq!(token.total_supply()?, initial_balance);
481
482            // Burn tokens
483            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            // Test name()
508            let name_call = ITIP20::nameCall {};
509            let calldata = name_call.abi_encode();
510            let result = token.call(&calldata, caller)?;
511            // HashMapStorageProvider does not do gas accounting, so we expect 0 here.
512            assert_eq!(result.gas_used, 0);
513            let name = String::abi_decode(&result.bytes)?;
514            assert_eq!(name, "Test Token");
515
516            // Test symbol()
517            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            // Test decimals()
525            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            // Test currency()
533            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            // Test totalSupply()
541            let total_supply_call = ITIP20::totalSupplyCall {};
542            let calldata = total_supply_call.abi_encode();
543            let result = token.call(&calldata, caller)?;
544            // HashMapStorageProvider does not do gas accounting, so we expect 0 here.
545            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            // Initialize TIP403 registry
681            let mut registry = TIP403Registry::new();
682            registry.initialize()?;
683
684            // Create a valid policy
685            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            // Create another valid policy for the unauthorized test
702            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        // Use T2 hardfork so T2-gated selectors (permit, nonces, DOMAIN_SEPARATOR) are active
753        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        // Pre-T2: permit/nonces/DOMAIN_SEPARATOR should return unknown selector
779        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            // Test permit selector is gated
786            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            // Test nonces selector is gated
801            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            // Test DOMAIN_SEPARATOR selector is gated
810            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}