OpenZeppelinのOwnableの取り扱い説明書
OpenZeppelinのライブラリの1つであるOwnableについてまとめていきます。これを使うとどういったメリットがあるのかを簡単に説明すると以下のようになります。
- オーナーシップによる関数などの制御ができる
- セキュリティの向上につながる
- オーナーシップの譲渡の実装などを簡単にできる
詳しくこの後に説明していきます!
Ownableとは?
OpwnZepplinのOwnableはコントラクトのオーナーシップを管理することができます。実際にスマートコントラクトはweb2と違い誰でもアクセスすることが可能です。しかし、中にはオーナ(所有者・作成者)のみしか使いたくないといった関数などもあります。そんな時に、このOwnableを使うことにより安全にオーナーシップを扱うことができます。
どんな時に使うのかというと、トークンを引き出すときやNFTをmintする時やburnする時など、使用用途は様々です。制御をかけてないとイタズラにNFTをburnされて失ったり、トークンなどを盗まれたりしますので、制御は必須なのです。
Ownableの中身
では、中身はどうなっているのでしょうか?
意外とシンプルでなので、コードと共に説明します。
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (access/Ownable.sol)
pragma solidity ^0.8.20;
import {Context} from "../utils/Context.sol";
abstract contract Ownable is Context {
// ownerのアドレス
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) {
if (initialOwner == address(0)) {
revert OwnableInvalidOwner(address(0));
}
_transferOwnership(initialOwner);
}
/**
* @dev Throws if called by any account other than the owner.
* オーナー以外が呼び出したらエラー出るようにmodifierを設定する
*/
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.
* オーナーを無しにする
* オーナーがいなくなるとonlyOwnerの関数は一生呼び出せなくなるので注意が必要
*/
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);
}
}
とこのように結構シンプルです。各関数を見ていきましょう。
modifier
modifier onlyOwner() {
_checkOwner();
_;
}
function _checkOwner() internal view virtual {
if (owner() != _msgSender()) {
revert OwnableUnauthorizedAccount(_msgSender());
}
}
こちらに関しては、ownerなら無事に通過できるようなmodifierになっていて、もしオーナーでない場合はリバートするようになってます。
owner()
function owner() public view virtual returns (address) {
return _owner;
}
こちらに関しては自分の関数においていつでもownerのアドレスを呼んで処理に使えます。
transferOwnership
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);
}
これはその名の通りオーナーシップを他のアドレスへ譲渡する処理になってます。これなら他のアドレスへオーナーシップを譲渡したい場合でも問題なく行えます。
renounceOwnership()
function renounceOwnership() public virtual onlyOwner {
_transferOwnership(address(0));
}
これを扱うときは要注意です!(これは必要なのか?)とも思える処理で、完全にオーナーシップを捨てます。これをした途端にコントラクトはオーナーが不在になり、onlyOwnerの付いた関数を一生呼べなくなります!
Ownableの使い方
後気になるのは使い方ですよね。使い方はすごく簡単です。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;
import "@openzeppelin/contracts/access/Ownable.sol";
contract MyContract is Ownable {
uint256 public value;
constructor(address initialOwner) Ownable(initialOwner) {
value = 0;
}
function increment() external {
value++;
}
function decrement() external {
require(value > 0, "Value cannot be negative");
value--;
}
function setValue(uint256 _value) external onlyOwner {
value = _value;
}
}
自分のcontractへ以下のようにOwnableを継承します。(importも忘れずに)
import "@openzeppelin/contracts/access/Ownable.sol";
contract MyContract is Ownable {
そしてconstructorでオーナーシップを得たいアドレスを受け渡します。
constructor(address initialOwner) Ownable(initialOwner) {}
これだけでOwnableの他の関数や制御を使うことができ安全なスマートコントラクトへと近づきます!
Ownableを自分で実装した場合
Ownableみたいなフルに機能が欲しいのではなくて、単純にオーナーしか触れない制御だけ欲しいとかもあるので、そういった場合に以下のように実装します。
contract MyContract2 {
address private _owner;
constructor(address initialOwner) {
_owner = initialOwner;
}
modifier onlyOwner() {
require(_owner == msg.sender, "Ownable: caller is not the owner");
_;
}
}
意外とシンプルで簡単です。これなら逆に自分で書いてもいいかなって思っちゃいますね。ですが、セキュリティの監査をしてない場合はなるべく安全に監査済みのOpenZeppelinを使った方がいい時もあります。意外としょうもない間違いを人は起こしてしまうので、要注意です!
まとめ
OpenZeppelinのOwnableはどうだったでしょうか?
スマートコントラクトを安全に運用したいなら、こういったライブラリも駆使してなるべく脆弱性を出さないように実装できたらストレスも少なく、安全にブロックチェーンを扱えるのではないでしょうか?ぜひ使ってみてください!