LOADING

加载过慢请开启缓存 浏览器默认开启

Code review: Openzeppelin - Ownable

2023/6/28 Solidity Coding

概述:

该合约名为 "Ownable",它提供了一种基本的访问控制机制,有一个账户(所有者)可以被赋予对特定功能的独家访问权。这个机制是通过继承来使用的,它会使得修饰符 onlyOwner 可用,该修饰符可以应用到你的函数上以限制他们的使用只能是所有者。

初始的所有者设置为部署者提供的地址,后期可以通过 transferOwnership 方法更改。

具体来看,该合约主要包括以下几个功能:

  • constructor:构造函数,在合约初始化时设置初始的所有者。

  • onlyOwner:只有所有者可以执行的修饰符。

  • owner:获取当前的所有者。

  • _checkOwner:检查发送者是否为所有者。

  • renounceOwnership:放弃所有权,只有当前所有者才能调用。放弃所有权后,合约将没有所有者,只有所有者才能使用的功能将被禁用。

  • transferOwnership:转让所有权,只有当前所有者才能调用。新所有者不能为0地址。

  • _transferOwnership:内部函数,用于将合约的所有权转移给新账户。

此外,还包括两个错误处理机制 OwnableUnauthorizedAccountOwnableInvalidOwner,用于处理未授权账户和无效所有者的问题。并且有一个事件 OwnershipTransferred,在所有权转移时触发。

完整代码:

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (access/Ownable.sol)

pragma solidity ^0.8.19;

import "../utils/Context.sol";

/**
 * @dev Contract module which provides a basic access control mechanism, where
 * there is an account (an owner) that can be granted exclusive access to
 * specific functions.
 *
 * The initial owner is set to the address provided by the deployer. This can
 * later be changed with {transferOwnership}.
 *
 * This module is used through inheritance. It will make available the modifier
 * `onlyOwner`, which can be applied to your functions to restrict their use to
 * the owner.
 */
