Перейти к основному содержимому
Блокчейн05ГЛАВА 05Ошибки и проверки: require, revert, assertrequireоткат целиком

Ошибки и проверки: require, revert, assert

Прочитано0%
Квизы0/5
Тренажёрыне пройден0/1
Уровень 1 · Новичок

Зачем это нужно​

Контракт после публикации нельзя ни исправить, ни остановить, а вызвать его функцию может любой адрес в сети — не только тот, для кого она написана. Значит, единственная защита контракта — проверки внутри него самого.

В конкурсном задании это отдельный оцениваемый пункт. Модуль В требует «предложить текст сообщений об ошибке в существующих в контракте проверках» — в эталонном контракте они стоят заглушками "_________", и написать их правильно нужно самому. А в отчёте по логике ищут перевёрнутые условия: в том же контракте require(role != _role) вместо == — один знак, который открывает функцию всем, кроме того, кому она предназначена.

Эта глава — про три способа сказать «нет» и про то, что при этом происходит с деньгами и данными.

Транзакция либо проходит целиком, либо не проходит вовсе​

Вызов функции, меняющей данные, — это транзакция. У неё ровно два исхода: статус 1 — всё записалось; статус 0 — откат (revert), и хранилище выглядит так, будто вызова не было. Промежуточного «половина записалась» не бывает.

Первая модель проверяет это буквально: счётчик попыток attempts += 1 стоит до проверки, и всё равно после отказа он остаётся нулём.

── до попытки ──
вклад Алисы: 1.0 ETH
attempts: 0

── withdraw(2 ETH) — проверка С текстом ──
VM Exception while processing transaction:
reverted with reason string 'Недостаточно средств на счёте'

── withdrawSilent(2 ETH) — проверка БЕЗ текста ──
Transaction reverted without a reason string

── состояние после двух отказов ──
вклад Алисы: 1.0 ETH
attempts: 0 (attempts += 1 стоял ДО require — и всё равно откатился)

── газ за отклонённую транзакцию ──
статус квитанции: 0 (0 = откат)
gasUsed: 46483
с Алисы списано: 0.000070385634774416 ETH — за газ, хотя ничего не произошло

── для сравнения: успешный withdraw(0.5 ETH) ──
статус квитанции: 1
gasUsed: 56385
вклад Алисы: 0.5 ETH
attempts: 1 — считается только успешная попытка
ОТКАТ СТИРАЕТ ВСЁ, ЧТО УСПЕЛА СДЕЛАТЬ ТРАНЗАКЦИЯattempts += 1запись сделанаrequire(…)условие ложнооткатattempts снова 0газ46 483данные вернулись как были — работа машины оплачена: 0,00007 ETH за отклонённую транзакциюпоэтому проверки ставят в начале функции, до первой записиrequire без сообщения: «Transaction reverted without a reason string» — пользователь не узнает ничегопромежуточного «половина записалась» не бывает: статус 1 или статус 0
Откат возвращает данные к исходному состоянию — но газ за проделанную работу списывается всё равно

Два вывода. Откат стирает всё, а не «начиная с точки отказа»: запись, сделанная строкой выше проверки, тоже исчезает. Газ всё равно списывается — 0,00007 ETH за транзакцию, которая ничего не изменила. Работа машины оплачена, даже если результат отменён.

Отсюда практическое правило: проверки ставят в самом начале функции, до первой записи в хранилище. Чем раньше отказ, тем меньше сожжено газа.

require: условие и сообщение​

Основная проверка выглядит так:

require(balance[msg.sender] >= amount, unicode"Недостаточно средств на счёте");
Нажми на часть выражения

Условие формулируется как «то, что должно быть правдой», а не «что запрещено». Это частый источник путаницы: require(amount > 0) означает «сумма обязана быть больше нуля», и отказ произойдёт при нуле.

Сообщение уходит наружу целиком: Hardhat печатает reverted with reason string 'Недостаточно средств на счёте', кошелёк показывает тот же текст пользователю.

require без сообщения оставляет пользователя с пустым экраном​

Написать проверку без второго аргумента можно:

require(balance[msg.sender] >= amount);

Это законно и компилируется. Но при отказе наружу не уходит ничего — даже служебного признака ошибки. Пользователь видит «транзакция не прошла» и не знает почему; разработчик в модели получил ровно это:

Transaction reverted without a reason string

Проверка без сообщения — прямая потеря баллов в модуле В, где текст сообщений оценивается отдельным отчётом.

Важно

