Post

Role Management In Solidity

Role Management In Solidity

In Solidity, managing permissions across multiple smart contracts can be challenging, especially when multiple contracts need to share role-based access control (RBAC). A common misconception when working with role management is misunderstanding how permissions are applied across different contracts and how the msg.sender behaves when calls are made through other contracts.

In this post, I’ll share a problem I encountered when trying to assign roles across two contracts (MilestoneManager and ProposalManager), and how I solved it using role delegation with an AdminManager contract.

The Problem: Misunderstanding Role Assignment Across Contracts

Image

Initially, I had two contracts MilestoneManager.sol and ProposalManager.sol both inheriting from a parent contract RoleManager.sol that used Solidity’s AccessControl mechanism. The RoleManager contract defined roles such as STUDENT_ROLE and DEFAULT_ADMIN_ROLE. Both child contracts had methods to add students and committee members via their own versions of the addStudent and addCommittee functions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
contract RoleManager is AccessControl {
    bytes32 public constant STUDENT_ROLE = keccak256("STUDENT_ROLE");
    bytes32 public constant COMMITTEE_ROLE = keccak256("COMMITTEE_ROLE");

    constructor(address initialAdmin) {
        _grantRole(DEFAULT_ADMIN_ROLE, initialAdmin);
    }

    function addStudent(address student) public onlyRole(DEFAULT_ADMIN_ROLE) {
        _grantRole(STUDENT_ROLE, student);
    }

    function addCommittee(address member) public onlyRole(DEFAULT_ADMIN_ROLE) {
        _grantRole(COMMITTEE_ROLE, member);
    }
}

contract MilestoneManager is RoleManager {
    // Milestone-specific logic
}

contract ProposalManager is RoleManager {
    // Proposal-specific logic
}

I initially assumed that calling addStudent in ProposalManager would automatically assign the STUDENT_ROLE in both ProposalManager and MilestoneManager. I believed that because both contracts inherited from RoleManager, the student role would be applied across both.

However, this was incorrect for two reasons:

  • Role Assignments Are Contract-Specific: In Solidity’s AccessControl, roles are specific to the contract where they are granted. So, assigning a role in ProposalManager does not automatically assign that role in MilestoneManager, even though they share the same RoleManager base contract.

  • Sender Context (msg.sender): When I called the addStudent method on ProposalManager to assign a role in MilestoneManager, I overlooked the fact that msg.sender in MilestoneManager would be ProposalManager (the contract making the call), not the account that initiated the transaction. Since ProposalManager did not have admin permissions in MilestoneManager, the call failed.

The Solution: Role Delegation Using AdminManager

To resolve this issue, I needed a way to assign roles in both ProposalManager and MilestoneManager with a single call. The solution was to create a separate AdminManager contract that could manage roles across both contracts. By delegating the role management to AdminManager, I ensured that roles could be granted in both contracts simultaneously without running into msg.sender permission issues.

AdminManager Contract

The AdminManager contract is responsible for handling role assignments for both ProposalManager and MilestoneManager. This contract holds the DEFAULT_ADMIN_ROLE for both contracts and can manage student and committee roles as needed.

Here’s the implementation of the AdminManager contract:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
contract AdminManager is AccessControl {
    MilestoneManager public milestoneManager;
    ProposalManager public proposalManager;

    constructor(
        address initialAdmin,
        address _milestoneManager,
        address _proposalManager
    ) {
        _grantRole(DEFAULT_ADMIN_ROLE, initialAdmin);
        milestoneManager = MilestoneManager(_milestoneManager);
        proposalManager = ProposalManager(_proposalManager);
    }

    function addStudent(address student) public onlyRole(DEFAULT_ADMIN_ROLE) {
        milestoneManager.addStudent(student);
        proposalManager.addStudent(student);
    }

    function addCommittee(
        address committee
    ) public onlyRole(DEFAULT_ADMIN_ROLE) {
        milestoneManager.addCommittee(committee);
        proposalManager.addCommittee(committee);
    }
}

Granting Permissions

To enable AdminManager to manage roles in both contracts, I needed to grant the DEFAULT_ADMIN_ROLE to the AdminManager contract in both MilestoneManager and ProposalManager.

1
2
3
// Deployer grants admin role to AdminManager in both contracts
milestoneManager.grantRole(DEFAULT_ADMIN_ROLE, address(adminManager));
proposalManager.grantRole(DEFAULT_ADMIN_ROLE, address(adminManager));

The Result

Now, when I call addStudentToBoth from AdminManager, the student role is applied in both MilestoneManager and ProposalManager simultaneously. This avoids the issue of contract-specific roles and bypasses the msg.sender issue by centralizing role assignment logic in a single contract.

This post is licensed under CC BY 4.0 by the author.