SELFDESTRUCT после EIP-6780: что изменилось в Ethereum и к чему готовиться

SELFDESTRUCT — опкод EVM, который традиционно «убивал» контракт: удалял код и хранилище, переводил остаток ETH на указанный адрес и давал газ-рефанд. После апгрейда Dencun и внедрения EIP-6780 его поведение в Ethereum кардинально изменилось: в большинстве случаев контракт больше не удаляется, а SELFDESTRUCT по сути превращается в «отправить весь баланс и остановить выполнение».

SELFDESTRUCT после EIP-6780: что изменилось в Ethereum и к чему готовиться

Как SELFDESTRUCT работал до EIP-6780

До изменений по EIP-6780 логика была такой:

  • при вызове SELFDESTRUCT(target):
    • текущий фрейм исполнения завершался;
    • код контракта удалялся из состояния;
    • все слоты хранилища (storage) очищались;
    • весь баланс контракта переводился на target;
    • аккаунт считался «пустым» и мог быть очищен из trie;
  • долгое время за SELFDESTRUCT выдавался значительный газ-рефанд (позже урезан и убран EIP-3529).

Из-за этого:

  • появлялся паттерн metamorphic contracts: контракт через CREATE2 и SELFDESTRUCT можно было пере-деплоить на тот же адрес с другим кодом;
  • использовались «эфемерные контракты», которые существовали только в одной транзакции;
  • можно было «сжечь» ETH, сделав SELFDESTRUCT с target == address(this).

Новое поведение SELFDESTRUCT по EIP-6780

EIP-6780 (вошёл в апгрейд Dencun в марте 2024 года) меняет семантику SELFDESTRUCT. Теперь различаются два случая:

1. Контракт создан в предыдущей транзакции

Если контракт не был создан в текущей транзакции, то при вызове SELFDESTRUCT(target):

  • исполнение текущего фрейма останавливается;
  • код и storage не удаляются — аккаунт остаётся в состоянии;
  • весь баланс контракта переводится на target (если баланс > 0);
  • если target == address(this), по сути ничего не происходит с балансом (нет «сжигания»);
  • газ-рефанд не выдаётся (как и раньше после EIP-3529).

То есть для «старых» контрактов SELFDESTRUCT теперь ведёт себя как «*send all ETH and halt*».

2. Контракт создан в этой же транзакции

Если контракт вызвал SELFDESTRUCT в той же транзакции, в которой был создан (через CREATE, CREATE2 или транзакцию-деплой):

  • сохраняется старое поведение:
    • код и storage удаляются согласно прежним правилам;
    • аккаунт в состоянии считается удалённым;
    • баланс переводится на target;
    • при target == address(this) баланс сжигается.

Такие контракты часто называют «эфемерными» — они живут только в рамках одной транзакции.

Важно: при дальнейшей эволюции протокола (Verkle Trees и др.) именно такое ограниченное использование SELFDESTRUCT проще поддерживать на уровне клиентских реализаций.

Почему от SELFDESTRUCT пришлось «отказаться»

Основные причины изменения поведения:

  • Сложность для будущей структуры состояния

При переходе к Verkle-деревьям и более сложным структурам хранения быстро и дёшево «подчистить» весь код и storage аккаунта становится проблемой.

  • Недетерминированность состояния и сложность анализа

Возможность в любой момент удалить контракт и заменить его код на том же адресе усложняет:

  • формальную верификацию протоколов;
  • анализ инвариантов;
  • рассуждение о том, «что значит адрес контракта».
  • Опасные паттерны (metamorphic contracts, upgradability через CREATE2+SELFDESTRUCT)

Такие конструкции работали, но делали экосистему менее предсказуемой и усиливали риск хрупких апгрейд-паттернов.

Что сломалось после EIP-6780

Metamorphic contracts и CREATE2 + SELFDESTRUCT

Паттерн:

  • задеплоить контракт по детерминированному адресу через CREATE2;
  • позже вызвать SELFDESTRUCT;
  • задеплоить новый контракт по тому же адресу (с тем же deployer, salt и init_code).

После EIP-6780:

  • SELFDESTRUCT для «старого» контракта не удаляет код и storage;
  • повторный деплой через CREATE2 по тому же адресу становится невозможен;
  • многие схемы «апгрейда через пере-деплой» оказываются сломанными.

Для новых проектов такой подход теперь считается небезопасным и не совместимым с текущей логикой (см. риски CREATE2).

