基本的に、コントラクトの所有権を移転する際には、例えばガバナンス権限を移転したり、プロキシ管理者を変更したりする場合など、非常にリスクが高い行動です。間違ったアカウントに権限を移転してしまった場合、ほとんどの場合、所有権を永久に失うことになります。
このOwnership権限の誤用やミスにより、コントラクトがハッキングされてプロジェクトに影響を与えてしまうケースも少なくはありません。
Ownershipの移転のプロセスをより安全にする方法を模索してみましょう。
現状
まず、現在のOwnership移転の方法を見てみましょう(出典:openzeppelin-contracts/contracts/access/Ownable.sol):
// From openzeppelin-contracts/contracts/access/Ownable.sol (v4.7.0)
address private _owner;
// ...
/**
* @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 {
require(newOwner != address(0), "Ownable: new owner is the zero address");
_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);
}
ご覧の通り、コントラクトの所有者がtransferOwnershipを呼び出すだけです。
Ownershipは、引数として渡されたアドレスにすぐに移転されます。
改善案の提案
単一のステップでOwnershipを移転するtransferOwnershipの代わりに、confirmOwnershipTransfer関数を追加して2ステップのプロセスにするとどうでしょうか。
つまり、transferOwnershipを行うと、Ownershipを移転するのではなく、移転をキューに入れるだけです。
次に、新しい所有者候補が明示的にconfirmOwnershipTransferを呼び出し、所有権を受け取ることを受け入れる必要があります。これは、新しい所有者候補が呼び出さない場合、処理が失敗することを意味します。
これは、間違ったアカウントに権限を移転している場合、期待される新しい所有者で確認を試みて失敗したときに気付くことになります。その場合、再びtransferOwnershipを呼び出して、新しい所有者候補を上書きすることができます(移転操作を完全にキャンセルしたい場合は、現在の所有者を候補として使用することもできます)。
上記で説明したフローは以下のようになります。
// Modification over openzeppelin-contracts/contracts/access/Ownable.sol (v4.7.0)
address private _owner;
address private _ownerCandidate;
// ...
/**
* @dev Enqueues the ownership transfer of the contract to a new account (`newOwnerCandidate`).
* Can only be called by the current owner.
*/
function transferOwnership(address newOwnerCandidate) public virtual onlyOwner {
_ownerCandidate = newOwnerCandidate;
}
/**
* @dev Confirms the ownership transfer of the contract to a new account.
* Can only be called by the new owner candidate.
*/
function confirmOwnershipTransfer() public virtual {
require(_ownerCandidate == _msgSender(), "Ownable: caller is not the owner candidate");
_transferOwnership(_ownerCandidate);
}
より強化したバージョン
さらなるセキュリティを求める場合、新しい所有者候補と現在の所有者の両方にconfirmOwnershipTransferを呼び出すよう要求することもできます。
この追加のステップは、間違ったアドレスにtransferOwnershipを行い、正しいものに候補を上書きする前に、confirmOwnershipTransferを実行して誤った移転を受け入れるというミスを防ぐのに役立ちます。
再び、以下は上記の説明に基づく実装です:
// Modification over openzeppelin-contracts/contracts/access/Ownable.sol (v4.7.0)
bool private _transferConfirmedByOwnerCandidate;
address private _owner;
address private _ownerCandidate;
// ...
/**
* @dev Enqueues the ownership transfer of the contract to a new account (`newOwnerCandidate`).
* Can only be called by the current owner.
*/
function transferOwnership(address newOwnerCandidate) public virtual onlyOwner {
_ownerCandidate = newOwnerCandidate;
}
/**
* @dev Confirms the ownership transfer of the contract to a new account.
* Must be first called by the owner candidate and then by the current owner.
*/
function confirmOwnershipTransfer() public virtual {
if (_transferConfirmedByOwnerCandidate) {
require(_owner == _msgSender(), "Ownable: caller is not the owner");
_transferConfirmedByOwnerCandidate = false;
_transferOwnership(_ownerCandidate);
} else {
require(_ownerCandidate == _msgSender(), "Ownable: caller is not the owner candidate");
_transferConfirmedByOwnerCandidate = true;
}
}
結論
今回はSmart ContractのOwnership移転方法をより安全な設計で実装する幾つかのパターンを紹介しました。上記で紹介したパターンは、所有権の移転において通常よりも多くのフローを要するものですが、Ownershipの移転はそう多く発生することではないので可用性とのバランスも非常に良いと思います。ぜひ検討してみて下さい。