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