Сегодня поговорим про такой полезный инструмент как 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
ссылка на оригинал статьи https://habr.com/ru/post/691274/
Добавить комментарий