A protocol announces counterfactual deployment: users can fund a wallet at a predicted address before the contract is deployed on-chain. The address is deterministic — computed from the factory address, a user-supplied salt, and the contract bytecode. The team publishes the salt generation logic. Two weeks later, an attacker reads the mempool, computes the same address, and races to deploy a malicious contract there first using a higher gas price. The user's funds arrive. They interact with the attacker's contract. The funds are gone.
This is the CREATE2 front-running attack. It is one of six vulnerability classes that live in factory contract patterns. This post covers all six with code.
How Factory Contracts Work
A factory contract deploys child contracts on demand with user-supplied parameters. Uniswap V2 uses this pattern — one pair contract per token combination, each created by the factory at runtime.
Two opcodes create child contracts:
CREATE derives the address from keccak256(rlp(deployer, nonce)). The nonce increments with each deployment, so the address is not known until you know the nonce — sequential but not pre-computable.
CREATE2 derives the address from four fixed inputs:
address = keccak256(0xff, deployer, salt, keccak256(bytecode))[12:]
deployer is the factory address. salt is any 32-byte value. The result is fully deterministic — compute it off-chain before sending any transaction. That is counterfactual deployment: fund a wallet before it exists, commit to a dispute contract in a state channel, deploy EIP-1167 clones at known addresses. It is also the attack surface.
Vulnerability 1: Front-Running CREATE2 Deployment
The attack is straightforward: the factory's deploy() transaction sits in the public mempool. An attacker reads the salt and bytecode, computes the target address, and sends their own deployment transaction to the same address with a higher gas price.
// VULNERABLE: factory deploy function
contract VulnerableFactory {
function deploy(bytes32 salt, bytes memory bytecode) external returns (address deployed) {
assembly {
deployed := create2(0, add(bytecode, 0x20), mload(bytecode), salt)
}
require(deployed != address(0), "Deployment failed");
}
}
The practical attack path: the same factory is used, the same salt is available to any caller, and the bytecode is known (it is in the mempool or published in the frontend). An attacker calls the factory's deploy() before the intended deployer — same inputs, higher gas price, wins.
Fix: bind salt to msg.sender
// SECURE: salt incorporates msg.sender — no two callers produce the same address
contract SecureFactory {
mapping(address => uint256) public deploymentNonce;
function deploy(bytes calldata initCode) external returns (address deployed) {
// Salt is derived from deployer identity + their nonce
// Attacker cannot reproduce this salt without controlling msg.sender
bytes32 salt = keccak256(abi.encode(msg.sender, deploymentNonce[msg.sender]++));
assembly {
deployed := create2(0, add(initCode, 0x20), mload(initCode), salt)
}
require(deployed != address(0), "Deployment failed");
}
function computeAddress(address deployer, uint256 nonce) external view returns (address) {
bytes32 salt = keccak256(abi.encode(deployer, nonce));
bytes32 hash = keccak256(abi.encodePacked(
bytes1(0xff),
address(this),
salt,
INIT_CODE_HASH // precomputed constant for your child contract
));
return address(uint160(uint256(hash)));
}
}
With the salt derived from msg.sender, an attacker using a different account produces a different address — the front-run lands elsewhere.
Vulnerability 2: Factory Not Initializing Clones Immediately
EIP-1167 minimal proxies (clones) delegate all calls to an implementation contract with no constructor logic — initialization happens via a separate initialize() call. The attack window opens when that call is a separate transaction from deployment.
// VULNERABLE: clone deployed in one tx, initialized in the next
contract VulnerableCloneFactory {
address public immutable implementation;
constructor(address _impl) {
implementation = _impl;
}
function deployClone() external returns (address clone) {
// Deploys the EIP-1167 proxy — but does NOT call initialize()
clone = Clones.clone(implementation);
emit CloneDeployed(clone);
// Gap between deploy and initialize — attacker can call initialize() here
}
}
// Victim follows up in a separate transaction:
// IMyContract(clone).initialize(msg.sender);
// Attacker front-runs that second transaction and calls:
// IMyContract(clone).initialize(attacker);
// Now attacker is the owner
This is the uninitialized proxy pattern applied to clones. The window between deployment and initialization is measured in blocks, and any address can call initialize() during that window.
Fix: deploy and initialize in the same transaction
// SECURE: clone is deployed and initialized atomically
contract SecureCloneFactory {
address public immutable implementation;
constructor(address _impl) {
implementation = _impl;
}
function deployAndInitialize(
address owner,
uint256 param1,
bytes32 param2
) external returns (address clone) {
clone = Clones.clone(implementation);
// Initialize in the same transaction — no gap, no front-running window
IMyContract(clone).initialize(owner, param1, param2);
// Verify initialization succeeded
require(IMyContract(clone).owner() == owner, "Init failed");
emit CloneDeployed(clone, owner);
}
}
The initialize() and clone() calls are atomic — no block boundary, no window for an attacker.
The implementation contract should also guard against double-initialization using OpenZeppelin's Initializable with _disableInitializers() in the constructor — this prevents anyone from calling initialize() directly on the implementation itself.
Vulnerability 3: Salt Reuse and Collision
CREATE2 reverts if the target address already has code, so overwriting a live contract is impossible. But accounting bugs arise when the factory allows the same user to reuse the same salt.
// VULNERABLE: no salt uniqueness enforcement
contract VulnerableRegistry {
mapping(address => address[]) public userContracts;
function deploy(bytes32 salt, bytes calldata initCode) external returns (address deployed) {
assembly {
deployed := create2(0, add(initCode, 0x20), mload(initCode), salt)
}
// CREATE2 reverts if address is occupied — but the revert message is opaque
// If the user self-destructs a contract and tries to redeploy with the same salt,
// they get a different contract at the same address — registry entry is stale
userContracts[msg.sender].push(deployed);
}
}
The nuanced case: if the deployed contract calls selfdestruct, the address clears. A re-deployment with the same salt succeeds with new bytecode — registries that trusted the original address now point to the replacement.
Fix: track used salts and prevent reuse
// SECURE: salt reuse is prevented
contract SecureRegistry {
mapping(address => mapping(bytes32 => bool)) public saltUsed;
mapping(address => address[]) public userContracts;
function deploy(bytes32 salt, bytes calldata initCode) external returns (address deployed) {
require(!saltUsed[msg.sender][salt], "Salt already used");
assembly {
deployed := create2(0, add(initCode, 0x20), mload(initCode), salt)
}
require(deployed != address(0), "Deployment failed");
saltUsed[msg.sender][salt] = true;
userContracts[msg.sender].push(deployed);
}
}
Vulnerability 4: Callback Reentrancy During Deployment
A contract's constructor runs during deployment. If the constructor calls out to an external contract — to mint an NFT, trigger a callback, or register itself — the factory may not have updated its state yet. That is a reentrancy window.
// VULNERABLE: child constructor triggers onERC721Received during deployment
contract ChildContract {
constructor(address factory, address nftContract, uint256 tokenId) {
// Pulls an NFT into the new contract during construction
// Triggers onERC721Received on THIS contract — before factory finishes deploy()
IERC721(nftContract).safeTransferFrom(msg.sender, address(this), tokenId);
}
}
contract VulnerableFactory {
uint256 public deployCount;
mapping(address => bool) public deployed;
function deploy(bytes calldata initCode) external returns (address child) {
assembly {
child := create2(0, add(initCode, 0x20), mload(initCode), 0)
}
// ChildContract constructor already ran — including the safeTransferFrom
// If safeTransferFrom called back into factory via onERC721Received,
// deployCount and deployed[] were not yet updated when the callback fired
deployCount++;
deployed[child] = true;
}
}
If the NFT contract calls onERC721Received back into the factory during that transfer, deployed[child] is still false — the factory's registry is inconsistent mid-execution.
Fix: precompute the child address, register it before deployment, and add a reentrancy guard
// SECURE: state updated before deployment, reentrancy guard applied
contract SecureFactory is ReentrancyGuard {
mapping(address => bool) public deployed;
function deploy(bytes calldata initCode) external nonReentrant returns (address child) {
bytes32 salt = keccak256(abi.encode(msg.sender, block.number));
bytes32 initCodeHash = keccak256(initCode);
// Precompute and register BEFORE deploying — consistent state during constructor callbacks
child = address(uint160(uint256(keccak256(abi.encodePacked(
bytes1(0xff), address(this), salt, initCodeHash
)))));
deployed[child] = true;
address result;
assembly {
result := create2(0, add(initCode, 0x20), mload(initCode), salt)
}
require(result == child, "Address mismatch");
}
}
Vulnerability 5: Deterministic Address Used as Trust Anchor
Protocols sometimes whitelist a predicted CREATE2 address before deployment. If the factory does not restrict who can deploy what bytecode, an attacker can race to put different code at that address.
// VULNERABLE: protocol whitelists the predicted address, attacker controls the bytecode
contract VulnerableProtocol {
mapping(address => bool) public trustedContracts;
// Admin pre-approves an address computed from expected deployment parameters
function whitelistPredicted(address factory, bytes32 salt, bytes32 initCodeHash) external onlyAdmin {
address predicted = address(uint160(uint256(keccak256(abi.encodePacked(
bytes1(0xff), factory, salt, initCodeHash
)))));
trustedContracts[predicted] = true;
// No deployment has happened yet — trust is granted to a phantom address
}
}
The address-to-code binding is not enforced by the whitelist alone. If the factory allows open deployment, the attacker deploys first — the protocol's whitelist entry now points to attacker code.
Fix: verify code hash post-deployment before trusting
// SECURE: whitelist only after deployment and code hash verification
contract SecureProtocol {
mapping(address => bool) public trustedContracts;
bytes32 public immutable expectedCodeHash;
constructor(bytes32 _expectedCodeHash) {
expectedCodeHash = _expectedCodeHash;
}
function registerDeployed(address deployed) external {
// Verify that the deployed contract has the expected runtime bytecode
bytes32 actualCodeHash;
assembly {
actualCodeHash := extcodehash(deployed)
}
require(actualCodeHash == expectedCodeHash, "Unexpected bytecode");
trustedContracts[deployed] = true;
}
}
Using extcodehash binds the trust grant to the actual runtime code at the address. If the attacker deployed different bytecode, the hash check fails.
Vulnerability 6: Missing Access Control on Factory Deploy
A factory with no deployment access control lets any caller create child contracts with arbitrary parameters, poisoning the factory's registry.
// VULNERABLE: unrestricted factory deploy
contract OpenFactory {
address[] public allChildren;
mapping(address => bool) public isChild;
function deploy(
address owner,
uint256 maxSupply,
string calldata name
) external returns (address child) {
// Any caller can set any owner, any maxSupply, any name
// Attacker deploys with owner = attacker, maxSupply = type(uint256).max
child = address(new ChildContract(owner, maxSupply, name));
allChildren.push(child);
isChild[child] = true; // Poisoned: attacker's malicious child is now "trusted"
}
}
If the protocol treats any isChild entry as trusted, the attacker now has a protocol-internal contract with full trust and attacker-controlled parameters.
Fix: access-controlled factory with parameter validation
// SECURE: access-controlled deploy with parameter validation
contract SecureFactory is AccessControl {
bytes32 public constant DEPLOYER_ROLE = keccak256("DEPLOYER_ROLE");
address[] public allChildren;
mapping(address => bool) public isChild;
uint256 public constant MAX_SUPPLY_CAP = 1_000_000_000 * 1e18;
constructor() {
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
}
function deploy(
uint256 maxSupply,
string calldata name
) external onlyRole(DEPLOYER_ROLE) returns (address child) {
require(maxSupply <= MAX_SUPPLY_CAP, "Supply exceeds cap");
require(bytes(name).length > 0 && bytes(name).length <= 64, "Invalid name");
child = address(new ChildContract(msg.sender, maxSupply, name));
allChildren.push(child);
isChild[child] = true;
emit ChildDeployed(child, msg.sender);
}
}
What ContractScan Detects
| Vulnerability | Detection Method |
|---|---|
| CREATE2 with user-controlled salt and no caller binding | Data flow: traces salt parameter from calldata to create2 opcode without msg.sender in derivation |
Clone deployed without same-transaction initialize() |
Control flow: detects Clones.clone() not followed by initialize() call in same function |
Salt reuse — no saltUsed tracking |
State analysis: create2 call with no mapping guard on the salt value |
| Constructor callback reentrancy | Call graph: external calls in constructors of deployed children with factory state not yet updated |
| Trust anchor whitelisted before deployment | Pattern match: trustedContracts[predicted] = true before extcodehash verification |
Factory deploy() with no access modifier |
AST analysis: public/external deploy function with no onlyOwner, onlyRole, or require on caller |
Missing nonReentrant on factory deployment function |
Modifier detection: create2 call without reentrancy guard when child constructor makes external calls |
Audit Your Factory Contract
A single vulnerability in a factory can compromise every child contract it deploys. The attack surface spans CREATE2 math, initialization ordering, constructor-boundary reentrancy, and registry trust models — none of which traditional static analysis handles well in isolation.
Audit your factory contract at https://contract-scanner.raccoonworld.xyz
Related Reading
- Upgradeable Contract Vulnerabilities: Uninitialized Proxy — the uninitialized proxy pattern that factory clones inherit, and how
_disableInitializers()prevents it - Solidity Access Control Patterns: onlyOwner, Roles, and Multisig — securing factory deploy functions with role-based access control and multisig enforcement
This post is for informational and educational purposes only. It does not constitute financial, legal, or investment advice. Always conduct a professional audit before deploying smart contracts to production.
\n\n## Important Notes\n\nThis post is for informational and educational purposes only. It does not constitute financial, legal, or investment advice. The security analysis provided is based on available data and automated tools, which may not capture all potential vulnerabilities. Always conduct a professional audit before deploying smart contracts.