22

OpenZeppelin 7个最常使用的合约

 3 years ago
source link: https://learnblockchain.cn/article/1012
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

使用 OpenZeppelin 来帮助进行合约开发,即可以提高代码的安全性,又可以提高开发效率。

OpenZeppelin的智能合约代码库 是以太坊开发者的宝库,OpenZeppelin代码库包含了经过社区审查的ERC代币标准、安全协议以及很多的辅助工具库,这些代码可以帮助开发者专注业务逻辑的,而无需重新发明轮子。

基于OpenZeppelin开发合约,即可以提高代码的安全性,又可以提高开发效率,文本列举了最应该添加到我们项目的 7个OpenZeppelin合约。

注意:在本文中我们使用的OpenZeppelin版本为2.5.x,使用 solidity 0.5.x编译器编译。

访问控制合约

1. 使用 Ownable 进行所有者限制

OpenZeppelin 的 Ownable 合约提供的 onlyOwner 修饰器是用来限制某些特定合约函数的访问权限。

我们很多时候需要这样做,因此这个模式在以太坊智能合约开发中非常流行。

Ownable合约的部署账号会被当做合约的拥有者(owner),某些合约函数,例如转移所有权,就限制在只允许拥有者(owner)调用。

下面是Ownable合约的源代码:

pragma solidity ^0.5.0;

import "../GSN/Context.sol";

contract Ownable is Context {
    address private _owner;

    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    constructor () internal {
        address msgSender = _msgSender();
        _owner = msgSender;
        emit OwnershipTransferred(address(0), msgSender);
    }

    function owner() public view returns (address) {
        return _owner;
    }

    modifier onlyOwner() {
        require(isOwner(), "Ownable: caller is not the owner");
        _;
    }

    function isOwner() public view returns (bool) {
        return _msgSender() == _owner;
    }

    function renounceOwnership() public onlyOwner {
        emit OwnershipTransferred(_owner, address(0));
        _owner = address(0);
    }

    function transferOwnership(address newOwner) public onlyOwner {
        _transferOwnership(newOwner);
    }

    function _transferOwnership(address newOwner) internal {
        require(newOwner != address(0), "Ownable: new owner is the zero address");
        emit OwnershipTransferred(_owner, newOwner);
        _owner = newOwner;
    }
}

注意在构造函数中如何设置合约的owner账号。当Ownable的子合约(即继承Ownable的合约)初始化时,部署的账号就会设置为 _owner

下面是一个简单的、继承自Ownable的合约:

pragma solidity ^0.5.5;

import "@openzeppelin/contracts/ownership/Ownable.sol";

contract OwnableContract is Ownable {

  function restrictedFunction() public onlyOwner returns (uint) {
    return 99;
  }

  function openFunction() public returns (uint) {
    return 1;
  }

}

通过添加 onlyOwner 修饰器 来限制 restrictedFunction 函数合约的owner账号可以成功调用:

2. 使用 Roles 进行角色控制

进行访问控制另一个相对于 Ownable 合约 更高级一些的是使用 Roles 库, 它可以定义多个角色,对于需要多个访问层次的控制时,应当考虑使用Roles库。

OpenZeppelinRoles 库的源代码如下:

pragma solidity ^0.5.0;

library Roles {
    struct Role {
        mapping (address => bool) bearer;
    }

    function add(Role storage role, address account) internal {
        require(!has(role, account), "Roles: account already has role");
        role.bearer[account] = true;
    }

    function remove(Role storage role, address account) internal {
        require(has(role, account), "Roles: account does not have role");
        role.bearer[account] = false;
    }

    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];
    }
}

由于 Roles 是一个Solidity库而非合约,因此不能通过继承的方式来使用,需要使用solidity的using语句来将库中定义的函数附加到指定的数据类型上。

下面的代码使用 Roles 库用 _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);
    }
}

第8行的作用是将 Roles 库中的函数附加到 Roles.Role 类型上。第18行就是在 Roles.Role 类型上直接使用这些库函数的方法: _minters.add() ,其中 add() 就是 Roles 库提供的实现。

算术运算

3. 安全的算术运算库:SafeMath