Кириллица в обычных кавычках не компилируется: "Сумма должна быть больше нуля" даёт ParserError: Invalid character in string. If you are trying to use Unicode characters, use a unicode"..." string literal. Приставка unicode пишется слитно с кавычкой, без пробела — так во всех сообщениях эталонного контракта.

Отвечено вопросов: 0 из 2
Строка attempts += 1 стоит ДО require, проверка не прошла. Чему равен attempts?
Транзакция откатилась. Что с газом?
Отвечено верно: 0 из 2

revert: отказ без условия​

Когда логика уже разложена по веткам, последняя ветка часто означает «так быть не должно». Условия для require там нет — есть просто место, куда попадать нельзя:

if (tokenPrice == pubPrice) {
// публичная фаза
} else if (tokenPrice == privPrice) {
// приватная фаза
} else {
revert(unicode"Во время подготовительной фазы нельзя покупать CMON");
}

Так написано в эталонном контракте. revert — это «отказать прямо сейчас», без проверки условия. Скобки обязательны: запись revert unicode"..." без скобок даёт ParserError.

Разница простая: require — «продолжай, если правда», revert — «остановись здесь».

Своя ошибка вместо строки: где на самом деле экономия​

С версии 0.8.4 отказ можно описать собственным типом ошибки:

error InsufficientBalance(uint256 available, uint256 required);

// в теле функции:
if (balance[msg.sender] < amount) {
revert InsufficientBalance(balance[msg.sender], amount);
}

Про такие ошибки часто говорят «они экономят газ». Вторая модель проверяет это на двух одинаковых контрактах — со строкой и со своей ошибкой:

── WalletString ──
газ на деплой: 332352
газ за откат: 24144
байт в данных отказа: 196 селектор: 0x08c379a0
сообщение Hardhat: reverted with reason string 'Недостаточно средств: …'

── WalletError ──
газ на деплой: 290471
газ за откат: 24105
байт в данных отказа: 68 селектор: 0xcf479181
сообщение Hardhat: reverted with custom error 'InsufficientBalance(5, 10)'

── разница (строка − ошибка) ──
деплой: 41881 газа
откат: 39 газа
данные: 128 байт
при этом строка НЕ несёт чисел, а ошибка несёт available=5 и required=10
СТРОКА ПРОТИВ СВОЕЙ ОШИБКИ: ГДЕ ЭКОНОМИЯ НА САМОМ ДЕЛЕгаз на развёртывание332 352290 471−41 881газ за сам откат24 14424 105−39байт в данных отказа19668−128строкасвоя ошибкаоткат стоит одинаково — экономия в байткоде: текст сообщения в него не зашитглавное отличие: своя ошибка несёт числа — InsufficientBalance(5, 10)строка так не умеет: пользователь видит текст, но не видит, сколько у него есть
Экономия — на развёртывании и в размере данных, а не на самом откате: 41 881 газа против 39

Популярное утверждение оказалось наполовину мифом. Откат стоит одинаково — разница 39 газа. Экономия в другом: текст сообщения не зашивается в байткод, поэтому развёртывание дешевле на 41 881 газа.

А главное преимущество вообще не про газ: своя ошибка несёт числа. Пользователь видит InsufficientBalance(5, 10) — сколько есть и сколько запрошено. Строка так не умеет.

assert — для того, чего не может быть​

Третья конструкция выглядит похоже, но означает другое:

assert(totalSupply >= balance[owner]);

assert проверяет инвариант — утверждение, которое обязано быть истинным всегда, если код написан правильно. При нарушении наружу уходит не текст, а panic code 0x1 — потому что текст читать некому: это сигнал программисту об ошибке в коде, а не пользователю об ошибке ввода.

Правило простое: require — для всего, что приходит снаружи (суммы, адреса, права, время). assert — для внутренних инвариантов, которые не должен нарушить даже самый странный ввод. Документация формулирует это так: правильно работающий код не должен порождать панику вообще, даже на неверных внешних данных.

Компилятор с версии 0.8 и сам вставляет паники: 0x11 при переполнении, 0x12 при делении на ноль, 0x32 при выходе за границу массива. Их сообщения бесполезны пользователю — ещё одна причина проверять входные данные заранее, своим require с понятным текстом.

Отвечено вопросов: 0 из 2
Правда ли, что кастомные ошибки экономят газ на откате?
Когда используют assert, а не require?
Отвечено верно: 0 из 2

Хорошее сообщение говорит, что сделать​

Отчёт по проверкам в модуле В — это буквально «напишите тексты вместо заглушек». Плохие сообщения: "error", "fail", "_________". Хорошие называют предмет и границу:

require(_amount / dec <= 5_000, unicode"Максимальное кол-во - 5.000 CMON");
require(!userMap[msg.sender].whitelist, unicode"Вы уже в вайтлисте");

