内容简介:使用 OpenZeppelin 来帮助进行合约开发,即可以提高代码的安全性,又可以提高开发效率。本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。
使用 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库。
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语句来将库中定义的函数附加到指定的数据类型上。
下面的代码使用 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); } }
下面的示例代码是如何使用 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代币合约 ,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(); } }
原文链接: 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库。
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语句来将库中定义的函数附加到指定的数据类型上。
下面的代码使用 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); } }
下面的示例代码是如何使用 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代币合约 ,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(); } }
原文链接: 7 OpenZeppelin Contracts You Should Always Use
作者: Alex Roan
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。
- 发表于 22分钟前
- 阅读 ( 45 )
- 学分 ( 0 )
- 分类:智能合约
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 使用智能合约实现自动分账
- 使用Java与区块链智能合约进行交互
- 如何使用 Solidity 和 JavaScript 测试智能合约【译】
- 使用Ink!开发Substrate ERC20智能合约
- 以太坊合约静态分析工具Slither简介与使用
- 智能合约语言DAML现在可以在R3的Corda上使用
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。