Skip to main content

tempo_precompiles/tip403_registry/
dispatch.rs

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