Что такое модификатор в 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. Всё о модификаторах
ссылка на оригинал статьи https://habr.com/ru/articles/572004/
Добавить комментарий