智能合约的3种访问控制模式解析:Ownable | Roles | AccessControl

简介:

OpenZeppelin提供了智能合约的三种访问控制模式:Ownable合约、Roles库和3.0新增的AccessControl合约。在这个教程中,我们将学习这三种访问控制模式的差异,以及如何在自己的以太坊智能合约中利用这些访问控制模式增强Solidity合约的安全性。

用你熟悉的开发语言学习以太坊DApp开发:Java | Php | Python | .Net / C# | Golang | Node.JS | Flutter / Dart

控制对智能合约特定方法的访问权限,对于智能合约的安全性非常重要。熟悉OpenZeppelin的智能合约库的开发者都知道这个库已经提供了根据访问等级进行访问限制的选项,其中最常见的就是Ownable合约管理的onlyOwner模式,另一个是OpenZeppelin的Roles库,它允许合约在部署前定义多种角色并为每个函数设置规则,以确保msg.sender具有正确的角色。在OpenZeppelin 3.0中又引入了更强大的AccessControl合约,其定位是一站式访问控制解决方案。

1、Ownable合约 —— 最简单最流行的访问控制模式

onlyOwner模式是最常见也最容易实现的访问控制方法,它虽然基础但非常有效。该模式假设智能合约存在单一管理员,支持管理员将全新转移给另一个账号。

通过扩展Ownable合约,子合约就可以在定义方法时使用onlyOwner修饰符,这些被修饰的方法就要求交易发起账号必须是合约的管理员。

下面这个简单的示例展示了如何使用Ownable合约提供的onlyOwner修饰符来限制对restrictedFunction的访问:

function normalFunction() public {
    // 任何人都可以调用
}

function restrictedFunction() public onlyOwner {
    // 只有合约管理员可以调用
}

2、Roles库 —— OpenZeppelin自己喜欢的访问控制模式

虽然Ownable合约简单易用,但是OpenZeppelin库中的其他合约都是使用Roles库来实现访问控制。这是因为Roles库比Ownable合约提供了更多的灵活性。

我们使用using语句引入Roles合约库,来为数据类型增加功能。Roles库为Role数据类型实现了三个方法。下面是Role的定义:

pragma solidity ^0.5.0;

/**
 * @title Roles
 * @dev Library for managing addresses assigned to a Role.
 */
library Roles {
    struct Role {
        mapping (address => bool) bearer;
    }

    /**
     * @dev Give an account access to this role.
     */
    function add(Role storage role, address account) internal {
        require(!has(role, account), "Roles: account already has role");
        role.bearer[account] = true;
    }

    /**
     * @dev Remove an account's access to this role.
     */
    function remove(Role storage role, address account) internal {
        require(has(role, account), "Roles: account does not have role");
        role.bearer[account] = false;
    }

    /**
     * @dev Check if an account has this role.
     * @return bool
     */
    function has(Role storage role, address account) internal view returns (bool) {
        require(account != address(0), "Roles: account is the zero address");
        return role.bearer[account];
    }
}

在代码的开头部分,我们剋有看到Role结构,合约使用该结构定义多个角色以及其成员。方法add()remove()has()则提供了与Role结构交互的接口。

例如,下面的代码展示了如何使用两个不同的角色 —— _minters_burners—— 来为特定的方法设定访问控制规则:

pragma solidity ^0.5.0;

import "@openzeppelin/contracts/access/Roles.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20Detailed.sol";

contract MyToken is ERC20, ERC20Detailed {
    using Roles for Roles.Role;

    Roles.Role private _minters;
    Roles.Role private _burners;

    constructor(address[] memory minters, address[] memory burners)
        ERC20Detailed("MyToken", "MTKN", 18)
        public
    {
        for (uint256 i = 0; i < minters.length; ++i) {
            _minters.add(minters[i]);
        }

        for (uint256 i = 0; i < burners.length; ++i) {
            _burners.add(burners[i]);
        }
    }

    function mint(address to, uint256 amount) public {
        // Only minters can mint
        require(_minters.has(msg.sender), "DOES_NOT_HAVE_MINTER_ROLE");

        _mint(to, amount);
    }

    function burn(address from, uint256 amount) public {
        // Only burners can burn
        require(_burners.has(msg.sender), "DOES_NOT_HAVE_BURNER_ROLE");

       _burn(from, amount);
    }
}

注意在mint()函数中,require语句确保交易发起方具有minter角色,
_minters.has(msg.sender)

3、AccessControl合约 —— 官方的一站式访问控制解决方案

Roles库虽然灵活,但也存在一定的局限性。因为它是一个Solidity库,所以它的数据存储是被引入的合约控制的,而理想的实现应当是让引入Roles库的合约只需要关心每个方法能实现的访问控制功能。

OpenZeppelin 3.0新增的AccessControl合约被官方称为:

可以满足所有身份验证需求的一站式解决方案,它允许你:1、轻松定义具有

