tempo_precompiles/tip20/
dispatch.rs

1use super::ITIP20;
2use crate::{
3    Precompile, fill_precompile_output, input_cost, metadata, mutate, mutate_void,
4    storage::Handler,
5    tip20::{IRolesAuth, TIP20Token},
6    unknown_selector, view,
7};
8use alloy::{primitives::Address, sol_types::SolCall};
9use revm::precompile::{PrecompileError, PrecompileResult};
10
11impl Precompile for TIP20Token {
12    fn call(&mut self, calldata: &[u8], msg_sender: Address) -> PrecompileResult {
13        self.storage
14            .deduct_gas(input_cost(calldata.len()))
15            .map_err(|_| PrecompileError::OutOfGas)?;
16
17        let selector: [u8; 4] = calldata
18            .get(..4)
19            .ok_or_else(|| {
20                PrecompileError::Other("Invalid input: missing function selector".into())
21            })?
22            .try_into()
23            .unwrap();
24
25        let result = match selector {
26            // Metadata
27            ITIP20::nameCall::SELECTOR => metadata::<ITIP20::nameCall>(|| self.name()),
28            ITIP20::symbolCall::SELECTOR => metadata::<ITIP20::symbolCall>(|| self.symbol()),
29            ITIP20::decimalsCall::SELECTOR => metadata::<ITIP20::decimalsCall>(|| self.decimals()),
30            ITIP20::currencyCall::SELECTOR => metadata::<ITIP20::currencyCall>(|| self.currency()),
31            ITIP20::totalSupplyCall::SELECTOR => {
32                metadata::<ITIP20::totalSupplyCall>(|| self.total_supply())
33            }
34            ITIP20::supplyCapCall::SELECTOR => {
35                metadata::<ITIP20::supplyCapCall>(|| self.supply_cap())
36            }
37            ITIP20::transferPolicyIdCall::SELECTOR => {
38                metadata::<ITIP20::transferPolicyIdCall>(|| self.transfer_policy_id())
39            }
40            ITIP20::pausedCall::SELECTOR => metadata::<ITIP20::pausedCall>(|| self.paused()),
41
42            // View functions
43            ITIP20::balanceOfCall::SELECTOR => {
44                view::<ITIP20::balanceOfCall>(calldata, |call| self.balance_of(call))
45            }
46            ITIP20::allowanceCall::SELECTOR => {
47                view::<ITIP20::allowanceCall>(calldata, |call| self.allowance(call))
48            }
49            ITIP20::quoteTokenCall::SELECTOR => {
50                view::<ITIP20::quoteTokenCall>(calldata, |_| self.quote_token())
51            }
52            ITIP20::nextQuoteTokenCall::SELECTOR => {
53                view::<ITIP20::nextQuoteTokenCall>(calldata, |_| self.next_quote_token())
54            }
55            ITIP20::PAUSE_ROLECall::SELECTOR => {
56                view::<ITIP20::PAUSE_ROLECall>(calldata, |_| Ok(Self::pause_role()))
57            }
58            ITIP20::UNPAUSE_ROLECall::SELECTOR => {
59                view::<ITIP20::UNPAUSE_ROLECall>(calldata, |_| Ok(Self::unpause_role()))
60            }
61            ITIP20::ISSUER_ROLECall::SELECTOR => {
62                view::<ITIP20::ISSUER_ROLECall>(calldata, |_| Ok(Self::issuer_role()))
63            }
64            ITIP20::BURN_BLOCKED_ROLECall::SELECTOR => {
65                view::<ITIP20::BURN_BLOCKED_ROLECall>(calldata, |_| Ok(Self::burn_blocked_role()))
66            }
67
68            // State changing functions
69            ITIP20::transferFromCall::SELECTOR => {
70                mutate::<ITIP20::transferFromCall>(calldata, msg_sender, |s, call| {
71                    self.transfer_from(s, call)
72                })
73            }
74            ITIP20::transferCall::SELECTOR => {
75                mutate::<ITIP20::transferCall>(calldata, msg_sender, |s, call| {
76                    self.transfer(s, call)
77                })
78            }
79            ITIP20::approveCall::SELECTOR => {
80                mutate::<ITIP20::approveCall>(calldata, msg_sender, |s, call| self.approve(s, call))
81            }
82            ITIP20::changeTransferPolicyIdCall::SELECTOR => {
83                mutate_void::<ITIP20::changeTransferPolicyIdCall>(
84                    calldata,
85                    msg_sender,
86                    |s, call| self.change_transfer_policy_id(s, call),
87                )
88            }
89            ITIP20::setSupplyCapCall::SELECTOR => {
90                mutate_void::<ITIP20::setSupplyCapCall>(calldata, msg_sender, |s, call| {
91                    self.set_supply_cap(s, call)
92                })
93            }
94            ITIP20::pauseCall::SELECTOR => {
95                mutate_void::<ITIP20::pauseCall>(calldata, msg_sender, |s, call| {
96                    self.pause(s, call)
97                })
98            }
99            ITIP20::unpauseCall::SELECTOR => {
100                mutate_void::<ITIP20::unpauseCall>(calldata, msg_sender, |s, call| {
101                    self.unpause(s, call)
102                })
103            }
104            ITIP20::setNextQuoteTokenCall::SELECTOR => {
105                mutate_void::<ITIP20::setNextQuoteTokenCall>(calldata, msg_sender, |s, call| {
106                    self.set_next_quote_token(s, call)
107                })
108            }
109            ITIP20::completeQuoteTokenUpdateCall::SELECTOR => {
110                mutate_void::<ITIP20::completeQuoteTokenUpdateCall>(
111                    calldata,
112                    msg_sender,
113                    |s, call| self.complete_quote_token_update(s, call),
114                )
115            }
116
117            ITIP20::feeRecipientCall::SELECTOR => {
118                if !self.storage.spec().is_allegretto() {
119                    return unknown_selector(
120                        selector,
121                        self.storage.gas_used(),
122                        self.storage.spec(),
123                    );
124                }
125                view::<ITIP20::feeRecipientCall>(calldata, |_call| self.fee_recipient.read())
126            }
127            ITIP20::setFeeRecipientCall::SELECTOR => {
128                if !self.storage.spec().is_allegretto() {
129                    return unknown_selector(
130                        selector,
131                        self.storage.gas_used(),
132                        self.storage.spec(),
133                    );
134                }
135                mutate_void::<ITIP20::setFeeRecipientCall>(calldata, msg_sender, |s, call| {
136                    self.set_fee_recipient(s, call.newRecipient)
137                })
138            }
139
140            ITIP20::mintCall::SELECTOR => {
141                mutate_void::<ITIP20::mintCall>(calldata, msg_sender, |s, call| self.mint(s, call))
142            }
143            ITIP20::mintWithMemoCall::SELECTOR => {
144                mutate_void::<ITIP20::mintWithMemoCall>(calldata, msg_sender, |s, call| {
145                    self.mint_with_memo(s, call)
146                })
147            }
148            ITIP20::burnCall::SELECTOR => {
149                mutate_void::<ITIP20::burnCall>(calldata, msg_sender, |s, call| self.burn(s, call))
150            }
151            ITIP20::burnWithMemoCall::SELECTOR => {
152                mutate_void::<ITIP20::burnWithMemoCall>(calldata, msg_sender, |s, call| {
153                    self.burn_with_memo(s, call)
154                })
155            }
156            ITIP20::burnBlockedCall::SELECTOR => {
157                mutate_void::<ITIP20::burnBlockedCall>(calldata, msg_sender, |s, call| {
158                    self.burn_blocked(s, call)
159                })
160            }
161            ITIP20::transferWithMemoCall::SELECTOR => {
162                mutate_void::<ITIP20::transferWithMemoCall>(calldata, msg_sender, |s, call| {
163                    self.transfer_with_memo(s, call)
164                })
165            }
166            ITIP20::transferFromWithMemoCall::SELECTOR => {
167                mutate::<ITIP20::transferFromWithMemoCall>(calldata, msg_sender, |sender, call| {
168                    self.transfer_from_with_memo(sender, call)
169                })
170            }
171            ITIP20::startRewardCall::SELECTOR => {
172                mutate::<ITIP20::startRewardCall>(calldata, msg_sender, |s, call| {
173                    self.start_reward(s, call)
174                })
175            }
176            ITIP20::setRewardRecipientCall::SELECTOR => {
177                mutate_void::<ITIP20::setRewardRecipientCall>(calldata, msg_sender, |s, call| {
178                    self.set_reward_recipient(s, call)
179                })
180            }
181            ITIP20::cancelRewardCall::SELECTOR => {
182                mutate::<ITIP20::cancelRewardCall>(calldata, msg_sender, |s, call| {
183                    self.cancel_reward(s, call)
184                })
185            }
186            ITIP20::claimRewardsCall::SELECTOR => {
187                mutate::<ITIP20::claimRewardsCall>(calldata, msg_sender, |_, _| {
188                    self.claim_rewards(msg_sender)
189                })
190            }
191
192            ITIP20::finalizeStreamsCall::SELECTOR => {
193                mutate_void::<ITIP20::finalizeStreamsCall>(calldata, msg_sender, |sender, call| {
194                    self.finalize_streams(sender, call.timestamp as u128)
195                })
196            }
197
198            ITIP20::totalRewardPerSecondCall::SELECTOR => {
199                view::<ITIP20::totalRewardPerSecondCall>(calldata, |_call| {
200                    self.get_total_reward_per_second()
201                })
202            }
203
204            ITIP20::optedInSupplyCall::SELECTOR => {
205                view::<ITIP20::optedInSupplyCall>(calldata, |_call| self.get_opted_in_supply())
206            }
207
208            ITIP20::getStreamCall::SELECTOR => view::<ITIP20::getStreamCall>(calldata, |call| {
209                self.get_stream(call.id).map(|stream| stream.into())
210            }),
211
212            ITIP20::nextStreamIdCall::SELECTOR => {
213                view::<ITIP20::nextStreamIdCall>(calldata, |_call| self.get_next_stream_id())
214            }
215
216            ITIP20::userRewardInfoCall::SELECTOR => {
217                view::<ITIP20::userRewardInfoCall>(calldata, |call| {
218                    self.get_user_reward_info(call.account)
219                        .map(|info| info.into())
220                })
221            }
222
223            // RolesAuth functions
224            IRolesAuth::hasRoleCall::SELECTOR => {
225                view::<IRolesAuth::hasRoleCall>(calldata, |call| self.has_role(call))
226            }
227            IRolesAuth::getRoleAdminCall::SELECTOR => {
228                view::<IRolesAuth::getRoleAdminCall>(calldata, |call| self.get_role_admin(call))
229            }
230            IRolesAuth::grantRoleCall::SELECTOR => {
231                mutate_void::<IRolesAuth::grantRoleCall>(calldata, msg_sender, |s, call| {
232                    self.grant_role(s, call)
233                })
234            }
235            IRolesAuth::revokeRoleCall::SELECTOR => {
236                mutate_void::<IRolesAuth::revokeRoleCall>(calldata, msg_sender, |s, call| {
237                    self.revoke_role(s, call)
238                })
239            }
240            IRolesAuth::renounceRoleCall::SELECTOR => {
241                mutate_void::<IRolesAuth::renounceRoleCall>(calldata, msg_sender, |s, call| {
242                    self.renounce_role(s, call)
243                })
244            }
245            IRolesAuth::setRoleAdminCall::SELECTOR => {
246                mutate_void::<IRolesAuth::setRoleAdminCall>(calldata, msg_sender, |s, call| {
247                    self.set_role_admin(s, call)
248                })
249            }
250
251            _ => unknown_selector(selector, self.storage.gas_used(), self.storage.spec()),
252        };
253
254        result.map(|res| fill_precompile_output(res, &mut self.storage))
255    }
256}
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261    use crate::{
262        PATH_USD_ADDRESS,
263        storage::{StorageCtx, hashmap::HashMapStorageProvider},
264        test_util::setup_storage,
265        tip20::{ISSUER_ROLE, PAUSE_ROLE, TIP20Token, UNPAUSE_ROLE, tests::initialize_path_usd},
266    };
267    use alloy::{
268        primitives::{Bytes, U256},
269        sol_types::{SolInterface, SolValue},
270    };
271    use tempo_chainspec::hardfork::TempoHardfork;
272    use tempo_contracts::precompiles::{RolesAuthError, TIP20Error};
273
274    #[test]
275    fn test_function_selector_dispatch() -> eyre::Result<()> {
276        let (mut storage, sender) = setup_storage();
277        storage.set_spec(TempoHardfork::Moderato);
278        let token_id = 1;
279
280        StorageCtx::enter(&mut storage, || {
281            initialize_path_usd(sender)?;
282            let mut token = TIP20Token::new(token_id);
283            token.initialize(
284                "Test",
285                "TST",
286                "USD",
287                PATH_USD_ADDRESS,
288                sender,
289                Address::ZERO,
290            )?;
291
292            // Test invalid selector - should return Ok with reverted status
293            let result = token.call(&Bytes::from([0x12, 0x34, 0x56, 0x78]), sender)?;
294            assert!(result.reverted);
295
296            // Test insufficient calldata
297            let result = token.call(&Bytes::from([0x12, 0x34]), sender);
298            assert!(matches!(result, Err(PrecompileError::Other(_))));
299
300            Ok(())
301        })
302    }
303
304    #[test]
305    fn test_balance_of_calldata_handling() -> eyre::Result<()> {
306        let (mut storage, admin) = setup_storage();
307        let sender = Address::random();
308        let account = Address::random();
309        let token_id = 1;
310
311        StorageCtx::enter(&mut storage, || {
312            initialize_path_usd(admin)?;
313
314            let mut token = TIP20Token::new(token_id);
315            token.initialize("Test", "TST", "USD", PATH_USD_ADDRESS, admin, Address::ZERO)?;
316            token.grant_role_internal(admin, *ISSUER_ROLE)?;
317
318            let test_balance = U256::from(1000);
319            token.mint(
320                admin,
321                ITIP20::mintCall {
322                    to: account,
323                    amount: test_balance,
324                },
325            )?;
326
327            let balance_of_call = ITIP20::balanceOfCall { account };
328            let calldata = balance_of_call.abi_encode();
329
330            let result = token.call(&calldata, sender)?;
331            assert_eq!(result.gas_used, 0);
332
333            let decoded = U256::abi_decode(&result.bytes)?;
334            assert_eq!(decoded, test_balance);
335
336            Ok(())
337        })
338    }
339
340    #[test]
341    fn test_mint_updates_storage() -> eyre::Result<()> {
342        let (mut storage, admin) = setup_storage();
343        let sender = Address::random();
344        let recipient = Address::random();
345        let token_id = 1;
346
347        StorageCtx::enter(&mut storage, || {
348            initialize_path_usd(admin)?;
349
350            let mut token = TIP20Token::new(token_id);
351            token.initialize("Test", "TST", "USD", PATH_USD_ADDRESS, admin, Address::ZERO)?;
352            token.grant_role_internal(sender, *ISSUER_ROLE)?;
353
354            let initial_balance = token.balance_of(ITIP20::balanceOfCall { account: recipient })?;
355            assert_eq!(initial_balance, U256::ZERO);
356
357            let mint_amount = U256::random().min(U256::from(u128::MAX)) % token.supply_cap()?;
358            let mint_call = ITIP20::mintCall {
359                to: recipient,
360                amount: mint_amount,
361            };
362            let calldata = mint_call.abi_encode();
363
364            let result = token.call(&calldata, sender)?;
365            assert_eq!(result.gas_used, 0);
366
367            let final_balance = token.balance_of(ITIP20::balanceOfCall { account: recipient })?;
368            assert_eq!(final_balance, mint_amount);
369
370            Ok(())
371        })
372    }
373
374    #[test]
375    fn test_transfer_updates_balances() -> eyre::Result<()> {
376        let (mut storage, admin) = setup_storage();
377        let sender = Address::random();
378        let recipient = Address::random();
379        let transfer_amount = U256::from(300);
380        let initial_sender_balance = U256::from(1000);
381
382        StorageCtx::enter(&mut storage, || {
383            initialize_path_usd(admin)?;
384            let mut token = TIP20Token::new(1);
385            token.initialize("Test", "TST", "USD", PATH_USD_ADDRESS, admin, Address::ZERO)?;
386            token.grant_role_internal(admin, *ISSUER_ROLE)?;
387            token.mint(
388                admin,
389                ITIP20::mintCall {
390                    to: sender,
391                    amount: initial_sender_balance,
392                },
393            )?;
394
395            assert_eq!(
396                token.balance_of(ITIP20::balanceOfCall { account: sender })?,
397                initial_sender_balance
398            );
399            assert_eq!(
400                token.balance_of(ITIP20::balanceOfCall { account: recipient })?,
401                U256::ZERO
402            );
403
404            let transfer_call = ITIP20::transferCall {
405                to: recipient,
406                amount: transfer_amount,
407            };
408            let calldata = transfer_call.abi_encode();
409            let result = token.call(&calldata, sender)?;
410            assert_eq!(result.gas_used, 0);
411
412            let success = bool::abi_decode(&result.bytes)?;
413            assert!(success);
414
415            let final_sender_balance =
416                token.balance_of(ITIP20::balanceOfCall { account: sender })?;
417            let final_recipient_balance =
418                token.balance_of(ITIP20::balanceOfCall { account: recipient })?;
419
420            assert_eq!(
421                final_sender_balance,
422                initial_sender_balance - transfer_amount
423            );
424            assert_eq!(final_recipient_balance, transfer_amount);
425
426            Ok(())
427        })
428    }
429
430    #[test]
431    fn test_approve_and_transfer_from() -> eyre::Result<()> {
432        let (mut storage, admin) = setup_storage();
433        let owner = Address::random();
434        let spender = Address::random();
435        let recipient = Address::random();
436        let approve_amount = U256::from(500);
437        let transfer_amount = U256::from(300);
438        let initial_owner_balance = U256::from(1000);
439
440        StorageCtx::enter(&mut storage, || {
441            initialize_path_usd(admin)?;
442            let mut token = TIP20Token::new(1);
443            token.initialize("Test", "TST", "USD", PATH_USD_ADDRESS, admin, Address::ZERO)?;
444            token.grant_role_internal(admin, *ISSUER_ROLE)?;
445            token.mint(
446                admin,
447                ITIP20::mintCall {
448                    to: owner,
449                    amount: initial_owner_balance,
450                },
451            )?;
452
453            let approve_call = ITIP20::approveCall {
454                spender,
455                amount: approve_amount,
456            };
457            let calldata = approve_call.abi_encode();
458            let result = token.call(&calldata, owner)?;
459            assert_eq!(result.gas_used, 0);
460            let success = bool::abi_decode(&result.bytes)?;
461            assert!(success);
462
463            let allowance = token.allowance(ITIP20::allowanceCall { owner, spender })?;
464            assert_eq!(allowance, approve_amount);
465
466            let transfer_from_call = ITIP20::transferFromCall {
467                from: owner,
468                to: recipient,
469                amount: transfer_amount,
470            };
471            let calldata = transfer_from_call.abi_encode();
472            let result = token.call(&calldata, spender)?;
473            assert_eq!(result.gas_used, 0);
474            let success = bool::abi_decode(&result.bytes)?;
475            assert!(success);
476
477            // Verify balances
478            assert_eq!(
479                token.balance_of(ITIP20::balanceOfCall { account: owner })?,
480                initial_owner_balance - transfer_amount
481            );
482            assert_eq!(
483                token.balance_of(ITIP20::balanceOfCall { account: recipient })?,
484                transfer_amount
485            );
486
487            // Verify allowance was reduced
488            let remaining_allowance = token.allowance(ITIP20::allowanceCall { owner, spender })?;
489            assert_eq!(remaining_allowance, approve_amount - transfer_amount);
490
491            Ok(())
492        })
493    }
494
495    #[test]
496    fn test_pause_and_unpause() -> eyre::Result<()> {
497        let (mut storage, admin) = setup_storage();
498        let pauser = Address::random();
499        let unpauser = Address::random();
500
501        StorageCtx::enter(&mut storage, || {
502            initialize_path_usd(admin)?;
503            let mut token = TIP20Token::new(1);
504            token.initialize("Test", "TST", "USD", PATH_USD_ADDRESS, admin, Address::ZERO)?;
505            token.grant_role_internal(pauser, *PAUSE_ROLE)?;
506            token.grant_role_internal(unpauser, *UNPAUSE_ROLE)?;
507            assert!(!token.paused()?);
508
509            // Pause the token
510            let pause_call = ITIP20::pauseCall {};
511            let calldata = pause_call.abi_encode();
512            let result = token.call(&calldata, pauser)?;
513            assert_eq!(result.gas_used, 0);
514            assert!(token.paused()?);
515
516            // Unpause the token
517            let unpause_call = ITIP20::unpauseCall {};
518            let calldata = unpause_call.abi_encode();
519            let result = token.call(&calldata, unpauser)?;
520            assert_eq!(result.gas_used, 0);
521            assert!(!token.paused()?);
522
523            Ok(())
524        })
525    }
526
527    #[test]
528    fn test_burn_functionality() -> eyre::Result<()> {
529        let (mut storage, admin) = setup_storage();
530        let burner = Address::random();
531        let initial_balance = U256::from(1000);
532        let burn_amount = U256::from(300);
533
534        StorageCtx::enter(&mut storage, || {
535            initialize_path_usd(admin)?;
536            let mut token = TIP20Token::new(1);
537            token.initialize("Test", "TST", "USD", PATH_USD_ADDRESS, admin, Address::ZERO)?;
538            token.grant_role_internal(admin, *ISSUER_ROLE)?;
539            token.grant_role_internal(burner, *ISSUER_ROLE)?;
540
541            // Mint initial balance to burner
542            token.mint(
543                admin,
544                ITIP20::mintCall {
545                    to: burner,
546                    amount: initial_balance,
547                },
548            )?;
549
550            // Check initial state
551            assert_eq!(
552                token.balance_of(ITIP20::balanceOfCall { account: burner })?,
553                initial_balance
554            );
555            assert_eq!(token.total_supply()?, initial_balance);
556
557            // Burn tokens
558            let burn_call = ITIP20::burnCall {
559                amount: burn_amount,
560            };
561            let calldata = burn_call.abi_encode();
562            let result = token.call(&calldata, burner)?;
563            assert_eq!(result.gas_used, 0);
564            assert_eq!(
565                token.balance_of(ITIP20::balanceOfCall { account: burner })?,
566                initial_balance - burn_amount
567            );
568            assert_eq!(token.total_supply()?, initial_balance - burn_amount);
569
570            Ok(())
571        })
572    }
573
574    #[test]
575    fn test_metadata_functions() -> eyre::Result<()> {
576        let (mut storage, admin) = setup_storage();
577        let caller = Address::random();
578
579        StorageCtx::enter(&mut storage, || {
580            initialize_path_usd(admin)?;
581            let mut token = TIP20Token::new(1);
582            token.initialize(
583                "Test Token",
584                "TEST",
585                "USD",
586                PATH_USD_ADDRESS,
587                admin,
588                Address::ZERO,
589            )?;
590
591            // Test name()
592            let name_call = ITIP20::nameCall {};
593            let calldata = name_call.abi_encode();
594            let result = token.call(&calldata, caller)?;
595            // HashMapStorageProvider does not do gas accounting, so we expect 0 here.
596            assert_eq!(result.gas_used, 0);
597            let name = String::abi_decode(&result.bytes)?;
598            assert_eq!(name, "Test Token");
599
600            // Test symbol()
601            let symbol_call = ITIP20::symbolCall {};
602            let calldata = symbol_call.abi_encode();
603            let result = token.call(&calldata, caller)?;
604            assert_eq!(result.gas_used, 0);
605            let symbol = String::abi_decode(&result.bytes)?;
606            assert_eq!(symbol, "TEST");
607
608            // Test decimals()
609            let decimals_call = ITIP20::decimalsCall {};
610            let calldata = decimals_call.abi_encode();
611            let result = token.call(&calldata, caller)?;
612            assert_eq!(result.gas_used, 0);
613            let decimals = ITIP20::decimalsCall::abi_decode_returns(&result.bytes)?;
614            assert_eq!(decimals, 6);
615
616            // Test currency()
617            let currency_call = ITIP20::currencyCall {};
618            let calldata = currency_call.abi_encode();
619            let result = token.call(&calldata, caller)?;
620            assert_eq!(result.gas_used, 0);
621            let currency = String::abi_decode(&result.bytes)?;
622            assert_eq!(currency, "USD");
623
624            // Test totalSupply()
625            let total_supply_call = ITIP20::totalSupplyCall {};
626            let calldata = total_supply_call.abi_encode();
627            let result = token.call(&calldata, caller)?;
628            // HashMapStorageProvider does not do gas accounting, so we expect 0 here.
629            assert_eq!(result.gas_used, 0);
630            let total_supply = U256::abi_decode(&result.bytes)?;
631            assert_eq!(total_supply, U256::ZERO);
632
633            Ok(())
634        })
635    }
636
637    #[test]
638    fn test_supply_cap_enforcement() -> eyre::Result<()> {
639        let (mut storage, admin) = setup_storage();
640        let recipient = Address::random();
641        let supply_cap = U256::from(1000);
642        let mint_amount = U256::from(1001);
643
644        StorageCtx::enter(&mut storage, || {
645            initialize_path_usd(admin)?;
646            let mut token = TIP20Token::new(1);
647            token.initialize("Test", "TST", "USD", PATH_USD_ADDRESS, admin, Address::ZERO)?;
648            token.grant_role_internal(admin, *ISSUER_ROLE)?;
649
650            let set_cap_call = ITIP20::setSupplyCapCall {
651                newSupplyCap: supply_cap,
652            };
653            let calldata = set_cap_call.abi_encode();
654            let result = token.call(&calldata, admin)?;
655            assert_eq!(result.gas_used, 0);
656
657            let mint_call = ITIP20::mintCall {
658                to: recipient,
659                amount: mint_amount,
660            };
661            let calldata = mint_call.abi_encode();
662            let output = token.call(&calldata, admin)?;
663            assert!(output.reverted);
664
665            let expected: Bytes = TIP20Error::supply_cap_exceeded().selector().into();
666            assert_eq!(output.bytes, expected);
667
668            Ok(())
669        })
670    }
671
672    #[test]
673    fn test_role_based_access_control() -> eyre::Result<()> {
674        let (mut storage, admin) = setup_storage();
675        let user1 = Address::random();
676        let user2 = Address::random();
677        let unauthorized = Address::random();
678
679        StorageCtx::enter(&mut storage, || {
680            initialize_path_usd(admin)?;
681            let mut token = TIP20Token::new(1);
682            token.initialize("Test", "TST", "USD", PATH_USD_ADDRESS, admin, Address::ZERO)?;
683
684            token.grant_role_internal(user1, *ISSUER_ROLE)?;
685            let has_role_call = IRolesAuth::hasRoleCall {
686                role: *ISSUER_ROLE,
687                account: user1,
688            };
689            let calldata = has_role_call.abi_encode();
690            let result = token.call(&calldata, admin)?;
691            assert_eq!(result.gas_used, 0);
692            let has_role = bool::abi_decode(&result.bytes)?;
693            assert!(has_role);
694
695            let has_role_call = IRolesAuth::hasRoleCall {
696                role: *ISSUER_ROLE,
697                account: user2,
698            };
699            let calldata = has_role_call.abi_encode();
700            let result = token.call(&calldata, admin)?;
701            let has_role = bool::abi_decode(&result.bytes)?;
702            assert!(!has_role);
703
704            let mint_call = ITIP20::mintCall {
705                to: user2,
706                amount: U256::from(100),
707            };
708            let calldata = mint_call.abi_encode();
709            let output = token.call(&Bytes::from(calldata.clone()), unauthorized)?;
710            assert!(output.reverted);
711            let expected: Bytes = RolesAuthError::unauthorized().selector().into();
712            assert_eq!(output.bytes, expected);
713
714            let result = token.call(&calldata, user1)?;
715            assert_eq!(result.gas_used, 0);
716
717            Ok(())
718        })
719    }
720
721    #[test]
722    fn test_transfer_with_memo() -> eyre::Result<()> {
723        let (mut storage, admin) = setup_storage();
724        let sender = Address::random();
725        let recipient = Address::random();
726        let transfer_amount = U256::from(100);
727        let initial_balance = U256::from(500);
728
729        StorageCtx::enter(&mut storage, || {
730            initialize_path_usd(admin)?;
731            let mut token = TIP20Token::new(1);
732            token.initialize("Test", "TST", "USD", PATH_USD_ADDRESS, admin, Address::ZERO)?;
733            token.grant_role_internal(admin, *ISSUER_ROLE)?;
734            token.mint(
735                admin,
736                ITIP20::mintCall {
737                    to: sender,
738                    amount: initial_balance,
739                },
740            )?;
741
742            let memo = alloy::primitives::B256::from([1u8; 32]);
743            let transfer_call = ITIP20::transferWithMemoCall {
744                to: recipient,
745                amount: transfer_amount,
746                memo,
747            };
748            let calldata = transfer_call.abi_encode();
749            let result = token.call(&calldata, sender)?;
750            assert_eq!(result.gas_used, 0);
751            assert_eq!(
752                token.balance_of(ITIP20::balanceOfCall { account: sender })?,
753                initial_balance - transfer_amount
754            );
755            assert_eq!(
756                token.balance_of(ITIP20::balanceOfCall { account: recipient })?,
757                transfer_amount
758            );
759
760            Ok(())
761        })
762    }
763
764    #[test]
765    fn test_change_transfer_policy_id() -> eyre::Result<()> {
766        let (mut storage, admin) = setup_storage();
767        let non_admin = Address::random();
768        let new_policy_id = 42u64;
769
770        StorageCtx::enter(&mut storage, || {
771            initialize_path_usd(admin)?;
772            let mut token = TIP20Token::new(1);
773            token.initialize("Test", "TST", "USD", PATH_USD_ADDRESS, admin, Address::ZERO)?;
774
775            let change_policy_call = ITIP20::changeTransferPolicyIdCall {
776                newPolicyId: new_policy_id,
777            };
778            let calldata = change_policy_call.abi_encode();
779            let result = token.call(&calldata, admin)?;
780            assert_eq!(result.gas_used, 0);
781            assert_eq!(token.transfer_policy_id()?, new_policy_id);
782
783            let change_policy_call = ITIP20::changeTransferPolicyIdCall { newPolicyId: 100 };
784            let calldata = change_policy_call.abi_encode();
785            let output = token.call(&calldata, non_admin)?;
786            assert!(output.reverted);
787            let expected: Bytes = RolesAuthError::unauthorized().selector().into();
788            assert_eq!(output.bytes, expected);
789
790            Ok(())
791        })
792    }
793
794    #[test]
795    fn tip20_test_selector_coverage() {
796        use crate::test_util::{assert_full_coverage, check_selector_coverage};
797        use tempo_contracts::precompiles::{IRolesAuth::IRolesAuthCalls, ITIP20::ITIP20Calls};
798
799        let (mut storage, admin) = setup_storage();
800
801        StorageCtx::enter(&mut storage, || {
802            initialize_path_usd(admin).unwrap();
803            let mut token = TIP20Token::new(1);
804            token
805                .initialize("Test", "TST", "USD", PATH_USD_ADDRESS, admin, Address::ZERO)
806                .unwrap();
807
808            let itip20_unsupported =
809                check_selector_coverage(&mut token, ITIP20Calls::SELECTORS, "ITIP20", |s| {
810                    ITIP20Calls::name_by_selector(s)
811                });
812
813            let roles_unsupported = check_selector_coverage(
814                &mut token,
815                IRolesAuthCalls::SELECTORS,
816                "IRolesAuth",
817                IRolesAuthCalls::name_by_selector,
818            );
819
820            assert_full_coverage([itip20_unsupported, roles_unsupported]);
821        })
822    }
823
824    #[test]
825    fn test_fee_recipient_pre_allegretto() -> eyre::Result<()> {
826        let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Adagio);
827        let admin = Address::random();
828
829        StorageCtx::enter(&mut storage, || {
830            initialize_path_usd(admin)?;
831            let mut token = TIP20Token::new(1);
832            token.initialize(
833                "Test",
834                "TST",
835                "USD",
836                PATH_USD_ADDRESS,
837                admin,
838                Address::from([0x11; 20]),
839            )?;
840
841            let call = ITIP20::feeRecipientCall {};
842            let calldata = call.abi_encode();
843            let result = token.call(&Bytes::from(calldata), admin);
844
845            assert!(matches!(
846                result,
847                Err(revm::precompile::PrecompileError::Other(ref msg)) if msg.contains("Unknown function selector")
848            ));
849
850            Ok(())
851        })
852    }
853
854    #[test]
855    fn test_fee_recipient_post_allegretto() -> eyre::Result<()> {
856        let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Allegretto);
857        let admin = Address::random();
858        let fee_recipient = Address::random();
859
860        StorageCtx::enter(&mut storage, || {
861            initialize_path_usd(admin)?;
862            let mut token = TIP20Token::new(1);
863            token.initialize("Test", "TST", "USD", PATH_USD_ADDRESS, admin, fee_recipient)?;
864
865            let call = ITIP20::feeRecipientCall {};
866            let calldata = call.abi_encode();
867            let result = token.call(&Bytes::from(calldata), admin)?;
868
869            assert!(!result.reverted);
870            let recipient = ITIP20::feeRecipientCall::abi_decode_returns(&result.bytes)?;
871            assert_eq!(recipient, fee_recipient);
872            Ok(())
873        })
874    }
875
876    #[test]
877    fn test_set_fee_recipient_pre_allegretto() -> eyre::Result<()> {
878        let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Adagio);
879        let admin = Address::random();
880
881        StorageCtx::enter(&mut storage, || {
882            initialize_path_usd(admin)?;
883            let mut token = TIP20Token::new(1);
884            token.initialize("Test", "TST", "USD", PATH_USD_ADDRESS, admin, admin)?;
885
886            let call = ITIP20::setFeeRecipientCall {
887                newRecipient: Address::from([0x33; 20]),
888            };
889            let calldata = call.abi_encode();
890            let result = token.call(&Bytes::from(calldata), admin);
891
892            assert!(matches!(
893                result,
894                Err(revm::precompile::PrecompileError::Other(ref msg)) if msg.contains("Unknown function selector")
895            ));
896
897            Ok(())
898        })
899    }
900
901    #[test]
902    fn test_set_fee_recipient_post_allegretto() -> eyre::Result<()> {
903        let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Allegretto);
904        let admin = Address::random();
905        let new_recipient = Address::random();
906
907        StorageCtx::enter(&mut storage, || {
908            initialize_path_usd(admin)?;
909            let mut token = TIP20Token::new(1);
910            token.initialize("Test", "TST", "USD", PATH_USD_ADDRESS, admin, admin)?;
911
912            let call = ITIP20::setFeeRecipientCall {
913                newRecipient: new_recipient,
914            };
915            let calldata = call.abi_encode();
916            let result = token.call(&Bytes::from(calldata), admin)?;
917
918            assert!(!result.reverted);
919
920            let call = ITIP20::feeRecipientCall {};
921            let calldata = call.abi_encode();
922            let result = token.call(&Bytes::from(calldata), admin)?;
923
924            let recipient = ITIP20::feeRecipientCall::abi_decode_returns(&result.bytes)?;
925            assert_eq!(recipient, new_recipient);
926
927            Ok(())
928        })
929    }
930}