永远不要直接使用算术运算符例如:+、-、*、/ 进行数学计算,除非你了解如何检查溢出漏洞,否则就没法保证这些算术计算的安全性。

SafeMath库的作用是帮我们进行算术运中进行必要的检查,避免代码中因算术运算(如溢出)而引入漏洞。

下面是SafeMath的源代码:

pragma solidity ^0.5.0;

library SafeMath {
    function add(uint256 a, uint256 b) internal pure returns (uint256) {
        uint256 c = a + b;
        require(c >= a, "SafeMath: addition overflow");

        return c;
    }

    function sub(uint256 a, uint256 b) internal pure returns (uint256) {
        return sub(a, b, "SafeMath: subtraction overflow");
    }

    function sub(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) {
        require(b <= a, errorMessage);
        uint256 c = a - b;

        return c;
    }

    function mul(uint256 a, uint256 b) internal pure returns (uint256) {
        // Gas optimization: this is cheaper than requiring 'a' not being zero, but the
        // benefit is lost if 'b' is also tested.
        // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522
        if (a == 0) {
            return 0;
        }

        uint256 c = a * b;
        require(c / a == b, "SafeMath: multiplication overflow");

        return c;
    }

    function div(uint256 a, uint256 b) internal pure returns (uint256) {
        return div(a, b, "SafeMath: division by zero");
    }

    function div(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) {
        // Solidity only automatically asserts when dividing by 0
        require(b > 0, errorMessage);
        uint256 c = a / b;
        // assert(a == b * c + a % b); // There is no case in which this doesn't hold

        return c;
    }

    function mod(uint256 a, uint256 b) internal pure returns (uint256) {
        return mod(a, b, "SafeMath: modulo by zero");
    }

    function mod(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) {
        require(b != 0, errorMessage);
        return a % b;
    }
}

和Roles库的用法类似,你需要使用using语句将SafeMath库中的函数附加到uint256类型上,例如:

using SafeMath for uint256;

4. 安全类型转换库:SafeCast

作为一个智能合约开发者,我们常常会思考如何减少合约的执行时间以及空间,节约代码空间的一个办法就是使用更少位数的整数类型。 但不幸的是,如果你使用 uint8 作为变量类型,那么在调用 SafeMath 库函数之前,就必须先将其转换为 uint256 类型,然后在调用 SafeMath 库函数之后,还需要再转换回 uint8 类型。 SafeCast 库的作用就在于可以帮你完成这些转换而无需担心溢出问题。

SafeCast的源代码如下:

pragma solidity ^0.5.0;

library SafeCast {

    function toUint128(uint256 value) internal pure returns (uint128) {
        require(value < 2**128, "SafeCast: value doesn\'t fit in 128 bits");
        return uint128(value);
    }

    function toUint64(uint256 value) internal pure returns (uint64) {
        require(value < 2**64, "SafeCast: value doesn\'t fit in 64 bits");
        return uint64(value);
    }

    function toUint32(uint256 value) internal pure returns (uint32) {
        require(value < 2**32, "SafeCast: value doesn\'t fit in 32 bits");
        return uint32(value);
    }

    function toUint16(uint256 value) internal pure returns (uint16) {
        require(value < 2**16, "SafeCast: value doesn\'t fit in 16 bits");
        return uint16(value);
    }

    function toUint8(uint256 value) internal pure returns (uint8) {
        require(value < 2**8, "SafeCast: value doesn\'t fit in 8 bits");
        return uint8(value);
    }
}

下面的示例代码是如何使用 SafeCastuint 转换为 uint8

pragma solidity ^0.5.5;

import "@openzeppelin/contracts/math/SafeCast.sol";

contract BasicSafeCast {

  using SafeCast for uint;
  
  function castToUint8(uint _a) public returns (uint8) {
    return _a.toUint8();
  }
}

Tokens (代币或通证)

ERC20Detailed

不需要自己实现完整的ERC20代币合约 ,OpenZeppelin已经帮我们实现好了, 我们只需要继承和初始化就好了。

OpenZeppelin 的ERC20进行了标准的基础实现,ERC20Detailed 合约包含了额外的选项:例如代币名称、代币代号以及小数点位数。

下面是一个利用 OpenZeppelinERC20ERC20Detailed 合约实现定制代币的例子:

