LOADING

加载过慢请开启缓存 浏览器默认开启

Code review: Openzeppelin - AccessControl

2023/6/25 Solidity Coding

概述:

这个合约的作用是实现基于角色的访问控制(RBAC)机制,可以通过定义角色和角色成员之间的关系,来限制合约中特定函数的访问权限。

合约中提供了一系列函数用于检查和修改角色和角色成员之间的关系,以实现对合约中特定函数的访问控制。通过使用 AccessControl 合约,可以让合约更加安全地运行,并限制非授权用户对合约的访问和操作。

该合约模块,允许子合约实现基于角色的访问控制机制。这是一个轻量级版本,不允许枚举角色成员,除非通过访问合约事件日志的离线手段进行。某些应用程序可能会从链上可枚举性中受益,对于这些情况,请参见 AccessControlEnumerable

角色由它们的 bytes32 标识符表示。这些标识符应该在外部 API 中公开,并且应该是唯一的。最好的方法是使用 public constant 的哈希摘要:

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

角色可用于表示一组权限。为了限制对函数调用的访问,请使用 {hasRole}:

function foo() public {
    require(hasRole(MY_ROLE, msg.sender));
    ...
}

角色可以通过 {grantRole} 和 {revokeRole} 函数进行动态授予和撤销。每个角色都有一个关联的管理员角色,只有拥有角色的管理员角色的帐户才能调用 {grantRole} 和 {revokeRole}。

默认情况下,所有角色的管理员角色都是 DEFAULT_ADMIN_ROLE,这意味着只有拥有该角色的帐户才能授予或撤销其他角色。可以使用 {_setRoleAdmin} 创建更复杂的角色关系。

警告:DEFAULT_ADMIN_ROLE 也是自己的管理员角色:它有权限授予和撤销此角色。应采取额外的预防措施来保护已授予该角色的帐户。建议使用 AccessControlDefaultAdminRules 为该角色实施额外的安全措施。

完整代码:

pragma solidity ^0.8.0;

import "./IAccessControl.sol";
import "../utils/Context.sol";
import "../utils/Strings.sol";
import "../utils/introspection/ERC165.sol";

/**
 * @dev Contract module that allows children to implement role-based access
 * control mechanisms. This is a lightweight version that doesn't allow enumerating role
 * members except through off-chain means by accessing the contract event logs. Some
 * applications may benefit from on-chain enumerability, for those cases see
 * {AccessControlEnumerable}.
 *
 * Roles are referred to by their `bytes32` identifier. These should be exposed
 * in the external API and be unique. The best way to achieve this is by
 * using `public constant` hash digests:
 *
 * ```solidity
 * bytes32 public constant MY_ROLE = keccak256("MY_ROLE");
 * ```
 *
 * Roles can be used to represent a set of permissions. To restrict access to a
 * function call, use {hasRole}:
 *
 * ```solidity
 * function foo() public {
 *     require(hasRole(MY_ROLE, msg.sender));
 *     ...
 * }
 * ```
 *
 * Roles can be granted and revoked dynamically via the {grantRole} and
 * {revokeRole} functions. Each role has an associated admin role, and only
 * accounts that have a role's admin role can call {grantRole} and {revokeRole}.
 *
 * By default, the admin role for all roles is `DEFAULT_ADMIN_ROLE`, which means
 * that only accounts with this role will be able to grant or revoke other
 * roles. More complex role relationships can be created by using
 * {_setRoleAdmin}.
 *
 * WARNING: The `DEFAULT_ADMIN_ROLE` is also its own admin: it has permission to
 * grant and revoke this role. Extra precautions should be taken to secure
 * accounts that have been granted it. We recommend using {AccessControlDefaultAdminRules}
 * to enforce additional security measures for this role.
 */
