CREATE2 в Ethereum: детерминированные адреса смарт-контрактов

CREATE2 — это вариант операции создания контракта в EVM, при котором адрес нового контракта заранее известен и детерминирован. В отличие от обычного CREATE, где адрес зависит от nonce создателя, при CREATE2 он вычисляется из фиксированного набора параметров: адреса деплоера, salt и хеша init-кода.

CREATE2 введён для того, чтобы:

  • заранее знать адрес контракта, который будет создан в будущем;
  • строить схемы «контракт по требованию» (counterfactual deployment), когда адрес используется до фактического деплоя;
  • надёжно связывать адрес контракта с конкретным init-кодом.

CREATE2 в Ethereum: детерминированные адреса смарт-контрактов

CREATE vs CREATE2: как считаются адреса

При обычном создании контракта (опкод CREATE) адрес вычисляется так:

  • берутся address создателя и его nonce;
  • вычисляется RLP-структура RLP(sender, nonce);
  • делается keccak256 от этого значения;
  • адрес контракта — последние 20 байт результата.

Следствия:

  • адрес зависит от истории транзакций создателя (nonce);
  • заранее предсказать адрес можно, только зная точное значение nonce в момент деплоя.

Для CREATE2 формула другая:

  • адрес контракта = последние 20 байт от

keccak256( 0xff, deployer, salt, keccak256(init_code) )

где:

  • 0xff — фиксированный префикс-байт;
  • deployer — адрес контракта/EOA, выполняющего CREATE2;
  • salt — 32-байтовое значение, задаваемое при вызове;
  • keccak256(init_code) — хеш кода инициализации контракта (байткод, который выполняется при создании).

Важные свойства:

  • адрес не зависит от nonce;
  • если не меняются deployer, salt и init_code, адрес будет всегда одинаковым;
  • для любого данного набора параметров существует ровно один возможный адрес контракта.

Компоненты формулы CREATE2

  • Адрес деплоера (deployer)

Это аккаунт или контракт, из которого делается создание. Он жёстко зашит в формулу, поэтому смена фабрики меняет и адрес создаваемых контрактов.

  • Значение salt

32-байтовый идентификатор, который задаётся вызывающим кодом. Его можно:

  • использовать как счётчик (0, 1, 2, …) для генерации серии адресов;
  • привязать к какому-то внешнему идентификатору (ID пользователя, хеш данных и т.п.).
  • init_code и его хеш

Это код инициализации, который:

  • выполняется один раз при создании контракта;
  • в конце возвращает итоговый байткод, который будет записан в хранилище кода по адресу контракта.

Любое изменение init-кода (даже один байт) меняет keccak256(init_code), а значит — и адрес результата.

Так обеспечивается важное свойство: адрес однозначно связан с ожидаемым кодом. Если init-код другой, то и адрес уже другой.

Практические сценарии использования

CREATE2 применяется в нескольких типичных паттернах:

  • Фабрики контрактов (factory pattern)

Фабрика может:

  • заранее вычислить адрес нового контракта по salt и init-коду;
  • показать пользователю этот адрес до фактического деплоя;
  • использовать его в других контрактах, ссылках, подписи оффчейн-соглашений и т.д.
  • Counterfactual deployment

Контракт «существует» логически (его адрес уже известен и используется), но фактически ещё не задеплоен:

  • пользователи могут подписывать сообщения, где указано «будущий адрес»;
  • позже фабрика создаёт контракт по CREATE2, и все ранее собранные данные начинают работать с уже существующим адресом.
  • Смарт-кошельки и account abstraction

В системах абстракции аккаунтов часто:

  • адрес кошелька пользователя заранее вычисляется через CREATE2;
  • пользователь может получать средства на этот адрес до того, как кошелёк развёрнут;
  • при первом использовании кошелька контракт создаётся и «подхватывает» уже лежащий на адресе баланс.
  • Детерминированные прокси

Шаблоны минимальных прокси (minimal proxy / clone) используют CREATE2, чтобы:

  • получать повторяемые, предсказуемые адреса клонов;
  • строить фронтенд- и ончейн-логику вокруг заранее известных адресов.

CREATE2 в Solidity

В Solidity поддержка CREATE2 есть на уровне языка:

  • синтаксис вида

new MyContract{salt: _salt}(args)

использует именно CREATE2, а не обычный CREATE;

  • компилятор сам:
    • рассчитывает init-код для MyContract;
    • подставляет salt и вызывает соответствующий опкод EVM.

При использовании inline-assembly разработчик может напрямую вызвать опкод CREATE2, передав:

  • количество wei для деплоя;
  • указатель на init-код в памяти и его длину;
  • 32-байтовый salt.

Важно:

  • адрес можно заранее посчитать в off-chain-коде (скриптах, фронтенде), используя ту же формулу с Keccak-256;
  • нужно гарантировать, что init-код, с которым считается адрес, действительно совпадает с тем, который будет использован при деплое.

Связь с безопасностью и риски

CREATE2 открывает и специфические риски, подробно рассматриваемые в рисках CREATE2:

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

Поэтому при проектировании протоколов и фабрик контрактов важно:

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

См. также

Task Runner