不同权限的多种角色 2、定义哪个账号可以进行角色的授权与回收 3、
枚举系统中所有的特权账号。

上述的3个特性中,后两点都是Roles库不支持的。看起来OpenZeppelin正在逐渐实现基于角色的访问控制和基于属性的访问控制这些在传统的计算安全中非常重要的标准。

4、AccessControl合约代码剖析

下面是OpenZeppelin的AccessControl合约的代码:

pragma solidity ^0.6.0;

import "../utils/EnumerableSet.sol";
import "../utils/Address.sol";
import "../GSN/Context.sol";

/**
 * @dev Contract module that allows children to implement role-based access
 * control mechanisms.
 *
 * 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:
 *
 * 
 * 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}:
 *
 * 
 * function foo() public {
 *     require(hasRole(MY_ROLE, _msgSender()));
 *     ...
 * }
 * 
 *
 * 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}.
 */
abstract contract AccessControl is Context {
    using EnumerableSet for EnumerableSet.AddressSet;
    using Address for address;

    struct RoleData {
        EnumerableSet.AddressSet members;
        bytes32 adminRole;
    }

    mapping (bytes32 => RoleData) private _roles;

    bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00;

    /**
     * @dev Emitted when `account` is granted `role`.
     *
     * `sender` is the account that originated the contract call, an admin role
     * bearer except when using {_setupRole}.
     */
    event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender);

    /**
     * @dev Emitted when `account` is revoked `role`.
     *
     * `sender` is the account that originated the contract call:
     *   - if using `revokeRole`, it is the admin role bearer
     *   - if using `renounceRole`, it is the role bearer (i.e. `account`)
     */
    event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender);

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

    /**
     * @dev Returns the number of accounts that have `role`. Can be used
     * together with {getRoleMember} to enumerate all bearers of a role.
     */
    function getRoleMemberCount(bytes32 role) public view returns (uint256) {
        return _roles[role].members.length();
    }

    /**
     * @dev Returns one of the accounts that have `role`. `index` must be a
     * value between 0 and {getRoleMemberCount}, non-inclusive.
     *
     * Role bearers are not sorted in any particular way, and their ordering may
     * change at any point.
     *
     * WARNING: When using {getRoleMember} and {getRoleMemberCount}, make sure
     * you perform all queries on the same block. See the following
     * https://forum.openzeppelin.com/t/iterating-over-elements-on-enumerableset-in-openzeppelin-contracts/2296[forum post]
     * for more information.
     */
    function getRoleMember(bytes32 role, uint256 index) public view returns (address) {
        return _roles[role].members.at(index);
    }

    /**
     * @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 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.
     */
    function grantRole(bytes32 role, address account) public virtual {
        require(hasRole(_roles[role].adminRole, _msgSender()), "AccessControl: sender must be an admin to grant");

        _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.
     */
    function revokeRole(bytes32 role, address account) public virtual {
        require(hasRole(_roles[role].adminRole, _msgSender()), "AccessControl: sender must be an admin to revoke");

        _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 granted `role`, emits a {RoleRevoked}
     * event.
     *
     * Requirements:
     *
     * - the caller must be `account`.
     */
    function renounceRole(bytes32 role, address account) public virtual {
        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.
     *
     * [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}.
     * ====
     */
    function _setupRole(bytes32 role, address account) internal virtual {
        _grantRole(role, account);
    }

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

    function _grantRole(bytes32 role, address account) private {
        if (_roles[role].members.add(account)) {
            emit RoleGranted(role, account, _msgSender());
        }
    }

    function _revokeRole(bytes32 role, address account) private {
        if (_roles[role].members.remove(account)) {
            emit RoleRevoked(role, account, _msgSender());
        }
    }
}

在42行定义的RoleData结构使用3.0版本中新增的EnumerableSet(枚举集合)作为保存角色成员的数据结构,这使得枚举特权用户成为可能。

RoleData结构将adminRole定义为bytes32变量,它表示哪个角色可以作为一个特定角色的管理员,也就是说负责这个角色成员的授权与回收。

在代码的57行和66行,分别触发角色授权和回收事件。

Roles库提供提供了三个函数: has(), add()和 remove()。AccessControl合约中也包含这些方法,并且提供额外的功能,例如获取角色数量、获取指定角色的特定成员等等。

5、使用AccessControl合约实现代币合约的访问控制

前面的代币合约示例使用Roles库定义了两种不同的角色_minters和_burners。在这里我们使用AccessControl实现同样的功能。

pragma solidity ^0.6.0;

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

contract MyToken is ERC20, AccessControl {
    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
    bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");

    constructor() public ERC20("MyToken", "TKN") {
        // Grant the contract deployer the default admin role: it will be able
        // to grant and revoke any roles
        _setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
    }

    function mint(address to, uint256 amount) public {
        require(hasRole(MINTER_ROLE, msg.sender), "Caller is not a minter");
        _mint(to, amount);
    }

    function burn(address from, uint256 amount) public {
        require(hasRole(BURNER_ROLE, msg.sender), "Caller is not a burner");
        _burn(from, amount);
    }
}

和用Roles库的实现有什么区别?

首先,不再需要在子合约中定义每一个角色,因为这些角色都保存在父合约中。只有bytes32类型的ID作为状态常量存在子合约中,例如这个示例中的MINTER_ROLE和BURNER_ROLE。

_setupRole()用于在构造函数中设置初始的角色管理员,以便跳过AccessControl中的grantRole()进行的检查(因为在创建合约时还没有管理员)。

另外,不再需要像调用库方法那样需要借助于特定的数据类型,例如_minters.has(msg.sender),现在角色操作的相关方法可以在子合约中直接调用,例如hasRole(MINTER_ROLE,msg.sender),这可以让子合约的代码看起来更加干净,可读性更高。

5、教程小结

在这个教程中,我们学习了OpenZeppelin合约库中的三种访问控制设计模式以及其使用方法:Ownable合约、Roles库以及3.0新增的AccessControl合约,Ownable最简单,AccessControl最强大,你可以根据自己的需求选择合适的访问控制手段来保护自己的Solidity智能合约。


原文链接:OpenZeppelin的3种访问控制模式 — 汇智网

相关实践学习
云安全基础课 - 访问控制概述
课程大纲 课程目标和内容介绍视频时长 访问控制概述视频时长 身份标识和认证技术视频时长 授权机制视频时长 访问控制的常见攻击视频时长
目录
相关文章
|
2月前
|
域名解析 移动开发 负载均衡
阿里云DNS常见问题之DNS负载均衡调加权模式失败如何解决
阿里云DNS(Domain Name System)服务是一个高可用和可扩展的云端DNS服务,用于将域名转换为IP地址,从而让用户能够通过域名访问云端资源。以下是一些关于阿里云DNS服务的常见问题合集:
|
3月前
|
监控 API 开发者
Sentinel之道:流控模式解析与深度探讨
Sentinel之道:流控模式解析与深度探讨
58 0
|
4月前
|
存储 安全 JavaScript
【分布式技术专题】「授权认证体系」深度解析OAuth2.0协议的原理和流程框架实现指南(授权流程和模式)
在传统的客户端-服务器身份验证模式中,客户端请求服务器上访问受限的资源(受保护的资源)时,需要使用资源所有者的凭据在服务器上进行身份验证。资源所有者为了给第三方应用提供受限资源的访问权限,需要与第三方共享它的凭据。这就导致一些问题和局限:
378 2
【分布式技术专题】「授权认证体系」深度解析OAuth2.0协议的原理和流程框架实现指南(授权流程和模式)
|
15天前
|
物联网 调度
思科无线 AP 模式全解析
【4月更文挑战第22天】
23 0
|
23天前
|
数据安全/隐私保护 C++
C++ 类方法解析:内外定义、参数、访问控制与静态方法详解
C++ 中的类方法(成员函数)分为类内定义和类外定义,用于操作类数据。类内定义直接在类中声明和定义,而类外定义则先在类中声明,再外部定义。方法可以有参数,访问权限可通过 public、private 和 protected 控制。静态方法与类关联,不依赖对象实例,直接用类名调用。了解这些概念有助于面向对象编程。
16 0
|
1月前
|
存储 安全 测试技术
网络奇谭:虚拟机中的共享、桥接与Host-Only模式解析
网络奇谭:虚拟机中的共享、桥接与Host-Only模式解析
24 0
|
2月前
|
存储 安全 区块链
NFT代币模式系统开发技术规则解析
随着区块链技术的飞速发展,NFT(非同质化代币)作为一种独特的数字资产,正在全球范围内掀起一股热潮。NFT不仅赋予了数字内容独一无二的身份标识,更让艺术品、游戏道具等虚拟物品具备了真实可交易的价值。本文将深入探讨NFT代币模式的系统开发源码,带您领略这一创新技术的魅力所在。
|
2月前
|
域名解析 缓存 网络协议
HTTP DNS的工作模式
【2月更文挑战第12天】
HTTP DNS的工作模式
|
4月前
|
开发框架 .NET C#
C# 10.0中的扩展属性与模式匹配:深入解析
【1月更文挑战第20天】C# 10.0引入了众多新特性,其中扩展属性与模式匹配的结合为开发者提供了更强大、更灵活的类型检查和代码分支能力。通过这一特性,开发者可以在不修改原始类的情况下,为其添加新的行为,并在模式匹配中利用这些扩展属性进行更精细的控制。本文将详细探讨C# 10.0中扩展属性与模式匹配的工作原理、使用场景以及最佳实践,帮助读者更好地理解和应用这一新功能。
|
4月前
|
供应链 安全 物联网
深度剖析:区块链技术掌握必备知识,加密货币与智能合约应用解析
深度剖析:区块链技术掌握必备知识,加密货币与智能合约应用解析
135 0

推荐镜像

更多