abstract contract AccessControl is Context, IAccessControl, ERC165 {
    struct RoleData {
        mapping(address => bool) members;
        bytes32 adminRole;
    }

    mapping(bytes32 => RoleData) private _roles;

    bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00;

    /**
     * @dev Modifier that checks that an account has a specific role. Reverts
     * with a standardized message including the required role.
     *
     * The format of the revert reason is given by the following regular expression:
     *
     *  /^AccessControl: account (0x[0-9a-f]{40}) is missing role (0x[0-9a-f]{64})$/
     *
     * _Available since v4.1._
     */
    modifier onlyRole(bytes32 role) {
        _checkRole(role);
        _;
    }

    /**
     * @dev See {IERC165-supportsInterface}.
     */
    function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
        return interfaceId == type(IAccessControl).interfaceId || super.supportsInterface(interfaceId);
    }

    /**
     * @dev Returns `true` if `account` has been granted `role`.
     */
    function hasRole(bytes32 role, address account) public view virtual override returns (bool) {
        return _roles[role].members[account];
    }

    /**
     * @dev Revert with a standard message if `_msgSender()` is missing `role`.
     * Overriding this function changes the behavior of the {onlyRole} modifier.
     *
     * Format of the revert message is described in {_checkRole}.
     *
     * _Available since v4.6._
     */
    function _checkRole(bytes32 role) internal view virtual {
        _checkRole(role, _msgSender());
    }

    /**
     * @dev Revert with a standard message if `account` is missing `role`.
     *
     * The format of the revert reason is given by the following regular expression:
     *
     *  /^AccessControl: account (0x[0-9a-f]{40}) is missing role (0x[0-9a-f]{64})$/
     */
    function _checkRole(bytes32 role, address account) internal view virtual {
        if (!hasRole(role, account)) {
            revert(
                string(
                    abi.encodePacked(
                        "AccessControl: account ",
                        Strings.toHexString(account),
                        " is missing role ",
                        Strings.toHexString(uint256(role), 32)
                    )
                )
            );
        }
    }

    /**
     * @dev Returns the admin role that controls `role`. See {grantRole} and
     * {revokeRole}.
     *
     * To change a role's admin, use {_setRoleAdmin}.
     */
    function getRoleAdmin(bytes32 role) public view virtual override returns (bytes32) {
        return _roles[role].adminRole;
    }

    /**
     * @dev Grants `role` to `account`.
     *
     * If `account` had not been already granted `role`, emits a {RoleGranted}
     * event.
     *
     * Requirements:
     *
     * - the caller must have ``role``'s admin role.
     *
     * May emit a {RoleGranted} event.
     */
    function grantRole(bytes32 role, address account) public virtual override onlyRole(getRoleAdmin(role)) {
        _grantRole(role, account);
    }

    /**
     * @dev Revokes `role` from `account`.
     *
     * If `account` had been granted `role`, emits a {RoleRevoked} event.
     *
     * Requirements:
     *
     * - the caller must have ``role``'s admin role.
     *
     * May emit a {RoleRevoked} event.
     */
    function revokeRole(bytes32 role, address account) public virtual override onlyRole(getRoleAdmin(role)) {
        _revokeRole(role, account);
    }

    /**
     * @dev Revokes `role` from the calling account.
     *
     * Roles are often managed via {grantRole} and {revokeRole}: this function's
     * purpose is to provide a mechanism for accounts to lose their privileges
     * if they are compromised (such as when a trusted device is misplaced).
     *
     * If the calling account had been revoked `role`, emits a {RoleRevoked}
     * event.
     *
     * Requirements:
     *
     * - the caller must be `account`.
     *
     * May emit a {RoleRevoked} event.
     */
    function renounceRole(bytes32 role, address account) public virtual override {
        require(account == _msgSender(), "AccessControl: can only renounce roles for self");

        _revokeRole(role, account);
    }

    /**
     * @dev Grants `role` to `account`.
     *
     * If `account` had not been already granted `role`, emits a {RoleGranted}
     * event. Note that unlike {grantRole}, this function doesn't perform any
     * checks on the calling account.
     *
     * May emit a {RoleGranted} event.
     *
     * [WARNING]
     * ====
     * This function should only be called from the constructor when setting
     * up the initial roles for the system.
     *
     * Using this function in any other way is effectively circumventing the admin
     * system imposed by {AccessControl}.
     * ====
     *
     * NOTE: This function is deprecated in favor of {_grantRole}.
     */
    function _setupRole(bytes32 role, address account) internal virtual {
        _grantRole(role, account);
    }

    /**
     * @dev Sets `adminRole` as ``role``'s admin role.
     *
     * Emits a {RoleAdminChanged} event.
     */
    function _setRoleAdmin(bytes32 role, bytes32 adminRole) internal virtual {
        bytes32 previousAdminRole = getRoleAdmin(role);
        _roles[role].adminRole = adminRole;
        emit RoleAdminChanged(role, previousAdminRole, adminRole);
    }

    /**
     * @dev Grants `role` to `account`.
     *
     * Internal function without access restriction.
     *
     * May emit a {RoleGranted} event.
     */
    function _grantRole(bytes32 role, address account) internal virtual {
        if (!hasRole(role, account)) {
            _roles[role].members[account] = true;
            emit RoleGranted(role, account, _msgSender());
        }
    }

    /**
     * @dev Revokes `role` from `account`.
     *
     * Internal function without access restriction.
     *
     * May emit a {RoleRevoked} event.
     */
    function _revokeRole(bytes32 role, address account) internal virtual {
        if (hasRole(role, account)) {
            _roles[role].members[account] = false;
            emit RoleRevoked(role, account, _msgSender());
        }
    }
}

