tempo_precompiles/tip403_registry/
mod.rs

1pub mod dispatch;
2
3pub use tempo_contracts::precompiles::{ITIP403Registry, TIP403RegistryError, TIP403RegistryEvent};
4use tempo_precompiles_macros::{Storable, contract};
5
6use crate::{
7    TIP403_REGISTRY_ADDRESS,
8    error::{Result, TempoPrecompileError},
9    storage::{Mapping, PrecompileStorageProvider},
10};
11use alloy::primitives::{Address, Bytes, IntoLogData};
12use revm::state::Bytecode;
13
14#[contract]
15pub struct TIP403Registry {
16    policy_id_counter: u64,
17    policy_data: Mapping<u64, PolicyData>,
18    policy_set: Mapping<u64, Mapping<Address, bool>>,
19}
20
21#[derive(Debug, Clone, Storable)]
22pub struct PolicyData {
23    // NOTE: enums are defined as u8, and leverage the sol! macro's `TryInto<u8>` impl
24    pub policy_type: u8,
25    pub admin: Address,
26}
27
28impl<'a, S: PrecompileStorageProvider> TIP403Registry<'a, S> {
29    /// Creates an instance of the precompile.
30    ///
31    /// Caution: This does not initialize the account, see [`Self::initialize`].
32    pub fn new(storage: &'a mut S) -> Self {
33        Self::_new(TIP403_REGISTRY_ADDRESS, storage)
34    }
35
36    /// Initializes the registry contract.
37    pub fn initialize(&mut self) -> Result<()> {
38        self.storage.set_code(
39            TIP403_REGISTRY_ADDRESS,
40            Bytecode::new_legacy(Bytes::from_static(&[0xef])),
41        )?;
42
43        Ok(())
44    }
45
46    // View functions
47    pub fn policy_id_counter(&mut self) -> Result<u64> {
48        let counter_val = self.sload_policy_id_counter()?;
49        // Initialize policy ID counter to 2 if it's 0 (skip special policies)
50        Ok(counter_val.max(2))
51    }
52
53    pub fn policy_data(
54        &mut self,
55        call: ITIP403Registry::policyDataCall,
56    ) -> Result<ITIP403Registry::policyDataReturn> {
57        let data = self.get_policy_data(call.policyId)?;
58        Ok(ITIP403Registry::policyDataReturn {
59            policyType: data
60                .policy_type
61                .try_into()
62                .map_err(|_| TempoPrecompileError::under_overflow())?,
63            admin: data.admin,
64        })
65    }
66
67    pub fn is_authorized(&mut self, call: ITIP403Registry::isAuthorizedCall) -> Result<bool> {
68        self.is_authorized_internal(call.policyId, call.user)
69    }
70
71    // State-changing functions
72    pub fn create_policy(
73        &mut self,
74        msg_sender: Address,
75        call: ITIP403Registry::createPolicyCall,
76    ) -> Result<u64> {
77        let new_policy_id = self.policy_id_counter()?;
78
79        // Increment counter
80        self.sstore_policy_id_counter(
81            new_policy_id
82                .checked_add(1)
83                .ok_or(TempoPrecompileError::under_overflow())?,
84        )?;
85
86        // Store policy data
87        self.sstore_policy_data(
88            new_policy_id,
89            PolicyData {
90                policy_type: call.policyType as u8,
91                admin: call.admin,
92            },
93        )?;
94
95        // Emit events
96        self.storage.emit_event(
97            TIP403_REGISTRY_ADDRESS,
98            TIP403RegistryEvent::PolicyCreated(ITIP403Registry::PolicyCreated {
99                policyId: new_policy_id,
100                updater: msg_sender,
101                policyType: call.policyType,
102            })
103            .into_log_data(),
104        )?;
105
106        self.storage.emit_event(
107            TIP403_REGISTRY_ADDRESS,
108            TIP403RegistryEvent::PolicyAdminUpdated(ITIP403Registry::PolicyAdminUpdated {
109                policyId: new_policy_id,
110                updater: msg_sender,
111                admin: call.admin,
112            })
113            .into_log_data(),
114        )?;
115
116        Ok(new_policy_id)
117    }
118
119    pub fn create_policy_with_accounts(
120        &mut self,
121        msg_sender: Address,
122        call: ITIP403Registry::createPolicyWithAccountsCall,
123    ) -> Result<u64> {
124        let (admin, policy_type) = (call.admin, call.policyType);
125        let new_policy_id = self.policy_id_counter()?;
126
127        // Increment counter
128        self.sstore_policy_id_counter(
129            new_policy_id
130                .checked_add(1)
131                .ok_or(TempoPrecompileError::under_overflow())?,
132        )?;
133
134        // Store policy data
135        self.set_policy_data(
136            new_policy_id,
137            PolicyData {
138                policy_type: policy_type as u8,
139                admin,
140            },
141        )?;
142
143        // Set initial accounts
144        for account in call.accounts.iter() {
145            self.set_policy_set(new_policy_id, *account, true)?;
146
147            match policy_type {
148                ITIP403Registry::PolicyType::WHITELIST => {
149                    self.storage.emit_event(
150                        TIP403_REGISTRY_ADDRESS,
151                        TIP403RegistryEvent::WhitelistUpdated(ITIP403Registry::WhitelistUpdated {
152                            policyId: new_policy_id,
153                            updater: msg_sender,
154                            account: *account,
155                            allowed: true,
156                        })
157                        .into_log_data(),
158                    )?;
159                }
160                ITIP403Registry::PolicyType::BLACKLIST => {
161                    self.storage.emit_event(
162                        TIP403_REGISTRY_ADDRESS,
163                        TIP403RegistryEvent::BlacklistUpdated(ITIP403Registry::BlacklistUpdated {
164                            policyId: new_policy_id,
165                            updater: msg_sender,
166                            account: *account,
167                            restricted: true,
168                        })
169                        .into_log_data(),
170                    )?;
171                }
172                ITIP403Registry::PolicyType::__Invalid => {
173                    return Err(TIP403RegistryError::incompatible_policy_type().into());
174                }
175            }
176        }
177
178        // Emit policy creation events
179        self.storage.emit_event(
180            TIP403_REGISTRY_ADDRESS,
181            TIP403RegistryEvent::PolicyCreated(ITIP403Registry::PolicyCreated {
182                policyId: new_policy_id,
183                updater: msg_sender,
184                policyType: call.policyType,
185            })
186            .into_log_data(),
187        )?;
188
189        self.storage.emit_event(
190            TIP403_REGISTRY_ADDRESS,
191            TIP403RegistryEvent::PolicyAdminUpdated(ITIP403Registry::PolicyAdminUpdated {
192                policyId: new_policy_id,
193                updater: msg_sender,
194                admin,
195            })
196            .into_log_data(),
197        )?;
198
199        Ok(new_policy_id)
200    }
201
202    pub fn set_policy_admin(
203        &mut self,
204        msg_sender: Address,
205        call: ITIP403Registry::setPolicyAdminCall,
206    ) -> Result<()> {
207        let data = self.get_policy_data(call.policyId)?;
208
209        // Check authorization
210        if data.admin != msg_sender {
211            return Err(TIP403RegistryError::unauthorized().into());
212        }
213
214        // Update admin policy ID
215        self.set_policy_data(
216            call.policyId,
217            PolicyData {
218                admin: call.admin,
219                ..data
220            },
221        )?;
222
223        self.storage.emit_event(
224            TIP403_REGISTRY_ADDRESS,
225            TIP403RegistryEvent::PolicyAdminUpdated(ITIP403Registry::PolicyAdminUpdated {
226                policyId: call.policyId,
227                updater: msg_sender,
228                admin: call.admin,
229            })
230            .into_log_data(),
231        )
232    }
233
234    pub fn modify_policy_whitelist(
235        &mut self,
236        msg_sender: Address,
237        call: ITIP403Registry::modifyPolicyWhitelistCall,
238    ) -> Result<()> {
239        let data = self.get_policy_data(call.policyId)?;
240
241        // Check authorization
242        if data.admin != msg_sender {
243            return Err(TIP403RegistryError::unauthorized().into());
244        }
245
246        // Check policy type
247        if data.policy_type != ITIP403Registry::PolicyType::WHITELIST as u8 {
248            return Err(TIP403RegistryError::incompatible_policy_type().into());
249        }
250
251        self.set_policy_set(call.policyId, call.account, call.allowed)?;
252
253        self.storage.emit_event(
254            TIP403_REGISTRY_ADDRESS,
255            TIP403RegistryEvent::WhitelistUpdated(ITIP403Registry::WhitelistUpdated {
256                policyId: call.policyId,
257                updater: msg_sender,
258                account: call.account,
259                allowed: call.allowed,
260            })
261            .into_log_data(),
262        )
263    }
264
265    pub fn modify_policy_blacklist(
266        &mut self,
267        msg_sender: Address,
268        call: ITIP403Registry::modifyPolicyBlacklistCall,
269    ) -> Result<()> {
270        let data = self.get_policy_data(call.policyId)?;
271
272        // Check authorization
273        if data.admin != msg_sender {
274            return Err(TIP403RegistryError::unauthorized().into());
275        }
276
277        // Check policy type
278        if data.policy_type != ITIP403Registry::PolicyType::BLACKLIST as u8 {
279            return Err(TIP403RegistryError::incompatible_policy_type().into());
280        }
281
282        self.set_policy_set(call.policyId, call.account, call.restricted)?;
283
284        self.storage.emit_event(
285            TIP403_REGISTRY_ADDRESS,
286            TIP403RegistryEvent::BlacklistUpdated(ITIP403Registry::BlacklistUpdated {
287                policyId: call.policyId,
288                updater: msg_sender,
289                account: call.account,
290                restricted: call.restricted,
291            })
292            .into_log_data(),
293        )
294    }
295
296    // Internal helper functions
297    fn get_policy_data(&mut self, policy_id: u64) -> Result<PolicyData> {
298        self.sload_policy_data(policy_id)
299    }
300
301    fn set_policy_data(&mut self, policy_id: u64, data: PolicyData) -> Result<()> {
302        self.sstore_policy_data(policy_id, data)
303    }
304
305    fn set_policy_set(&mut self, policy_id: u64, account: Address, value: bool) -> Result<()> {
306        self.sstore_policy_set(policy_id, account, value)
307    }
308
309    // NOTE: must be synced with `fn can_fee_payer_transfer` @crates/revm/src/common.rs
310    fn is_authorized_internal(&mut self, policy_id: u64, user: Address) -> Result<bool> {
311        // Special case for always-allow and always-reject policies
312        if policy_id < 2 {
313            // policyId == 0 is the "always-reject" policy
314            // policyId == 1 is the "always-allow" policy
315            return Ok(policy_id == 1);
316        }
317
318        let data = self.get_policy_data(policy_id)?;
319        let is_in_set = self.sload_policy_set(policy_id, user)?;
320
321        let auth = match data
322            .policy_type
323            .try_into()
324            .map_err(|_| TempoPrecompileError::under_overflow())?
325        {
326            ITIP403Registry::PolicyType::WHITELIST => is_in_set,
327            ITIP403Registry::PolicyType::BLACKLIST => !is_in_set,
328            ITIP403Registry::PolicyType::__Invalid => false,
329        };
330        Ok(auth)
331    }
332}
333
334#[cfg(test)]
335mod tests {
336    use crate::storage::hashmap::HashMapStorageProvider;
337
338    use super::*;
339
340    #[test]
341    fn test_create_policy() -> eyre::Result<()> {
342        let mut storage = HashMapStorageProvider::new(1);
343        let mut registry = TIP403Registry::new(&mut storage);
344        let admin = Address::from([1u8; 20]);
345
346        // Initial counter should be 2 (skipping special policies)
347        assert_eq!(registry.policy_id_counter()?, 2);
348
349        // Create a whitelist policy
350        let result = registry.create_policy(
351            admin,
352            ITIP403Registry::createPolicyCall {
353                admin,
354                policyType: ITIP403Registry::PolicyType::WHITELIST,
355            },
356        );
357        assert!(result.is_ok());
358        assert_eq!(result?, 2);
359
360        // Counter should be incremented
361        assert_eq!(registry.policy_id_counter()?, 3);
362
363        // Check policy data
364        let data = registry.policy_data(ITIP403Registry::policyDataCall { policyId: 2 })?;
365        assert_eq!(data.policyType, ITIP403Registry::PolicyType::WHITELIST);
366        assert_eq!(data.admin, admin);
367        Ok(())
368    }
369
370    #[test]
371    fn test_is_authorized_special_policies() -> eyre::Result<()> {
372        let mut storage = HashMapStorageProvider::new(1);
373        let mut registry = TIP403Registry::new(&mut storage);
374        let user = Address::from([1u8; 20]);
375
376        // Policy 0 should always reject
377        assert!(!registry.is_authorized(ITIP403Registry::isAuthorizedCall { policyId: 0, user })?);
378
379        // Policy 1 should always allow
380        assert!(registry.is_authorized(ITIP403Registry::isAuthorizedCall { policyId: 1, user })?);
381        Ok(())
382    }
383
384    #[test]
385    fn test_whitelist_policy() -> eyre::Result<()> {
386        let mut storage = HashMapStorageProvider::new(1);
387        let mut registry = TIP403Registry::new(&mut storage);
388        let admin = Address::from([1u8; 20]);
389        let user = Address::from([2u8; 20]);
390
391        // Create whitelist policy
392        let policy_id = registry.create_policy(
393            admin,
394            ITIP403Registry::createPolicyCall {
395                admin,
396                policyType: ITIP403Registry::PolicyType::WHITELIST,
397            },
398        )?;
399
400        // User should not be authorized initially
401        assert!(!registry.is_authorized(ITIP403Registry::isAuthorizedCall {
402            policyId: policy_id,
403            user,
404        })?);
405
406        // Add user to whitelist
407        registry.modify_policy_whitelist(
408            admin, // Anyone is authorized with policy 1
409            ITIP403Registry::modifyPolicyWhitelistCall {
410                policyId: policy_id,
411                account: user,
412                allowed: true,
413            },
414        )?;
415
416        // User should now be authorized
417        assert!(registry.is_authorized(ITIP403Registry::isAuthorizedCall {
418            policyId: policy_id,
419            user,
420        })?);
421
422        Ok(())
423    }
424
425    #[test]
426    fn test_blacklist_policy() -> eyre::Result<()> {
427        let mut storage = HashMapStorageProvider::new(1);
428        let mut registry = TIP403Registry::new(&mut storage);
429        let admin = Address::from([1u8; 20]);
430        let user = Address::from([2u8; 20]);
431
432        // Create blacklist policy
433        let policy_id = registry.create_policy(
434            admin,
435            ITIP403Registry::createPolicyCall {
436                admin,
437                policyType: ITIP403Registry::PolicyType::BLACKLIST,
438            },
439        )?;
440
441        // User should be authorized initially (not in blacklist)
442        assert!(registry.is_authorized(ITIP403Registry::isAuthorizedCall {
443            policyId: policy_id,
444            user,
445        })?);
446
447        // Add user to blacklist
448        registry.modify_policy_blacklist(
449            admin,
450            ITIP403Registry::modifyPolicyBlacklistCall {
451                policyId: policy_id,
452                account: user,
453                restricted: true,
454            },
455        )?;
456
457        // User should no longer be authorized
458        assert!(!registry.is_authorized(ITIP403Registry::isAuthorizedCall {
459            policyId: policy_id,
460            user,
461        })?);
462
463        Ok(())
464    }
465}