权限控制 Ownable 合约
本章讲解智能合约的权限控制,以及如何编写一个通用权限控制合约。
智能合约通常也有权限控制功能,有些功能可以提供给任何人使用,而有些功能只能由管理员,也就是合约拥有者使用。
在智能合约中,通过对于调用者地址的判断,来决定调用者是否有权利使用。它的实现通常有两种方法:使用 if
语句判断,或者使用 require
判断。
其实,Openzepplin
库中专门实现了这样的一个通用的抽象合约,名称为 Ownable
,我们可以直接拿来使用。
Ownable
合约对外提供了 3 个主要功能:
1. 提供 onlyOwner 修饰器
onlyOwner
修饰器用来限制函数的调用者,只有合约拥有者 owner
才能使用。其它地址调用,都会报错,交易被 revert
。
2. 转移控制权 transferOwnership 函数
当前合约拥有者将合约的所有权转移到新帐户。
3. 放弃控制权 renounceOwnership 函数
当前合约拥有者放弃对合约的所有权。这将造成合约没有拥有者,从而禁用了仅供拥有者使用的任何功能。 这么做的好处就是增强了用户对合约的信任,因为任何人都没有特权,无法操纵合约。
1. 编写 Ownable 合约
我们可以来编写一下这个合约,名称为 Ownable
。Ownable
合约提供了一个基本的访问控制机制,可以授予一个帐户对特定函数的独占访问权,这个账户成为合约拥有者。
默认情况下,合约拥有者帐户就是部署合约的帐户,部署后可以使用 transferOwnership
更改它。
另外,这个合约是一个抽象 abstract
合约,也就是说,这个合约不能单独使用,不能单独部署,只能由其它合约通过继承来使用。
以下就是 Ownable
合约的代码:
// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; /** * 这个合约模块提供了一个基本的访问控制机制,其中有一个帐户(拥有者)可以被授予对特定函数的独占访问权。 * 默认情况下,拥有者帐户将是部署合约的帐户。可以使用 {transferOwnership} 后来更改它。 * 这个模块通过继承来使用。它将提供修饰符 `onlyOwner`,可以应用于您的函数,以将它们的使用限制为拥有者。 */ abstract contract Ownable { address private _owner; // 合约拥有者 // 合约控制权转移事件 event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); // 初始化合约,将合约部署者设置为初始拥有者 constructor() { // 将合约部署者设置为初始拥有者 _transferOwnership(msg.sender); } // 如果由拥有者以外的任何帐户调用,则抛出异常 modifier onlyOwner() { // 确保调用者是合约的拥有者 require(owner() == msg.sender, "Ownable: caller is not the owner"); // 继续执行被修饰的函数 _; } // 返回当前合约拥有者的地址 function owner() public view virtual returns (address) { // 返回当前合约拥有者地址 return _owner; } /** * 放弃合约的控制权。 * 它只能由当前拥有者调用。 * 注意:放弃所有权将使合约没有拥有者,从而禁用仅供拥有者使用的任何功能。 */ function renounceOwnership() public virtual onlyOwner { // 调用内部函数,将合约拥有权设置为零地址,从而放弃控制权 _transferOwnership(address(0)); } /** * 将合约的所有权转移到新帐户 newOwner。 * 它只能由当前拥有者调用。 */ function transferOwnership(address newOwner) public virtual onlyOwner { // 确保新拥有者不是零地址 require(newOwner != address(0), "Ownable: new owner is the zero address"); // 将合约的所有权转移到新帐户 newOwner _transferOwnership(newOwner); } /** * 将合约的所有权转移到新帐户 newOwner。 * 这是一个内部函数,仅运行内部或者继承合约的函数调用。 */ function _transferOwnership(address newOwner) internal virtual { // 记录当前拥有者地址 address oldOwner = _owner; // 更新拥有者地址 _owner = newOwner; // 触发拥有权转移事件 emit OwnershipTransferred(oldOwner, newOwner); } }
2. 使用 Ownable 合约
如果一个合约需要控制访问权限,那么可以直接继承 Ownable
合约即可。
// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; // MyCoin 继承 Ownable 合约的功能 contract MyCoin is Ownable { mapping (address => bool) blacklist; // 记录账户是否在黑名单中 // 设置账户进出黑名单 function setBlacklist(address account, bool flag) public onlyOwner { // 设置账户在黑名单中的状态为 flag blacklist[account] = flag; } // 检查账户是否在黑名单中 function isBlaclisted(address account) public view returnsl(bool) { // 返回账户在黑名单中的状态 return blacklist[account]; } }
这是一个实现黑名单功能的合约。
其中,函数 setBlacklist
用来设置一个账户是否进入黑名单。它只能由合约拥有者使用,而其它用户无权使用。所以,这个函数后面增加了 onlyOwner
修饰器。
函数 isBlaclisted
,就没有设置权限控制,任何人都可以调用。
我们在编写合约的时候,如果需要进行权限控制,就可以导入并继承 Ownable
合约。
3. 部署和测试
我们在编写合约的时候,如果需要进行权限控制,就可以导入并继承 Ownable
合约。
比如,ERC-20
代币合约、ERC721 NFT
合约,往往要求具有权限控制的功能。 NFT
交易市场 OpenSea
,就明确要求实现 Ownable
功能。
我们以 ERC-20
代币合约为例,为它添加了权限控制功能,代码如下:
// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; // 引入代币合约需要继承的 openzeppelin 的 ERC-20 合约 import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; contract MyCoin is ERC20, Ownable{ // 构造函数,调用了openzeppelin的ERC-20合约的构造函数,传入代币名称和符号 constructor() ERC20("BinSchool Coin", "BC") Ownable(msg.sender) { // 铸造 100 个 BC 给合约部署者 _mint(msg.sender, 100*10**18); } }
我们把上面的代码复制到 Remix
进行编译,然后部署到区块链上。那么,这个代币合约就已经拥有了权限控制的功能了。