Разработка factory-контрактов с CREATE2 (детерминистический деплой)
CREATE2 — опкод EVM (EIP-1014), введённый в Constantinople hard fork. Адрес деплоимого контракта вычисляется заранее: он детерминирован от deployer address, salt и keccak256(initcode). Изменишь любой из параметров — получишь другой адрес. Это радикально меняет подход к архитектуре протоколов.
Где CREATE2 решает реальные проблемы
Counterfactual deployment. Пользователь может получить адрес своего Smart Account до деплоя. Передаёт этот адрес для получения средств — а сам контракт деплоится только при первой транзакции (вместе с ней же). EIP-4337 account abstraction полностью построен на этом паттерне: initCode в UserOperation содержит вызов factory, который через CREATE2 деплоит Account контракт на заранее известный адрес.
Uniswap V2/V3 pair addresses. Adres любого пула 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, задеплоен на сотнях чейнов с одинаковым адресом.
Реализация 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, код остаётся — адрес не переиспользуется.
Salt design
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).
Minimal Proxy (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 с CREATE2 и вычислением адресов: 2-3 дня. Factory с minimal proxy паттерном и инициализацией: 3-4 дня. Мультичейн deployment infrastructure с Arachnid proxy: 4-5 дней включая скрипты деплоя и верификацию.
Стоимость рассчитывается после уточнения требований к инфраструктуре деплоя и количеству целевых чейнов.







