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
ProposalManager
does not automatically assign that role inMilestoneManager
, even though they share the sameRoleManager
base contract.Sender Context (msg.sender): When I called the addStudent method on
ProposalManager
to assign a role inMilestoneManager
, I overlooked the fact thatmsg.sender
inMilestoneManager
would beProposalManager
(the contract making the call), not the account that initiated the transaction. SinceProposalManager
did 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.