OpenZeppelin的智慧合約程式碼庫[1]是以太坊開發者的寶庫,OpenZeppelin程式碼庫包含了經過社羣審查的ERC代幣標準、安全協議以及很多的輔助工具庫,這些程式碼可以幫助開發者專注業務邏輯的,而無需重新發明輪子。基於OpenZeppelin開發合約,即可以提高程式碼的安全性,又可以提高開發效率,文字列舉了最應該新增到我們專案的 7個OpenZeppelin合約。注意:在本文中我們使用的OpenZeppelin版本為2.5.x,使用 solidity 0.5.x編譯器編譯。訪問控制合約1. 使用 Ownable 進行所有者限制OpenZeppelin 的 Ownable合約提供的onlyOwner 修飾器[2]是用來限制某些特定合約函式的訪問許可權。我們很多時候需要這樣做,因此這個模式在以太坊智慧合約開發中非常流行。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語句[3]來將庫中定義的函式附加到指定的資料型別上。下面的程式碼使用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代幣[4]合約 ,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 / ERC721FullOpenZeppelin也提供了非同質化代幣的實現,我們同樣不需要把完整的把標準實現一次。如果需要列舉一個賬號的所持有的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[5]作者:Alex Roan[6]References[1] OpenZeppelin的智慧合約程式碼庫: https://openzeppelin.com/[2] 修飾器: https://learnblockchain.cn/docs/solidity/structure-of-a-contract.html#modifier[3] using語句: https://learnblockchain.cn/docs/solidity/contracts.html#using-for[4] ERC20代幣: https://learnblockchain.cn/docs/eips/eip-20.html[5] 7 OpenZeppelin Contracts You Should Always Use: https://medium.com/better-programming/7-openzeppelin-contracts-you-should-always-use-5ba2e7953cc4[6] Alex Roan: https://medium.com/@alexroan?source=post_page-----5ba2e7953cc4----------------------