💹

権限をbytes32のIDで管理するOpenZeppelinのAccessControl.solのコードを読む

2022/06/17に公開約7,500字

こんにちは。JPYC研究開発チーム所属のretocroomanです。

皆さん、権限管理のライブラリは何を使っていますか?権限管理のライブラリとしてはOwnable.solが有名で、もはや一番使われているライブラリなのではないかとさえ思いますが、Ownable.solが扱えるのは一つの権限と一つのアドレスだけです。しかし、実際のところ複数の権限を複数のアドレスに対して付与したいと思ったことはありませんか?そんな時のためにOpenZeppelinはAccessControl.solを用意しています!

今回はそのコードを見ていきましょう。

前提知識

Solidity(前提知識集)

AccessControl.solの目的

複数の権限を複数のアドレスに他対他で付与したいときに使うライブラリです。もちろん、一対他、他対一、一対一でも使えます。基本的には権限のIDが0x00の権限を持っているアドレスがAccessContolコントラクトの管理者のようなイメージで、権限の管理をしています。権限を新しく作りたい場合は権限の名前を以下のようにハッシュ化して新しいIDを生成することができます。
bytes32 public constant MY_ROLE = keccak256("MY_ROLE");
このコントラクトを継承することにより複雑な権限の構成と直感的な管理が可能になりました。

AccessControl.solのコード解説

解説するコントラクトのコードのリンク
AccessControl.sol(OpenZeppelin)

https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/access/AccessControl.sol

AcceessControl.solの状態変数と修飾子の解説

    // 権限情報の構造体
    struct RoleData {
        mapping(address => bool) members;
        // adminRoleは対象の権限の管理者のIDが入る
        // デフォルトは0x00に設定されている
        bytes32 adminRole;
    }

    // bytes32のIDをkeyに権限情報の構造体をvalueにする
    mapping(bytes32 => RoleData) private _roles;

    // デフォルトの管理者権限のIDは0x00にしている
    bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00;

    // msg.senderがbytes32のIDの権限を持っているかどうか確かめる修飾子
    modifier onlyRole(bytes32 role) {
        _checkRole(role);
        _;
    }

onlyRole修飾子の中身ではIDを_checkRole関数に渡しています。では_checkRoke関数を見てみましょう。

    function _checkRole(bytes32 role) internal view virtual {
        // _msgSender()はAccessControlが継承しているContextライブラリのinternal関数でmsg.senderを返す
        _checkRole(role, _msgSender());
    }

_checkRole関数ではさらにmsg.senderの引数を渡して_checkRole関数を読んでいます。Solidityは引数の型が異なれば同じ関数名でも定義できます。しかし、なぜonlyRole修飾子から直接呼ばなかったのでしょうか…

    // 今度は権限のIDとアドレスが引数になっている
    function _checkRole(bytes32 role, address account) internal view virtual {
        if (!hasRole(role, account)) {
            // string型同士の結合文をエラー文に
            revert(
                string(
                    abi.encodePacked(
                        "AccessControl: account ",
                        // 権限を持っていないアドレスをstring型に変換
                        Strings.toHexString(uint160(account), 20),
                        " is missing role ",
                        // 権限のIDをstring型に変換
                        Strings.toHexString(uint256(role), 32)
                    )
                )
            );
        }
    }

Solidityでは文字列の扱いには特に注意が必要です。string型同士を結合する時は上記のように一度全てbytes型に変換して最後にstring型に戻すか、一つずつ文字を指定して結合していく方法があります。

条件文ではpublic view関数のhasRole関数の返り値がtrueかどうか判定して、falseの場合はrevertさせています。hasRole関数は次にやりますが、mapping型の状態変数_rolesを調べてbool値を返す関数です。

AcceessControl.solのhasRole関数(84行目)の解説

    // 権限をアカウントが持っていたらtrue、持っていなかったらfalseを返す
    // publicなので外部からも参照できる
    function hasRole(bytes32 role, address account) public view virtual override returns (bool) {
        // _rolesのvalueは構造体なのでメンバーのmembersを指定して、さらにmembersはmapping型なのでkeyを指定している
        return _roles[role].members[account];
    }

いろんなところから呼ばれますが、対象のアカウントが対象の権限を持っているかを確認するだけの関数です。

AcceessControl.solのgetRoleAdmin関数(128行目)の解説

    // 対象の権限の管理者のIDを取得する
    function getRoleAdmin(bytes32 role) public view virtual override returns (bytes32) {
        // _rolesのvalueは構造体なのでメンバーのadminRoleを指定している
        return _roles[role].adminRole;
    }

