Что такое модификатор в Solidity?

В документации Solidity модификаторы определяются следующим образом:

Модификаторы можно использовать для изменения поведения функций декларативным способом.

Из этого определения можно понять, что модификатор направлен на изменение поведения функции, к которой он присоединен.

Например, автоматическая проверка условий перед выполнением функции (для этого модификаторы в основном и используются).

Модификаторы уменьшают избыточность кода. Вы можете повторно использовать один и тот же модификатор в нескольких функциях, если вы проверяете одно и то же условие в смарт-контракте.

Когда использовать модификатор в Solidity?

Основное применение модификаторов - автоматическая проверка условия перед выполнением функции. Если функция не удовлетворяет требованию модификатора, возникает исключение, и выполнение функции прекращается.

Как создавать и использовать модификаторы?

Модификаторы могут быть созданы (объявлены) следующим образом:

modifier MyModifier {
   // здесь код модификатора...
}

Вы можете написать модификатор с аргументами или без них. Если модификатор не имеет аргумента, можно опустить круглые скобки ().

modifier ModifierWithArguments(uint256 a) {
   // ...
}

modifier ModifierWithoutArguments() {
   // ...
}

modifier ModifierWithoutArguments {
   // ...
}

Как работают модификаторы?

Давайте начнем с базовых примеров, приведенных ниже.

modifier OnlyOwner {
   require(msg.sender == owner);
   _;
}

Символ _;

Символ _; называется подстановочным знаком. Он объединяет код функции с кодом модификатора.

Другими словами, тело функции (к которой присоединен модификатор) будет вставлено туда, где в определении модификатора появляется специальный символ _;.

Используя термины документации Solidity, этот символ "возвращает поток выполнения к исходному коду функции".

Модификатор должен содержать символ _; в своем теле. Это обязательно.

Куда поместить _; ?

Вместо _; будет подставлена функция, поэтому выполнение функции зависит от того места, где вы укажите этот символ: до, посредине или после основного кода модификатора.

modifier SomethingBefore {
   require(/* сначала проверьте что-нибудь */);
   _; // возобновить выполнение функции
}

modifier SomethingAfter {
   _; // запускаем сперва функцию
   require(/* затем проверяем что-нибудь */)
}

Как показано в примере выше, вы можете поместить символ _; в начало, середину или конец тела модификатора.

На практике наиболее безопасной схемой использования является размещение _; в конце. В этом сценарии модификатор служит для последовательной проверки условий, то есть для того, чтобы сначала проверить условие, а затем продолжить выполнение функции. Приведенный ниже код демонстрирует это на примере:

function isOkay() public view returns(bool) {
   // выполнить проверку истинности
   return true;
}

function isAuthorised(address _user) public view returns(bool) {
   // логика проверки авторизации _user
   return true;
}

modifier OnlyIfOkAndAuthorised {
   require(isOkay());
   require(isAuthorised(msg.sender));
   _;
}

Передача аргументов модификаторам

Модификаторы также могут принимать аргументы. Как и в случае с функцией, перед именем модификатора нужно передать тип переменной и её имя в круглых скобках.

modifier Fee(uint _fee) {
   if (msg.value >= _fee) {
       _;
   }
}

Используя приведенный выше пример, вы можете убедиться, что пользователь (или смарт-контракт), вызывающий одну из ваших функций смарт-контракта, отправил несколько eth для оплаты требуемого сбора.

Давайте проиллюстрируем это на простом примере.

Вы хотите, чтобы каждый пользователь, желающий забрать свои деньги, хранящиеся в смарт-контракте, платил минимальную сбор в размере 2,5 % eth в пользу смарт-контракта.

Модификатор с аргументами может исполнить такое поведение.

contract Vault {
    modifier fee(uint256 _fee) {
        if (msg.value != _fee) {
            revert("Вы должны заплатить комиссию, чтобы снять свои эфиры");
        } else {
            _;
        }
    }

    function deposit(address _user, uint256 _amount) external {
        // ...
    }

    function withdraw(uint256 _amount) external payable fee(0.025 ether) {
        // ...
    }
}

Иной пример: нужно проверить, что функцию может вызывать только определенный адрес.

modifier notSpecificAddress (address _user) {
  if (_user === msg.sender) throw;
  _;
}

function someFunction(address _user) notSpecificAddress("0x......") {
   // ...
}

Все аргументы вводимые в функцию можно использовать в модификаторе.

Применение нескольких модификаторов к функции.

К одной функции можно применить несколько модификаторов. Это можно сделать следующим образом:

contract OwnerContract {
    address public owner = msg.sender;
    uint256 public creationTime = now;

    modifier onlyBy(address _account) {
        require(msg.sender == _account, "Отправитель не авторизован.");
        _;
    }
    modifier onlyAfter(uint256 _time) {
        require(now >= _time, "Функция вызвана слишком рано");
        _;
    }

    function disown() public onlyBy(owner) onlyAfter(creationTime + 6 weeks) {
        delete owner;
    }
}

