Модификаторы доступа и 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 откатывают транзакцию при нарушении условий. Контроль доступа — критичнейшая часть безопасности. Дальше — события и логи.