代码分析:

pragma solidity ^0.8.0;

首先,这个代码的作用是指定 Solidity 编译器的版本,以便编译器可以正确解析和编译合约代码。在这个例子中,指定的版本是 0.8.0,表示合约使用 Solidity 0.8.0 或更高版本进行编译。

此外,还可以在 pragma 中设置一些选项,例如启用实验性功能,指定优化器的版本等。该 pragma 语句通常应该作为合约代码文件的第一行。

import "./IAccessControl.sol";
import "../utils/Context.sol";
import "../utils/Strings.sol";
import "../utils/introspection/ERC165.sol";

这段代码是 Solidity 合约的导入语句,用于导入其他合约中定义的接口和库,以便在当前合约中使用它们。

  • import "./IAccessControl.sol"; 导入 IAccessControl.sol 合约接口,用于实现访问控制机制。
  • import "../utils/Context.sol"; 导入 Context.sol 库,包含与 Solidity 消息和交易相关的函数和变量,例如 _msgSender()_msgData()_gasleft()
  • import "../utils/Strings.sol"; 导入 Strings.sol 库,包含 Solidity 中处理字符串的实用函数,例如 uint2str()strConcat()
  • import "../utils/introspection/ERC165.sol"; 导入 ERC165.sol 库,包含用于 Solidity 智能合约接口支持的函数。其中包括 supportsInterface() 函数,用于在 Solidity 合约中检查是否实现了指定的接口标识符。

这些导入语句的作用是使当前合约能够使用这些接口和库中定义的函数和变量,从而更方便地实现所需的功能。

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

这段代码定义了一个名为 AccessControl 的抽象合约,并声明了它实现了三个接口:ContextIAccessControlERC165。同时定义了一个名为 RoleData 的结构体,它具有两个成员:membersadminRole,分别表示角色的成员和角色的管理员角色。

这个合约是一个抽象合约,它提供了一些函数的声明和定义,以实现基于角色的访问控制(RBAC)机制。由于它是一个抽象合约,因此不能直接部署,需要被其他合约继承和实现其抽象函数,从而实现访问控制的功能。

在结构体 RoleData 中,members 是一个映射,用于存储特定角色的所有成员地址及其是否是该角色的成员的状态。adminRole 是一个 bytes32 类型的变量,用于存储角色的管理员角色。

mapping(bytes32 => RoleData) private _roles;

bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00;

这段代码定义了两个状态变量:

  1. _roles:这是一个映射(mapping)类型的变量,它将 bytes32 类型的标识符映射到一个名为 RoleData 的结构体。每个 RoleData 结构体包含了角色成员的映射,以及角色的管理员角色。

  2. DEFAULT_ADMIN_ROLE:这是一个公共的、不可变的 bytes32 常量,表示默认管理员角色的标识符。默认情况下,所有角色都具有该管理员角色,它只能由拥有此角色的帐户授予或撤销其他角色。

这些变量与 AccessControl 合约的基于角色的访问控制机制相关联。_roles 映射存储了每个角色及其对应的角色成员,而 DEFAULT_ADMIN_ROLE 常量表示默认的管理员角色。这些变量将在实现访问控制相关的函数中被使用。

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

这段代码定义了一个名为 onlyRole 的修饰器,它接受一个 bytes32 类型的参数 role。当该修饰器被添加到一个函数之前时,它将首先调用内部函数 _checkRole(role) 来检查当前调用者是否具有指定的 role 权限,如果检查失败则会抛出异常,否则将执行被修饰的函数。

这个修饰器的作用是限制访问某些敏感函数的权限,只有具有指定角色的账户才能调用这些函数。如果调用者没有指定的角色,那么调用会立即失败并抛出异常,保证了函数的安全性和可靠性。

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

这个函数是用于检查合约是否实现了指定的接口。它接受一个 bytes4 类型的参数 interfaceId,表示待检查的接口标识符。

函数首先检查传入的 interfaceId 是否与 IAccessControl 接口的标识符匹配。如果匹配,则返回 true,表示合约实现了该接口。

