Using Pauser Role AccessControl with ERC20 token (UpgradeSafe)

Trey Grainger reported on Telegram:

So I’m running into some incredibly bizarre behavior related to AccessControl in contracts-ethereum-package that I’ve been fighting with for over a day. My contract is modeled after the ERC20PresetMinterPauserUpgradeSafe, which I believe suffers from the same problem (I’ll verify that shortly).

Here’s the issue:

let tok = await MyToken.deployed()
tok.hasRole(tok.PAUSER_ROLE, accounts[1]) //false
tok.grantRole(tok.PAUSER_ROLE, accounts[1]) //success
tok.hasRole(tok.PAUSER_ROLE, accounts[1]) //true

But whenever I call pre.pause(), the fails on the “require(hasRole(tok.PAUSER_ROLE, “…”))” line

 function pause() public {
        require(hasRole(PAUSER_ROLE, _msgSender()), "ERC20PresetMinterPauser: must have pauser role to pause");
        _pause();
    }

What’s very strange is that if my contract calls hasRole(PAUSER_ROLE, _msgSender()) it successfully returns (true or false), but if it calls require(hasRole(PAUSER_ROLE, _msgSender()) then it throws an exception.

In stepping through the debugger, it initially seems like the _roles mapping is empty whenever I call require(hasRole(…)) in my contract, but is populated with the correctly-assigned roles when I just call hasRole(…))

Has anyone seen this before, or (as a sanity check), has anyone successfully used AccessControl in your upgradeable smart contracts before? I’m assuming the answer to the latter question is a definitive yes, but it’s definitely not working for me and very bizarre, so wondering if anyone else has seen this before or has any insight.

Ok, here’s what I’ve found thusfar.

I just added a bunch of helper methods and they are pointing to the issue being that if I pass in the PAUSER_ROLE const then it works, but if I reference it within the function on the contract then it doesn’t.

bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");

function testAddressHasRole(bytes32 role, address account) public view returns (bool) {
    return hasRole(role, account);
}

function testHasRole(bytes32 role) public view returns (bool) {
    return hasRole(role, _msgSender());
}

function testHasPauserRole() public view returns (bool) {
    return hasRole(PAUSER_ROLE, _msgSender());
}

function testRequireHasPauserRole() public view returns (bool){
    require(hasRole(PAUSER_ROLE, _msgSender()), "PauseableEnhancedERC20: must have pauser role to unpause");
    return hasRole(PAUSER_ROLE, _msgSender());
}

The following is what I see:

let tok = await MyToken.deployed()
tok.grantRole(tok.PAUSER_ROLE, accounts[1])

truffle(development)> tok.testAddressHasRole(tok.PAUSER_ROLE, accounts[1], {from: accounts[0]})

true

truffle(development)> tok.testAddressHasRole(tok.PAUSER_ROLE, accounts[1], {from: accounts[1]})

true

truffle(development)> tok.testHasRole(tok.PAUSER_ROLE, {from: accounts[0]})

true

truffle(development)> tok.testHasRole(tok.PAUSER_ROLE, {from: accounts[1]})

true

truffle(development)> tok.testHasPauserRole({from: accounts[0]})

true

 //Note: likely only true because accounts[0] is DEFAULT_ADMIN_ROLE
truffle(development)> tok.testHasPauserRole({from: accounts[1]})

false

truffle(development)> tok.testRequireHasPauserRole({from: accounts[0]})

true
//Note: likely only true because accounts[0] is DEFAULT_ADMIN_ROLE
truffle(development)> tok.testRequireHasPauserRole({from: accounts[1]})

ERROR
Error: Returned error: VM Exception while processing transaction: revert PauseableEnhancedERC20: must have pauser role to unpause

So essentially, if I refer to the public const PAUSER_ROLE from within the contract then it fails, but if I refer to it externally and pass it in then it succeeds… Not sure what’s going on there.

1 Like

Hi @cryptotrey,

I can reproduce the issue that you are seeing. I will see what I can find out.

truffle(develop)> migrate
truffle(develop)> token = await MyToken.deployed()
undefined
truffle(develop)> token.hasRole(token.PAUSER_ROLE, accounts[1])
false
truffle(develop)> token.grantRole(token.PAUSER_ROLE, accounts[1])
{ tx:
...
truffle(develop)> token.hasRole(token.PAUSER_ROLE, accounts[1])
true
truffle(develop)> token.hasRole(token.PAUSER_ROLE, accounts[1])
true
truffle(develop)> token.pause({from: accounts[0]})
{ tx:
...
truffle(develop)> token.paused()
true
truffle(develop)> token.unpause({from: accounts[1]})
Thrown:
{ Error: Returned error: VM Exception while processing transaction: revert MyToken: must have pauser role to unpause -- Reason given: MyToken: must have pauser role to unpause.

MyToken.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import "@openzeppelin/contracts-ethereum-package/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts-ethereum-package/contracts/GSN/Context.sol";
import "@openzeppelin/contracts-ethereum-package/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts-ethereum-package/contracts/token/ERC20/ERC20Burnable.sol";
import "@openzeppelin/contracts-ethereum-package/contracts/token/ERC20/ERC20Pausable.sol";
import "@openzeppelin/contracts-ethereum-package/contracts/Initializable.sol";

contract MyToken is Initializable, ContextUpgradeSafe, AccessControlUpgradeSafe, ERC20BurnableUpgradeSafe, ERC20PausableUpgradeSafe {
    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
    bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");

    function initialize(string memory name, string memory symbol) public initializer {
        __Context_init_unchained();
        __AccessControl_init_unchained();
        __ERC20_init_unchained(name, symbol);
        __ERC20Burnable_init_unchained();
        __Pausable_init_unchained();
        __ERC20Pausable_init_unchained();

        _setupRole(DEFAULT_ADMIN_ROLE, _msgSender());

        _setupRole(MINTER_ROLE, _msgSender());
        _setupRole(PAUSER_ROLE, _msgSender());
    }

    function mint(address to, uint256 amount) public {
        require(hasRole(MINTER_ROLE, _msgSender()), "MyToken: must have minter role to mint");
        _mint(to, amount);
    }

    function pause() public {
        require(hasRole(PAUSER_ROLE, _msgSender()), "MyToken: must have pauser role to pause");
        _pause();
    }

    function unpause() public {
        require(hasRole(PAUSER_ROLE, _msgSender()), "MyToken: must have pauser role to unpause");
        _unpause();
    }

    function _beforeTokenTransfer(address from, address to, uint256 amount) internal override(ERC20UpgradeSafe, ERC20PausableUpgradeSafe) {
        super._beforeTokenTransfer(from, to, amount);
    }
}

2_deploy.js

// migrations/2_deploy.js
const { deployProxy } = require('@openzeppelin/truffle-upgrades');

const MyToken = artifacts.require('MyToken');

module.exports = async function (deployer) {
  await deployProxy(MyToken, ["My Token", "TKN"], { deployer, unsafeAllowCustomTypes: true });
};

Hi @cryptotrey,

The issue is that we need to call the getter for PAUSER_ROLE. token.PAUSER_ROLE()

truffle(develop)> await token.grantRole(await token.PAUSER_ROLE(), accounts[1])
{ tx:
...
truffle(develop)> await token.hasRole(await token.PAUSER_ROLE(), accounts[1])
true
truffle(develop)> await token.pause({from: accounts[1]})
{ tx:
...
truffle(develop)> token.paused()
true

If we just call PAUSER_ROLE we get the following, which we then grant to an account but this isn’t the same role as PAUSER_ROLE().

truffle(develop)> token.PAUSER_ROLE
{ [Function]
  call: [Function],
  sendTransaction: [Function],
  estimateGas: [Function],
  request: [Function] }
truffle(develop)> token.PAUSER_ROLE()
'0x65d7a28e3265b37a6474929f336521b332c1681b933f6cb9f3376673440d862a'

Oh wow, yep that makes total sense. I’m surprised it didn’t error out on me somewhere doing that!

Going to test it out now an make sure everything works, sounds like that should fix the issue. Thanks @abcoathup!

1 Like

Hi @cryptotrey,

The joy of JavaScript.

I’m sorry I didn’t spot this right away. It was only after I reproduced what you were seeing I went back to check the value of PAUSER_ROLE() .

Heh - you spotted it in like 10 minutes. I’ve been struggling to figure this out for the better part of a day!

What tripped me up was that it such that it appeared to be working because it was assigning the accounts to a bogus role based upon the function (instead of the getter function’s return value), apparently.

WRONG, but seemingly working in console:

truffle(development)> accounts[1]
'0xD31B1CaD024a068D80df10b2dd45D01Ac2Cf33c4'

pre.grantRole(pre.PAUSER_ROLE, accounts[3])
[SUCCESS]

truffle(development)> pre.hasRole(pre.PAUSER_ROLE, accounts[1])
true

truffle(development)> pre.getRoleMember(pre.PAUSER_ROLE, 1)
'0xD31B1CaD024a068D80df10b2dd45D01Ac2Cf33c4'

Seemed like everything was working until I actually tried do do something with the role (call pause) from inside the contract, and then KABOOM.

Glad it was something simple to fix. Thanks again for your help @abcoathup!

1 Like