Skip to main content

tempo_precompiles/tip20/
dispatch.rs

1//! ABI dispatch for the [`TIP20Token`] precompile.
2
3use 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
23/// Selectors added at T5: TIP-1026 Token Logo URI.
24const T5_ADDED: &[[u8; 4]] = &[
25    ITIP20::logoURICall::SELECTOR,
26    ITIP20::setLogoURICall::SELECTOR,
27];
28
29/// Decoded call variant — either a TIP-20 token call or a role-management call.
30enum TIP20Call {
31    TIP20(ITIP20Calls),
32    RolesAuth(IRolesAuthCalls),
33}
34
35impl TIP20Call {
36    fn decode(calldata: &[u8]) -> Result<Self, alloy::sol_types::Error> {
37        // safe to expect as `dispatch_call` pre-validates calldata len
38        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        // Ensure that the token is initialized (has bytecode)
55        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                // Metadata functions (no calldata decoding needed)
73                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                // View functions
102                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                // State changing functions
126                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                // RolesAuth functions
216                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        // T1: invalid selector returns reverted output
263        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            // T1: insufficient calldata also returns reverted output
271            let result = token.call(&Bytes::from([0x12, 0x34]), sender)?;
272            assert!(result.is_revert());
273
274            Ok(())
275        })?;
276
277        // Pre-T1 (T0): insufficient calldata returns halt
278        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            // Verify balances
436            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            // Verify allowance was reduced
446            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            // Pause the token
467            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            // Unpause the token
474            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            // Check initial state
499            assert_eq!(
500                token.balance_of(ITIP20::balanceOfCall { account: burner })?,
501                initial_balance
502            );
503            assert_eq!(token.total_supply()?, initial_balance);
504
505            // Burn tokens
506            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            // Test name()
531            let name_call = ITIP20::nameCall {};
532            let calldata = name_call.abi_encode();
533            let result = token.call(&calldata, caller)?;
534            // HashMapStorageProvider does not do gas accounting, so we expect 0 here.
535            assert!(result.status.is_success());
536            let name = String::abi_decode(&result.bytes)?;
537            assert_eq!(name, "Test Token");
538
539            // Test symbol()
540            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            // Test decimals()
548            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            // Test currency()
556            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            // Test totalSupply()
564            let total_supply_call = ITIP20::totalSupplyCall {};
565            let calldata = total_supply_call.abi_encode();
566            let result = token.call(&calldata, caller)?;
567            // HashMapStorageProvider does not do gas accounting, so we expect 0 here.
568            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            // Initialize TIP403 registry
704            let mut registry = TIP403Registry::new();
705            registry.initialize()?;
706
707            // Create a valid policy
708            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            // Create another valid policy for the unauthorized test
725            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        // Use T5 hardfork so all selectors are active.
776        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        // Pre-T5: logoURI/setLogoURI should return unknown selector.
802        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            // logoURI selector is gated
809            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            // setLogoURI selector is gated
815            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        // Activate T5; the token deployed under T4 is now read under T5.
837        storage.set_spec(TempoHardfork::T5);
838
839        StorageCtx::enter(&mut storage, || {
840            let mut token = TIP20Token::from_address(token_address)?;
841
842            // Direct accessor: empty by default for pre-T5-deployed tokens.
843            assert_eq!(token.logo_uri()?, "");
844
845            // ABI-level: the previously-gated selector now dispatches and returns "".
846            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        // Pre-T2: permit/nonces/DOMAIN_SEPARATOR should return unknown selector
859        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            // Test permit selector is gated
866            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            // Test nonces selector is gated
881            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            // Test DOMAIN_SEPARATOR selector is gated
890            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}