Три правила:

  1. Одно сообщение — одна причина. Если один текст стоит у трёх разных проверок, пользователь не поймёт, какая сработала.
  2. Без технических слов. Не «msg.sender не соответствует owner», а «Действие доступно только владельцу».
  3. Называть границу числом. Не «слишком много», а «Максимальное кол-во - 5.000 CMON».

Тексты видит эксперт при сверке с ключом — и пользователь в кошельке, если решение показывают на демонстрации.

Порядок проверок: сначала дешёвые​

Каждое чтение из хранилища стоит около 2100 газа при первом обращении, а сравнение двух чисел из параметров — около трёх. Отсюда порядок: сначала то, что дёшево проверить.

function sendRequest() public {
require(getLifeTime() > seedPhase, unicode"Заявку можно подать только после старта"); // сравнение чисел
require(!userMap[msg.sender].whitelist, unicode"Вы уже в вайтлисте"); // одно чтение
for (uint256 i = 0; i < requests.length; i++) { // цикл — самое дорогое
require(requests[i].wallet != msg.sender, unicode"Вы уже подали заявку");
}
requests.push(Whitelist(userMap[msg.sender].login, msg.sender, false));
}

Ровно этот порядок стоит в эталонном контракте. Логика та же, что с ранним отказом: если транзакция всё равно будет отклонена, пусть это случится на третьем газе, а не после цикла.

Перевёрнутое условие: ошибка, которую не ловит никто​

В эталонном контракте есть строка, которая компилируется без единого предупреждения и полностью ломает защиту:

require(userMap[msg.sender].role != _role, "_________");

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

── RoleGateBroken: changePrice() только для Role.Provider ──
владелец (деплоил) → ПРОПУЩЕН (price = 200)
провайдер → ОТКАЗ reverted with reason string 'Недостаточно прав'
посторонний → ПРОПУЩЕН (price = 400)

── RoleGateFixed: changePrice() только для Role.Provider ──
владелец (деплоил) → ОТКАЗ reverted with reason string 'Недостаточно прав'
провайдер → ПРОПУЩЕН (price = 300)
посторонний → ОТКАЗ reverted with reason string 'Недостаточно прав'
ОДИН ЗНАК ПЕРЕВОРАЧИВАЕТ ВСЮ ЗАЩИТУrequire(role != _role)require(role == _role)владелец (он же деплоил)ПРОПУЩЕНОТКАЗпровайдер — единственный, кому можноОТКАЗПРОПУЩЕНпостороннийПРОПУЩЕНОТКАЗпроверка руками ничего не покажет: по умолчанию вызывает тот, кто разворачивалсломанный контракт владельца пропускает — автор решает «работает» и идёт дальшекомпилятор молчит: с точки зрения языка обе записи одинаково правильны
Сломанная проверка пропускает всех, кроме того, кому функция предназначена, — а руками это не обнаруживается

Самое неприятное здесь — не сама ошибка, а то, почему её не находят. Проверка руками ничего не показывает: в редакторе по умолчанию функцию вызывает тот, кто разворачивал контракт, — владелец. На сломанном контракте владельца пропускает. Автор видит «работает» и идёт дальше. А единственного, кого пускать нельзя, — постороннего — никто не проверил.

Проверка прав тестируется минимум с двух адресов: того, кому можно, и того, кому нельзя.

Совет

Эта проверка стоит внутри модификатора — заготовки, вынесенной за скобки функции, чтобы не повторять один и тот же require в десяти местах. В эталонном контракте один такой модификатор защищает десяток функций, поэтому ошибка в нём ломает сразу все. Подробно модификаторы разберём в главе про функции.

Типичные ошибки​

Кириллица без приставки. ParserError: Invalid character in string. — нужно unicode"…".

revert без скобок. revert unicode"текст"; → ParserError: Expected ';' but got 'UnicodeStringLiteral'. Скобки обязательны.

Опечатка reqiure. DeclarationError: Undeclared identifier. Did you mean "require"? — компилятор подсказывает сам.

Своя ошибка без revert. Объявленный error нельзя просто «вызвать»: TypeError: Errors can only be used with revert statements.

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

Одно сообщение на все проверки. Пользователь и эксперт не понимают, какая именно сработала.

assert вместо require для внешних данных. Пользователь получает panic code 0x1 без объяснения; в отчёте по логике это отмечают как ошибку.

Условие сформулировано наоборот. require(x != y) вместо == — компилятор молчит, ручная проверка не ловит. Тестировать с двух адресов.

Проверка без сообщения. Transaction reverted without a reason string — и потерянный пункт отчёта по проверкам.

