Factory-контракты с CREATE2: детерминистический деплой

Factory-контракты с CREATE2: детерминистический деплой Мы часто сталкиваемся с задачей детерминистического деплоя смарт-контрактов. `CREATE2` — опкод EVM (EIP-1014), введённый в Constantinople hard fork. Адрес деплоимого контракта вычисляется заранее: он детерминирован от `deployer address`, `sal

Направления блокчейн-разработки

Часто задаваемые вопросы

Последние работы

  • image_website-b2b-advance_0.webp
    Разработка сайта компании B2B ADVANCE
    1450
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1308
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    1003
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1269
  • image_logo-advance_0.webp
    Разработка логотипа компании B2B Advance
    717
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    1008

Factory-контракты с CREATE2: детерминистический деплой

Мы часто сталкиваемся с задачей детерминистического деплоя смарт-контрактов. CREATE2 — опкод EVM (EIP-1014), введённый в Constantinople hard fork. Адрес деплоимого контракта вычисляется заранее: он детерминирован от deployer address, salt и keccak256(initcode). Изменишь любой из параметров — получишь другой адрес. Это радикально меняет подход к архитектуре протоколов. Наш опыт показывает, что правильная реализация factory с CREATE2 сокращает время развёртывания и исключает ошибки при адресации контрактов. Мы гарантируем безопасное развертывание с защитой от frontrunning.

Почему CREATE2 важен для детерминированного деплоя?

Counterfactual deployment. Пользователь может получить адрес своего Smart Account до деплоя. Передаёт этот адрес для получения средств — а сам контракт деплоится только при первой транзакции (вместе с ней же). EIP-4337 account abstraction полностью построен на этом паттерне: initCode в UserOperation содержит вызов factory, который через CREATE2 деплоит Account контракт на заранее известный адрес.

Uniswap V2/V3 pair addresses. Адрес любого пула Uniswap V2 вычисляется off-chain через CREATE2 формулу: keccak256(abi.encodePacked(hex'ff', factory, keccak256(abi.encodePacked(token0, token1)), INIT_CODE_PAIR_HASH)). Router не хранит маппинг пар — он вычисляет адрес на лету. Это экономит тысячи операций SLOAD.

Cross-chain консистентность. Протокол деплоит контракты на 8 чейнах по одному и тому же адресу. Пользователи и интеграторы знают адрес заранее, whitelist настраивается один раз. Достигается через Arachnid's Deterministic Deployment Proxy (0x4e59b44847b379578588920cA78FbF26c0B4956C) — он же Nick's factory, задеплоен на сотнях чейнов с одинаковым адресом.

CREATE vs CREATE2: сравнительная таблица

Характеристика CREATE CREATE2
Вычисление адреса от nonce деплоера от salt + initcode + deployer address
Предсказуемость нет (nonce меняется) да (детерминирован)
Counterfactual deployment невозможен полностью поддерживается
Защита от frontrunning встроенная (nonce) требует salt с msg.sender
Газ для деплоя ~60K gas ~40-50K gas (меньше)
Повторный деплой на тот же адрес возможен после selfdestruct (до Cancun) только после selfdestruct (до Cancun)

Как защитить salt от frontrunning?

Salt — это bytes32. Неосторожный salt открывает frontrunning: кто-то видит в mempool вашу транзакцию деплоя, берёт тот же bytecode и salt, деплоит первым на желаемый адрес. Ваша транзакция упадёт.

Защита: включать msg.sender в salt:

bytes32 salt = keccak256(abi.encodePacked(msg.sender, userProvidedSalt)); 

Теперь adversary с другим msg.sender получит другой адрес — ваш адрес ему недоступен.

Для протоколов, где адрес должен быть одинаковым на всех чейнах независимо от деплоера, используем фиксированный salt без msg.sender — но тогда деплой идёт через доверенный deployer (multisig или deployment script с DEPLOY_KEY).

Реализация Factory с CREATE2

contract ContractFactory { event Deployed(address indexed contractAddress, bytes32 indexed salt); function deploy(bytes memory bytecode, bytes32 salt) external returns (address contractAddress) { assembly { contractAddress := create2( 0, // value (ETH) add(bytecode, 0x20), // bytecode start (skip length prefix) mload(bytecode), // bytecode length salt // salt ) } require(contractAddress != address(0), "Deploy failed"); emit Deployed(contractAddress, salt); } function computeAddress(bytes memory bytecode, bytes32 salt) external view returns (address) { bytes32 hash = keccak256(abi.encodePacked( bytes1(0xff), address(this), salt, keccak256(bytecode) )); return address(uint160(uint256(hash))); } } 

Инициализация через constructor vs initializer

При CREATE2 деплое контракт с constructor-параметрами включает параметры в initcode — тогда одинаковые параметры дадут один адрес, разные — разные. Это нормально.

Если контракт использует proxy паттерн (деплоится minimal proxy через CREATE2, а реализация — отдельно), constructor не запускается. Инициализация через initialize() функцию обязательна, и она должна быть защищена от двойного вызова (initializer modifier от OpenZeppelin или ручной флаг).

Тонкость: CREATE2 с одним salt можно выполнить только один раз — если контракт по этому адресу уже существует, деплой вернёт address(0). Если контракт был selfdestruct-нут (до Cancun EIP-6780), адрес освобождается и CREATE2 с тем же salt можно повторить. После Cancun EIP-6780 selfdestruct убирает только ETH, код остаётся — адрес не переиспользуется.

Минимальный прокси (EIP-1167) + CREATE2

Комбинация часто используется в протоколах с тысячами инстансов (lending positions, yield vaults, game characters). Minimal proxy — 45-байтовый контракт, который делегирует все вызовы к implementation. Деплой стоит ~40K gas вместо 200K-500K для полного контракта.

function deployProxy(address implementation, bytes32 salt) external returns (address proxy) { bytes memory bytecode = abi.encodePacked( hex"3d602d80600a3d3981f3363d3d373d3d3d363d73", implementation, hex"5af43d82803e903d91602b57fd5bf3" ); assembly { proxy := create2(0, add(bytecode, 0x20), mload(bytecode), salt) } require(proxy != address(0), "Deploy failed"); IInitializable(proxy).initialize(/* params */); } 

OpenZeppelin предоставляет Clones.cloneDeterministic(implementation, salt) — готовый wrapper над этим паттерном.

Тестирование

Foundry упрощает тестирование CREATE2: vm.computeCreate2Address(salt, keccak256(bytecode), deployer) даёт предсказуемый адрес в тесте. Проверяем:

  • Задеплоированный адрес совпадает с вычисленным computeAddress()
  • Повторный деплой с тем же salt возвращает address(0)
  • Инициализация через initialize() не вызывается дважды
  • Frontrunning salt защищён (если нужно)

Что входит в работу

  • Документация архитектуры factory
  • Реализация factory с CREATE2 (с защитой salt)
  • Интеграция minimal proxy при необходимости
  • Набор тестов (Foundry) с покрытием граничных случаев
  • Скрипты деплоя для одного или нескольких чейнов
  • Верификация контрактов в Etherscan (или аналогах)
  • Поддержка после деплоя в течение 2 недель

Мы разрабатываем factory-контракты под ключ — от проектирования до деплоя. Оцениваем проект бесплатно, свяжитесь с нами. Более 5 лет опыта в Solidity и 30+ успешных проектов — гарантируем безопасность и надёжность вашего кода.

Сроки

Базовый factory с CREATE2 и вычислением адресов: 2-3 дня. Factory с minimal proxy паттерном и инициализацией: 3-4 дня. Мультичейн deployment infrastructure с Arachnid proxy: 4-5 дней включая скрипты деплоя и верификацию.

Стоимость рассчитывается после уточнения требований к инфраструктуре деплоя и количеству целевых чейнов.