Модификаторы доступа и require

Модификаторы — это переиспользуемые проверки, которые оборачивают функцию. Главная из них — «кто ты такой?».
Improper access control — взломщик #1 в смарт-контрактах. Один забытый модификатор onlyOwner — и кто угодно выводит казну.

Модификатор — это блок кода, который вставляется до (и/или после) тела функции. Символ _; отмечает место, куда подставится тело. Классический пример — onlyOwner: проверяет, что вызывающий равен владельцу, иначе откатывает транзакцию.

Откат делают через require(условие, "сообщение") или, эффективнее по газу, через custom errors и revert. В современном Solidity (0.8.4+) кастомные ошибки — рекомендованный способ: они дешевле и информативнее строк.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract Vault {
    address public owner;
    uint256 public balance;

    error NotOwner();          // кастомная ошибка — дешевле строки
    error InsufficientFunds();

    constructor() { owner = msg.sender; }

    modifier onlyOwner() {
        if (msg.sender != owner) revert NotOwner();
        _; // сюда подставится тело функции
    }

    function withdraw(uint256 amount) external onlyOwner {
        require(amount <= balance, "too much"); // классический require
        balance -= amount;
    }
}
   ВЫЗОВ withdraw()                modifier onlyOwner
   ================                ==================
   msg.sender ----------------->  if (sender != owner)
                                       |        |
                                  revert     _;  <- тело функции
                                  (откат)    (исполняется)

Как работает под капотом (EVM/газ)

require со строкой кодирует сообщение об ошибке в ABI и возвращает его при откате — это занимает место в байт-коде и газ на разворачивание строки. Custom error кодируется как 4-байтовый селектор (плюс аргументы, если есть) — это заметно дешевле и компактнее. И require, и revert откатывают все изменения состояния транзакции и возвращают неизрасходованный газ. Модификатор не «функция»: его код встраивается в каждую функцию, к которой применён.

# Та же логика на Python: require как assert + модификатор доступа
class NotOwner(Exception): pass

class Vault:
    def __init__(self, owner):
        self.owner = owner
        self.balance = 100

    def _only_owner(self, caller):       # модификатор onlyOwner
        if caller != self.owner:
            raise NotOwner("REVERT: NotOwner")

    def withdraw(self, caller, amount):
        self._only_owner(caller)         # проверка ДО тела
        assert amount <= self.balance, "REVERT: too much"  # require
        self.balance -= amount
        return self.balance

v = Vault(owner="0xOWNER")
print("owner withdraw:", v.withdraw("0xOWNER", 40))
try:
    v.withdraw("0xHACKER", 10)
except NotOwner as e:
    print(e)

«Та же логика на Python ▶». Проверка владельца срабатывает до тела функции; чужой вызов откатывается, как revert в Solidity.

Частые ошибки

  • Забыть применить модификатор доступа к опасной функции (минт, вывод средств, смена параметров) — критическая уязвимость.
  • Использовать tx.origin вместо msg.sender в проверке — это открывает фишинговую атаку (разберём в безопасности).
  • Писать длинные строки в require везде — раздувает байт-код; для частых проверок выгоднее custom errors.

Best practices

  • Для контроля доступа берите проверенный Ownable или AccessControl от OpenZeppelin вместо самописного.
  • Применяйте принцип наименьших привилегий: каждой роли — только нужные права.
  • Используйте custom errors для экономии газа и понятных сообщений; именуйте их по сути проблемы.

require, assert и revert — в чём разница

В Solidity три способа прервать выполнение, и их путают. require предназначен для проверки входных данных и условий, зависящих от пользователя: «достаточно ли средств», «правильный ли вызывающий». При провале он откатывает транзакцию и возвращает неизрасходованный газ. revert делает то же самое, но вызывается явно — удобно внутри сложного if или с custom error. А вот assert служит для проверки инвариантов, которые в корректном коде нарушаться не должны вообще: его срабатывание означает баг, и под капотом он даёт ошибку Panic. Практическое правило: require/revert — для ожидаемых ситуаций (плохой ввод, нет прав), assert — только для «этого не может быть никогда». Все три откатывают все изменения состояния транзакции целиком, сохраняя атомарность.

Итоги

Модификаторы инкапсулируют проверки и встраиваются в функции; require/revert/custom errors откатывают транзакцию при нарушении условий. Контроль доступа — критичнейшая часть безопасности. Дальше — события и логи.

Проверьте себя
1. Что делает оператор _; внутри модификатора?
AЗавершает транзакцию
BОтмечает место, куда подставится тело функции
CОткатывает изменения
DОбъявляет приватную переменную
2. Почему custom errors предпочтительнее require со строкой?
AОни работают только в 0.7
BОни кодируются 4-байтовым селектором и дешевле по газу
CОни не откатывают транзакцию
DОни скрывают причину ошибки