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::RoleMembershipUpdated(
33            IRolesAuth::RoleMembershipUpdated {
34                role: DEFAULT_ADMIN_ROLE,
35                account: admin,
36                sender: msg_sender,
37                hasRole: true,
38            },
39        ))
40    }
41
42    /// Returns whether `account` holds the given `role`.
43    pub fn has_role(&self, call: IRolesAuth::hasRoleCall) -> Result<bool> {
44        self.has_role_internal(call.account, call.role)
45    }
46
47    /// Returns the admin role that governs `role`.
48    pub fn get_role_admin(&self, call: IRolesAuth::getRoleAdminCall) -> Result<B256> {
49        self.get_role_admin_internal(call.role)
50    }
51
52    /// Grants `role` to `account`.
53    ///
54    /// # Errors
55    /// - `Unauthorized` — caller does not hold the admin role for `role`
56    pub fn grant_role(
57        &mut self,
58        msg_sender: Address,
59        call: IRolesAuth::grantRoleCall,
60    ) -> Result<()> {
61        let admin_role = self.get_role_admin_internal(call.role)?;
62        self.check_role_internal(msg_sender, admin_role)?;
63        self.grant_role_internal(call.account, call.role)?;
64
65        self.emit_event(RolesAuthEvent::RoleMembershipUpdated(
66            IRolesAuth::RoleMembershipUpdated {
67                role: call.role,
68                account: call.account,
69                sender: msg_sender,
70                hasRole: true,
71            },
72        ))
73    }
74
75    /// Revokes `role` from `account`.
76    ///
77    /// # Errors
78    /// - `Unauthorized` — caller does not hold the admin role for `role`
79    pub fn revoke_role(
80        &mut self,
81        msg_sender: Address,
82        call: IRolesAuth::revokeRoleCall,
83    ) -> Result<()> {
84        let admin_role = self.get_role_admin_internal(call.role)?;
85        self.check_role_internal(msg_sender, admin_role)?;
86        self.revoke_role_internal(call.account, call.role)?;
87
88        self.emit_event(RolesAuthEvent::RoleMembershipUpdated(
89            IRolesAuth::RoleMembershipUpdated {
90                role: call.role,
91                account: call.account,
92                sender: msg_sender,
93                hasRole: false,
94            },
95        ))
96    }
97
98    /// Allows the caller to voluntarily give up their own `role`.
99    ///
100    /// # Errors
101    /// - `Unauthorized` — caller does not hold `role`
102    pub fn renounce_role(
103        &mut self,
104        msg_sender: Address,
105        call: IRolesAuth::renounceRoleCall,
106    ) -> Result<()> {
107        self.check_role_internal(msg_sender, call.role)?;
108        self.revoke_role_internal(msg_sender, call.role)?;
109
110        self.emit_event(RolesAuthEvent::RoleMembershipUpdated(
111            IRolesAuth::RoleMembershipUpdated {
112                role: call.role,
113                account: msg_sender,
114                sender: msg_sender,
115                hasRole: false,
116            },
117        ))
118    }
119
120    /// Changes the admin role that governs `role`.
121    ///
122    /// # Errors
123    /// - `Unauthorized` — caller does not hold the current admin role for `role`
124    pub fn set_role_admin(
125        &mut self,
126        msg_sender: Address,
127        call: IRolesAuth::setRoleAdminCall,
128    ) -> Result<()> {
129        let current_admin_role = self.get_role_admin_internal(call.role)?;
130        self.check_role_internal(msg_sender, current_admin_role)?;
131
132        self.set_role_admin_internal(call.role, call.adminRole)?;
133
134        self.emit_event(RolesAuthEvent::RoleAdminUpdated(
135            IRolesAuth::RoleAdminUpdated {
136                role: call.role,
137                newAdminRole: call.adminRole,
138                sender: msg_sender,
139            },
140        ))
141    }
142
143    /// Reverts if `account` does not hold `role`.
144    ///
145    /// # Errors
146    /// - `Unauthorized` — account does not hold `role`
147    pub fn check_role(&self, account: Address, role: B256) -> Result<()> {
148        self.check_role_internal(account, role)
149    }
150
151    /// Low-level role check without calldata decoding.
152    pub fn has_role_internal(&self, account: Address, role: B256) -> Result<bool> {
153        self.roles[account][role].read()
154    }
155
156    /// Low-level role grant without authorization checks or events.
157    pub fn grant_role_internal(&mut self, account: Address, role: B256) -> Result<()> {
158        self.roles[account][role].write(true)
159    }
160
161    fn revoke_role_internal(&mut self, account: Address, role: B256) -> Result<()> {
162        self.roles[account][role].write(false)
163    }
164
165    /// If sloads 0, will be equal to DEFAULT_ADMIN_ROLE
166    fn get_role_admin_internal(&self, role: B256) -> Result<B256> {
167        self.role_admins[role].read()
168    }
169
170    fn set_role_admin_internal(&mut self, role: B256, admin_role: B256) -> Result<()> {
171        self.role_admins[role].write(admin_role)
172    }
173
174    fn check_role_internal(&self, account: Address, role: B256) -> Result<()> {
175        if !self.has_role_internal(account, role)? {
176            return Err(RolesAuthError::unauthorized().into());
177        }
178        Ok(())
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use alloy::primitives::keccak256;
185
186    use super::*;
187    use crate::{error::TempoPrecompileError, storage::StorageCtx, test_util::TIP20Setup};
188
189    #[test]
190    fn test_role_contract_grant_and_check() -> eyre::Result<()> {
191        let mut storage = crate::storage::hashmap::HashMapStorageProvider::new(1);
192        let admin = Address::random();
193        let user = Address::random();
194        let custom_role = keccak256(b"CUSTOM_ROLE");
195
196        StorageCtx::enter(&mut storage, || {
197            let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
198
199            // Test hasRole
200            let has_admin = token.has_role(IRolesAuth::hasRoleCall {
201                account: admin,
202                role: DEFAULT_ADMIN_ROLE,
203            })?;
204            assert!(has_admin);
205
206            // Grant custom role
207            token.grant_role(
208                admin,
209                IRolesAuth::grantRoleCall {
210                    role: custom_role,
211                    account: user,
212                },
213            )?;
214
215            // Check custom role
216            let has_custom = token.has_role(IRolesAuth::hasRoleCall {
217                account: user,
218                role: custom_role,
219            })?;
220            assert!(has_custom);
221
222            // Verify events were emitted
223            token.assert_emitted_events(vec![
224                // Event from grant_default_admin during token initialization
225                RolesAuthEvent::RoleMembershipUpdated(IRolesAuth::RoleMembershipUpdated {
226                    role: DEFAULT_ADMIN_ROLE,
227                    account: admin,
228                    sender: admin,
229                    hasRole: true,
230                }),
231                // Event from grant_role call above
232                RolesAuthEvent::RoleMembershipUpdated(IRolesAuth::RoleMembershipUpdated {
233                    role: custom_role,
234                    account: user,
235                    sender: admin,
236                    hasRole: true,
237                }),
238            ]);
239
240            Ok(())
241        })
242    }
243
244    #[test]
245    fn test_role_admin_functions() -> eyre::Result<()> {
246        let mut storage = crate::storage::hashmap::HashMapStorageProvider::new(1);
247        let admin = Address::random();
248        let custom_role = keccak256(b"CUSTOM_ROLE");
249        let admin_role = keccak256(b"ADMIN_ROLE");
250
251        StorageCtx::enter(&mut storage, || {
252            let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
253
254            // Set custom admin for role
255            token.set_role_admin(
256                admin,
257                IRolesAuth::setRoleAdminCall {
258                    role: custom_role,
259                    adminRole: admin_role,
260                },
261            )?;
262
263            // Check role admin
264            let retrieved_admin =
265                token.get_role_admin(IRolesAuth::getRoleAdminCall { role: custom_role })?;
266            assert_eq!(retrieved_admin, admin_role);
267
268            Ok(())
269        })
270    }
271
272    #[test]
273    fn test_renounce_role() -> eyre::Result<()> {
274        let mut storage = crate::storage::hashmap::HashMapStorageProvider::new(1);
275        let admin = Address::random();
276        let user = Address::random();
277        let custom_role = keccak256(b"CUSTOM_ROLE");
278
279        StorageCtx::enter(&mut storage, || {
280            let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
281            token.grant_role_internal(user, custom_role).unwrap();
282
283            // Renounce role
284            token.renounce_role(user, IRolesAuth::renounceRoleCall { role: custom_role })?;
285
286            // Check role is removed
287            assert!(!token.has_role_internal(user, custom_role)?);
288
289            Ok(())
290        })
291    }
292
293    #[test]
294    fn test_unauthorized_access() -> eyre::Result<()> {
295        let mut storage = crate::storage::hashmap::HashMapStorageProvider::new(1);
296        let admin = Address::random();
297        let user = Address::random();
298        let other = Address::random();
299        let custom_role = keccak256(b"CUSTOM_ROLE");
300
301        StorageCtx::enter(&mut storage, || {
302            let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;
303
304            // Try to grant role without permission
305            let result = token.grant_role(
306                user,
307                IRolesAuth::grantRoleCall {
308                    role: custom_role,
309                    account: other,
310                },
311            );
312
313            assert!(matches!(
314                result,
315                Err(TempoPrecompileError::RolesAuthError(
316                    RolesAuthError::Unauthorized(IRolesAuth::Unauthorized {})
317                ))
318            ));
319
320            Ok(())
321        })
322    }
323}