Skip to content

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_ROLE to the contract deployer
  • Sets ADMIN_ROLE as the admin role for MANAGER_ROLE
  • Sets MANAGER_ROLE as the admin role for DISTRIBUTOR_ROLE
  • Creates a three-tier hierarchy: Admin → Manager → Distributor

acceptOwnership()

function acceptOwnership() public virtual override

Purpose: Accepts ownership transfer and migrates admin privileges.

Security Features:

  • Revokes ADMIN_ROLE from the previous owner
  • Grants ADMIN_ROLE to the new owner
  • Ensures seamless admin privilege transition

Access: Only the pending owner can call this function

renounceOwnership()

function renounceOwnership() public virtual override

Purpose: 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 override

Purpose: Revokes a role from an account with admin role protection.

Parameters:

  • role: The role identifier to revoke
  • account: 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 override

Purpose: Allows self-renunciation of roles with admin role protection.

Parameters:

  • role: The role identifier to renounce
  • account: The address renouncing the role (must be msg.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:

  1. No Ownership Renunciation: renounceOwnership() always reverts
  2. Admin Role Protection: Owner cannot lose admin role through revocation or renunciation
  3. Two-Step Transfer: Uses OpenZeppelin's Ownable2Step for safe ownership transfer

Role Administration

  • ADMIN_ROLE holders can manage MANAGER_ROLE
  • MANAGER_ROLE holders can manage DISTRIBUTOR_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.