Модификаторы будут выполняться в том порядке, в котором они определены, то есть слева направо. Так, в приведенном выше примере функция будет проверять следующие условия перед запуском:

  1. onlyBy(...) : является ли адрес, вызывающий смарт-контракт, владельцем?

  2. onlyAfter(...) : является ли вызывающий владельцем смарт-контракта более 6 недель?

Переопределение модификаторов

Как и функции, модификаторы, определенные в одном смарт-контракте, могут быть переопределены другими смарт-контрактами, которые являются производными от него. Как следует из документации Solidity:

  • Модификаторы являются наследуемыми свойствами смарт-контрактов и могут быть переопределены производными смарт-контрактами.

Поэтому символы, введенные в модификатор, не видны в функции (поскольку модификатор может измениться, если он будет переопределен в производном смарт-контракте).

Модификаторы с перечислениями

Если ваш смарт-контракт содержит переменную типа enum, вы можете проверить значение, которое она содержит, передав один из доступных вариантов в качестве аргумента модификатору.

enum State { Created, Locked, Inactive }

State state;

modifier isState(State _expectedState) {
   require(state == _expectedState);
   _;
}

Модификаторы на практике

Только владелец или определенные пользователи

Можно использовать модификатор для проверки того, что функция может быть вызвана и выполнена только по определенному адресу.

modifier OnlyBy(address _account) {
   require(msg.sender == _account, "отправитель не авторизован");
   _;
}

Поддержка нескольких администраторов

Или вместо того чтобы ограничивать вызов функции только одним определенным пользователем, мы можем дать это право запуска функции нескольким заранее определенным пользователям.

modifier onlyAdmins {
    // функция isUserAdmin проверяет вхождение пользователя 
    // в список админов
    if (!isUserAdmin(msg.sender)) throw;
    _;
  }

Проверка данных

Еще один отличный вариант использования модификаторов - проверка вводимых данных.

Ниже приведены некоторые примеры, основанные на различных типах данных.

modifier throwIfAddressIsInvalid(address _target) {
   if (_target == 0x0) throw;
   _;
}

modifier throwIfIsEmptyString(string _id) {
   if (bytes(_id).length == 0) throw;
   _;
}

modifier throwIfEqualToZero(uint _id) {
   if (_id == 0) throw;
   _;

}

modifier throwIfIsEmptyBytes32(bytes32 _id) {
   if (_id == "") throw;
   _;
}

Проверка истечения промежутка времени

modifier OnlyAfter(uint _time) {
   require(now >= _time, "функция вызвана слишком рано!");
   _;
}

Возврат случайно отправленного eth

Мир блокчейна не допускает ошибок. Если eth или другие ценности были случайно отправлены не по адресу, то тут нет кого-то, к которому можно обратиться с претензиями, поскольку нет банка или центрального органа, контролирующего транзакции.

Однако вы можете подготовить свой смарт-контракт к непредвиденным ситуациям, сказав ему, что делать, когда пользователи ведут себя странно и шлют в ваши функции eth.

modifier refundEtherSentByAccident() {
   if(msg.value > 0) throw;
   _;
}

Взимать сборы

Ваш смарт-контракт может взимать сборы с пользователей, которые хотят вызвать определенную функцию вашего смарт-контакта.

modifier Fee (uint _fee) {
   if (msg.value >= _fee) {
       _;
   }
}

Вернуть сдачу

Мы уже видели, как модификатор может быть использован для возврата eth, отправленных случайно на смарт-контракт. Но как насчет случая, когда пользователь отправляет больше eth, чем следовало бы?

Пример реализации выглядит следующим образом.

modifier GiveChangeBack(uint _amount) {
   _;
   if (msg.value > _amount) {
       msg.sender.transfer(msg.value - _amount);
   }
}

Переключение между состояниями

Модификатор может быть использован для сложных образцов проектирования. К ним относится образец проектирования - Машина состояний.

Предотвращение повторного вызова

Посмотрите на модификатор ReentrancyGuard из набора OpenZeppelin.

Запретить смарт-контрактам взаимодействовать со смарт-контрактами

SUSHISWAP приводит отличный пример модификатора, который позволяет только внешним учетным записям (т.е. пользователям) требовать вознаграждения по своему смарт-контракту.

В результате это не позволяет другим смарт-контрактам вызывать функцию, к которой прикреплен модификатор.

modifier onlyEOA() {
   require(msg.sender == tx.origin, "Must use EOA");
   _;  
}

Проверка типа account’а

Этот модификатор дополняет предыдущий, который проверял, принадлежит ли address человеку или является смарт-контрактом. А этот модификатор проверяет есть ли по адресу код кода, используя опкод extcodesize(...) через assembly. Если есть, то это смарт-контракт.

modifier onlyHuman {
   uint size;
   address addr = msg.sender;
   assembly { size := extcodesize(addr) }
   require(size == 0, "разрешено только людям! (присутствует код по адресу)");
   _;
}

Заключение

Модификаторы позволяют снизить избыточность кода. При этом при помощи модификаторов можно создавать композицию чистых функций.

Какие самые интересные модификаторы вы писали или находили? Поделитесь в ответах.

Серия статей

  1. Учебник по Solidity. Всё о модификаторах

  2. Учебник по Solidity. Все об адресах