Skip to main content

tempo_precompiles/tip403_registry/
dispatch.rs

1//! ABI dispatch for the [`TIP403Registry`] precompile.
2
3use crate::{
4    Precompile, dispatch_call, input_cost, mutate, mutate_void,
5    tip403_registry::{AuthRole, TIP403Registry},
6    unknown_selector, view,
7};
8use alloy::{
9    primitives::Address,
10    sol_types::{SolCall, SolInterface},
11};
12use revm::precompile::{PrecompileError, PrecompileResult};
13use tempo_contracts::precompiles::ITIP403Registry::{
14    ITIP403RegistryCalls, compoundPolicyDataCall, createCompoundPolicyCall,
15    isAuthorizedMintRecipientCall, isAuthorizedRecipientCall, isAuthorizedSenderCall,
16};
17
18impl Precompile for TIP403Registry {
19    fn call(&mut self, calldata: &[u8], msg_sender: Address) -> PrecompileResult {
20        self.storage
21            .deduct_gas(input_cost(calldata.len()))
22            .map_err(|_| PrecompileError::OutOfGas)?;
23
24        dispatch_call(
25            calldata,
26            ITIP403RegistryCalls::abi_decode,
27            |call| match call {
28                ITIP403RegistryCalls::policyIdCounter(call) => {
29                    view(call, |_| self.policy_id_counter())
30                }
31                ITIP403RegistryCalls::policyExists(call) => view(call, |c| self.policy_exists(c)),
32                ITIP403RegistryCalls::policyData(call) => view(call, |c| self.policy_data(c)),
33                ITIP403RegistryCalls::isAuthorized(call) => view(call, |c| {
34                    self.is_authorized_as(c.policyId, c.user, AuthRole::Transfer)
35                }),
36                // TIP-1015: T2+ only
37                ITIP403RegistryCalls::isAuthorizedSender(call) => {
38                    if !self.storage.spec().is_t2() {
39                        return unknown_selector(
40                            isAuthorizedSenderCall::SELECTOR,
41                            self.storage.gas_used(),
42                        );
43                    }
44                    view(call, |c| {
45                        self.is_authorized_as(c.policyId, c.user, AuthRole::Sender)
46                    })
47                }
48                ITIP403RegistryCalls::isAuthorizedRecipient(call) => {
49                    if !self.storage.spec().is_t2() {
50                        return unknown_selector(
51                            isAuthorizedRecipientCall::SELECTOR,
52                            self.storage.gas_used(),
53                        );
54                    }
55                    view(call, |c| {
56                        self.is_authorized_as(c.policyId, c.user, AuthRole::Recipient)
57                    })
58                }
59                ITIP403RegistryCalls::isAuthorizedMintRecipient(call) => {
60                    if !self.storage.spec().is_t2() {
61                        return unknown_selector(
62                            isAuthorizedMintRecipientCall::SELECTOR,
63                            self.storage.gas_used(),
64                        );
65                    }
66                    view(call, |c| {
67                        self.is_authorized_as(c.policyId, c.user, AuthRole::MintRecipient)
68                    })
69                }
70                ITIP403RegistryCalls::compoundPolicyData(call) => {
71                    if !self.storage.spec().is_t2() {
72                        return unknown_selector(
73                            compoundPolicyDataCall::SELECTOR,
74                            self.storage.gas_used(),
75                        );
76                    }
77                    view(call, |c| self.compound_policy_data(c))
78                }
79                ITIP403RegistryCalls::createPolicy(call) => {
80                    mutate(call, msg_sender, |s, c| self.create_policy(s, c))
81                }
82                ITIP403RegistryCalls::createPolicyWithAccounts(call) => {
83                    mutate(call, msg_sender, |s, c| {
84                        self.create_policy_with_accounts(s, c)
85                    })
86                }
87                ITIP403RegistryCalls::setPolicyAdmin(call) => {
88                    mutate_void(call, msg_sender, |s, c| self.set_policy_admin(s, c))
89                }
90                ITIP403RegistryCalls::modifyPolicyWhitelist(call) => {
91                    mutate_void(call, msg_sender, |s, c| self.modify_policy_whitelist(s, c))
92                }
93                ITIP403RegistryCalls::modifyPolicyBlacklist(call) => {
94                    mutate_void(call, msg_sender, |s, c| self.modify_policy_blacklist(s, c))
95                }
96                // TIP-1015: T2+ only
97                ITIP403RegistryCalls::createCompoundPolicy(call) => {
98                    if !self.storage.spec().is_t2() {
99                        return unknown_selector(
100                            createCompoundPolicyCall::SELECTOR,
101                            self.storage.gas_used(),
102                        );
103                    }
104                    mutate(call, msg_sender, |s, c| self.create_compound_policy(s, c))
105                }
106            },
107        )
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114    use crate::{
115        storage::{StorageCtx, hashmap::HashMapStorageProvider},
116        test_util::{assert_full_coverage, check_selector_coverage},
117        tip403_registry::ITIP403Registry,
118    };
119    use alloy::sol_types::{SolCall, SolValue};
120    use tempo_chainspec::hardfork::TempoHardfork;
121    use tempo_contracts::precompiles::ITIP403Registry::ITIP403RegistryCalls;
122
123    #[test]
124    fn test_is_authorized_precompile() -> eyre::Result<()> {
125        let mut storage = HashMapStorageProvider::new(1);
126        let user = Address::random();
127        StorageCtx::enter(&mut storage, || {
128            let mut registry = TIP403Registry::new();
129
130            // Test policy 1 (always allow)
131            let call = ITIP403Registry::isAuthorizedCall { policyId: 1, user };
132            let calldata = call.abi_encode();
133            let result = registry.call(&calldata, Address::ZERO);
134
135            assert!(result.is_ok());
136            let output = result.unwrap();
137            let decoded: bool =
138                ITIP403Registry::isAuthorizedCall::abi_decode_returns(&output.bytes).unwrap();
139            assert!(decoded);
140
141            Ok(())
142        })
143    }
144
145    #[test]
146    fn test_create_policy_precompile() -> eyre::Result<()> {
147        let mut storage = HashMapStorageProvider::new(1);
148        let admin = Address::random();
149        StorageCtx::enter(&mut storage, || {
150            let mut registry = TIP403Registry::new();
151
152            let call = ITIP403Registry::createPolicyCall {
153                admin,
154                policyType: ITIP403Registry::PolicyType::WHITELIST,
155            };
156            let calldata = call.abi_encode();
157            let result = registry.call(&calldata, admin);
158
159            assert!(result.is_ok());
160            let output = result.unwrap();
161            let decoded: u64 =
162                ITIP403Registry::createPolicyCall::abi_decode_returns(&output.bytes).unwrap();
163            assert_eq!(decoded, 2); // First created policy ID
164
165            Ok(())
166        })
167    }
168
169    #[test]
170    fn test_policy_id_counter_initialization() -> eyre::Result<()> {
171        let mut storage = HashMapStorageProvider::new(1);
172        let sender = Address::random();
173        StorageCtx::enter(&mut storage, || {
174            let mut registry = TIP403Registry::new();
175
176            // Get initial counter
177            let counter_call = ITIP403Registry::policyIdCounterCall {};
178            let calldata = counter_call.abi_encode();
179            let result = registry.call(&calldata, sender).unwrap();
180            let counter = u64::abi_decode(&result.bytes).unwrap();
181            assert_eq!(counter, 2); // Counter starts at 2 (policies 0 and 1 are reserved)
182
183            Ok(())
184        })
185    }
186
187    #[test]
188    fn test_create_policy_with_accounts() -> eyre::Result<()> {
189        let mut storage = HashMapStorageProvider::new(1);
190        let admin = Address::random();
191        let account1 = Address::random();
192        let account2 = Address::random();
193        let other_account = Address::random();
194        StorageCtx::enter(&mut storage, || {
195            let mut registry = TIP403Registry::new();
196
197            let accounts = vec![account1, account2];
198            let call = ITIP403Registry::createPolicyWithAccountsCall {
199                admin,
200                policyType: ITIP403Registry::PolicyType::WHITELIST,
201                accounts,
202            };
203            let calldata = call.abi_encode();
204            let result = registry.call(&calldata, admin).unwrap();
205
206            let policy_id: u64 =
207                ITIP403Registry::createPolicyWithAccountsCall::abi_decode_returns(&result.bytes)
208                    .unwrap();
209            assert_eq!(policy_id, 2);
210
211            // Check that accounts are authorized
212            let is_auth_call = ITIP403Registry::isAuthorizedCall {
213                policyId: policy_id,
214                user: account1,
215            };
216            let calldata = is_auth_call.abi_encode();
217            let result = registry.call(&calldata, admin).unwrap();
218            let is_authorized = bool::abi_decode(&result.bytes).unwrap();
219            assert!(is_authorized);
220
221            let is_auth_call = ITIP403Registry::isAuthorizedCall {
222                policyId: policy_id,
223                user: account2,
224            };
225            let calldata = is_auth_call.abi_encode();
226            let result = registry.call(&calldata, admin).unwrap();
227            let is_authorized = bool::abi_decode(&result.bytes).unwrap();
228            assert!(is_authorized);
229
230            // Check that other accounts are not authorized
231            let is_auth_call = ITIP403Registry::isAuthorizedCall {
232                policyId: policy_id,
233                user: other_account,
234            };
235            let calldata = is_auth_call.abi_encode();
236            let result = registry.call(&calldata, admin).unwrap();
237            let is_authorized = bool::abi_decode(&result.bytes).unwrap();
238            assert!(!is_authorized);
239
240            Ok(())
241        })
242    }
243
244    #[test]
245    fn test_blacklist_policy() -> eyre::Result<()> {
246        let mut storage = HashMapStorageProvider::new(1);
247        let admin = Address::random();
248        let blocked_account = Address::random();
249        let allowed_account = Address::random();
250        StorageCtx::enter(&mut storage, || {
251            let mut registry = TIP403Registry::new();
252
253            // Create blacklist policy
254            let call = ITIP403Registry::createPolicyCall {
255                admin,
256                policyType: ITIP403Registry::PolicyType::BLACKLIST,
257            };
258            let calldata = call.abi_encode();
259            let result = registry.call(&calldata, admin).unwrap();
260            let policy_id: u64 =
261                ITIP403Registry::createPolicyCall::abi_decode_returns(&result.bytes).unwrap();
262
263            // Initially, all accounts should be authorized (empty blacklist)
264            let is_auth_call = ITIP403Registry::isAuthorizedCall {
265                policyId: policy_id,
266                user: blocked_account,
267            };
268            let calldata = is_auth_call.abi_encode();
269            let result = registry.call(&calldata, admin).unwrap();
270            let is_authorized = bool::abi_decode(&result.bytes).unwrap();
271            assert!(is_authorized);
272
273            // Add account to blacklist
274            let modify_call = ITIP403Registry::modifyPolicyBlacklistCall {
275                policyId: policy_id,
276                account: blocked_account,
277                restricted: true,
278            };
279            let calldata = modify_call.abi_encode();
280            registry.call(&calldata, admin).unwrap();
281
282            // Now blocked account should not be authorized
283            let is_auth_call = ITIP403Registry::isAuthorizedCall {
284                policyId: policy_id,
285                user: blocked_account,
286            };
287            let calldata = is_auth_call.abi_encode();
288            let result = registry.call(&calldata, admin).unwrap();
289            let is_authorized = bool::abi_decode(&result.bytes).unwrap();
290            assert!(!is_authorized);
291
292            // Other accounts should still be authorized
293            let is_auth_call = ITIP403Registry::isAuthorizedCall {
294                policyId: policy_id,
295                user: allowed_account,
296            };
297            let calldata = is_auth_call.abi_encode();
298            let result = registry.call(&calldata, admin).unwrap();
299            let is_authorized = bool::abi_decode(&result.bytes).unwrap();
300            assert!(is_authorized);
301
302            // Remove account from blacklist
303            let modify_call = ITIP403Registry::modifyPolicyBlacklistCall {
304                policyId: policy_id,
305                account: blocked_account,
306                restricted: false,
307            };
308            let calldata = modify_call.abi_encode();
309            registry.call(&calldata, admin).unwrap();
310
311            // Account should be authorized again
312            let is_auth_call = ITIP403Registry::isAuthorizedCall {
313                policyId: policy_id,
314                user: blocked_account,
315            };
316            let calldata = is_auth_call.abi_encode();
317            let result = registry.call(&calldata, admin).unwrap();
318            let is_authorized = bool::abi_decode(&result.bytes).unwrap();
319            assert!(is_authorized);
320
321            Ok(())
322        })
323    }
324
325    #[test]
326    fn test_modify_policy_whitelist() -> eyre::Result<()> {
327        let mut storage = HashMapStorageProvider::new(1);
328        let admin = Address::random();
329        let account1 = Address::random();
330        let account2 = Address::random();
331        StorageCtx::enter(&mut storage, || {
332            let mut registry = TIP403Registry::new();
333
334            // Create whitelist policy
335            let call = ITIP403Registry::createPolicyCall {
336                admin,
337                policyType: ITIP403Registry::PolicyType::WHITELIST,
338            };
339            let calldata = call.abi_encode();
340            let result = registry.call(&calldata, admin).unwrap();
341            let policy_id: u64 =
342                ITIP403Registry::createPolicyCall::abi_decode_returns(&result.bytes).unwrap();
343
344            // Add multiple accounts to whitelist
345            let modify_call1 = ITIP403Registry::modifyPolicyWhitelistCall {
346                policyId: policy_id,
347                account: account1,
348                allowed: true,
349            };
350            let calldata = modify_call1.abi_encode();
351            registry.call(&calldata, admin).unwrap();
352
353            let modify_call2 = ITIP403Registry::modifyPolicyWhitelistCall {
354                policyId: policy_id,
355                account: account2,
356                allowed: true,
357            };
358            let calldata = modify_call2.abi_encode();
359            registry.call(&calldata, admin).unwrap();
360
361            // Both accounts should be authorized
362            let is_auth_call = ITIP403Registry::isAuthorizedCall {
363                policyId: policy_id,
364                user: account1,
365            };
366            let calldata = is_auth_call.abi_encode();
367            let result = registry.call(&calldata, admin).unwrap();
368            let is_authorized = bool::abi_decode(&result.bytes).unwrap();
369            assert!(is_authorized);
370
371            let is_auth_call = ITIP403Registry::isAuthorizedCall {
372                policyId: policy_id,
373                user: account2,
374            };
375            let calldata = is_auth_call.abi_encode();
376            let result = registry.call(&calldata, admin).unwrap();
377            let is_authorized = bool::abi_decode(&result.bytes).unwrap();
378            assert!(is_authorized);
379
380            // Remove one account from whitelist
381            let modify_call = ITIP403Registry::modifyPolicyWhitelistCall {
382                policyId: policy_id,
383                account: account1,
384                allowed: false,
385            };
386            let calldata = modify_call.abi_encode();
387            registry.call(&calldata, admin).unwrap();
388
389            // Account1 should not be authorized, account2 should still be
390            let is_auth_call = ITIP403Registry::isAuthorizedCall {
391                policyId: policy_id,
392                user: account1,
393            };
394            let calldata = is_auth_call.abi_encode();
395            let result = registry.call(&calldata, admin).unwrap();
396            let is_authorized = bool::abi_decode(&result.bytes).unwrap();
397            assert!(!is_authorized);
398
399            let is_auth_call = ITIP403Registry::isAuthorizedCall {
400                policyId: policy_id,
401                user: account2,
402            };
403            let calldata = is_auth_call.abi_encode();
404            let result = registry.call(&calldata, admin).unwrap();
405            let is_authorized = bool::abi_decode(&result.bytes).unwrap();
406            assert!(is_authorized);
407
408            Ok(())
409        })
410    }
411
412    #[test]
413    fn test_set_policy_admin() -> eyre::Result<()> {
414        let mut storage = HashMapStorageProvider::new(1);
415        let admin = Address::random();
416        let new_admin = Address::random();
417        StorageCtx::enter(&mut storage, || {
418            let mut registry = TIP403Registry::new();
419
420            // Create a policy
421            let call = ITIP403Registry::createPolicyCall {
422                admin,
423                policyType: ITIP403Registry::PolicyType::WHITELIST,
424            };
425            let calldata = call.abi_encode();
426            let result = registry.call(&calldata, admin).unwrap();
427            let policy_id: u64 =
428                ITIP403Registry::createPolicyCall::abi_decode_returns(&result.bytes).unwrap();
429
430            // Get initial policy data
431            let policy_data_call = ITIP403Registry::policyDataCall {
432                policyId: policy_id,
433            };
434            let calldata = policy_data_call.abi_encode();
435            let result = registry.call(&calldata, admin).unwrap();
436            let policy_data =
437                ITIP403Registry::policyDataCall::abi_decode_returns(&result.bytes).unwrap();
438            assert_eq!(policy_data.admin, admin);
439
440            // Change policy admin
441            let set_admin_call = ITIP403Registry::setPolicyAdminCall {
442                policyId: policy_id,
443                admin: new_admin,
444            };
445            let calldata = set_admin_call.abi_encode();
446            registry.call(&calldata, admin).unwrap();
447
448            // Verify policy admin was changed
449            let policy_data_call = ITIP403Registry::policyDataCall {
450                policyId: policy_id,
451            };
452            let calldata = policy_data_call.abi_encode();
453            let result = registry.call(&calldata, admin).unwrap();
454            let policy_data =
455                ITIP403Registry::policyDataCall::abi_decode_returns(&result.bytes).unwrap();
456            assert_eq!(policy_data.admin, new_admin);
457
458            Ok(())
459        })
460    }
461
462    #[test]
463    fn test_special_policy_ids() -> eyre::Result<()> {
464        let mut storage = HashMapStorageProvider::new(1);
465        let user = Address::random();
466        StorageCtx::enter(&mut storage, || {
467            let mut registry = TIP403Registry::new();
468
469            // Test policy 0 (always deny)
470            let is_auth_call = ITIP403Registry::isAuthorizedCall { policyId: 0, user };
471            let calldata = is_auth_call.abi_encode();
472            let result = registry.call(&calldata, Address::ZERO).unwrap();
473            let is_authorized = bool::abi_decode(&result.bytes).unwrap();
474            assert!(!is_authorized);
475
476            // Test policy 1 (always allow)
477            let is_auth_call = ITIP403Registry::isAuthorizedCall { policyId: 1, user };
478            let calldata = is_auth_call.abi_encode();
479            let result = registry.call(&calldata, Address::ZERO).unwrap();
480            let is_authorized = bool::abi_decode(&result.bytes).unwrap();
481            assert!(is_authorized);
482
483            Ok(())
484        })
485    }
486
487    #[test]
488    fn test_invalid_selector() -> eyre::Result<()> {
489        let sender = Address::random();
490
491        // T1: invalid selector returns reverted output
492        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T1);
493        StorageCtx::enter(&mut storage, || -> eyre::Result<()> {
494            let mut registry = TIP403Registry::new();
495
496            let invalid_data = vec![0x12, 0x34, 0x56, 0x78];
497            let result = registry.call(&invalid_data, sender)?;
498            assert!(result.reverted);
499
500            // T1: insufficient data also returns reverted output
501            let short_data = vec![0x12, 0x34];
502            let result = registry.call(&short_data, sender)?;
503            assert!(result.reverted);
504
505            Ok(())
506        })?;
507
508        // Pre-T1 (T0): insufficient data returns error
509        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T0);
510        StorageCtx::enter(&mut storage, || {
511            let mut registry = TIP403Registry::new();
512
513            let short_data = vec![0x12, 0x34];
514            let result = registry.call(&short_data, sender);
515            assert!(result.is_err());
516
517            Ok(())
518        })
519    }
520
521    #[test]
522    fn test_create_multiple_policies() -> eyre::Result<()> {
523        let mut storage = HashMapStorageProvider::new(1);
524        let admin = Address::random();
525        StorageCtx::enter(&mut storage, || {
526            let mut registry = TIP403Registry::new();
527
528            // Create multiple policies with different types
529            let whitelist_call = ITIP403Registry::createPolicyCall {
530                admin,
531                policyType: ITIP403Registry::PolicyType::WHITELIST,
532            };
533            let calldata = whitelist_call.abi_encode();
534            let result = registry.call(&calldata, admin).unwrap();
535            let whitelist_id: u64 =
536                ITIP403Registry::createPolicyCall::abi_decode_returns(&result.bytes).unwrap();
537
538            let blacklist_call = ITIP403Registry::createPolicyCall {
539                admin,
540                policyType: ITIP403Registry::PolicyType::BLACKLIST,
541            };
542            let calldata = blacklist_call.abi_encode();
543            let result = registry.call(&calldata, admin).unwrap();
544            let blacklist_id: u64 =
545                ITIP403Registry::createPolicyCall::abi_decode_returns(&result.bytes).unwrap();
546
547            // Verify IDs are sequential
548            assert_eq!(whitelist_id, 2);
549            assert_eq!(blacklist_id, 3);
550
551            // Verify counter has been updated
552            let counter_call = ITIP403Registry::policyIdCounterCall {};
553            let calldata = counter_call.abi_encode();
554            let result = registry.call(&calldata, admin).unwrap();
555            let counter = u64::abi_decode(&result.bytes).unwrap();
556            assert_eq!(counter, 4);
557
558            Ok(())
559        })
560    }
561
562    #[test]
563    fn test_selector_coverage() -> eyre::Result<()> {
564        // Use T2 to test all selectors including TIP-1015 compound policy functions
565        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T2);
566        StorageCtx::enter(&mut storage, || {
567            let mut registry = TIP403Registry::new();
568
569            let unsupported = check_selector_coverage(
570                &mut registry,
571                ITIP403RegistryCalls::SELECTORS,
572                "ITIP403Registry",
573                ITIP403RegistryCalls::name_by_selector,
574            );
575
576            assert_full_coverage([unsupported]);
577
578            Ok(())
579        })
580    }
581}