構造体で指定されたadminRoleの権限を持つアカウントはその権限の付与(grantRole)や剥奪(revokeRole)をすることができます。ちなみにadminRoleの初期値は0x00ですがinternal関数の_setRoleAdmin関数から変更できます。

    // roleのadminRoleを変更する
    function _setRoleAdmin(bytes32 role, bytes32 adminRole) internal virtual {
        // Event用に前の権限管理者のIDを取得しておく
        bytes32 previousAdminRole = getRoleAdmin(role);
        _roles[role].adminRole = adminRole;
        emit RoleAdminChanged(role, previousAdminRole, adminRole);
    }

このinternal関数である_setRoleAdmin関数はAccessControl.sol内部からは一度も呼ばれません。なので継承先のコントラクトから制限付きで呼び出されることを想定していると思われます。

AcceessControl.solのgrantRole関数(142行目)の解説

    // onlyRole(getRoleAdmin(role))でその権限の管理者のみ呼び出せるよう制限している
    function grantRole(bytes32 role, address account) public virtual override onlyRole(getRoleAdmin(role)) {
        _grantRole(role, account);
    }

権限で制限した後、_grantRole関数を呼んでいますね。では_grantRole関数を見てみましょう。

    function _grantRole(bytes32 role, address account) internal virtual {
        if (!hasRole(role, account)) {
            _roles[role].members[account] = true;
            emit RoleGranted(role, account, _msgSender());
        }
    }

また、hasRole(role, account)でbool値を呼んでいますね。これでmapping型の_rolesのメンバーを変更し、アドレスを登録しています。実はこのinternal関数は_setupRole関数からも呼ばれています。

    function _setupRole(bytes32 role, address account) internal virtual {
        _grantRole(role, account);
    }

_grantRole関数を呼んでいるだけですが、こちらは呼び出すアカウントがadminRoleの権限を持っている必要はありません。権限などの制限が一切ないためこちらはconstuctor関数もしくはinitialize関数で呼ばれるように用意されたみたいです。

AcceessControl.solのrevokeRole関数(155行目)とrenounceRole関数(173行目)の解説

続いてrevokeRole関数とrenounceRole関数です。二つとも対象のアカウントの権限を消す関数ですがrevokeRole関数はその権限の管理者が消し、renounceRole関数はその対象のアカウントが自分の権限を消します。

    // onlyRole(getRoleAdmin(role))でその権限の管理者のみ呼び出せるよう制限している
    function revokeRole(bytes32 role, address account) public virtual override onlyRole(getRoleAdmin(role)) {
        _revokeRole(role, account);
    }

internal関数の_revokeRole関数を呼んでいますね。renounceRole関数もこの_renounceRole関数を呼んでいます。

    function _revokeRole(bytes32 role, address account) internal virtual {
        // 対象のアカウントが対象の権限を持っているか確認してから権限を消す
        // 権限を持っていなければ何もしない
        if (hasRole(role, account)) {
            _roles[role].members[account] = false;
            emit RoleRevoked(role, account, _msgSender());
        }
    }

falseにして_rolesmembersから外していますね。これで次からhasRole関数でfalseが返されるようになります。ではrenounceRole関数を見ていきましょう。

     // 制限する修飾子はない
    function renounceRole(bytes32 role, address account) public virtual override {
        // 対象のアカウントがこの関数を呼び出したアカウントと一致するか確認している
        require(account == _msgSender(), "AccessControl: can only renounce roles for self");

        _revokeRole(role, account);
    }

こちらは修飾子ではなくrequire文で呼び出せるアカウントを制限をしていますね。それ以外はrevokeRole関数と全く同じです。

まとめ

今回コードを解説したAccessControl.solには拡張版があります。AccessControl.solには特定の権限が付与されたアドレスを列挙する関数が用意されていません。アドレスを列挙することはイベントログを集めれば出せますが、もしかしたらオンチェーンのアプリケーションに役に立つかもしれないということで、OpenZeppelinではAccessControlEnumerable.solも用意されています。こちらの拡張版を使えば特定の権限を持つアドレスリストをbytes32のroleから列挙することができます。正確にはアドレスリストの長さを返す変数があり、インデックスを指定して調べられます。

もし、複数の権限または権限を持つアドレスが複数の場合はこちらのライブラリの使用を検討してみませんか?

JPYC研究開発チームではこれからもOpenZeppelin等のコード解説を発信していく予定ですので、ご愛読のほどよろしくお願いいたします。

日本初のブロックチェーン技術(ERC20)を活用した日本円ステーブルコインJPYCはこちらから購入できます!
JPYC社はブロックチェーンエンジニアを募集中です!こちらからご応募お願いします!(タイミングにより募集を行なっていない場合があります)
また、ラボ形式でブロックチェーンに関する講義をしているJPYC開発コミュニティにも是非ご参加ください!

Discussion

ログインするとコメントできます