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
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
ProposalManagerdoes not automatically assign that role inMilestoneManager, even though they share the sameRoleManagerbase contract.Sender Context (msg.sender): When I called the addStudent method on
ProposalManagerto assign a role inMilestoneManager, I overlooked the fact thatmsg.senderinMilestoneManagerwould beProposalManager(the contract making the call), not the account that initiated the transaction. SinceProposalManagerdid not have admin permissions inMilestoneManager, 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.

