
В этой статье я разбираю, как работает CREATE2, чем он отличается от CREATE, зачем нужен в контексте state channels и ERC-4337, и как его можно использовать — от вычисления адресов в Uniswap V2 до взлома Tornado Cash на $1M. Показываю примеры на Solidity и Assembly, а ещё — как на одном и том же адресе можно развернуть два разных смарт-контракта. Да, и такое возможно.
Опкод CREATE
Раньше было лучше — ну или, по крайней мере, надежнее. Так можно сказать про опкод CREATE
, предшественника CREATE2
. Он был прост и не создавал проблем (возможных уязвимостей).
На самом деле, опкод CREATE
никуда не делся — он используется всегда, когда смарт-контракт создается через ключевое слово new
:
contract Bar {
// @notice Создание смарт-контракта через create без отправки ETH на новый адрес
function createFoo() external returns (address) {
Foo foo = new Foo();
return address(foo);
}
// @notice Создание смарт-контракта через create с отправкой ETH на новый адрес
function createBaz() external payable returns (address) {
Baz baz = new Baz{value: msg.value}();
return address(baz);
}
}
Полный код контракта приведен здесь.
Опкод CREATE
принимает три аргумента и возвращает одно значение:
Входные данные (Stack input):
value
— количество нативной валюты в wei, которое будет отправлено на новый адрес.offset
— смещение байтов, с которого начинается код инициализации контракта.size
— размер кода инициализации.
Выходные данные (Stack output):
address
— адрес развернутого смарт-контракта либо0
, если произошла ошибка.
Развертывание смарт-контракта через assembly выглядит нагляднее:
contract Deployer {
// @notice Создание смарт-контракта через create без отправки wei на новый адрес
function deployFoo() public returns (address) {
address foo;
bytes memory initCode = type(Foo).creationCode;
assembly {
// Загружаем код инициализации в память
let codeSize := mload(initCode) // Размер кода инициализации
let codeOffset := add(initCode, 0x20) // Пропускаем 32 байта, содержащие длину массива initCode
// Вызываем CREATE без отправки msg.value
foo := create(0, codeOffset, codeSize)
// Проверяем, что контракт был успешно создан
if iszero(foo) { revert(0, 0) }
}
return foo;
}
}
Полный код смарт-контракта с созданием через assembly — здесь.
Вычисление адреса опкодом CREATE (0xf0)
Чтобы опкод CREATE
мог вернуть адрес развернутого контракта, ему необходимы адрес вызывающей стороны (msg.sender
) и ее nonce
:
В упрощенном виде это выглядит так:
address = hash(sender, nonce)
Но на самом деле процесс сложнее:
address = keccak256(rlp([sender_address, sender_nonce]))[12:]
Где:
sender_address
— адрес отправителя, создающего контракт.sender_nonce
— nonce отправителя (количество транзакций, отправленных с этого адреса).rlp
— функция RLP-кодирования. RLP (Recursive Length Prefix) используется для сериализации данных в Ethereum, обеспечивая однозначное и предсказуемое кодирование.keccak256
— хеш-функция Keccak-256.[12:]
— первые 12 байт отбрасываются, посколькуkeccak256
возвращает 32 байта, а адрес в Ethereum занимает последние 20 байт хеша (32 - 20 = 12).
Таким образом, в теории можно вычислить адрес будущего смарт-контракта заранее. Однако есть проблема: этот адрес зависит от nonce
. Если перед развертыванием смарт-контракта будет отправлена другая транзакция, nonce
увеличится, и вычисленный адрес станет недействительным.
Из-за использования RLP для вычисления адреса в Solidity перед развертыванием необходима следующая громоздкая функция:
function computeAddressWithCreate(uint256 _nonce) public view returns (address) {
address _origin = address(this);
bytes memory data;
if (_nonce == 0x00) {
data = abi.encodePacked(bytes1(0xd6), bytes1(0x94), _origin, bytes1(0x80));
} else if (_nonce <= 0x7f) {
data = abi.encodePacked(bytes1(0xd6), bytes1(0x94), _origin, uint8(_nonce));
} else if (_nonce <= 0xff) {
data = abi.encodePacked(bytes1(0xd7), bytes1(0x94), _origin, bytes1(0x81), uint8(_nonce));
} else if (_nonce <= 0xffff) {
data = abi.encodePacked(bytes1(0xd8), bytes1(0x94), _origin, bytes1(0x82), uint16(_nonce));
} else if (_nonce <= 0xffffff) {
data = abi.encodePacked(bytes1(0xd9), bytes1(0x94), _origin, bytes1(0x83), uint24(_nonce));
} else {
data = abi.encodePacked(bytes1(0xda), bytes1(0x94), _origin, bytes1(0x84), uint32(_nonce));
}
return address(uint160(uint256(keccak256(data))));
}
Длина всего кодирования зависит от того, сколько байт нужно для кодирования nonce
, так как адрес имеет постоянную длину 20 байт, отсюда и много if
.
Например, если nonce 0, то параметры значат следующее:
0xd6
- длина всей структуры 22 байта (в случае сnonce
равным 0).bytes1(0x94)
- означает, что дальше идет поле длиной в 20 байт._origin
- поле адреса.bytes1(0x80)
означает, чтоnonce
равен 0, согласно RLP.
Остальное аналогично, только добавляется nonce
, как один байт и так далее. То есть в кодировке RLP важно явно указывать длину данных перед самими данными.
Я добавил эту функцию смарт-контракту Deployer — можете протестировать в Remix.
Мы с коллегами периодически пишем в нашем Telegram-канале. Иногда это просто мысли вслух, иногда какие-то наблюдения с проектной практики. Не всегда всё оформляем в статьи, иногда проще написать пост в телегу. Так что, если интересно, что у нас в работе и что обсуждаем, можете заглянуть.
Предпосылки создания CREATE2
В 2018 году Виталик Бутерин предложил EIP-1014: Skinny CREATE2 со следующей мотивацией:
"Позволяет проводить взаимодействия (фактически или контрфактически в каналах) с адресами, которые еще не существуют на блокчейне, но могут быть использованы, предполагая, что в будущем они будут содержать код, созданный определенным кодом инициализации. Важно для случаев использования каналов состояния, связанных с контрфактическими взаимодействиями с контрактами."
Звучит сложно, но попробую объяснить. Дело в state channels. До появления rollups они рассматривались как способ масштабирования Ethereum.
Если коротко, в каналах состояния существовали неэффективности, которые можно было устранить с помощью counterfactual instantiation. Суть в том, что смарт-контракт мог существовать контрфактически — то есть его не нужно было развертывать, но его адрес был известен заранее.
Этот контракт мог быть развернут ончейн в случае необходимости — например, если один из участников канала пытался обмануть другого в процессе офф-чейн транзакции.
Пример из описания механизма:
"Представьте платежный канал между Алисой и Бобом. Алиса отправляет Бобу 4 ETH через канал, подписав соответствующую транзакцию. Эта транзакция может быть развернута ончейн в любой момент, но этого не происходит. Таким образом, можно сказать: 'Контрфактически Алиса отправила Бобу 4 ETH'. Это позволяет им действовать так, как будто транзакция уже состоялась — она окончательная в рамках заданных моделей угроз."
То есть, по теории игр, зная, что существует такая "страховка", стороны не будут пытаться обмануть друг друга, а сам контракт, скорее всего, так и не придется развертывать.
Подробнее об этом можно почитать здесь и здесь, но тема непростая — я вас предупредил.
Как работает опкод CREATE2 (0xf5)
Опкод CREATE2
был введен в хардфорке Константинополь как альтернатива CREATE
. Главное отличие — способ вычисления адреса создаваемого контракта. Вместо nonce
деплойера используется код инициализации (creationCode
) и соль (salt
).
Новая формула вычисления адреса:
address = keccak256(0xff + sender_address + salt + keccak256(initialisation_code))[12:]
0xff
— префикс, предотвращающий коллизии с адресами, созданными черезCREATE
. В RLP-кодировке0xff
может использоваться только для данных петабайтного размера, что нереалистично в EVM. Дополнительноkeccak256
защищает от коллизий.sender_address
— адрес отправителя, создающего смарт-контракт.salt
— 32-байтовое значение, обычноkeccak256
от некоторого набора данных, который обеспечивает уникальность этой соли.initialisation_code
— код инициализации смарт-контракта.
Важно! Если CREATE
или CREATE2
вызывается в транзакции создания и адрес назначения уже содержит ненулевой nonce
или непустой code
, создание немедленно завершается (revert), аналогично ситуации, когда первый байт initialisation_code
— недействительный опкод.
Это означает, что если при деплое случится коллизия адреса с уже существующим смарт-контрактом (например, развернутым через CREATE
), произойдет revert
, так как nonce
адреса уже ненулевой. Это поведение нельзя изменить даже через SELFDESTRUCT
, так как он не сбрасывает nonce
в той же транзакции.
По сравнению с CREATE
, CREATE2
отличается лишь добавлением одного параметра на входе — salt
.
Входные данные (Stack input):
value
— количество нативной валюты (wei) для отправки на новый адрес.offset
— смещение байтов, с которого начинается код инициализации.size
— размер кода инициализации.salt
— 32-байтовое значение, используемое при создании контракта.
Выходные данные (Stack output):
address
— адрес развернутого контракта или0
, если произошла ошибка.
Использование CREATE2 в Solidity
В Solidity CREATE2
можно использовать так же, как CREATE
, просто добавив salt
:
contract DeployerCreate2 {
/// @notice Создание смарт-контракта через create2 без отправки wei
function create2Foo(bytes32 _salt) external returns (address) {
Foo foo = new Foo{salt: _salt}();
return address(foo);
}
/// @notice Создание смарт-контракта через create2 с отправкой wei
function create2Bar(bytes32 _salt) external payable returns (address) {
Bar bar = new Bar{value: msg.value, salt: _salt}();
return address(bar);
}
}
Полный код контракта здесь.
Важно! Опкоды CREATE
и CREATE2
используются только для создания смарт-контрактов из других смарт-контрактов. При первоначальном развертывании смарт-контракта все происходит совсем иначе - в поле транзакции to
записывается nil
(аналог null
), а его фактическое создание выполняется опкодом RETURN
в creationCode
, а не CREATE
.
CREATE2 с помощью Assembly
Пример кода на Assembly (взято из Cyfrin):
function deploy(bytes memory bytecode, uint256 _salt) public payable {
address addr;
/*
NOTE: Как вызвать create2
create2(v, p, n, s)
создает новый смарт-контракт с кодом в памяти от p до p + n
и отправляет v wei
и возвращает новый адрес
где новый адрес = первые 20 байт keccak256(0xff + address(this) + s + keccak256(mem[p…(p+n)]))
s = big-endian 256-битное значение
*/
assembly {
addr :=
create2(
callvalue(), // wei, отправленный с вызовом
add(bytecode, 0x20), // Код начинается после первых 32 байт (длина массива)
mload(bytecode), // Размер кода (первые 32 байта)
_salt // Соль
)
if iszero(extcodesize(addr)) { revert(0, 0) }
}
emit Deployed(addr, _salt);
}
Полный код контракта здесь.
Траты на газ
Ранее, при вычислении адреса через CREATE
, использовались только address
и nonce
, занимающие не более 64 байт. Поэтому дополнительная плата за вычисления не взималась (см. evm.codes).
В CREATE2
добавилось вычисление хеша от кода инициализации (hash_cost
), так как его размер может сильно варьироваться. Это изменило формулу расчета газа:
minimum_word_size = (size + 31) / 32
init_code_cost = 2 * minimum_word_size
hash_cost = 6 * minimum_word_size
code_deposit_cost = 200 * deployed_code_size
static_gas = 32000
dynamic_gas = init_code_cost + hash_cost + memory_expansion_cost + deployment_code_execution_cost + code_deposit_cost
Таким образом, использование CREATE2
обходится дороже CREATE
, но дает возможность более гибко работать с адресом смарт-контракта до его создания, что открывает новые возможности.
Преимущества CREATE2
Что дало введение нового опкода?
Контрфактическая инициализация
CREATE2
позволяет резервировать адреса смарт-контрактов до их фактического развертывания. Это особенно полезно в каналах состояния, о которых мы говорили ранее.-
Упрощение онбординга пользователей
В контексте абстракции аккаунтов контрфактическая инициализация позволяет создавать аккаунты оффчейн и развертывать их только при первой транзакции, которая к тому же может быть оплачена через релейный сервер. Это делает создание абстрактного аккаунта проще, чем создание EOA.В момент появления
CREATE2
это было лишь идеей, но спустя три года концепция была реализована в ERC-4337. Для этого используется статический вызовentryPoint.getSenderAddress(bytes initCode)
, который позволяет получить контрфактический адрес кошелька до его создания. Vanity-адреса
Можно подобрать "красивый" адрес, перебираяsalt
, например, если хотите, чтобы он начинался или заканчивался на определенные символы:0xC0FFEE...
,0xDEADBEEF...
и т. д.-
Эффективные адреса
В EVM стоимость нулевых и ненулевых байт различается. За каждый ненулевой байтcalldata
взимаетсяG_txdatanonzero
(16 газа), а за нулевой —G_txdatazero
(4 газа). Это значит, что если ваш адрес начинается с нулей, его использование будет дешевле.Здесь подробно разобран этот аспект: On Efficient Ethereum Addresses (хотя расчеты по газу уже устарели из-за изменения стоимости
calldata
). -
Метаморфичные контракты
Способ обновления смарт-контрактов черезCREATE2
, при котором смарт-контракт уничтожается (SELFDESTRUCT
) и создается заново с тем же адресом, но с новым кодом. К счастью сообщество не приняло этот подход, например, в этой статье его называют "уродливым сводным братом Transparent Proxy".Примеры кода можно посмотреть здесь.
-
Вычисление адреса вместо хранения
В ряде случаев проще вычислить адрес контракта, развернутого черезCREATE2
, чем хранить его. Яркий пример — Uniswap v2.Как это работает?
-
Для создания пар через UniswapV2Factory используется
CREATE2
, а в качестве соли используются адреса двух токенов в паре. Обратите внимание, что смарт-контракт пары использует функциюinitialize
для сохранения адресов токенов, это важный момент.function createPair(address tokenA, address tokenB) external returns (address pair) { /// ... bytes memory bytecode = type(UniswapV2Pair).creationCode; bytes32 salt = keccak256(abi.encodePacked(token0, token1)); assembly { pair := create2(0, add(bytecode, 32), mload(bytecode), salt) } IUniswapV2Pair(pair).initialize(token0, token1); /// ... }
-
Теперь библиотека
UniswapV2Library
может вычислять адрес пары, используя заранее известныйinit code hash
в функции pairFor. При этом код инициализации можно просто захардкодить, потому что нам не нужно добавлять аргументы конструктора (именно поэтому используетсяinitialize
):function pairFor(address factory, address tokenA, address tokenB) internal pure returns (address pair) { (address token0, address token1) = sortTokens(tokenA, tokenB); pair = address(uint(keccak256(abi.encodePacked( hex'ff', factory, keccak256(abi.encodePacked(token0, token1)), hex'96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f' // init code hash )))); }
⚠️ Если форкаете Uniswap v2, не забудьте поменять
init code hash
, так как он зависит от фабрики (factory
), адрес которой устанавливается в конструкторе смарт-контракта пары. -
Ну и теперь имея функцию
pairFor
можно спокойно вычислять этот адрес когда необходимо. Только посмотрите как часто эта функция используется в UniswapV2Router01. К примеру так выглядит функция добавления ликвидности:function addLiquidity( address tokenA, address tokenB, uint amountADesired, uint amountBDesired, uint amountAMin, uint amountBMin, address to, uint deadline ) external override ensure(deadline) returns (uint amountA, uint amountB, uint liquidity) { (amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin); address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB); ///... }
-
Как видно, CREATE2
открыл множество новых возможностей, хотя у него есть и недостатки.
Уязвимость CREATE2
В документации Solidity можно встретить следующее предупреждение:
"Создание солей имеет некоторые особенности. Контракт может быть повторно создан по тому же адресу после того, как он был уничтожен. При этом вновь созданный контракт может иметь другой развернутый байткод, даже если байткод создания был тем же самым (что является обязательным условием, поскольку в противном случае адрес изменился бы). Это связано с тем, что конструктор может запросить внешнее состояние, которое могло измениться между двумя созданиями, и включить его в развернутый байткод до того, как он будет сохранен."
Речь идет о хорошо известной уязвимости: комбинация CREATE
и CREATE2
в сочетании с SELFDESTRUCT
позволяет развернуть на одном и том же адресе разные контракты. Именно этот метод использовали при взломе Tornado Cash, когда был украден $1M.
Это также касается метаморфических контрактов.
Демонстрация атаки
Есть репозиторий, в котором повторяется подобная атака. Я немного изменил этот код, чтобы можно было проверить его в Remix, ниже мы его разберем чуть подробнее:
Контракт фабрики:
contract Factory {
function createFirst() public returns (address) {
return address(new First());
}
function createSecond(uint256 _number) public returns (address) {
return address(new Second(_number));
}
function kill() public {
selfdestruct(payable(address(0)));
}
}
Эта фабрика создает смарт-контракты с идентичными адресами, но разным кодом. ?
Шаг 1. Деплоим MetamorphicContract
и вызываем функцию firstDeploy
:
function firstDeploy() external {
factory = new Factory{salt: keccak256(abi.encode("evil"))}();
first = First(factory.createFirst());
emit FirstDeploy(address(factory), address(first));
first.kill();
factory.kill();
}
Этот вызов:
Деплоит фабрику и первую версию смарт-контракта.
Уничтожает их сразу после развертывания.
Логирует их адреса.
Уничтожает оба контракта.
Результат в Remix:

Шаг 2. Теперь можно вызывать функцию secondDeploy
:
function secondDeploy() external {
/// Проверяем, что контракты удалены
emit CodeLength(address(factory).code.length, address(first).code.length);
/// Деплоим фабрику на тот же адрес
factory = new Factory{salt: keccak256(abi.encode("evil"))}();
/// Деплоим новый контракт на тот же адрес, что и первый
second = Second(factory.createSecond(42));
/// Проверяем, что адреса совпадают
require(address(first) == address(second));
/// Выполняем логику нового контракта
second.setNumber(21);
/// Логируем адреса
emit SecondDeploy(address(factory), address(second));
}
Результат в Remix:

Что здесь произошло?
Развернули фабрику через
CREATE2
с фиксированной солью.Фабрика через
CREATE
создала контракт-имплементацию. Ее адрес зависит от адреса фабрики иnonce
.Уничтожили оба контракта (
SELFDESTRUCT
). Это обнулило nonce фабрики.Развернули ту же фабрику по тому же адресу (так как соль не изменилась).
Развернули другую имплементацию по тому же адресу, так как
nonce
фабрики снова0
.Теперь на одном и том же адресе — совершенно другой код!
Полный код контракта тут.
Теперь вы знаете, как можно развернуть разные смарт-контракты на одном адресе. ?