Сжигание ETH через SELFDESTRUCT

До EIP-6780:

  • можно было вызвать SELFDESTRUCT с target == address(this) и тем самым сжечь баланс контракта (код при этом удалялся).

Теперь:

  • если контракт старый (создан не в этой транзакции), SELFDESTRUCT в такой конфигурации просто остановит выполнение, не удалит код и не сожжёт ETH — баланс останется на контракте;
  • только для «эфемерных» контрактов (созданных и уничтоженных в одной транзакции) паттерн сжигания сохраняется.

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

Паттерны «контракт отключается через selfdestruct»

Контракты, которые:

  • рассчитывали «отключиться» и навсегда запретить вызовы через SELFDESTRUCT;
  • или использовать удаление кода как «сигнал» другим протоколам,

теперь работают иначе:

  • код и storage остаются, и вызовы по адресу по-прежнему могут выполняться;
  • логика проверки «контракт жив или нет» должна быть переписана на явные флаги (paused, killed) и проверки внутри кода, а не на анализ наличия байткода.

Что НЕ изменилось (с точки зрения разработчика)

  • SELFDESTRUCT по-прежнему:
    • завершает текущий фрейм исполнения;
    • переводит баланс на target (если он есть);
  • газ-рефанд за SELFDESTRUCT по-прежнему отсутствует (EIP-3529 это уже сделал раньше);
  • для эфемерных контрактов (создание и уничтожение в одной транзакции) поведение максимально близко к старому — код и storage можно «обнулять».

Но теперь нужно мыслить так: для постоянных (долгоживущих) контрактов SELFDESTRUCT — это просто «отправить все ETH и остановиться», а не «удалить контракт».

Практические выводы для безопасности и дизайна протоколов

Не использовать SELFDESTRUCT как механизм апгрейда

  • Апгрейды через «убить контракт, затем задеплоить новый по тому же адресу» больше не работают на Ethereum L1.
  • Для апгрейдируемости:
    • используйте прокси-паттерны (Transparent/UUPS, ERC-1967/2535 и т.п.);
    • внимательно анализируйте риски DELEGATECALL и прокси.

Не полагаться на SELFDESTRUCT как на «стирание состояния»

  • Нельзя считать, что storage обнулится и контракт «исчезнет».
  • Инварианты вида «если контракта больше нет, значит баланс 0 и storage пустой» больше невалидны.
  • Для отключения протокола:
    • вводите явные флаги (stopped, emergency_shutdown);
    • реализуйте логику, которая не использует SELFDESTRUCT, а ограничивает или запрещает действия через require-проверки.

Переосмыслить паттерны с «эфемерными контрактами»

  • Эфемерные контракты (создание + SELFDESTRUCT в одной транзакции) по-прежнему возможны.
  • Но:
    • за них нет газ-рефанда;
    • поведение SELFDESTRUCT в будущем может быть ещё больше ограничено (операнд остаётся «депрекейтнутым» и его использование в новых контрактах не рекомендуется).

Если проект сильно опирается на такие паттерны (MEV-боты, сложные DeFi-конвейеры), стоит учитывать, что экосистема движется к их постепенному вымыванию.

Аудит: что проверять после EIP-6780

При аудите смарт-контрактов:

  • искать все места использования SELFDESTRUCT в коде;
  • проверять, нет ли устаревших предположений:
    • о возможности пере-деплоя через CREATE2;
    • о «полном удалении» storage;
    • о сжигании ETH;
  • анализировать взаимодействие SELFDESTRUCT с CREATE2 и прокси-архитектурами;
  • учитывать, на какой сети разворачивается код (не все EVM-совместимые сети могли внедрить такую же семантику сразу). Для EVM-эквивалентных L2 предполагается то же поведение, но его нужно подтверждать в документации сети.

Рекомендации по использованию SELFDESTRUCT после 6780

  • По возможности избегать использования SELFDESTRUCT в новых контрактах.
  • Если всё-таки нужно:
    • ограничивать его вызов только админами/ролями;
    • явно документировать, что он делает в новой модели (только «send all ETH and halt»);
    • не использовать его для:
      1. апгрейда;
      2. очистки состояния;
      3. сжигания ETH.
  • Вместо этого:
    • строить апгрейды через прокси;
    • реализовывать явные механизмы остановки протокола;
    • для сжигания — использовать адреса-«мусорки», а не SELFDESTRUCT.

См. также

Task Runner