pragma solidity ^0.5.0;

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

contract GLDToken is ERC20, ERC20Detailed {
    constructor(uint256 initialSupply) ERC20Detailed("Gold", "GLD", 18) public {
        _mint(msg.sender, initialSupply);
    }
}

6. 非同质化代币:ERC721Enumerable / ERC721Full

OpenZeppelin也提供了非同质化代币的实现,我们同样不需要把完整的把标准实现一次。

如果需要枚举一个账号的所持有的ERC721资产,需要使用 ERC721Enumerable 合约而不是基础的 ERC721

ERC721Enumerable 提供了 _tokensOfOwner() 方法 直接支持枚举特定账号的所有资产。如果你希望有所有的扩展功能合约,那么可以直接选择 ERC721Full 。下面的代码展示了基于 ERC721Full 定制非同质化代币:

pragma solidity ^0.5.0;

import "@openzeppelin/contracts/token/ERC721/ERC721Full.sol";
import "@openzeppelin/contracts/drafts/Counters.sol";

contract GameItem is ERC721Full {
    using Counters for Counters.Counter;
    Counters.Counter private _tokenIds;

    constructor() ERC721Full("GameItem", "ITM") public {
    }

    function awardItem(address player, string memory tokenURI) public returns (uint256) {
        _tokenIds.increment();

        uint256 newItemId = _tokenIds.current();
        _mint(player, newItemId);
        _setTokenURI(newItemId, tokenURI);

        return newItemId;
    }
}

辅助工具库

7. 用 Address库识别地址

有时候在Solidity合约中需要了解一个地址是普通钱包地址还是合约地址。 OpenZeppelin的 Address 库提供了一个方法 isContract() 可以帮我们解决这个问题。

下面的代码展示了如何使用 isContract() 函数:

pragma solidity ^0.5.5;

import "@openzeppelin/contracts/utils/Address.sol";

contract BasicUtils {
    using Address for address;

    function checkIfContract(address _addr) public {
        return _addr.isContract();
    }
}

原文链接: 7 OpenZeppelin Contracts You Should Always Use

作者: Alex Roan

OpenZeppelin的智能合约代码库 是以太坊开发者的宝库,OpenZeppelin代码库包含了经过社区审查的ERC代币标准、安全协议以及很多的辅助工具库,这些代码可以帮助开发者专注业务逻辑的,而无需重新发明轮子。

基于OpenZeppelin开发合约,即可以提高代码的安全性,又可以提高开发效率,文本列举了最应该添加到我们项目的 7个OpenZeppelin合约。

注意:在本文中我们使用的OpenZeppelin版本为2.5.x,使用 solidity 0.5.x编译器编译。

访问控制合约

1. 使用 Ownable 进行所有者限制

OpenZeppelin 的 Ownable 合约提供的 onlyOwner 修饰器是用来限制某些特定合约函数的访问权限。

我们很多时候需要这样做,因此这个模式在以太坊智能合约开发中非常流行。

Ownable合约的部署账号会被当做合约的拥有者(owner),某些合约函数,例如转移所有权,就限制在只允许拥有者(owner)调用。

下面是Ownable合约的源代码:

pragma solidity ^0.5.0;

import "../GSN/Context.sol";

contract Ownable is Context {
    address private _owner;

    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    constructor () internal {
        address msgSender = _msgSender();
        _owner = msgSender;
        emit OwnershipTransferred(address(0), msgSender);
    }

    function owner() public view returns (address) {
        return _owner;
    }

    modifier onlyOwner() {
        require(isOwner(), "Ownable: caller is not the owner");
        _;
    }

    function isOwner() public view returns (bool) {
        return _msgSender() == _owner;
    }

    function renounceOwnership() public onlyOwner {
        emit OwnershipTransferred(_owner, address(0));
        _owner = address(0);
    }

    function transferOwnership(address newOwner) public onlyOwner {
        _transferOwnership(newOwner);
    }

    function _transferOwnership(address newOwner) internal {
        require(newOwner != address(0), "Ownable: new owner is the zero address");
        emit OwnershipTransferred(_owner, newOwner);
        _owner = newOwner;
    }
}