如果传入的 interfaceIdIAccessControl 接口的标识符不匹配,则调用 super.supportsInterface(interfaceId) 函数,检查合约是否实现了其父类合约(也就是 ERC165 合约)中的指定接口。如果实现了该接口,则返回 true,否则返回 false

这个函数是 Solidity 合约标准 ERC165 中定义的函数,用于检查合约是否支持某些特定的接口。如果一个合约实现了 supportsInterface 函数,就意味着它支持了 ERC165 标准。

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

这个函数的作用是检查指定帐户是否具有指定角色。函数参数为角色标识符 role 和帐户地址 account,返回一个布尔值表示帐户是否具有指定角色。

函数通过访问 _roles 映射中存储的角色数据来判断帐户是否属于指定角色。具体地,它通过访问 _roles[role].members[account] 来获取帐户是否属于指定角色。如果该值为 true,则表示该帐户具有指定角色,函数返回 true;否则,表示该帐户不具有指定角色,函数返回 false。

通过使用这个函数,可以在合约中的某些函数中实现对帐户访问权限的控制。例如,在一个需要授权才能执行的函数中,可以使用这个函数来检查调用者是否具有指定的角色,以限制只有具有指定角色的帐户才能调用该函数。

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

这个代码定义了一个名为 _checkRole 的内部函数,它接受一个 bytes32 类型的参数 role,并且在内部调用另一个名为 _checkRole 的函数,传递了 role_msgSender() 作为参数。这个函数的作用是用于在执行需要特定角色权限的函数时,检查当前调用方是否具有该角色权限。

在默认情况下,_checkRole 函数将检查当前调用方是否具有指定角色的权限,并且如果没有,则会抛出一个异常。如果要修改此默认行为,可以重写此函数。

注意,这个函数是虚函数,这意味着它可以被子合约重写以改变其行为。

function _checkRole(bytes32 role, address account) internal view virtual {
    if (!hasRole(role, account)) {
        revert(
            string(
                abi.encodePacked(
                    "AccessControl: account ",
                    Strings.toHexString(account),
                    " is missing role ",
                    Strings.toHexString(uint256(role), 32)
                )
            )
        );
    }
}

这个函数的作用是检查给定地址是否具有特定角色的权限,如果没有权限则抛出异常,提示该地址缺少相应的角色。

函数首先调用 hasRole 函数来检查指定地址是否具有特定角色的权限,如果没有,则使用 revert 函数抛出异常,其中异常信息包含以下内容:

  • "AccessControl: account ":固定前缀,表示缺少角色的帐户
  • Strings.toHexString(account):将缺少角色的地址转换为十六进制字符串表示
  • " is missing role ":缺少角色的提示
  • Strings.toHexString(uint256(role), 32):将角色哈希值转换为十六进制字符串表示

可以看出,这个函数的实现是通过使用 Solidity 中的字符串拼接功能和哈希值转换函数来生成异常信息,并使用 revert 函数抛出异常。

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

这个函数的作用是返回一个角色的管理员角色。该函数接受一个 bytes32 类型的角色标识符作为参数,返回该角色对应的管理员角色的标识符。

代码实现中,首先访问了 _roles 存储变量中以 role 作为键值的角色的数据结构,其中包含了这个角色的管理员角色。然后返回了这个管理员角色的标识符。如果该角色不存在,则返回 0。这个函数是公开的(public),允许外部调用并且不能修改存储状态(view),因此不会消耗 gas。此外,这个函数是虚函数(virtual),可以被子类重写。

需要注意的是,通过 {_setRoleAdmin} 函数可以更改角色的管理员角色,因此这个函数返回的管理员角色可能会随着时间的推移而变化。

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

这个函数的作用是将指定角色授予给指定地址的帐户。调用此函数需要满足以下条件:

  • 调用者必须拥有此角色的管理员角色。
  • 此函数是虚函数,可被子合约重写。

如果满足条件,该函数将调用内部函数 _grantRole 来执行实际的角色授予操作。具体而言,该函数将检查指定地址的帐户是否已经拥有该角色,如果没有,则将其添加到角色成员中,并发出一个 RoleGranted 事件。如果帐户已经拥有该角色,则不执行任何操作。

代码实现上,该函数采用了 Solidity 的函数修饰符 onlyRole(getRoleAdmin(role)),表示只有拥有指定角色的管理员才能调用该函数。如果调用者不满足该条件,函数将会抛出异常并中止执行。

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

