Role-Based Access Control (RBAC)

An access control pattern where permissions are assigned to roles, and roles are assigned to addresses, enabling granular and flexible authorization.

Role-Based Access Control (RBAC) is an advanced access control pattern that assigns permissions to roles rather than directly to addresses. Multiple addresses can share a role, and addresses can hold multiple roles. This pattern provides more flexibility and security than simple Ownable patterns, especially for complex protocols requiring different permission levels for various administrative functions.

OpenZeppelin AccessControl

The standard implementation in Solidity:

1import "@openzeppelin/contracts/access/AccessControl.sol";
2
3contract Protocol is AccessControl {
4 bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
5 bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
6 bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
7
8 constructor() {
9 // Deployer gets admin role
10 _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
11 _grantRole(ADMIN_ROLE, msg.sender);
12 }
13
14 function pause() external onlyRole(PAUSER_ROLE) {
15 // Only addresses with PAUSER_ROLE
16 }
17
18 function mint(address to, uint256 amount) external onlyRole(MINTER_ROLE) {
19 // Only addresses with MINTER_ROLE
20 }
21
22 function setParameter(uint256 value) external onlyRole(ADMIN_ROLE) {
23 // Only addresses with ADMIN_ROLE
24 }
25}

Role Hierarchy

Roles can have admin roles that control them:

1constructor() {
2 // DEFAULT_ADMIN_ROLE can grant/revoke any role by default
3 _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
4
5 // Set ADMIN_ROLE as the admin for MINTER_ROLE
6 _setRoleAdmin(MINTER_ROLE, ADMIN_ROLE);
7
8 // Now ADMIN_ROLE holders can grant/revoke MINTER_ROLE
9}
1DEFAULT_ADMIN_ROLE
2
3
4 ADMIN_ROLE ───────► manages MINTER_ROLE, PAUSER_ROLE
5
6
7 OPERATOR_ROLE ───► manages day-to-day operations

RBAC vs Ownable

AspectOwnableRBAC
PermissionsAll or nothingGranular by role
Addresses per role1 (owner)Multiple
Roles per address1Multiple
Single point of failureYesReduced
ComplexityLowMedium
Key compromise impactTotalLimited to role

Common Role Patterns

DeFi Protocol

1bytes32 public constant GUARDIAN_ROLE = keccak256("GUARDIAN_ROLE"); // Emergency pause
2bytes32 public constant STRATEGIST_ROLE = keccak256("STRATEGIST_ROLE"); // Strategy management
3bytes32 public constant HARVESTER_ROLE = keccak256("HARVESTER_ROLE"); // Yield harvesting
4bytes32 public constant GOVERNANCE_ROLE = keccak256("GOVERNANCE_ROLE"); // Parameter changes

Token Contract

1bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
2bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");
3bytes32 public constant BLACKLISTER_ROLE = keccak256("BLACKLISTER_ROLE");

Upgradeable Contract

1bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE");
2bytes32 public constant PROPOSER_ROLE = keccak256("PROPOSER_ROLE");
3bytes32 public constant EXECUTOR_ROLE = keccak256("EXECUTOR_ROLE");

Granting and Revoking Roles

1// Grant a role
2function grantMinter(address account) external onlyRole(ADMIN_ROLE) {
3 grantRole(MINTER_ROLE, account);
4}
5
6// Revoke a role
7function revokeMinter(address account) external onlyRole(ADMIN_ROLE) {
8 revokeRole(MINTER_ROLE, account);
9}
10
11// Renounce own role (can't renounce others' roles)
12function renounceMinting() external {
13 renounceRole(MINTER_ROLE, msg.sender);
14}

Security Considerations

Role Admin Misconfiguration

1// DANGEROUS: Role is its own admin - holders can grant to anyone
2_setRoleAdmin(MINTER_ROLE, MINTER_ROLE);
3
4// CORRECT: Separate admin role controls minter role
5_setRoleAdmin(MINTER_ROLE, ADMIN_ROLE);

Over-Privileged Roles

1// BAD: Single role with too many permissions
2function doEverything() external onlyRole(ADMIN_ROLE) {
3 // pause, upgrade, mint, change params...
4}
5
6// GOOD: Separate roles for different actions
7function pause() external onlyRole(PAUSER_ROLE) { }
8function upgrade() external onlyRole(UPGRADER_ROLE) { }
9function mint() external onlyRole(MINTER_ROLE) { }

Missing Role Checks

1// VULNERABLE: No access control!
2function withdrawFees() external {
3 payable(msg.sender).transfer(address(this).balance);
4}
5
6// SECURE: Role check added
7function withdrawFees() external onlyRole(TREASURY_ROLE) {
8 payable(treasury).transfer(address(this).balance);
9}

Combining with Timelock

For critical roles, require a time delay:

1import "@openzeppelin/contracts/governance/TimelockController.sol";
2
3// Set timelock as the only holder of UPGRADER_ROLE
4// All upgrades must go through timelock delay
5grantRole(UPGRADER_ROLE, address(timelock));

AccessControlEnumerable

Track all role holders:

1import "@openzeppelin/contracts/access/AccessControlEnumerable.sol";
2
3contract Protocol is AccessControlEnumerable {
4 function getAllMinters() external view returns (address[] memory) {
5 uint256 count = getRoleMemberCount(MINTER_ROLE);
6 address[] memory minters = new address[](count);
7 for (uint256 i = 0; i < count; i++) {
8 minters[i] = getRoleMember(MINTER_ROLE, i);
9 }
10 return minters;
11 }
12}

Audit Checklist

When auditing RBAC implementations:

  • All sensitive functions have appropriate role checks
  • Role hierarchy properly configured
  • DEFAULT_ADMIN_ROLE securely managed (multisig)
  • No roles that are their own admin (unless intended)
  • Critical roles use timelock
  • Role assignment/revocation emits events
  • No way to lock out all admins accidentally
  • Principle of least privilege followed

Role-Based Access Control provides the flexibility and security needed for production DeFi protocols. Proper role design, combined with timelocks and multi-signature requirements, significantly reduces the risk and impact of key compromises.

Need expert guidance on Role-Based Access Control (RBAC)?

Our team at Zealynx has deep expertise in blockchain security and DeFi protocols. Whether you need an audit or consultation, we're here to help.

Get a Quote

oog
zealynx

Subscribe to Our Newsletter

Stay updated with our latest security insights and blog posts

© 2024 Zealynx