Слова главы​

  • require(условие, сообщение) — продолжить, если условие истинно; иначе откат с текстом.
  • revert(сообщение) — отказать сразу, без условия; скобки обязательны.
  • assert(условие) — проверка инварианта; при нарушении panic code 0x1 без текста.
  • error Имя(типы) — собственная ошибка; вызывается через revert Имя(значения).
  • Откат (revert) — отмена всей транзакции; изменения исчезают целиком.
  • Статус квитанции — 1 (прошла) или 0 (откат).
  • gasUsed — сколько газа сожжено; списывается и при откате.
  • unicode"…" — строковый литерал с кириллицей; приставка слитно с кавычкой.
  • Инвариант — утверждение, истинное всегда при правильном коде.
  • Panic-код — служебный номер ошибки без текста: 0x1 assert, 0x11 переполнение, 0x12 деление на ноль, 0x32 выход за границу.
  • Модификатор (modifier) — проверка, вынесенная за скобки функции; подробно впереди.

Тренажёр: набрать вслепую​

Цель: скорость ≥ 100 зн/мин, точность ≥ 88%

require(msg.sender == owner, unicode"Только владелец");

На чемпионате​

Модуль В, 14 баллов, шесть отчётов. Два из них — прямо про эту главу.

«Отчёт по проверкам: предложить текст сообщений об ошибке в существующих в контракте проверках». В эталонном контракте заглушки "_________" стоят в модификаторе доступа, во входе по паролю, в покупке токенов, в переводе и в получении разрешённой суммы. К каждой нужен текст: предмет, граница, что сделать.

«Отчёт по логике: указать на ошибки в логике работы контракта, внести предложения по их устранению». Перевёрнутые условия из этой главы — готовые пункты: require(role != _role) в модификаторе и require(passwordMap[_login] != _password) при входе. Оба должны быть ==.

Отчёты сверяются с ключами-ответами разработчиков задания: каждый ненайденный пункт — потерянный балл. И наоборот: за вредное предложение снимают, поэтому писать только то, в чём уверены.

Модуль А. Все проверки из требований задания — «сумма не менее 10 USDC», «сумма не превышает баланс», «выдан approve», «пользователь не должен быть ликвидирован после получения займа» — это require с понятным текстом в начале функции. Эксперт на демонстрации проверяет и отказы тоже.

Отвечено вопросов: 0 из 2
Почему перевёрнутое условие require(role != _role) не находят при ручной проверке?
Что требует отчёт по проверкам в модуле В?
Отвечено верно: 0 из 2

Что дальше​

Проверки — это ответ на вопрос «делать или отказать». Следующая глава — про выбор между несколькими вариантами: if и else, циклы и время. Там же выяснится, почему цикл по списку неизвестной длины — не просто медленно, а опасно: он может перестать помещаться в блок, и функция откажет навсегда.

Отвечено вопросов: 0 из 1
В каком порядке ставить три проверки: сравнение чисел, чтение из хранилища, цикл по массиву?
Отвечено верно: 0 из 1

Углубиться​

  • Полная таблица panic-кодов — от 0x00 до 0x51; в документации, в разделе про обработку ошибок.
  • require со своей ошибкой — с версии 0.8.26 доступна запись require(условие, MyError(...)).
  • try/catch — как поймать откат чужого контракта, а не упасть вместе с ним.
  • Возврат остатка газа — при откате неизрасходованный лимит возвращается отправителю; сгорает только потраченное.
  • Как получить квитанцию отклонённой транзакции — инструмент обычно бросает ошибку заранее; чтобы увидеть статус 0 и gasUsed, отключают автоматическую сборку блоков.

Сводная проверка по всей главе: 8 вопросов, 5:00 на всё, без подсказок по ходу. Вопросы показываются по одному, ответ изменить нельзя. Если время выйдет — экзамен завершится с тем, что успел ответить. Пересдавать можно сколько угодно раз.

Куда дальше: проверенные ресурсы​

Видео по теме

ВидеоСобытия и кастомные ошибки в Solidity: полный разбор на примере токенаGuide DAO
ВидеоSolidity и смарт-контракты Ethereum, урок #7 | События, модификаторы, require/reIlya Krukowski
ВидеоРазработка смарт-контрактов и Dapp на Solidity #7 - Обработка ошибок, МодификатоAlefman
ВидеоКак обрабатываются ошибки в смарт контрактах ? | Require (); Assert();Ruslan Ravana
ВидеоHow to Handle Errors in Solidity | Custom Errors, require, revert & assert | Smathebasicsdotacademy

Комментарии

Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.