这段代码实现了一个名为 revokeRole 的函数,用于撤销某个角色中的指定成员的权限。函数接受两个参数:role 表示要撤销的角色的标识符,account 表示要撤销权限的成员地址。

该函数包含了一个名为 onlyRole 的修饰符,它要求调用者必须拥有指定角色的管理员角色才能执行该函数。这样可以确保只有有权管理该角色的管理员才能执行撤销操作。

函数内部调用了 _revokeRole 函数来实际执行撤销操作。_revokeRole 函数会检查成员是否已经拥有该角色的权限,如果有权限则撤销它,并在事件日志中记录该操作。

这样通过调用 revokeRole 函数,合约的管理员可以撤销指定成员的权限,从而限制其访问合约中特定的函数。这种机制可以帮助保护合约中重要的数据和操作,提高合约的安全性。

function renounceRole(bytes32 role, address account) public virtual override {
    require(account == _msgSender(), "AccessControl: can only renounce roles for self");

    _revokeRole(role, account);
}

这段代码实现了一个名为 renounceRole 的公共函数,用于让角色成员主动放弃(即撤销)一个角色。

函数接受两个参数:role 表示要撤销的角色,account 表示要撤销角色的成员。

函数首先通过 require 语句判断调用该函数的帐户是否等于 msg.sender,即只有角色成员自己才能主动撤销自己的角色。如果条件不成立,函数会中止执行,并抛出 "AccessControl: can only renounce roles for self" 的错误消息。

如果条件成立,函数会调用 _revokeRole 内部函数撤销该成员的角色。

需要注意的是,这个函数只允许成员主动放弃自己的角色,而不允许管理员或其他成员撤销别人的角色。

function _setupRole(bytes32 role, address account) internal virtual {
    _grantRole(role, account);
}

这个函数名为 _setupRole,是一个内部函数,只能被合约内部调用。该函数用于将角色授予给指定账户,但与 grantRole 函数不同,它不会检查调用者是否拥有相应角色的管理员权限。因此,它只应该在合约的构造函数中使用,以设置系统的初始角色和权限。

该函数使用 _grantRole 函数实现角色授权,将指定账户添加到对应角色的成员列表中,如果账户之前没有被授予该角色,则会触发 RoleGranted 事件。

代码实现非常简单,只需要调用 _grantRole 函数,并传入要授权的角色和账户地址即可。

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

该函数用于设置角色的管理员角色。输入参数为要设置管理员角色的角色标识符 role,以及要分配给该角色的新管理员角色标识符 adminRole

函数会先通过 getRoleAdmin(role) 函数获取当前 role 对应的管理员角色标识符,保存到 previousAdminRole 变量中。

然后,该函数将更新 _roles[role].adminRole,将其设置为新的管理员角色标识符 adminRole。最后,函数会触发 RoleAdminChanged 事件,将 rolepreviousAdminRoleadminRole 的值作为参数进行传递。

在实现上,该函数使用了 Solidity 的结构体,通过 _roles 映射存储角色数据。该映射的键为 bytes32 类型的角色标识符,值为 RoleData 结构体,包含 members 映射(用于存储该角色的成员)和 adminRole 变量(用于存储该角色的管理员角色)。

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

这个函数名为 _grantRole,是一个内部的虚函数,用于将特定角色授予给一个指定的地址。

函数首先检查目标地址是否已经拥有该角色,如果已经拥有该角色,则不执行任何操作。如果目标地址没有该角色,则在角色成员映射表 _roles[role].members 中设置该地址为角色成员,并发出 RoleGranted 事件,以通知其它合约该角色已经被授予给了该地址。

该函数的实现方式使用了条件判断,判断该地址是否已经拥有该角色,如果没有,才执行授予角色和发出事件的操作。

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

这段代码实现了撤销指定角色中某个账户的成员资格,其作用是限制该账户对被该角色保护的合约函数的访问权限。

函数的参数为 roleaccount,其中 role 为要撤销成员资格的角色,account 为要撤销成员资格的账户地址。

该函数首先调用 hasRole 函数检查账户是否是该角色的成员,如果是,就将该账户从该角色中删除。删除操作是通过在 _roles 映射中设置对应角色的成员映射的指定账户的值为 false 来实现的。

最后,该函数通过调用 RoleRevoked 事件来通知其他合约和外部观察者该账户已被从该角色中删除。事件包括角色标识符、被删除的账户地址和调用函数的地址(通常是合约的地址)。

该函数被标记为 internal,表示它只能被当前合约或继承它的合约内部的函数调用。同时,它被标记为 virtual,表示它可以被子合约覆盖实现。