Skip to main content

tempo_precompiles/tip20/
roles.rs

1//! Role-based [access control] for TIP-20 tokens.
2//!
3//! Implements `AccessControl`: each role has an admin role that can grant/revoke it.
4//! [`DEFAULT_ADMIN_ROLE`] is the root admin; [`UNGRANTABLE_ROLE`] is self-administered
5//! and cannot be granted externally.
6//!
7//! [Access control]: <https://docs.tempo.xyz/protocol/tip20/overview#role-based-access-control-rbac>
8
9use alloy::primitives::{Address, B256};
10
11use crate::{
12    error::Result,
13    storage::Handler,
14    tip20::{IRolesAuth, RolesAuthError, RolesAuthEvent, TIP20Token},
15};
16
17/// The default admin role (zero hash). Holders can grant/revoke any role.
18pub const DEFAULT_ADMIN_ROLE: B256 = B256::ZERO;
19/// A self-administered role that cannot be granted by any admin.
20pub const UNGRANTABLE_ROLE: B256 = B256::new([0xff; 32]);
21
22impl TIP20Token {
23    /// Initializes the roles precompile by setting [`UNGRANTABLE_ROLE`] to be self-administered.
24    pub fn initialize_roles(&mut self) -> Result<()> {
25        self.set_role_admin_internal(UNGRANTABLE_ROLE, UNGRANTABLE_ROLE)
26    }
27
28    /// Grants `DEFAULT_ADMIN_ROLE` to `admin`. Used during token initialization.
29    pub fn grant_default_admin(&mut self, msg_sender: Address, admin: Address) -> Result<()> {
30        self.grant_role_internal(admin, DEFAULT_ADMIN_ROLE)?;
31
32        self.emit_event(RolesAuthEvent::role_membership_updated(
33            DEFAULT_ADMIN_ROLE,
34            admin,
35            msg_sender,
36            true,
37        ))
38    }
39
40    /// Returns whether `account` holds the given `role`.
41    pub fn has_role(&self, call: IRolesAuth::hasRoleCall) -> Result<bool> {
42        self.has_role_internal(call.account, call.role)
43    }
44
45    /// Returns the admin role that governs `role`.
46    pub fn get_role_admin(&self, call: IRolesAuth::getRoleAdminCall) -> Result<B256> {
47        self.get_role_admin_internal(call.role)
48    }
49
50    /// Grants `role` to `account`.
51    ///
52    /// # Errors
53    /// - `Unauthorized` — caller does not hold the admin role for `role`
54    pub fn grant_role(
55        &mut self,
56        msg_sender: Address,
57        call: IRolesAuth::grantRoleCall,
58    ) -> Result<()> {
59        let admin_role = self.get_role_admin_internal(call.role)?;
60        self.check_role_internal(msg_sender, admin_role)?;
61        self.grant_role_internal(call.account, call.role)?;
62
63        self.emit_event(RolesAuthEvent::role_membership_updated(
64            call.role,
65            call.account,
66            msg_sender,
67            true,
68        ))
69    }
70
71    /// Revokes `role` from `account`.
72    ///
73    /// # Errors
74    /// - `Unauthorized` — caller does not hold the admin role for `role`
75    pub fn revoke_role(
76        &mut self,
77        msg_sender: Address,
78        call: IRolesAuth::revokeRoleCall,
79    ) -> Result<()> {
80        let admin_role = self.get_role_admin_internal(call.role)?;
81        self.check_role_internal(msg_sender, admin_role)?;
82        self.revoke_role_internal(call.account, call.role)?;
83
84        self.emit_event(RolesAuthEvent::role_membership_updated(
85            call.role,
86            call.account,
87            msg_sender,
88            false,
89        ))
90    }
91
92    /// Allows the caller to voluntarily give up their own `role`.
93    ///
94    /// # Errors
95    /// - `Unauthorized` — caller does not hold `role`
96    pub fn renounce_role(
97        &mut self,
98        msg_sender: Address,
99        call: IRolesAuth::renounceRoleCall,
100    ) -> Result<()> {
101        self.check_role_internal(msg_sender, call.role)?;
102        self.revoke_role_internal(msg_sender, call.role)?;
103
104        self.emit_event(RolesAuthEvent::role_membership_updated(
105            call.role, msg_sender, msg_sender, false,
106        ))
107    }
108
109    /// Changes the admin role that governs `role`.
110    ///
111    /// # Errors
112    /// - `Unauthorized` — caller does not hold the current admin role for `role`
113    pub fn set_role_admin(
114        &mut self,
115        msg_sender: Address,
116        call: IRolesAuth::setRoleAdminCall,
117    ) -> Result<()> {
118        let current_admin_role = self.get_role_admin_internal(call.role)?;
119        self.check_role_internal(msg_sender, current_admin_role)?;
120
121        self.set_role_admin_internal(call.role, call.adminRole)?;
122
123        self.emit_event(RolesAuthEvent::role_admin_updated(
124            call.role,
125            call.adminRole,
126            msg_sender,
127        ))
128    }
129
130    /// Reverts if `account` does not hold `role`.
131    ///
132    /// # Errors
133    /// - `Unauthorized` — account does not hold `role`
134    pub fn check_role(&self, account: Address, role: B256) -> Result<()> {
135        self.check_role_internal(account, role)
136    }
137
138    /// Low-level role check without calldata decoding.
139    pub fn has_role_internal(&self, account: Address, role: B256) -> Result<bool> {
140        self.roles[account][role].read()
141    }
142
143    /// Low-level role grant without authorization checks or events.
144    pub fn grant_role_internal(&mut self, account: Address, role: B256) -> Result<()> {
145        self.roles[account][role].write(true)
146    }
147
148    fn revoke_role_internal(&mut self, account: Address, role: B256) -> Result<()> {
149        self.roles[account][role].write(false)
150    }
151
152    /// Returns the admin role for `role`. An unset entry reads as zero, which is `DEFAULT_ADMIN_ROLE`.
153    fn get_role_admin_internal(&self, role: B256) -> Result<B256> {
154        self.role_admins[role].read()
155    }
156
157    fn set_role_admin_internal(&mut self, role: B256, admin_role: B256) -> Result<()> {
158        self.role_admins[role].write(admin_role)
159    }
160
161    fn check_role_internal(&self, account: Address, role: B256) -> Result<()> {
162        if !self.has_role_internal(account, role)? {
163            return Err(RolesAuthError::unauthorized().into());
164        }
165        Ok(())
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use alloy::primitives::keccak256;
172
173    use super::*;
174    use crate::{error::TempoPrecompileError, storage::StorageCtx, test_util::TIP20Setup};
175
176    #[test]
177    fn test_role_contract_grant_and_check() -> eyre::Result<()> {
178        let mut storage = crate::storage::hashmap::HashMapStorageProvider::new(1);
179        let admin = Address::random();
180        let user = Address::random();
181        let custom_role = keccak256(b"CUSTOM_ROLE");
182
183        StorageCtx::enter(&mut storage, || {
184            let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
185
186            // Test hasRole
187            let has_admin = token.has_role(IRolesAuth::hasRoleCall {
188                account: admin,
189                role: DEFAULT_ADMIN_ROLE,
190            })?;
191            assert!(has_admin);
192
193            // Grant custom role
194            token.grant_role(
195                admin,
196                IRolesAuth::grantRoleCall {
197                    role: custom_role,
198                    account: user,
199                },
200            )?;
201
202            // Check custom role
203            let has_custom = token.has_role(IRolesAuth::hasRoleCall {
204                account: user,
205                role: custom_role,
206            })?;
207            assert!(has_custom);
208
209            // Verify events were emitted
210            token.assert_emitted_events(vec![
211                // Event from grant_default_admin during token initialization
212                RolesAuthEvent::role_membership_updated(DEFAULT_ADMIN_ROLE, admin, admin, true),
213                // Event from grant_role call above
214                RolesAuthEvent::role_membership_updated(custom_role, user, admin, true),
215            ]);
216
217            Ok(())
218        })
219    }
220
221    #[test]
222    fn test_role_admin_functions() -> eyre::Result<()> {
223        let mut storage = crate::storage::hashmap::HashMapStorageProvider::new(1);
224        let admin = Address::random();
225        let custom_role = keccak256(b"CUSTOM_ROLE");
226        let admin_role = keccak256(b"ADMIN_ROLE");
227
228        StorageCtx::enter(&mut storage, || {
229            let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
230
231            // Set custom admin for role
232            token.set_role_admin(
233                admin,
234                IRolesAuth::setRoleAdminCall {
235                    role: custom_role,
236                    adminRole: admin_role,
237                },
238            )?;
239
240            // Check role admin
241            let retrieved_admin =
242                token.get_role_admin(IRolesAuth::getRoleAdminCall { role: custom_role })?;
243            assert_eq!(retrieved_admin, admin_role);
244
245            Ok(())
246        })
247    }
248
249    #[test]
250    fn test_renounce_role() -> eyre::Result<()> {
251        let mut storage = crate::storage::hashmap::HashMapStorageProvider::new(1);
252        let admin = Address::random();
253        let user = Address::random();
254        let custom_role = keccak256(b"CUSTOM_ROLE");
255
256        StorageCtx::enter(&mut storage, || {
257            let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
258            token.grant_role_internal(user, custom_role).unwrap();
259
260            // Renounce role
261            token.renounce_role(user, IRolesAuth::renounceRoleCall { role: custom_role })?;
262
263            // Check role is removed
264            assert!(!token.has_role_internal(user, custom_role)?);
265
266            Ok(())
267        })
268    }
269
270    #[test]
271    fn test_unauthorized_access() -> eyre::Result<()> {
272        let mut storage = crate::storage::hashmap::HashMapStorageProvider::new(1);
273        let admin = Address::random();
274        let user = Address::random();
275        let other = Address::random();
276        let custom_role = keccak256(b"CUSTOM_ROLE");
277
278        StorageCtx::enter(&mut storage, || {
279            let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
280
281            // Try to grant role without permission
282            let result = token.grant_role(
283                user,
284                IRolesAuth::grantRoleCall {
285                    role: custom_role,
286                    account: other,
287                },
288            );
289
290            assert!(matches!(
291                result,
292                Err(TempoPrecompileError::RolesAuthError(
293                    RolesAuthError::Unauthorized(IRolesAuth::Unauthorized {})
294                ))
295            ));
296
297            Ok(())
298        })
299    }
300}