Часть III · Глава 10 из 43 · Byzantium

EIP-140: REVERT — дёшево проваливаться и объяснять почему

До Byzantium контракт, столкнувшийся с плохим условием, имел только один грубый вариант — throw: он откатывал работу, но при этом сжигал весь газ вызывающего до последней капли и не возвращал никакого объяснения. REVERT сохраняет откат, возвращает газ и позволяет контракту вернуть ошибку. Каждый require("reason"), кастомная ошибка и try/catch, которые вы когда-либо писали, — потомки этого опкода.

Обновлено 24 июн. 2026 г. · 10 мин
Предполагается
  • EVM, газ и модель вызовов
  • изменения состояния

Byzantium, октябрь 2017. Крупный форк — он принёс криптографические прекомпайлы, сделавшие возможными доказательства с нулевым разглашением on-chain, и тип вызова, который не может трогать состояние. Но изменение, которое каждый Solidity-разработчик ощущает ежедневно, куда скромнее: новый способ проваливаться. До этого момента контракт, обнаруживший плохое условие — списание сверх баланса, неудачную проверку прав, — мог только throw, а throw был актом выжженной земли: он откатывал работу, но при этом сжигал каждую единицу газа, оставшуюся у вызывающего, и не возвращал ни байта объяснения. EIP-140 вводит REVERT, и вместе с ним провал становится дешёвым, точным и читаемым. Давайте выведем его.

1 шаг Провал был выжженной землёй

Проблема: прерывание стоило всего и не говорило ничего

Вызов контракта либо завершается успешно и фиксирует изменения состояния, либо прерывается — и тогда их нужно откатить: вы никогда не хотите наполовину применённого перевода. До Byzantium единственным способом прерваться было throw Способ прерывания до Byzantium: выполнение невалидной инструкции (или throw в Solidity), которое откатывало изменения состояния фрейма, но потребляло весь оставшийся газ и не возвращало данных. — обычно натыкаясь на невалидный опкод. Это действительно откатывало состояние, и это правильно. Но было два жестоких побочных эффекта: потреблялся весь оставшийся у вызывающего газ, и не возвращалось никаких данных. Так что вполне разумный require(balance >= amount), который просто не выполнился, сжигал весь газовый бюджет пользователя и оставлял его смотреть на голое «транзакция не выполнена» без единой причины.

до: неудачная проверка бросала исключение — дорого и незаметно ваша транзакция require(bal ≥ amt) throw / invalid opcode state undone — but… ✗ весь оставшийся газ сожжён · ✗ reason не возвращён законный провал условия стоил вам всего и не объяснил ничего
До REVERT неудачная проверка вызывала throw: состояние фрейма корректно откатывалось, но весь оставшийся газ сжигался, и данные об ошибке не возвращались. Законный, ожидаемый провал стоил пользователю всего газового бюджета и ничего не сообщал о том, что пошло не так.

→ Шаг 2: сохраняем откат, убираем наказание, добавляем причину.

2 шаг Откат, возврат газа, объяснение

REVERT: три исправления в одном опкоде

REVERT Опкод Byzantium 0xfd. Останавливает текущий вызов, атомарно откатывает все изменения состояния этого фрейма, возвращает вызывающему неиспользованный газ и возвращает блок данных (offset, length) — им пользуются, чтобы передать причину ошибки. (опкод 0xfd) делает ровно то, что нам нужно, — сразу три вещи. Он откатывает все изменения состояния, сделанные в текущем фрейме вызова, — атомарно, будто фрейм вообще не выполнялся. Он возвращает оставшийся газ вместо того, чтобы его сжигать, поэтому ожидаемый провал стоит только газа, реально потраченного до этого момента. И он возвращает данные — произвольный блок (offset, length) из памяти, — в котором и уезжает обратно строка ошибки или структурированный код ошибки.

REVERT (0xfd) — три вещи сразу REVERT ① состояние — отменено slot = 9 slot = 14 ✓ ② газ — сохранён 100% refunded ③ reason — возвращён "InsufficientBal" откат фрейма · возврат газа · возврат ошибки — не как raw throw
REVERT атомарно делает три вещи: ① откатывает каждое изменение состояния в текущем фрейме, ② возвращает вызывающему неиспользованный газ вместо того, чтобы его сжигать, и ③ возвращает блок данных — так контракт может вернуть причину вроде «InsufficientBalance» вместо того, чтобы провалиться молча.
Формула
REVERT = undo(state) 1 + refund(gas) 2 + return(data) 3
  1. 1 каждый SSTORE, изменение баланса и лог в этом фрейме откатывается — атомарно
  2. 2 неиспользованный газ возвращается вызывающему; теряется только уже потраченный газ
  3. 3 блок памяти всплывает наверх — это строка ошибки или данные кастомной ошибки
Старый throw делал только первое, да ещё и по максимальной цене. REVERT сохраняет атомарный откат, но добавляет возврат газа и причину.

→ Шаг 3: пусть вызывающий её перехватит.

3 шаг Перехватывается вызывающим

Выигрыш: провал, который вызывающий может перехватить и прочитать

Поскольку REVERT возвращает данные и не уничтожает транзакцию целиком, вложенный вызов, который делает revert, возвращается к своему вызывающему как обычный проваленный вызов, несущий свою причину, — а не убивающий всю цепочку взрыв. Собственные изменения состояния вызываемого откатываются, управление возвращается вызывающему, и тот решает: пробросить ошибку выше или обработать её. Это в точности тот механизм, что стоит за обработкой ошибок, которую вы пишете каждый день.

подвызов может провалиться, не убив вызывающего вызывающий try — call catch (reason) ✓ call revert("reason") ↩ вызываемый REVERT + reason его состояние откатывается revert возвращается как неудачный вызов — вызывающий решает, что делать на этом построены require(reason), custom errors и try/catch
Когда вызываемый делает REVERT, провал возвращается к вызывающему как перехватываемый результат с прикреплённой причиной — состояние вызываемого откатывается, но вызывающий продолжает работу и сам решает, что делать. Именно на этом построены require(reason), кастомные ошибки и try/catch в Solidity.
04 Глубже Куда двигаться дальше