Roles Contract
The Roles contract provides a sophisticated role-based access control system that combines OpenZeppelin's AccessControl and Ownable2Step patterns. It establishes a hierarchical permission system with built-in security protections against accidental privilege loss.
Overview
Contract Address: contracts/src/core/Roles.sol
License: GPL-3.0-only
Solidity Version: 0.8.19
Key Features
- Three-Tier Role Hierarchy: Admin → Manager → Distributor role structure
- Ownership Protection: Prevents accidental ownership renunciation
- Secure Role Transfer: Admin role automatically transfers with ownership
- Access Control: Fine-grained permissions using OpenZeppelin's AccessControl
Role Hierarchy
The contract implements a three-tier hierarchical role system:
ADMIN_ROLE
- Purpose: Highest privilege level, tied to contract ownership
- Permissions: Can grant/revoke Manager roles
- Protection: Cannot be revoked from the contract owner
- Transfer: Automatically transfers during ownership changes
MANAGER_ROLE
- Purpose: Intermediate operational privileges
- Admin: Managed by ADMIN_ROLE holders
- Permissions: Can grant/revoke Distributor roles
- Use Case: Business logic operations requiring elevated permissions
DISTRIBUTOR_ROLE
- Purpose: Specific operational privileges for distribution activities
- Admin: Managed by MANAGER_ROLE holders
- Use Case: Token/asset distribution and related operations
Core Functions
Constructor
constructor()Purpose: Initializes the contract with the deployer as the initial admin.
Actions:
- Grants
ADMIN_ROLEto the contract deployer - Sets
ADMIN_ROLEas the admin role forMANAGER_ROLE - Sets
MANAGER_ROLEas the admin role forDISTRIBUTOR_ROLE - Creates a three-tier hierarchy: Admin → Manager → Distributor
acceptOwnership()
function acceptOwnership() public virtual overridePurpose: Accepts ownership transfer and migrates admin privileges.
Security Features:
- Revokes
ADMIN_ROLEfrom the previous owner - Grants
ADMIN_ROLEto the new owner - Ensures seamless admin privilege transition
Access: Only the pending owner can call this function
renounceOwnership()
function renounceOwnership() public virtual overridePurpose: Prevents ownership renunciation to maintain contract control.
Behavior: Always reverts with TransferOwnershipRequired() error
Security Rationale: Prevents accidental loss of contract control
revokeRole()
function revokeRole(bytes32 role, address account) public virtual overridePurpose: Revokes a role from an account with admin role protection.
Parameters:
role: The role identifier to revokeaccount: The address from which to revoke the role
Protection: Cannot revoke ADMIN_ROLE from the contract owner
Access: Requires appropriate role admin permissions
renounceRole()
function renounceRole(bytes32 role, address account) public virtual overridePurpose: Allows self-renunciation of roles with admin role protection.
Parameters:
role: The role identifier to renounceaccount: The address renouncing the role (must bemsg.sender)
Protection: Owner cannot renounce ADMIN_ROLE
Access: Account holder can renounce their own roles
Security Considerations
Ownership Protection
The contract implements multiple safeguards against accidental privilege loss:
- No Ownership Renunciation:
renounceOwnership()always reverts - Admin Role Protection: Owner cannot lose admin role through revocation or renunciation
- Two-Step Transfer: Uses OpenZeppelin's Ownable2Step for safe ownership transfer
Role Administration
ADMIN_ROLEholders can manageMANAGER_ROLEMANAGER_ROLEholders can manageDISTRIBUTOR_ROLE- Role hierarchy creates clear delegation of authority
- Standard AccessControl permissions apply to all role operations
Error Handling
TransferOwnershipRequired(): Thrown when attempting to renounce ownership or remove admin role from owner
Usage Examples
Deploying the Contract
// Deploy with deployer as initial admin
Roles roles = new Roles();
// Deployer automatically has ADMIN_ROLE
assert(roles.hasRole(Helper.ADMIN_ROLE, deployer));Granting Roles
// Admin grants manager role
roles.grantRole(Helper.MANAGER_ROLE, managerAddress);
// Manager grants distributor role
vm.prank(managerAddress);
roles.grantRole(Helper.DISTRIBUTOR_ROLE, distributorAddress);Transferring Ownership
// Current owner initiates transfer
roles.transferOwnership(newOwner);
// New owner accepts (becomes new admin)
vm.prank(newOwner);
roles.acceptOwnership();
// Admin role is now with newOwner
assert(roles.hasRole(roles.ADMIN_ROLE(), newOwner));Role Management
// Check role status
bool isManager = roles.hasRole(roles.MANAGER_ROLE(), address);
// Admin revokes manager role
roles.revokeRole(roles.MANAGER_ROLE(), address);
// Manager revokes distributor role
vm.prank(managerAddress);
roles.revokeRole(roles.DISTRIBUTOR_ROLE(), address);
// Self-renounce role
vm.prank(address);
roles.renounceRole(roles.MANAGER_ROLE(), address);Testing
The contract includes comprehensive tests covering:
- Role Hierarchy: Verification of admin role relationships
- Role Operations: Grant, revoke, and renounce functionality
- Ownership Transfer: Complete ownership transfer workflow
- Security Protections: Prevention of ownership/admin role loss
- Edge Cases: Invalid operations and error conditions
Test Coverage
- ✅ Three-tier role hierarchy setup (Admin → Manager → Distributor)
- ✅ Role granting with proper delegation (Manager grants Distributor)
- ✅ Role revocation and renunciation
- ✅ Ownership transfer with admin migration
- ✅ Ownership renunciation prevention
- ✅ Admin role protection mechanisms
Integration
The Roles contract serves as the foundation for access control throughout the ifif protocol. Other contracts can inherit from or integrate with Roles to implement permission-based functionality.
Inheritance Pattern
import {Roles} from "./core/Roles.sol";
contract ProtocolContract is Roles {
modifier onlyManager() {
require(hasRole(MANAGER_ROLE, msg.sender), "Not manager");
_;
}
function managerFunction() external onlyManager {
// Manager-only functionality
}
}External Integration
contract ExternalContract {
Roles public immutable roles;
constructor(address _roles) {
roles = Roles(_roles);
}
modifier onlyManager() {
require(roles.hasRole(roles.MANAGER_ROLE(), msg.sender), "Not manager");
_;
}
modifier onlyDistributor() {
require(roles.hasRole(roles.DISTRIBUTOR_ROLE(), msg.sender), "Not distributor");
_;
}
function managerFunction() external onlyManager {
// Manager-only functionality
}
function distributorFunction() external onlyDistributor {
// Distributor-only functionality
}
}Gas Considerations
- Role operations have standard OpenZeppelin AccessControl gas costs
- Admin role protection adds minimal overhead to revoke/renounce operations
- Ownership transfer includes admin role migration with predictable gas usage
Upgradeability
The contract is not upgradeable by design to ensure immutable access control logic. Role management and ownership transfer provide sufficient flexibility for operational needs while maintaining security guarantees.