Как стать автором
Обновить

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

Время на прочтение9 мин
Количество просмотров1.7K

Сегодня поговорим про такой полезный инструмент как 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

Теги:
Хабы:
Рейтинг0
Комментарии0

Публикации

Истории

Работа

Ближайшие события

15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань