OpenZeppelin's Access Control: A User Guide

OpenZeppelin's Access Control: A User Guide
⚠️
Version of OpenZeppelin introduced: v5.0.0

In this post, we'll explore OpenZeppelin's Access Control library. Let's start with the benefits of using Access Control:

  • Implements role-based access control
  • Easy assignment of roles to multiple accounts
  • Simple and secure access control structure

Using Access Control, you can easily and efficiently implement permissions and other access controls in your projects. Let's dive in!

What is Access Control?

Access Control is a library provided by OpenZeppelin that allows you to use role-based access control in a simple and secure way.

Role-based access control is common in many projects, so you'll likely use this library frequently. While you can implement access control yourself, using this library is easier and more secure, eliminating unnecessary worries.

Inside Access Control

Let's take a look at what's inside:

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (access/AccessControl.sol)

pragma solidity ^0.8.20;

import {IAccessControl} from "./IAccessControl.sol";
import {Context} from "../utils/Context.sol";
import {ERC165} from "../utils/introspection/ERC165.sol";

abstract contract AccessControl is Context, IAccessControl, ERC165 {
    struct RoleData {
        mapping(address => bool) hasRole;
        bytes32 adminRole;
    }

    mapping(bytes32 => RoleData) private _roles;
    bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00;

    modifier onlyRole(bytes32 role) {
        _checkRole(role);
        _;
    }

    function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
        return interfaceId == type(IAccessControl).interfaceId || super.supportsInterface(interfaceId);
    }

    function hasRole(bytes32 role, address account) public view virtual returns (bool) {
        return _roles[role].hasRole[account];
    }

    function _checkRole(bytes32 role) internal view virtual {
        _checkRole(role, _msgSender());
    }

    function _checkRole(bytes32 role, address account) internal view virtual {
        if (!hasRole(role, account)) {
            revert AccessControlUnauthorizedAccount(account, role);
        }
    }

    function getRoleAdmin(bytes32 role) public view virtual returns (bytes32) {
        return _roles[role].adminRole;
    }

    function grantRole(bytes32 role, address account) public virtual onlyRole(getRoleAdmin(role)) {
        _grantRole(role, account);
    }

    function revokeRole(bytes32 role, address account) public virtual onlyRole(getRoleAdmin(role)) {
        _revokeRole(role, account);
    }

    function renounceRole(bytes32 role, address callerConfirmation) public virtual {
        if (callerConfirmation != _msgSender()) {
            revert AccessControlBadConfirmation();
        }
        _revokeRole(role, callerConfirmation);
    }

    function _setRoleAdmin(bytes32 role, bytes32 adminRole) internal virtual {
        bytes32 previousAdminRole = getRoleAdmin(role);
        _roles[role].adminRole = adminRole;
        emit RoleAdminChanged(role, previousAdminRole, adminRole);
    }

    function _grantRole(bytes32 role, address account) internal virtual returns (bool) {
        if (!hasRole(role, account)) {
            _roles[role].hasRole[account] = true;
            emit RoleGranted(role, account, _msgSender());
            return true;
        } else {
            return false;
        }
    }

    function _revokeRole(bytes32 role, address account) internal virtual returns (bool) {
        if (hasRole(role, account)) {
            _roles[role].hasRole[account] = false;
            emit RoleRevoked(role, account, _msgSender());
            return true;
        } else {
            return false;
        }
    }
}

Now, let's highlight some key functions:

hasRole

function hasRole(bytes32 role, address account) public view virtual returns (bool) {
    return _roles[role].hasRole[account];
}

hasRole is simple; it checks if a given account has a given role and returns a boolean.

grantRole

function grantRole(bytes32 role, address account) public virtual onlyRole(getRoleAdmin(role)) {
    _grantRole(role, account);
}

function _grantRole(bytes32 role, address account) internal virtual returns (bool) {
    if (!hasRole(role, account)) {
        _roles[role].hasRole[account] = true;
        emit RoleGranted(role, account, _msgSender());
        return true;
    } else {
        return false;
    }
}

grantRole is used to assign a role to an account. If the account already has the role, it returns false.

revokeRole

function revokeRole(bytes32 role, address account) public virtual onlyRole(getRoleAdmin(role)) {
    _revokeRole(role, account);
}

function _revokeRole(bytes32 role, address account) internal virtual returns (bool) {
    if (hasRole(role, account)) {
        _roles[role].hasRole[account] = false;
        emit RoleRevoked(role, account, _msgSender());
        return true;
    } else {
        return false;
    }
}

revokeRole is used to revoke a role from an account. If the account does not have the role, it returns false.

renounceRole

function renounceRole(bytes32 role, address callerConfirmation) public virtual {
    if (callerConfirmation != _msgSender()) {
        revert AccessControlBadConfirmation();
    }
    _revokeRole(role, callerConfirmation);
}

renounceRole should be used with caution. It allows an account to renounce its own role, which can be useful if the account is compromised. However, if no one else has the role, it can lead to a situation where certain functions can no longer be called.

Usage Example

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

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

contract AccessContract is AccessControl {
    bytes32 public constant WRITER_ROLE = keccak256("WRITER_ROLE");
    bytes32 public constant MEMBER_ROLE = keccak256("MEMBER_ROLE");
    uint256 public value;

    constructor(address defaultAdmin, address writer) {
        _grantRole(DEFAULT_ADMIN_ROLE, defaultAdmin);
        _setRoleAdmin(WRITER_ROLE, DEFAULT_ADMIN_ROLE);
        _setRoleAdmin(MEMBER_ROLE, DEFAULT_ADMIN_ROLE);
        _grantRole(WRITER_ROLE, writer);
    }

    function increment() external {
        require(hasRole(MEMBER_ROLE, msg.sender), "Caller is not a member");
        value++;
    }

    function decrement() external onlyRole(WRITER_ROLE) {
        require(value > 0, "Value cannot be negative");
        value--;
    }

    function setValue(uint256 _value) external onlyRole(DEFAULT_ADMIN_ROLE) {
        value = _value;
    }

    function addMember(address member) external onlyRole(DEFAULT_ADMIN_ROLE) {
        require(member != address(0), "member is the zero address");
        _grantRole(MEMBER_ROLE, member);
    }
}

In this example, we have roles for writing (WRITER_ROLE) and membership (MEMBER_ROLE). The constructor sets up the default admin and assigns roles. The increment function can only be called by members, decrement can only be called by writers, and setValue can only be called by the default admin. The addMember function allows the admin to add new members.

Conclusion

OpenZeppelin's Access Control is a library for implementing role-based access control in a simple and secure way. It offers ease of role assignment to multiple accounts and provides a clear access control structure. It is highly recommended for projects that require permission handling. Using OpenZeppelin's Access Control can lead to more secure and efficient implementations compared to custom access control implementations.