abstract contract Ownable is Context {
    address private _owner;

    /**
     * @dev The caller account is not authorized to perform an operation.
     */
    error OwnableUnauthorizedAccount(address account);

    /**
     * @dev The owner is not a valid owner account. (eg. `address(0)`)
     */
    error OwnableInvalidOwner(address owner);

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

    /**
     * @dev Initializes the contract setting the address provided by the deployer as the initial owner.
     */
    constructor(address initialOwner) {
        _transferOwnership(initialOwner);
    }

    /**
     * @dev Throws if called by any account other than the owner.
     */
    modifier onlyOwner() {
        _checkOwner();
        _;
    }

    /**
     * @dev Returns the address of the current owner.
     */
    function owner() public view virtual returns (address) {
        return _owner;
    }

    /**
     * @dev Throws if the sender is not the owner.
     */
    function _checkOwner() internal view virtual {
        if (owner() != _msgSender()) {
            revert OwnableUnauthorizedAccount(_msgSender());
        }
    }

    /**
     * @dev Leaves the contract without owner. It will not be possible to call
     * `onlyOwner` functions. Can only be called by the current owner.
     *
     * NOTE: Renouncing ownership will leave the contract without an owner,
     * thereby disabling any functionality that is only available to the owner.
     */
    function renounceOwnership() public virtual onlyOwner {
        _transferOwnership(address(0));
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Can only be called by the current owner.
     */
    function transferOwnership(address newOwner) public virtual onlyOwner {
        if (newOwner == address(0)) {
            revert OwnableInvalidOwner(address(0));
        }
        _transferOwnership(newOwner);
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Internal function without access restriction.
     */
    function _transferOwnership(address newOwner) internal virtual {
        address oldOwner = _owner;
        _owner = newOwner;
        emit OwnershipTransferred(oldOwner, newOwner);
    }
}

代码分析:

import "../utils/Context.sol";

这段代码是一个 Solidity 的导入语句,它的作用是导入了位于 ../utils/ 路径下的 Context.sol 文件。在 Solidity 中,导入语句允许你在一个文件中使用另一个文件定义的合约、库、接口等。

Context.sol 通常是一个在 OpenZeppelin 合约库中定义的工具合约,它为合约提供了 _msgSender()_msgData() 方法,这两个方法返回交易的发送者(msg.sender)和数据载荷(msg.data),并且在某些上下文(例如当合约作为 delegatecall 的目标)中,这些方法比直接使用 msg.sendermsg.data 更加安全。

Ownable 合约中通过导入和继承 Context 合约,获得了使用 _msgSender() 方法的能力,这在 _checkOwner() 函数中使用到,用于判断当前交易的发送者是否为合约的所有者。

abstract contract Ownable is Context {
    address private _owner;

    /**
     * @dev The caller account is not authorized to perform an operation.
     */
    error OwnableUnauthorizedAccount(address account);

这段代码定义了一个名为 Ownable 的抽象合约,该合约继承了 Context 合约。抽象合约是一种包含了至少一个未实现(只有声明没有定义)函数的合约,它不能被直接部署,只能被继承。

Ownable 合约中,声明了一个私有的地址类型变量 _owner,用来存储该合约的所有者地址。

Ownable 合约也定义了一个名为 OwnableUnauthorizedAccount 的自定义错误。在 Solidity 0.8.4 及以后的版本中,允许定义自定义错误,以便在合约代码中通过 revert 语句抛出。这个 OwnableUnauthorizedAccount 错误在合约中的任何地方都可以使用 revert OwnableUnauthorizedAccount(account); 来抛出,其中 account 参数会被传递给错误。在代码中,当一个未授权的账户尝试执行某些只能由所有者执行的操作时,可以抛出这个错误。

/**
     * @dev The owner is not a valid owner account. (eg. `address(0)`)
     */
    error OwnableInvalidOwner(address owner);

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

这段代码继续定义了 Ownable 合约的两个要素:一个自定义错误 OwnableInvalidOwner 和一个事件 OwnershipTransferred

  • OwnableInvalidOwner 是一个自定义错误,用于处理所有者账户无效的情况,例如所有者账户地址为 address(0)。在 Solidity 中,address(0) 通常被视为无效地址,因为它是地址空间的默认值。这个错误在合约的 transferOwnership 函数中会被用到,如果尝试将所有权转移给 address(0),则会抛出这个错误。

  • OwnershipTransferred 是一个事件,它在所有权从一个账户转移给另一个账户时被触发。在 Solidity 中,事件主要用于在区块链上记录特定的状态改变,这些记录在以太坊区块链上是永久存储的。OwnershipTransferred 事件接受两个参数:previousOwnernewOwner,表示所有权的转移关系。这两个参数都使用了 indexed 关键字,这意味着这些参数在记录事件时被索引,以便在搜索和过滤事件时使用。

 /**
     * @dev Initializes the contract setting the address provided by the deployer as the initial owner.
     */
    constructor(address initialOwner) {
        _transferOwnership(initialOwner);
    }

这段代码定义了 Ownable 合约的构造函数。在 Solidity 中,构造函数是一个特殊的函数,它在合约被部署(创建)时被执行,且只执行一次。

constructor 函数接收一个 address 类型的参数 initialOwner,该参数预期是合约的初始所有者的地址。函数体中调用了 _transferOwnership 函数,将合约的所有权转移给 initialOwner

这意味着,当 Ownable 合约被部署时,部署者需要提供一个地址作为初始所有者。这个地址就会成为合约的所有者,能够调用合约中被 onlyOwner 修饰符修饰的函数。在此后,所有权可以通过 transferOwnership 函数进行更改。

/**
     * @dev Throws if called by any account other than the owner.
     */
    modifier onlyOwner() {
        _checkOwner();
        _;
    }

这段代码定义了一个名为 onlyOwner 的修饰符。在 Solidity 中,修饰符是一种特殊的函数,它可以被用于修改其他函数的行为。当一个函数被一个修饰符修饰时,修饰符的代码将在函数的代码之前(有时也可能是之后)执行。

在这个 onlyOwner 修饰符中,首先调用了 _checkOwner 函数。_checkOwner 函数的预期行为应该是检查当前的交易发送者是否为合约的所有者,如果不是,则抛出错误。这样,当一个函数被 onlyOwner 修饰符修饰时,如果这个函数被非所有者调用,则会因为 _checkOwner 函数的错误抛出而终止执行。

符号 _ 在修饰符中代表被修饰的函数的代码。在执行到 _ 时,控制权会转到被修饰的函数的代码,然后在被修饰的函数代码执行完毕后返回修饰符。在这个 onlyOwner 修饰符中,_ 后面没有其他代码,因此被修饰的函数执行完毕后,onlyOwner 修饰符也就执行完毕。

这个 onlyOwner 修饰符可以被用于限制只有合约的所有者才能调用的函数。例如,transferOwnershiprenounceOwnership 函数可能会被 onlyOwner 修饰,以保证只有当前所有者才能转移或放弃所有权。

 /**
     * @dev Returns the address of the current owner.
     */
    function owner() public view virtual returns (address) {
        return _owner;
    }

这段代码定义了一个名为 owner 的公开函数,它返回当前合约的所有者地址。

在 Solidity 中,函数的可见性可以是 publicprivateinternalexternalpublic 表示该函数可以在合约内部或外部被调用。

view 是一个关键字,表示该函数不会修改合约的状态,只会读取状态。这意味着,调用这个 owner 函数不会消耗任何 Gas,因为它不会改变区块链上的任何数据。

virtual 关键字表明该函数可以在派生合约中被重写。也就是说,任何继承了 Ownable 合约的合约都可以提供自己的 owner 函数实现。

该函数返回的是 _owner,这是一个在合约中定义的私有变量,存储的是当前合约的所有者地址。当你调用这个 owner 函数时,就可以获取到当前合约的所有者是谁。

function _checkOwner() internal view virtual {
        if (owner() != _msgSender()) {
            revert OwnableUnauthorizedAccount(_msgSender());
        }
    }

这段代码定义了一个名为 _checkOwner 的内部函数,它的作用是检查当前交易的发送者是否为合约的所有者。

该函数首先调用 owner 函数获取当前的所有者,然后调用 _msgSender 函数获取当前交易的发送者,然后比较这两个地址是否相等。如果不相等,表示当前的交易发送者不是所有者,然后函数通过 revert 语句抛出一个 OwnableUnauthorizedAccount 错误,并将当前交易的发送者地址传递给错误。

通常来说,这个函数以 _ 开头,按照 Solidity 的命名习惯,这通常表示这个函数是一个内部函数,不应该在合约外部被直接调用。

 /**
     * @dev Leaves the contract without owner. It will not be possible to call
     * `onlyOwner` functions. Can only be called by the current owner.
     *
     * NOTE: Renouncing ownership will leave the contract without an owner,
     * thereby disabling any functionality that is only available to the owner.
     */
    function renounceOwnership() public virtual onlyOwner {
        _transferOwnership(address(0));
    }

这段代码定义了一个名为 renounceOwnership 的公开函数,这个函数允许当前的合约所有者放弃所有权。

函数被 onlyOwner 修饰符修饰,表示只有合约的所有者才能调用这个函数。如果非所有者尝试调用这个函数,onlyOwner 修饰符将抛出一个错误。

函数体中调用了 _transferOwnership 函数,并向其传递了 address(0) 作为参数。_transferOwnership 函数预期会实现所有权的实际转移,并触发一个 OwnershipTransferred 事件。通过将所有权转移给 address(0),实际上就是使合约无主,也就是放弃所有权。

需要注意的是,一旦所有权被放弃,就无法再调用被 onlyOwner 修饰符修饰的函数,因为没有账户会满足这个修饰符的所有者条件。因此,使用这个 renounceOwnership 函数需要谨慎,因为它可能会禁用合约的部分功能。

/**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Can only be called by the current owner.
     */
    function transferOwnership(address newOwner) public virtual onlyOwner {
        if (newOwner == address(0)) {
            revert OwnableInvalidOwner(address(0));
        }
        _transferOwnership(newOwner);
    }

这段代码定义了一个名为 transferOwnership 的公开函数,这个函数用于将合约的所有权转移给新的所有者。

transferOwnership 函数接收一个 address 类型的参数 newOwner,表示新的所有者的地址。

这个函数被 onlyOwner 修饰符修饰,表示只有合约的当前所有者才能调用这个函数。

在函数体中,首先检查 newOwner 是否为 address(0),如果是,则通过 revert 语句抛出一个 OwnableInvalidOwner 错误,并传递 address(0) 给错误。这是因为在 Solidity 中,address(0) 通常被视为无效地址,因为它是地址空间的默认值。

然后,函数调用 _transferOwnership 函数,将所有权转移给 newOwner_transferOwnership 函数预期会实现所有权的实际转移,并触发一个 OwnershipTransferred 事件。

这个 transferOwnership 函数可以用于在必要时更改合约的所有者。例如,如果原所有者失去了私钥,或者由于某种原因需要将所有权交给其他人,就可以通过这个函数完成。

/**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Internal function without access restriction.
     */
    function _transferOwnership(address newOwner) internal virtual {
        address oldOwner = _owner;
        _owner = newOwner;
        emit OwnershipTransferred(oldOwner, newOwner);
    }

这段代码定义了一个名为 _transferOwnership 的内部函数,这个函数用于将合约的所有权转移给新的所有者。

_transferOwnership 函数接收一个 address 类型的参数 newOwner,表示新的所有者的地址。

在函数体中,首先将 _owner(当前的所有者地址)保存到 oldOwner 中,然后将 _owner 设置为 newOwner,从而完成所有权的实际转移。

然后,函数触发一个 OwnershipTransferred 事件,并传递 oldOwnernewOwner 给事件。这个事件可以被外部监听器用于监视所有权的变更。

这个函数是一个内部函数,按照 Solidity 的命名习惯,它以 _ 开头,表示这个函数不应该在合约外部被直接调用。而是应该通过其他函数(例如 transferOwnershiprenounceOwnership)间接调用。