注意在构造函数中如何设置合约的owner账号。当Ownable的子合约(即继承Ownable的合约)初始化时,部署的账号就会设置为 _owner

下面是一个简单的、继承自Ownable的合约:

pragma solidity ^0.5.5;

import "@openzeppelin/contracts/ownership/Ownable.sol";

contract OwnableContract is Ownable {

  function restrictedFunction() public onlyOwner returns (uint) {
    return 99;
  }

  function openFunction() public returns (uint) {
    return 1;
  }

}

通过添加 onlyOwner 修饰器 来限制 restrictedFunction 函数合约的owner账号可以成功调用:

2. 使用 Roles 进行角色控制

进行访问控制另一个相对于 Ownable 合约 更高级一些的是使用 Roles 库, 它可以定义多个角色,对于需要多个访问层次的控制时,应当考虑使用Roles库。

OpenZeppelinRoles 库的源代码如下:

pragma solidity ^0.5.0;

library Roles {
    struct Role {
        mapping (address => bool) bearer;
    }

    function add(Role storage role, address account) internal {
        require(!has(role, account), "Roles: account already has role");
        role.bearer[account] = true;
    }

    function remove(Role storage role, address account) internal {
        require(has(role, account), "Roles: account does not have role");
        role.bearer[account] = false;
    }

    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];
    }
}

由于 Roles 是一个Solidity库而非合约,因此不能通过继承的方式来使用,需要使用solidity的using语句来将库中定义的函数附加到指定的数据类型上。

下面的代码使用 Roles 库用 _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);
    }
}

第8行的作用是将 Roles 库中的函数附加到 Roles.Role 类型上。第18行就是在 Roles.Role 类型上直接使用这些库函数的方法: _minters.add() ,其中 add() 就是 Roles 库提供的实现。

算术运算

3. 安全的算术运算库:SafeMath

永远不要直接使用算术运算符例如:+、-、*、/ 进行数学计算,除非你了解如何检查溢出漏洞,否则就没法保证这些算术计算的安全性。

SafeMath库的作用是帮我们进行算术运中进行必要的检查,避免代码中因算术运算(如溢出)而引入漏洞。

下面是SafeMath的源代码:

pragma solidity ^0.5.0;

library SafeMath {
    function add(uint256 a, uint256 b) internal pure returns (uint256) {
        uint256 c = a + b;
        require(c >= a, "SafeMath: addition overflow");

        return c;
    }

    function sub(uint256 a, uint256 b) internal pure returns (uint256) {
        return sub(a, b, "SafeMath: subtraction overflow");
    }

    function sub(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) {
        require(b <= a, errorMessage);
        uint256 c = a - b;

        return c;
    }

    function mul(uint256 a, uint256 b) internal pure returns (uint256) {
        // Gas optimization: this is cheaper than requiring 'a' not being zero, but the
        // benefit is lost if 'b' is also tested.
        // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522
        if (a == 0) {
            return 0;
        }

        uint256 c = a * b;
        require(c / a == b, "SafeMath: multiplication overflow");

        return c;
    }

    function div(uint256 a, uint256 b) internal pure returns (uint256) {
        return div(a, b, "SafeMath: division by zero");
    }

    function div(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) {
        // Solidity only automatically asserts when dividing by 0
        require(b > 0, errorMessage);
        uint256 c = a / b;
        // assert(a == b * c + a % b); // There is no case in which this doesn't hold

        return c;
    }

    function mod(uint256 a, uint256 b) internal pure returns (uint256) {
        return mod(a, b, "SafeMath: modulo by zero");
    }

    function mod(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) {
        require(b != 0, errorMessage);
        return a % b;
    }
}

和Roles库的用法类似,你需要使用using语句将SafeMath库中的函数附加到uint256类型上,例如:

using SafeMath for uint256;

4. 安全类型转换库:SafeCast

作为一个智能合约开发者,我们常常会思考如何减少合约的执行时间以及空间,节约代码空间的一个办法就是使用更少位数的整数类型。 但不幸的是,如果你使用 uint8 作为变量类型,那么在调用 SafeMath 库函数之前,就必须先将其转换为 uint256 类型,然后在调用 SafeMath 库函数之后,还需要再转换回 uint8 类型。 SafeCast 库的作用就在于可以帮你完成这些转换而无需担心溢出问题。

