Учебник по Solidity: Все о модификаторах
Что такое модификатор в 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;
}
}
Модификаторы будут выполняться в том порядке, в котором они определены, то есть слева направо. Так, в приведенном выше примере функция будет проверять следующие условия перед запуском:
onlyBy(...) : является ли адрес, вызывающий смарт-контракт, владельцем?
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, "разрешено только людям! (присутствует код по адресу)");
_;
}
Заключение
Модификаторы позволяют снизить избыточность кода. При этом при помощи модификаторов можно создавать композицию чистых функций.
Какие самые интересные модификаторы вы писали или находили? Поделитесь в ответах.
Серия статей
Учебник по Solidity. Всё о модификаторах