Руководство по AccessControl от OpenZeppelin

от автора

Сегодня поговорим про такой полезный инструмент как 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) функциям и их назначению мы ещё вернемся.

  1. модификатор 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);     } }
  1. supportsInterface(bytes4 interfaceId) — эта функция не имеет прямого отношения к теме текущего материала, если хотите узнать о её назначении, рекомендую ознакомится со стандартом EIP-165.

  2. 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);     } }
  1. 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).

  1. 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.

  1. revokeRole(bytes32 role, address account) — функция обратная grantRole, если grantRole кому-то дает роль, то revokeRole, наоборот, забирает.

  2. 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

  1. _checkRole(bytes32 role) — проверить обладает ли отправитель ролью role

function _checkRole(bytes32 role) internal view virtual {     // внутри вызывается другая _checkRole, но она принимает уже два аргумента     // роль и интересующий нас адрес     _checkRole(role, _msgSender());  }
  1. _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)                 )             )         );     } }
  1. _setupRole(bytes32 role, address account) — тот же смысл, что и grantRole, но эту функцию рекомендуются вызывать только в конструкторе:

function _setupRole(bytes32 role, address account) internal virtual {     _grantRole(role, account); }
  1. _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); }
  1. _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());     } }
  1. _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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *