Сегодня поговорим про такой полезный инструмент как AccessControl от OpenZeppelin, данная библиотека позволит вам регулировать доступ к разного рода функционалу на ваших умных контрактах и не только.
Вы сможете объявлять роли, присваивать эти роли другим пользователям и даже назначать роли для адресов, которые будут назначать другие роли. Это невероятный, гибкий и простой инструмент, с которым должен быть знаком каждый разработчик умных контрактов. А теперь к делу!
Для лучшего понимания материала рекомендую открыть код библиотеки в соседней вкладке.
Как роли хранятся в контракте
Первое что мы видим в контракте AccessControl это структура RoleData - она содержит в себе всю необходимую информацию, которая относится к роли:
mapping members - показывает какие адреса обладают ролью, возвращая true/false;
adminRole - здесь хранится хэш роли администратора, который может назначать новые адреса на роль, то есть если у меня есть adminRole, то я могу изменять состояние mapping members, если не назначить администратора, то в слоте adminRole по умолчанию будет хранится нулевой хэш(0x0000000000000000000000000000000000000000000000000000000000000000), то есть администратором будет обладатель роли DEFAULT_ADMIN_ROLE. Почему именно DEFAULT_ADMIN_ROLE? Потому что этой роли по умолчанию присвоен нулевой хэш на строке 57.
В свою очередь структуры RoleData хранятся в mapping _roles, в качестве входного аргумента mapping _roles принимает хэш роли, информацию по которой мы хотим получить. То есть: передаем в _roles хэш интересующей нас роли, а в ответ получаем объект, который содержит адреса всех обладателей этой роли и adminRole - хэш админской роли, её обладатель может назначать и удалять members.
Давайте получим хэш для импровизированной роли, которая будет называться ADMIN_ROLE, для этого достаточно передать в хэш-функцию keccak256 строку "ADMIN_ROLE":
// SPDX-License-Identifier: MIT
pragma solidity =0.8.9;
import "@openzeppelin/contracts/access/AccessControl.sol";
contract A is AccessControl {
// Если мы объявим в контракте такую константу, а затем обратимся к ней,
// то в ответ получим "0xa49807205ce4d355092ef5a8a18f56e8913cf4a201fbe287825b095693c21775"
// это и есть хэш роли
bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
// если обратимся к DEFAULT_ADMIN_ROLE, который объявлен в AccessControl,
// то в ответ получим "0x0000000000000000000000000000000000000000000000000000000000000000"
}
Обязательно ли объявлять роли в контракте? Нет, но такая практика упростит вам жизнь, потому что, во-первых, мы будем обращаться к ним, чтобы уточнить хэш и не получать его каждый раз в ручную при помощи своих сил, а во-вторых, сам контракт будет обращаться к этим переменным(об этом ещё будет сказано).
Внешние функции контракта AccessControl
В первую очередь будут описаны функции с модификатором видимости public, к внутренним(internal) функциям и их назначению мы ещё вернемся.
модификатор onlyRole(bytes32 role) - используется для того, чтобы указать обладатель какой роли может вызывать функцию.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.11;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
contract Token is ERC20, AccessControl {
// напишем самый простой токен стндарта ERC-20 с возможностью чеканить монеты
// объявляем роль того, кто может чеканить новые монеты
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
constructor() ERC20("Some token", "STKN") {
// даем деплоеру контракта роль DEFAULT_ADMIN_ROLE
// теперь деплоер сможет давать MINTER_ROLE другим адресам
_setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
}
// обратите внимание на onlyRole(MINTER_ROLE)
// благодаря этому модификатору функцию mint сможет вызвать только обладатель MINTER_ROLE
function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) {
_mint(to, amount);
}
}
supportsInterface(bytes4 interfaceId) - эта функция не имеет прямого отношения к теме текущего материала, если хотите узнать о её назначении, рекомендую ознакомится со стандартом EIP-165.
hasRole(bytes32 role, address account) - возвращает true/false в зависимости от того, есть ли у адреса(аргумент account) эта роль(аргумент role), можно использовать аналогично модификатору onlyRole, либо каким-то иным образом внутри функции, чтобы убедиться что адрес обладает ролью. Также можно вызывать hasRole снаружи напрямую, чтобы узнать есть ли у адреса какая-то роль.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.11;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
contract Token is ERC20, AccessControl {
// объявляем роль того, кто может чеканить новые монеты
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
constructor() ERC20("Some token", "STKN") {
// даем деплоеру контракта роль DEFAULT_ADMIN_ROLE
// теперь деплоер сможет давать MINTER_ROLE другим адресам
_setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
}
function mint(address to, uint256 amount) public {
// hasRole возвращает в require true/false,
// после этого в зависимости от значения
// выполнение функции продолжается, либо откатывается с
// сообщением "You are not a minter." соответственно
require(
hasRole(MINTER_ROLE, msg.sender),
"You are not a minter."
);
_mint(to, amount);
}
}
getRoleAdmin(bytes32 role) - возвращает хэш админской роли для роли, которая была передана как аргумент(role)
// SPDX-License-Identifier: MIT
pragma solidity 0.8.11;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
contract Token is ERC20, AccessControl {
// эта переменная вернет хэш "0xa49807205ce4d355092ef5a8a18f56e8913cf4a201fbe287825b095693c21775"
bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
// эта переменная вернет хэш "0x9f2df0fed2c77648de5860a4cc508cd0818c85b8b8a1ab4ceeef8d981c8956a6"
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
// эта переменная вернет хэш "0x3c11d16cbaffd01df69ce1c404f6340ee057498f5f00246190ea54220576a848"
bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");
constructor() ERC20("Some token", "STKN") {
// даем деплоеру контракта роль DEFAULT_ADMIN_ROLE
_setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
// эта функция устанавливет админскую роль для роли BURNER_ROLE
// другими словами: обладатель роли ADMIN_ROLE
// сможет назначать/удалять новых BURNER_ROLE
_setRoleAdmin(BURNER_ROLE, ADMIN_ROLE);
// обратите внимание, мы вызвали _setRoleAdmin только для BURNER_ROLE
// давать MINTER_ROLE, сможет только обладатель DEFAULT_ADMIN_ROLE
}
// эту функцию может вызвать только обладатель MINTER_ROLE
function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) {
_mint(to, amount);
}
// эту функцию может вызвать только обладатель BURNER_ROLE
function burn(address to, uint256 amount) public onlyRole(BURNER_ROLE) {
_burn(to, amount);
}
}
если вызвать getRoleAdmin(0x0000000000000000000000000000000000000000000000000000000000000000), то есть, если передать хэш роли DEFAULT_ADMIN_ROLE, то в ответ получим 0x0000000000000000000000000000000000000000000000000000000000000000, потому что по умолчанию DEFAULT_ADMIN_ROLE является администратором для всех ролей, для которых не был назначен администратор, для удобства в следующих примерах вместо хэша будут указаны переменные в которых этот хэш хранится
getRoleAdmin(ADMIN_ROLE) -> DEFAULT_ADMIN_ROLE, получим нулевой хэш, так как для этой роли мы не назначали администратора
getRoleAdmin(MINTER_ROLE) -> DEFAULT_ADMIN_ROLE, снова получим нулевой хэш в ответ, так как ситуация аналогична примеру выше(для ADMIN_ROLE)
getRoleAdmin(BURNER_ROLE) -> ADMIN_ROLE, здесь нам функция getRoleAdmin сообщает, что вот, мол, для BURNER_ROLE админ это обладатель роли ADMIN_ROLE, почему не DEFAULT_ADMIN_ROLE? потому что в конструкторе мы вызвали _setRoleAdmin(BURNER_ROLE, ADMIN_ROLE).
grantRole(bytes32 role, address account) - функция дающая роль(role) адресу(account), чтобы это сработало, вызывающий должен обладать ролью, которая является администратором по отношению к role, пример:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.11;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
contract Token is ERC20, AccessControl {
bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");
constructor() ERC20("Some token", "STKN") {
_setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
// эта функция устанавливет админскую роль для роли BURNER_ROLE
// другими словами: обладатель роли ADMIN_ROLE
// сможет назначать/удалять новых BURNER_ROLE
_setRoleAdmin(BURNER_ROLE, ADMIN_ROLE);
// обратите внимание, мы вызвали _setRoleAdmin только для BURNER_ROLE
// давать MINTER_ROLE, сможет только обладатель DEFAULT_ADMIN_ROLE
}
// эту функцию может вызвать только обладатель MINTER_ROLE
function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) {
_mint(to, amount);
}
// эту функцию может вызвать только обладатель BURNER_ROLE
function burn(address to, uint256 amount) public onlyRole(BURNER_ROLE) {
_burn(to, amount);
}
}
например, мы хотим дать адресу address1 роль ADMIN_ROLE, для этого обладатель DEFAULT_ADMIN_ROLE должен вызвать grantRole(address1, ADMIN_ROLE), после успешного завершения транзакции address1 пополнит обладателей роли ADMIN_ROLE
теперь, address1 хочет дать роль BURNER_ROLE адресу address2, он может это сделать, так как в пункте выше обладатель DEFAULT_ADMIN_ROLE дал роль ADMIN_ROLE адресу address1. Адрес address1 вызывает grantRole(address2, BURNER_ROLE), теперь адрес address2 стал обладателем роли BURNER_ROLE.
revokeRole(bytes32 role, address account) - функция обратная grantRole, если grantRole кому-то дает роль, то revokeRole, наоборот, забирает.
renounceRole(bytes32 role, address account) - функция отказа от роли, отказаться от роли может только её обладатель, то есть в качестве аргумента account должен быть передан адрес вызывающего транзакцию, в противном случае функция откатится и вы получите сообщение "AccessControl: can only renounce roles for self", представим ситуацию где это может использоваться:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.11;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
contract Token is ERC20, AccessControl {
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
constructor() ERC20("Some token", "STKN") {
// когда я буду деплоить контракт в сеть, то сделаю себя админом
_setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
// благодаря этому я могу назначать новых MINTER_ROLE
}
// эту функцию может вызвать только обладатель MINTER_ROLE
function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) {
_mint(to, amount);
}
// как вы уже поняли в этом контракте есть две роли:
// DEFAULT_ADMIN_ROLE - чтобы назначать новых MINTER_ROLE
// MINTER_ROLE - право чеканить монеты
// но что делать если я хочу передать администрирование
// контракта другому адресу, например, заказчику,
// для которого я делаю этот токен
// для этого сначала назначаем ещё одного DEFAULT_ADMIN_ROLE
// им будет заказчик:
// grantRole(DEFAULT_ADMIN_ROLE, customerAccount)
// теперь у нас есть два обладателя DEFAULT_ADMIN_ROLE: я и заказчик
// отказываемся от роли DEFAULT_ADMIN_ROLE:
// renounceRole(DEFAULT_ADMIN_ROLE, myAccount)
// теперь у контракта один DEFAULT_ADMIN_ROLE - заказчик
}
Внутренние функции контракта AccessControl
Все функции описанные выше имели модификатор видимости public, это значит что их можно вызвать откуда угодно. Функции же описанные в этом разделе все без исключения имеют модификатор видимости internal, это значит, что их можно вызвать либо в конструкторе, либо в внутри функции которая объявлена в контракте, наследуемый от AccessControl.
Давайте рассмотрим устройство каждой функции изнутри, заодно поймем, как изменяется storage AccessControl
_checkRole(bytes32 role) - проверить обладает ли отправитель ролью role
function _checkRole(bytes32 role) internal view virtual {
// внутри вызывается другая _checkRole, но она принимает уже два аргумента
// роль и интересующий нас адрес
_checkRole(role, _msgSender());
}
_checkRole(bytes32 role, address account) - проверит обладает ли account ролью role:
function _checkRole(bytes32 role, address account) internal view virtual {
// в if убеждаемся в том что у account нет role,
// если есть, то выполнение фукции идет дальше, минуя блок if
if (!hasRole(role, account)) {
// если роли нет, то все выполнение функции откатывается с сообщением
// "AccessControl: account (0x[0-9a-f]{40}) is missing role (0x[0-9a-f]{64})"
revert(
string(
abi.encodePacked(
"AccessControl: account ",
Strings.toHexString(uint160(account), 20),
" is missing role ",
Strings.toHexString(uint256(role), 32)
)
)
);
}
}
_setupRole(bytes32 role, address account) - тот же смысл, что и grantRole, но эту функцию рекомендуются вызывать только в конструкторе:
function _setupRole(bytes32 role, address account) internal virtual {
_grantRole(role, account);
}
_setRoleAdmin(bytes32 role, bytes32 adminRole) - установить администратора(adminRole) для роли(role):
function _setRoleAdmin(bytes32 role, bytes32 adminRole) internal virtual {
// записываем предыдущего админа
bytes32 previousAdminRole = getRoleAdmin(role);
// записываем нового админа для роли role
_roles[role].adminRole = adminRole;
// отправляем событие о смене админа
emit RoleAdminChanged(role, previousAdminRole, adminRole);
}
_grantRole(bytes32 role, address account) - дать адресу account роль role:
function _grantRole(bytes32 role, address account) internal virtual {
// проверяем что у адреса нет этой роли,
// чтобы лишний раз не перезаписывать слоты памяти
if (!hasRole(role, account)) {
// записываем адрес в число обладателей роли role
_roles[role].members[account] = true;
emit RoleGranted(role, account, _msgSender());
}
}
_revokeRole(bytes32 role, address account) - забрать у адреса account роль role:
function _revokeRole(bytes32 role, address account) internal virtual {
// проверяем что у адреса есть эта роль,
// чтобы лишний раз не перезаписывать слоты памяти
if (hasRole(role, account)) {
// убираем адрес account из числа обладателей роли role
_roles[role].members[account] = false;
emit RoleRevoked(role, account, _msgSender());
}
}
Послесловие
Остались вопросы? С чем-то не согласны? Пишите комментарии
Поддержать автора криптовалютой: 0x021Db128ceab47C66419990ad95b3b180dF3f91F