[OpenZeppelin的智能合约代码库](OpenZeppelin)是以太坊开发者的宝库,OpenZeppelin代码库包含了经过社区审查的ERC代币标准、安全协议以及很多的辅助工具库,这些代码可以帮助开发者专注业务逻辑的,而无需重新发明*。
基于OpenZeppelin开发合约,即可以提高代码的安全性,又可以提高开发效率,文本列举了最应该添加到我们项目的 7个OpenZeppelin合约。
注意:在本文中我们使用的OpenZeppelin版本为2.5.x,使用 solidity 0.5.x编译器编译。
访问控制合约
1. 使用 Ownable 进行所有者限制
OpenZeppelin 的 `Ownable `合约提供的`onlyOwner` [修饰器](合约结构 - Solidity 中文文档 - 登链社区 - 深入浅出区块链)是用来限制某些特定合约函数的访问权限。
我们很多时候需要这样做,因此这个模式在以太坊智能合约开发中非常流行。
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库。
`OpenZeppelin`的`Roles`库的源代码如下:
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语句](合约 - Solidity 中文文档 - 登链社区 - 深入浅出区块链)来将库中定义的函数附加到指定的数据类型上。
下面的代码使用`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: improve mul performance and reduce gas cost by emn178 · Pull Request #522 · OpenZeppelin/openzeppelin-contracts
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);
}
}
下面的示例代码是如何使用`SafeCast`将`uint`转换为`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代币](EIP 20: ERC-20 代币标准(Token Standard))合约 ,OpenZeppelin已经帮我们实现好了, 我们只需要继承和初始化就好了。
`OpenZeppelin`的ERC20进行了标准的基础实现,ERC20Detailed 合约包含了额外的选项:例如代币名称、代币代号以及小数点位数。
下面是一个利用`OpenZeppelin`的`ERC20`和`ERC20Detailed`合约实现定制代币的例子:
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();
}
}