EVM — это изолированная среда выполнения смарт-контрактов для сети Ethereum и всех EVM-совместимых блокчейнов. Она задаёт правила изменения глобального состояния: как транзакции и вызовы меняют балансы, хранилище контрактов, логи и пр. В отличие от обычной «виртуалки», EVM жёстко лимитирована по ресурсам и цене операций через газ, а вычисления детерминированы — одинаковый байткод при одинаковом входе даст одинаковый результат на любом узле.
Эта статья — «центральный хаб» по теме EVM. В ней мы:
- разложим по полочкам архитектуру (стек, память, хранилище, calldata/returndata, логи);
- объясним модель счетов и состояние сети;
- разберём ключевые опкоды и их эволюцию на хардфорках (включая PUSH0, MCOPY, TSTORE/TLOAD, изменения SELFDESTRUCT);
- дадим практические советы для разработчиков и аудиторов;
- покажем, что такое EVM-совместимость/эквивалентность на L2 и почему это важно;
- оставим дорожную карту «что почитать дальше» в нашей вики для хорошей перелинковки.
1) Что такое EVM простыми словами
EVM — это стековая (stack-based) машина, исполняющая байткод смарт-контрактов. Контракты компилируются из языков высокого уровня (чаще всего Solidity, реже Vyper, либо из промежуточного языка Yul) в набор инструкций (опкодов). Каждая инструкция стоит газ, а у транзакции есть лимит газа — он и ограничивает вычисления. Такой дизайн решает две задачи:
- предотвращает атаки типа «бесконечного цикла» — газ закончится;
- делает стоимость вычислений предсказуемой — каждая операция тарифицируется.
EVM — детерминирована и изолирована: она не может «выйти в интернет», запросить системное время или файл — все данные должны прийти через транзакцию (calldata) или из состояния блокчейна. Это позволяет всем узлам сети прийти к одному и тому же новому состоянию после выполнения блока.
2) Архитектура EVM: из чего она состоит
2.1 Стек, слово, глубина
Стек — LIFO-структура максимум на 1024 элемента.
Слово EVM — 256-битное (32 байта). Все арифметические операции идут над 256-битными беззнаковыми整数ами.
Типичные стек-инструкции: PUSHn, POP, DUPn, SWAPn, ADD, MUL, SHL/SHR, AND, OR, XOR.
Это сделано ради удобства криптографии (256-битные числа), адресов и хешей (Keccak-256). См. также Opcodes для систематизации опкодов.
2.2 Память (memory)
- Временная, байтовая область, очищается после завершения вызова.
- Используется для сборки/парсинга данных, ABI-кодирования/декодирования, временных структур.
- Расширение памяти дороже чем чтение/запись уже «освоенных» областей (существует формула «memory expansion cost»).
2.3 Хранилище (storage)
- Постоянное KV-хранилище контракта, адресуемое по 32-байтным слотам slot → 32 bytes.
- Это основная «плата за ресурсы»: операции SSTORE/SLOAD самые дорогие.
- Компиляторы (Solidity/Vyper) отображают переменные/структуры/маппинги в слоты; для маппингов используется хеширование keccak256(key . slot). Правильная работа со слотами — база для апгрейд-прокси, минимизации газа и безопасности.
2.4 Calldata и returndata
Calldata — входные байты вызова (транзакции/внутреннего вызова), только для чтения.
Returndata — байты, возвращённые последним внешним вызовом: читаются RETURNDATASIZE/RETURNDATACOPY.
2.5 Логи (events)
- Контракт может эмитить события LOG0 … LOG4. Они пишутся в receipt транзакции и индексируются topics (до 4 штук).
- Для обычных событий topic[0] — это хеш сигнатуры события (keccak256(«Transfer(address,address,uint256)»)). См. Abi и селекторы функций и события.
3) Модель аккаунтов и мировое состояние
Ethereum использует account-based модель:
- EOA (Externally-Owned Account) — «кошелёк», управляемый приватным ключом; может инициировать транзакции.
- Контракт-аккаунт — имеет код и хранилище; не может инициировать транзакции сам, только реагирует на входящие сообщения/транзакции.
- Состояние — это отображение адрес → (nonce, balance, storageRoot, codeHash). Полный узел хранит это как модифицированное Patricia-дерево; именно хэш-корни делают состояние проверяемым. Подробнее см. состояние Ethereum.
Создание контракта:
CREATE — адрес = keccak256(rlp(sender, nonce))[12:].
CREATE2 — адрес = keccak256(0xff ++ deployer ++ salt ++ keccak256(init_code))[12:]. Это позволяет детерминированные адреса (важно для фабрик, мета-транзакций, адресов «на будущее»).
4) Газ и цена вычислений
4.1 Базовые принципы
- Каждая операция имеет стоимость в газе; у транзакции — лимит газа и предлагаемая цена (в эпоху EIP-1559 это base fee + tip).
- Есть база транзакции (обычно 21 000 газа), далее — плата за calldata и за исполнение опкодов.
- Вызовы в другие контракты следуют правилу «63/64»: по умолчанию потомку нельзя передать весь газ (для устойчивости), и есть нюансы со «стипендией» 2300 газа при отправке ETH без явной передачи газа.
4.2 «Дорогие» части EVM
- Хранилище: SSTORE и SLOAD — самые чувствительные к изменениям EIP-ов (см. ниже).
- Хеширование: KECCAK256 — часто используется (ABI, маппинги, селекторы).
- Копирование/память: раньше типичные шаблоны кода тратили много газа на CODECOPY/MSTORE/MLOAD; с появлением MCOPY стало дешевле.
4.3 Эволюция газовой модели на хардфорках (важно разработчикам)
- Istanbul: EIP-1884 репрайс опкодов, EIP-2200 — новая модель SSTORE (net gas metering).
- Berlin: EIP-2929 «тёплые/холодные» доступы к состоянию; EIP-2930 — Access List-транзакции (предзаявляете адреса/слоты для удешевления «первого» доступа).
- London: EIP-3529 сокращение газ-рефандов (например, за очищение слота).
- Shanghai: EIP-3855 PUSH0 и EIP-3860 лимит и тарификация initcode.
- Cancun/Dencun: добавлены EVM-оптимизации TSTORE/TLOAD (transient storage, не сохраняется между транзакциями) и MCOPY (быстрое копирование в памяти) — сильно полезно для конструкторов данных, пулов, AMM и др.
См. раздел «Эволюция опкодов» ниже для деталей и безопасных практик.
5) Ядро опкодов, которые обязан знать каждый разработчик
5.1 Вызовы и выполнение
CALL — обычный внешний вызов (можно передать ETH и газ).
STATICCALL — «только чтение» (запрещены изменения состояния).
DELEGATECALL — выполняет код другого контракта в контексте текущего хранилища (ключ к прокси-паттернам, но и к уязвимостям).
CREATE / CREATE2 — развёртывание контрактов.
5.2 Память и код
MLOAD/MSTORE, CALLDATASIZE/CALLDATALOAD/CALLDATACOPY, RETURNDATASIZE/RETURNDATACOPY.
CODECOPY — копирование байткода контракта в память.
MCOPY — эффективное копирование внутри памяти (см. «Новые опкоды»).
5.3 Хеширование и арифметика
KECCAK256 (в байткоде исторически именуется SHA3), ADD/MUL/SUB, SHL/SHR, AND/OR/XOR.
5.4 Хранилище
SLOAD, SSTORE — читать/писать слоты (учитывайте net gas metering, warm/cold).
5.5 Логи
LOG0…LOG4 — события (topics + data), см. Abi и Function Selector.
5.6 Управление потоком
JUMP/JUMPI, JUMPDEST, REVERT, RETURN.
6) Эволюция опкодов и поведения EVM на хардфорках
Byzantium принесла критические опкоды:
- REVERT (аккуратный откат с данными);
- STATICCALL (жёсткое «только чтение»);
- новые предкомпилы для zk-криптографии (см. ниже).
Constantinople / Petersburg:
- CREATE2 (детерминированное развёртывание);
- уточнения SSTORE (дальше — в EIP-2200).
Istanbul:
- Репрайс SLOAD, BALANCE, EXTCODE* и пр. (EIP-1884);
- Вводится модель net gas metering для SSTORE (EIP-2200).
Berlin:
- Ввод «тёплых/холодных» доступов к адресам/слотам (EIP-2929): первый доступ дороже, последующие дешевле в рамках транзакции;
- Появляются access list-транзакции (EIP-2930): объявляете наперёд, чтобы снизить стоимость «холодного» доступа.
London:
- Сильное сокращение refunds (EIP-3529): нельзя «майнить рефанды» через массовые очищения слотов.
Shanghai:
- PUSH0 — теперь пуш «нуля» не требует 2-байтного PUSH1 0x00 (меньше байткода/газа);
- EIP-3860: лимит initcode = 49 152 байта и дополнительная тарификация за каждые 32 байта (важно для больших фабрик/прокси).
Dencun (Cancun-Deneb):
- Transient storage: TSTORE/TLOAD — сверхдешёвое временное хранилище внутри одной транзакции (идеально для «переноса состояния» между внутренними вызовами без записи в постоянное storage);
- MCOPY — быстрый memory-to-memory copy.
Shanghai также изменила SELFDESTRUCT:
- EIP-6780: SELFDESTRUCT больше не удаляет контракт и не очищает storage, если контракт не был создан в той же транзакции. По сути — возвращает эфир на адрес-бенефициар, но код/хранилище остаются (важно для миграций и безопасности).
Практика: не используйте SELFDESTRUCT как «кнопку удаления» — после EIP-6780 это не очистка состояния. Для «отключения» логики закладывайте флаги/роллинг-админа или апгрейдируемую архитектуру.
7) Предкомпилированные контракты (precompiles)
Ряд «тяжёлых» операций вынесен в фиксированные адреса 0x01–0x09. Чаще всего используются:
0x01 — ECRECOVER (восстановление адреса из подписи).
0x02 — SHA-256.
0x03 — RIPEMD-160.
0x04 — IDENTITY.
0x05 — модульное возведение в степень (MODEXP).
0x06/0x07/0x08 — операции над alt_bn128 (BN254): сложение, умножение, паринг — для zk-доказательств.
0x09 — BLAKE2f (компрессионная функция; используется редко).
Нюанс: набор и цена предкомпилов — часть консенсуса L1. На L2/парачейнах список может различаться. При портировании кода на «EVM-совместимые» сети проверяйте поддержку и цены предкомпилов.
8) Keccak-256, селекторы и ABI
В байткоде EVM используется Keccak-256 (исторически опкод называется SHA3, но это не NIST SHA-3). Отсюда ряд правил:
- Селектор функции — первые 4 байта keccak256(«foo(address,uint256)») из ABI-сигнатуры. Именно эти 4 байта стоят в начале calldata.
- Сигнатура события — topic[0] = keccak256(«Transfer(address,address,uint256)»).
- Маппинги/слоты — хеширование keccak256(key . slot).
Практика: всегда формируйте сигнатуры канонически (без пробелов, с точными типами), иначе селекторы/события не совпадут.
9) Безопасность: что чаще всего ломают в EVM
Реинэнтрантность (особенно вокруг call{value:…} и взаимодействия с внешними контрактами). Используйте checks-effects-interactions, ReentrancyGuard, избегайте логики в fallback/receive. Помните про «стипендию» 2300 газа — она защищала только «старый мир», сейчас её недостаточно.
DELEGATECALL и прокси: при ошибках в инициализации/слотах хранения возможны критические уязвимости. Всегда фиксируйте storage layout и используйте проверенные шаблоны (UUPS/Transparent).
Неучёт gas repricing: жёстко захардкоженные лимиты газа к внешним вызовам/петлям могут ломаться после репрайсов.
SELFDESTRUCT после EIP-6780: не рассчитывайте на очистку состояния; прорабатывайте «деактивацию» логики флагами.
CREATE2-коллизии и «подмена кода»: адрес заранее известен — подписывайте данные и проверяйте «код по адресу после деплоя», если от этого зависит безопасность.
См. Reentrancy, Delegatecall, Create2 Risks (перспективные статьи).
10) Совместимость/эквивалентность EVM на L2
Термины:
- EVM-совместимость — «код исполняется», но поведение/цены/предкомпилы могут отличаться.
- EVM-эквивалентность — цельная гарантия: «то же поведение байткода и опкодов», минимальные дельты по газу/краевым случаям. Это снижает риски «сюрпризов» при миграциях, важнее всего для аудита.
- В мире L2 (Optimism/OP Stack, Arbitrum, zkEVM-решения) проекты стремятся к эквивалентности, но детали критичны: сравнивайте поддержку новых опкодов (PUSH0/MCOPY/TSTORE), предкомпилов и лимитов.
11) Практика для разработчиков: короткая «шпаргалка»
Оптимизация газа:
- Минимизируйте SSTORE (батчи, флаги-битпак, инкрементальные обновления).
- Используйте PUSH0, MCOPY, transient storage (TSTORE/TLOAD) там, где допустимо.
- Предзаявляйте access list (тип-1/тип-3 транзакции) при массовых внешних чтениях.
Структуры данных:
- Хеш-ключи маппингов и структуры в слотах документируйте в Storage Layout.
- Для больших массивов — думайте о event-логах вместо постоянного storage, если данные не нужны в EVM-логике.
CREATE2:
- Храните salt и init_code_hash для воспроизводимости.
- Проверяйте «что по адресу уже есть код» перед деплоем (защита от подмен).
Логи/индексация:
- Используйте indexed для полей-фильтров; помните лимит «до 3 indexed» у неанонимных событий.
Обновления сети:
- Отслеживайте, как новые EIP-ы меняют газ/поведение (SSTORE, SELFDESTRUCT, новые опкоды).
- Не полагайтесь на «магические числа газа» — используйте безопасные паттерны.
12) Частые вопросы (FAQ)
EVM использует SHA-3? Исторически опкод назван SHA3, но в EVM используется Keccak-256 (пред-NIST версия), а не финальный NIST SHA-3. Для селекторов/событий берётся именно Keccak-256.
Сколько «памяти» у контракта? Память динамична и очищается после вызова. Постоянные данные — только в storage, который дорог по газу.
Почему мой контракт «не удаляется» через SELFDESTRUCT? Потому что после EIP-6780 удаление/очистка состояния фактически отключены (кроме случая «создан и тут же удалён в одной транзакции»).
Можно ли обойти лимит размера кода? Есть лимит размера развернутого кода (по EIP-170) и initcode (EIP-3860). Обходят архитектурой (разбиение на модули/библиотеки, прокси, апгрейды), оптимизируют компиляцию, используют PUSH0/MCOPY.
13) Перспективы и «куда движется EVM»
- EOF (EVM Object Format) — модульность и структурированный формат кода (пакеты, секции), уменьшение неоднозначностей исполнения и новые оптимизации. Следим за включением в будущие апгрейды L1 и поддержкой на L2.
- Новые EIP-ы по лимитам кода — обсуждаются поднятия/метризация лимита развернутого кода сверх 24 576 байт; проверяйте актуальный статус перед релизами.
- Account Abstraction — влияет на типы транзакций, но не меняет базовую модель EVM; важно для UX и газовой экономики.
14) Перелинковка по нашей вики (что читать далее)
Газ в Ethereum — полная таблица цен, base fee/priority fee, calldata-pricing.
ABI и кодирование — как кодируется calldata/returndata, динамические типы.
Keccak-256 — разница с SHA-3, практические советы.
Solidity и Vyper — языки для EVM.
Yul/Yul+ — промежуточный язык, инлайн-ассемблер.
Селекторы функций и события — 4-байтовые метод-ID и topic[0].
Реинэнтрантность и DELEGATECALL-риски — ключевые векторы атак.
CREATE2 и детерминированные адреса — фабрики, «адреса-заглушки».
Предкомпилы EVM — назначение и цена на L1/L2.
EVM-эквивалентность на L2 — чем отличается от «совместимости».
Памятка по storage-layout в Solidity · Шаблоны оптимизации газа · EOF: формат объектов EVM · SELFDESTRUCT после EIP-6780.
15) Мини-глоссарий
Word (слово) — 32 байта; базовая единица для большинства операций EVM.
Warm/Cold — тёплый/холодный доступ к адресу/слоту в рамках одной транзакции; первый доступ дороже (Berlin).
Transient storage — временное «хранилище» уровня транзакции (TSTORE/TLOAD), не сохраняется в состоянии.
Precompile — «вшитые» по адресам контракты для дорогих криптоопераций.
Access list — список адресов/слотов, объявленных заранее, чтобы удешевить «холодные» доступы.
16) Чек-лист для аудита EVM-контрактов
Флоу ценностей: где и как уходят ETH/tokens (порядок checks-effects-interactions, отсутствие «голых» call без лимита газа, обоснованный transfer).
DELEGATECALL-поверхность: строго фиксированный implementation слот, защита инициализации, «сторож» апгрейдов.
SSTORE-экономика: нет ли «лишних записей»; конструирование значений (битпак, merge-update), batched-модификации.
CREATE2: проверка адресов до/после деплоя, невозможность «подмены» кода.
События: все критичные state-change отражены в событиях; корректные indexed поля.
EIP-совместимость: PUSH0/MCOPY/TSTORE/TLOAD/SELFDESTRUCT-семантика тестируется на целевой сети (особенно L2).
Итог
EVM — строгая, минималистичная машина, идеально приспособленная для детерминированных вычислений с измеримой стоимостью. Глубокое понимание стека-памяти-хранилища, газовой модели и эволюции опкодов — ключ к безопасным и экономным контрактам. Сохраняйте эту страницу в закладки — мы будем дополнять разделы и расширять перелинковку по мере развития нашей вики.