SafeCast的源代码如下:

pragma solidity ^0.5.0;

library SafeCast {

    function toUint128(uint256 value) internal pure returns (uint128) {
        require(value < 2**128, "SafeCast: value doesn\'t fit in 128 bits");
        return uint128(value);
    }

    function toUint64(uint256 value) internal pure returns (uint64) {
        require(value < 2**64, "SafeCast: value doesn\'t fit in 64 bits");
        return uint64(value);
    }

    function toUint32(uint256 value) internal pure returns (uint32) {
        require(value < 2**32, "SafeCast: value doesn\'t fit in 32 bits");
        return uint32(value);
    }

    function toUint16(uint256 value) internal pure returns (uint16) {
        require(value < 2**16, "SafeCast: value doesn\'t fit in 16 bits");
        return uint16(value);
    }

    function toUint8(uint256 value) internal pure returns (uint8) {
        require(value < 2**8, "SafeCast: value doesn\'t fit in 8 bits");
        return uint8(value);
    }
}

下面的示例代码是如何使用 SafeCastuint 转换为 uint8

pragma solidity ^0.5.5;

import "@openzeppelin/contracts/math/SafeCast.sol";

contract BasicSafeCast {

  using SafeCast for uint;

  function castToUint8(uint _a) public returns (uint8) {
    return _a.toUint8();
  }
}

Tokens (代币或通证)

ERC20Detailed

不需要自己实现完整的ERC20代币合约 ,OpenZeppelin已经帮我们实现好了, 我们只需要继承和初始化就好了。

OpenZeppelin 的ERC20进行了标准的基础实现,ERC20Detailed 合约包含了额外的选项:例如代币名称、代币代号以及小数点位数。

下面是一个利用 OpenZeppelinERC20ERC20Detailed 合约实现定制代币的例子:

pragma solidity ^0.5.0;

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

contract GLDToken is ERC20, ERC20Detailed {
    constructor(uint256 initialSupply) ERC20Detailed("Gold", "GLD", 18) public {
        _mint(msg.sender, initialSupply);
    }
}

6. 非同质化代币:ERC721Enumerable / ERC721Full

OpenZeppelin也提供了非同质化代币的实现,我们同样不需要把完整的把标准实现一次。

如果需要枚举一个账号的所持有的ERC721资产,需要使用 ERC721Enumerable 合约而不是基础的 ERC721

ERC721Enumerable 提供了 _tokensOfOwner() 方法 直接支持枚举特定账号的所有资产。如果你希望有所有的扩展功能合约,那么可以直接选择 ERC721Full 。下面的代码展示了基于 ERC721Full 定制非同质化代币:

pragma solidity ^0.5.0;

import "@openzeppelin/contracts/token/ERC721/ERC721Full.sol";
import "@openzeppelin/contracts/drafts/Counters.sol";

contract GameItem is ERC721Full {
    using Counters for Counters.Counter;
    Counters.Counter private _tokenIds;

    constructor() ERC721Full("GameItem", "ITM") public {
    }

    function awardItem(address player, string memory tokenURI) public returns (uint256) {
        _tokenIds.increment();

        uint256 newItemId = _tokenIds.current();
        _mint(player, newItemId);
        _setTokenURI(newItemId, tokenURI);

        return newItemId;
    }
}

辅助工具库

7. 用 Address库识别地址

有时候在Solidity合约中需要了解一个地址是普通钱包地址还是合约地址。 OpenZeppelin的 Address 库提供了一个方法 isContract() 可以帮我们解决这个问题。

下面的代码展示了如何使用 isContract() 函数:

pragma solidity ^0.5.5;

import "@openzeppelin/contracts/utils/Address.sol";

contract BasicUtils {
    using Address for address;

    function checkIfContract(address _addr) public {
        return _addr.isContract();
    }
}

原文链接: 7 OpenZeppelin Contracts You Should Always Use

作者: Alex Roan

本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

  • 发表于 22分钟前
  • 阅读 ( 45 )
  • 学分 ( 0 )
  • 分类:智能合约

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK