OpenZeppelinのAccess Controlの取り扱い説明書
今回はOpenZeppelinのAccess Controlに関しての説明をしていきます。まずはAccess Controlを使ってなんのメリットがあるのかを紹介します。
- ロールベースのアクセス制御を実装できる
- 複数アカウントへのロール割り当てが簡単に行える
- 明確なアクセス制御構造でシンプルで安全
といったように、Access Controlを使うと権限などの処理を容易にかつ簡単に実装できるので、権限周りで実装が必要な場合はこれを使うと良いでしょう!
Access Controlとは?
Access ControlとはOpenZeppelinが提供している権限やロールなどのアクセス制御を簡単&セキュアに使用できるライブラリです。
どんなプロジェクトでもロールベースのアクセス制御を行うことが多いので、このライブラリを使用する頻度は多くなるはずです。自分で全て書いて実装してもいいのですが、簡単&セキュアなので、変な心配をする必要がないので、必要ならば使うのは必須だと思います。
Access Controlの中身
実際に中身がどのようになっているのか見てみましょう!
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (access/AccessControl.sol)
pragma solidity ^0.8.20;
import {IAccessControl} from "./IAccessControl.sol";
import {Context} from "../utils/Context.sol";
import {ERC165} from "../utils/introspection/ERC165.sol";
abstract contract AccessControl is Context, IAccessControl, ERC165 {
// RoleData
struct RoleData {
// ここで誰がロールを持っているかを確認できる
mapping(address account => bool) hasRole;
// 管理者の名称を保存している(ex. SUPER_ADMIN)
bytes32 adminRole;
}
// ロール名=> RoleDataのマッピング
mapping(bytes32 role => RoleData) private _roles;
// デフォルトの管理者の名称
bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00;
/**
* @dev Modifier that checks that an account has a specific role. Reverts
* with an {AccessControlUnauthorizedAccount} error including the required role.
* ロールの持ち主かどうかを確認するmodifier
*/
modifier onlyRole(bytes32 role) {
_checkRole(role);
_;
}
/**
* @dev See {IERC165-supportsInterface}.
* ERC165のインターフェースをサポートしているかどうかを確認する
*/
function supportsInterface(
bytes4 interfaceId
) public view virtual override returns (bool) {
return
interfaceId == type(IAccessControl).interfaceId ||
super.supportsInterface(interfaceId);
}
/**
* @dev Returns `true` if `account` has been granted `role`.
* `account`が`role`を持っているかどうかを確認する
*/
function hasRole(
bytes32 role,
address account
) public view virtual returns (bool) {
return _roles[role].hasRole[account];
}
/**
* @dev Reverts with an {AccessControlUnauthorizedAccount} error if `_msgSender()`
* is missing `role`. Overriding this function changes the behavior of the {onlyRole} modifier.
* ロールの持ち主かどうかを確認する
*/
function _checkRole(bytes32 role) internal view virtual {
_checkRole(role, _msgSender());
}
/**
* @dev Reverts with an {AccessControlUnauthorizedAccount} error if `account`
* is missing `role`.
*/
function _checkRole(bytes32 role, address account) internal view virtual {
if (!hasRole(role, account)) {
revert AccessControlUnauthorizedAccount(account, role);
}
}
/**
* @dev Returns the admin role that controls `role`. See {grantRole} and
* {revokeRole}.
*
* To change a role's admin, use {_setRoleAdmin}.
* ロールの管理者を確認する
*/
function getRoleAdmin(bytes32 role) public view virtual returns (bytes32) {
return _roles[role].adminRole;
}
/**
* @dev Grants `role` to `account`.
*
* If `account` had not been already granted `role`, emits a {RoleGranted}
* event.
*
* Requirements:
*
* - the caller must have ``role``'s admin role.
*
* May emit a {RoleGranted} event.
* ロールの付与
* role: 付与するロール
* account: 付与するアカウント
*/
function grantRole(
bytes32 role,
address account
) public virtual onlyRole(getRoleAdmin(role)) {
_grantRole(role, account);
}
/**
* @dev Revokes `role` from `account`.
*
* If `account` had been granted `role`, emits a {RoleRevoked} event.
*
* Requirements:
*
* - the caller must have ``role``'s admin role.
*
* May emit a {RoleRevoked} event.
* ロールを剥奪する
*/
function revokeRole(
bytes32 role,
address account
) public virtual onlyRole(getRoleAdmin(role)) {
_revokeRole(role, account);
}
/**
* @dev Revokes `role` from the calling account.
*
* Roles are often managed via {grantRole} and {revokeRole}: this function's
* purpose is to provide a mechanism for accounts to lose their privileges
* if they are compromised (such as when a trusted device is misplaced).
*
* If the calling account had been revoked `role`, emits a {RoleRevoked}
* event.
*
* Requirements:
*
* - the caller must be `callerConfirmation`.
*
* May emit a {RoleRevoked} event.
* 自発的にロールを剥奪する
*/
function renounceRole(
bytes32 role,
address callerConfirmation
) public virtual {
if (callerConfirmation != _msgSender()) {
revert AccessControlBadConfirmation();
}
_revokeRole(role, callerConfirmation);
}
/**
* @dev Sets `adminRole` as ``role``'s admin role.
*
* Emits a {RoleAdminChanged} event.
* ロールの管理者を設定する
*/
function _setRoleAdmin(bytes32 role, bytes32 adminRole) internal virtual {
bytes32 previousAdminRole = getRoleAdmin(role);
_roles[role].adminRole = adminRole;
emit RoleAdminChanged(role, previousAdminRole, adminRole);
}
/**
* @dev Attempts to grant `role` to `account` and returns a boolean indicating if `role` was granted.
*
* Internal function without access restriction.
*
* May emit a {RoleGranted} event.
* ロールを付与する
*/
function _grantRole(
bytes32 role,
address account
) internal virtual returns (bool) {
if (!hasRole(role, account)) {
_roles[role].hasRole[account] = true;
emit RoleGranted(role, account, _msgSender());
return true;
} else {
return false;
}
}
/**
* @dev Attempts to revoke `role` to `account` and returns a boolean indicating if `role` was revoked.
*
* Internal function without access restriction.
*
* May emit a {RoleRevoked} event.
* ロールを剥奪する
* role: 剥奪するロール
* account: 剥奪するアカウント
*/
function _revokeRole(
bytes32 role,
address account
) internal virtual returns (bool) {
if (hasRole(role, account)) {
_roles[role].hasRole[account] = false;
emit RoleRevoked(role, account, _msgSender());
return true;
} else {
return false;
}
}
}
主要な関数をピックアップして紹介していきます。
hasRole
function hasRole(
bytes32 role,
address account
) public view virtual returns (bool) {
return _roles[role].hasRole[account];
}
hasRoleはとてもシンプルで、対象のロールを対象のアドレス(アカウント)が持っているかどうかを確認してboolで返却します。
grantRole
function grantRole(
bytes32 role,
address account
) public virtual onlyRole(getRoleAdmin(role)) {
_grantRole(role, account);
}
function _grantRole(
bytes32 role,
address account
) internal virtual returns (bool) {
if (!hasRole(role, account)) {
_roles[role].hasRole[account] = true;
emit RoleGranted(role, account, _msgSender());
return true;
} else {
return false;
}
}
grantRoleはロールの付与です。
付与したいroleと付与したいアドレスを渡してロール付与を行います。
もし既に付与したいroleを持っていたらfalseが返るので注意!
revokeRole
function revokeRole(
bytes32 role,
address account
) public virtual onlyRole(getRoleAdmin(role)) {
_revokeRole(role, account);
}
function _revokeRole(
bytes32 role,
address account
) internal virtual returns (bool) {
if (hasRole(role, account)) {
_roles[role].hasRole[account] = false;
emit RoleRevoked(role, account, _msgSender());
return true;
} else {
return false;
}
}
revokeRoleはロールを剥奪する関数です。
剥奪したいroleと対象のaddressを指定してロールの剥奪をします。
これも同じく、剥奪したいroleを持っていないならfalseが返ります。
renounceRole
function renounceRole(
bytes32 role,
address callerConfirmation
) public virtual {
// 対象のアドレスと今実行しているアドレスが一致しないならリバート
if (callerConfirmation != _msgSender()) {
revert AccessControlBadConfirmation();
}
_revokeRole(role, callerConfirmation);
}
renounceRoleは少し取り扱いに注意が必要です。
何をする関数なのかというと、もし自分のデバイスが損失したりなんらかの理由で、アカウントが乗っ取られたりする可能性があったりしたときに、自分のロールを剥奪しないと、何かしら困ることがある時に使用します。なので、自分で自分のロールを剥奪できます。
ですが、これはロール管理して権限などを実装している場合は、自分で剥奪したロールには誰もおらず、そのロールしか扱えない関数などあった場合には大問題です。何もできなくなる可能性もあるので扱いに注意が必要です。
使い方
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;
import "@openzeppelin/contracts/access/AccessControl.sol";
contract AccessContract is AccessControl {
// 書き込みができるロールとしてWRITER_ROLEを定義
bytes32 public constant WRITER_ROLE = keccak256("WRITER_ROLE");
bytes32 public constant MEMBER_ROLE = keccak256("MEMBER_ROLE");
uint256 public value;
constructor(address defaultAdmin, address writer) {
// adminとしてdefaultAdminを設定
_grantRole(DEFAULT_ADMIN_ROLE, defaultAdmin);
// WRITER_ROLEのadminとしてDEFAULT_ADMIN_ROLEを設定
_setRoleAdmin(WRITER_ROLE, DEFAULT_ADMIN_ROLE);
// MEMBER_ROLEのadminとしてDEFAULT_ADMIN_ROLEを設定
_setRoleAdmin(MEMBER_ROLE, DEFAULT_ADMIN_ROLE);
// writerとしてwriterを設定
_grantRole(WRITER_ROLE, writer);
}
// Memberのみがincrementを実行できる
function increment() external {
require(hasRole(MEMBER_ROLE, msg.sender), "Caller is not a member");
value++;
}
// WRITER_ROLEを持っているアカウントのみがdecrementを実行できる
function decrement() external onlyRole(WRITER_ROLE) {
require(value > 0, "Value cannot be negative");
value--;
}
// Default adminのみがsetValueを実行できる
function setValue(uint256 _value) external onlyRole(DEFAULT_ADMIN_ROLE) {
value = _value;
}
// Memberを追加する
// Default adminのみが実行できる
function addMember(address member) external onlyRole(DEFAULT_ADMIN_ROLE) {
require(member != address(0), "member is the zero address");
_grantRole(MEMBER_ROLE, member);
}
}
ざっくりですが、上記が使用例になります。
今回は、WRITER_ROLE(書き込みの役割)と MEMBER_ROLE(メンバー)の適当な名前で用意してます。
constructor
constructor(address defaultAdmin, address writer) {
// adminとしてdefaultAdminを設定
_grantRole(DEFAULT_ADMIN_ROLE, defaultAdmin);
// WRITER_ROLEのadminとしてDEFAULT_ADMIN_ROLEを設定
_setRoleAdmin(WRITER_ROLE, DEFAULT_ADMIN_ROLE);
// MEMBER_ROLEのadminとしてDEFAULT_ADMIN_ROLEを設定
_setRoleAdmin(MEMBER_ROLE, DEFAULT_ADMIN_ROLE);
// writerとしてwriterを設定
_grantRole(WRITER_ROLE, writer);
}
ここで、defaltAdminのアドレスにコントラクトのDEFAULT_ADMIN_ROLEの権限を付与します。そして、各ロールのadminの権限ロールをDEFAULT_ADMIN_ROLEに付与してdefaltAdminのアドレスが色々対応できるようにしてます。
ここでDEFAULT_ADMIN_ROLEのロールを付与しないパターンもできます。その場合は、ロール付与などの関数を別途作成したりしないといけません。
最後にWRITER_ROLEのロールをwriterのアドレスに対して付与してます。この付与はここでしなくても大丈夫ですが、今回は例として書いてます。
increment
function increment() external {
require(hasRole(MEMBER_ROLE, msg.sender), "Caller is not a member");
value++;
}
ここは単純で、incrementはMEMBER_ROLEを持った人しかできないようにしています。ここでは、requireを使った書き方で制御しています。
decrement
function decrement() external onlyRole(WRITER_ROLE) {
require(value > 0, "Value cannot be negative");
value--;
}
ここでは、onlyRoleのmodifierを使ってWRITER_ROLEを持っている人のみが扱えるようにしています。
setValue
function setValue(uint256 _value) external onlyRole(DEFAULT_ADMIN_ROLE) {
value = _value;
}
ここはDEFAULT_ADMIN_ROLEを持った人、すなわち初期にロール付与したアドレスのみを許可している関数です。事実上1人しか操作できないonlyOwnerのmodifierと似てますね。
addMember
function addMember(address member) external onlyRole(DEFAULT_ADMIN_ROLE) {
require(member != address(0), "member is the zero address");
_grantRole(MEMBER_ROLE, member);
}
ここでもDEFAULT_ADMIN_ROLEを持った人のみが使えるようにしてあります。メンバーのロールを増やしたい時に使う関数です。
まとめ
OpenZeppelinのAccess Controlは、ロールベースのアクセス制御を簡単かつ安全に実装するためのライブラリです。Access Controlの利点としては、複数のアカウントへのロール割り当ての容易さや、明確なアクセス制御構造の提供が挙げられます。特に権限周りの処理が必要なプロジェクトでは、このライブラリの利用は大いに推奨されます。独自にアクセス制御の実装を行うよりも、OpenZeppelinのAccess Controlを用いることで、セキュアかつ効率的な実装が可能になるため、多くのプロジェクトでの利用が期待されます。