SELFDESTRUCT — опкод EVM, который традиционно «убивал» контракт: удалял код и хранилище, переводил остаток ETH на указанный адрес и давал газ-рефанд. После апгрейда Dencun и внедрения EIP-6780 его поведение в Ethereum кардинально изменилось: в большинстве случаев контракт больше не удаляется, а SELFDESTRUCT по сути превращается в «отправить весь баланс и остановить выполнение».
Как 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»);
- не использовать его для:
- апгрейда;
- очистки состояния;
- сжигания ETH.
- Вместо этого:
- строить апгрейды через прокси;
- реализовывать явные механизмы остановки протокола;
- для сжигания — использовать адреса-«мусорки», а не SELFDESTRUCT.
