AccessControl: using a quorum to let a smart contract grant / revoke roles

:computer: Environment
Truffle / Solidity 6

:memo:Details
I’m trying to build a “dao”-like smart contract that implements AccessControl and grants roles to accounts upon other accounts voting on that grant proposal. the idea is that no EOA controls the role granting but the contract does. This collides with security checks in oz’s AccessControl interface though: when I want a smart contract to control the roles (e.g. by reacting on a positive quorum of other role members to accept a new member to the role), the "msg.sender must be role admin for this role " requirement doesn’t allow the contract to grant / revoke roles for accounts.

I already tried to make the contract itself an admin for the role but since AccessControl checks against msg.sender that won’t let us pass since our contract is not the msg.sender :wink: . Tried to hack that with a self.delegatecall(abi) approach but think this is leading nowhere.

Any ideas how to achieve that in a “secure” way?

:1234: Code to reproduce

(omitted the whole quorum / voting code here)

pragma solidity ^0.6;

import '@openzeppelin/contracts/access/AccessControl.sol';

contract QuorumRoleControl is AccessControl {
  
  bytes32 public constant ROLE_VOTER = keccak256('ROLE_VOTER');
  bytes32 public constant ROLE_VOTER_ADMIN = keccak256('ROLE_VOTER_ADMIN');

  constructor() public  AccessControl() {
    _setRoleAdmin(ROLE_VOTER, ROLE_VOTER_ADMIN);
    _setupRole(ROLE_VOTER_ADMIN, address(this));
    //democracy starts with the first user on earth ;) 
    _setupRole(ROLE_VOTER, msg.sender);
  }

  // ... lots of voting code, this is pseudo:  ...
  function vote(address shouldBecomeRoleMember) {
      require(hasRole(ROLE_VOTER,msg.sender));
      votes[shouldBecomeRoleMember]++;
  }

  function settleVoting(address shouldBecomeRoleMember) public {
    require(hasRole(ROLE_VOTER, msg.sender)); //unnecessary
    
    //grant / revoke roles, according to the quorum's verdict
    if (votes[shouldBecomeRoleMember] > 3)
        setupRole(ROLE_VOTER, address(theMemberWeVotedOn));
    else 
        revokeRole(ROLE_VOTER, address(someoneWeAllReallyDislike));
  }
}

setupRole / revokeRole will revert with AccessControl: sender must be an admin to revoke. I can discouragedly use _setupRole for the grant case but there’s no “hack” for the revoke case.

2 Likes

You should do this + call the function through this as in: this.grantRole(...) and this.revokeRole(...).

We've implemented something similar to this pattern ourselves in TimelockController:

(The code highlighting seems a bit messed up)

Though in this case we didn't need to explicitly call this.grantRole anywhere, that's because the TimelockController can execute arbitary calls.

1 Like

Using this solves your issue because that way it performs an external call, which changes the msg.sender to the calling contract (in this case, it’s also the called contract).

1 Like

wow. I thought I tried that but turns out, I didn’t. It’s so simple once you know it :slight_smile: I’m actually not able to find that properly documented on the Solidity docs, maybe I should open a PR for that.

2 Likes