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