Opcode Summary
| Property | Value |
|---|---|
| Opcode | 0xF0 |
| Mnemonic | CREATE |
| Gas | 32000 + memory_expansion_cost + code_deposit_cost (200 * deployed_code_size) |
| Stack Input | value, offset, length (ETH to send, memory offset of init code, length of init code) |
| Stack Output | addr (address of newly created contract, or 0 on failure) |
| Behavior | Creates a new contract by executing the init code read from memory at [offset, offset+length). The new contract’s address is computed as keccak256(rlp([sender_address, sender_nonce]))[12:]. Transfers value wei from the caller to the new contract before init code execution. The init code runs in the new contract’s context and returns the runtime bytecode via RETURN. If creation fails (init code reverts, insufficient gas, insufficient balance, call depth exceeded, or deployed code exceeds 24576 bytes), pushes 0 onto the stack and all state changes from the init code are reverted, but the CREATE instruction itself does not revert — execution continues in the parent context. The sender’s nonce is incremented regardless of success or failure. |
Threat Surface
CREATE is one of the most powerful opcodes in the EVM. It spawns entirely new execution contexts, transfers ETH, increments nonces, and persists new code on-chain — all within a single instruction. This breadth of side effects produces an unusually wide attack surface.
The threat surface centers on six properties:
-
Nonce-dependent address derivation is predictable. The address of a CREATE-deployed contract is
keccak256(rlp([sender, nonce]))[12:]. Both the sender address and the nonce are publicly observable on-chain. Anyone who knows the deployer’s current nonce can precompute the address of the next contract deployment. This enables front-running attacks, cross-chain replay attacks, and assumptions about address ownership that break when nonces diverge across chains. -
Init code executes in a constructor context with full capabilities. The init code supplied to CREATE runs as a full EVM execution context inside the newly created contract. It can call other contracts, transfer ETH, read/write storage, and even invoke CREATE again (nested creation). This means a malicious init code can perform arbitrary operations — including reentering the parent contract — before the parent receives the return value. The parent has no visibility into what the init code does during execution.
-
Code deposit cost creates economic constraints. After init code execution, the EVM charges 200 gas per byte of the returned runtime bytecode (the “code deposit cost”). Combined with the 32000-gas base cost and memory expansion costs, CREATE can consume substantial gas. If the total gas required exceeds the gas available, the creation silently fails (returns 0) rather than reverting the entire transaction.
-
Silent failure: returns 0 on failure without reverting. Unlike a
REVERTorINVALID, a failed CREATE does not halt execution. It pushesaddress(0)onto the stack and continues. If the caller does not explicitly check for zero, subsequent operations (e.g., storing the address, calling it, approving it) proceed withaddress(0), leading to lost funds, broken state, or exploitable race conditions. -
Gas forwarding follows the 63/64 rule. CREATE forwards at most 63/64 of the remaining gas to the init code execution (EIP-150). An attacker who controls the gas supplied to a transaction can cause CREATE to receive insufficient gas for the init code, triggering a silent failure even when the init code is well-formed. This is a vector for griefing attacks.
-
Value transfer during creation. CREATE transfers
valuewei from the calling contract to the new contract before init code execution. If creation fails, the value transfer is rolled back. But the checkaddress(this).balance >= valuemust pass before execution begins — if the caller doesn’t have sufficient balance, CREATE fails silently. Combined with the silent failure property, this can lead to scenarios where contracts believe they funded a new contract but the creation never happened.
Smart Contract Threats
T1: Unchecked CREATE Return Value — Silent Deployment Failure (High)
CREATE returns address(0) on failure. The most common vulnerability is failing to check this return value, allowing the parent contract to continue execution as if deployment succeeded. This manifests in several dangerous patterns:
-
Factory contracts storing address(0). A factory that calls CREATE and stores the returned address in a mapping or array without checking for zero will record a null entry. If other contracts or users interact with this address, ETH and token transfers to
address(0)are effectively burned (irretrievable for ERC-20 tokens) or simply lost (for ETH in most implementations). -
Conditional logic on the returned address. If the parent uses the returned address for follow-up operations (e.g.,
IERC20(newContract).approve(...)orIChild(newContract).initialize(...)), callingaddress(0)may silently succeed (the zero address has no code, so static calls return empty data) or fail in unpredictable ways depending on the callee’s fallback behavior. -
Assembly-level deployments. When developers use inline assembly (
create(value, offset, length)) instead of Solidity’snewkeyword, the compiler does not automatically insert a zero-check. This is especially dangerous in low-level factory patterns, proxy deployers, and minimal proxy (EIP-1167) cloners.
Why it matters: Solidity’s new ContractName() reverts on failure (since Solidity 0.8.x), but inline assembly CREATE does not. Any protocol using assembly-level CREATE without a zero-check has this vulnerability.
T2: Init Code Reentrancy During Construction (High)
The init code executed by CREATE runs in a full execution context and can make external calls, including calling back into the parent contract:
-
Callback during creation. The init code can call the parent contract before CREATE returns. At this point, the parent’s state may be in a partially updated condition (e.g., the nonce has incremented but the returned address hasn’t been stored yet). The reentrant call sees this inconsistent state and can exploit it.
-
Constructor callback patterns. Some designs intentionally have the new contract call back to the parent during construction (e.g., to register itself). If the parent doesn’t use reentrancy guards, the callback can be exploited to deploy additional contracts, drain funds, or manipulate storage.
-
EXTCODESIZE == 0 during construction. While the init code is executing, the new contract’s address has no deployed code yet (
EXTCODESIZEreturns 0). Any contract that usesEXTCODESIZE > 0as a proxy for “is this a contract?” will incorrectly classify the new contract as an EOA during construction, potentially bypassing access control checks that intend to block contract callers.
Why it matters: Init code reentrancy is less studied than CALL-based reentrancy but equally dangerous. The parent contract is in a mid-operation state during init code execution, and any callback exploits this temporal gap.
T3: Nonce Prediction for Front-Running and Cross-Chain Replay (Critical)
Since CREATE addresses depend only on sender and nonce, anyone who can observe or predict these values can precompute the deployment address:
-
Front-running factory deployments. An attacker who monitors the mempool can see a pending CREATE transaction, compute the resulting address, and front-run with a transaction that interacts with that address (e.g., pre-funding it, approving it, or registering it in another contract). If the attacker can also deploy to that address first (by manipulating the factory’s nonce), they can claim the address entirely.
-
Cross-chain nonce divergence (Wintermute/Optimism incident). When the same deployer address exists on multiple chains, the nonce may differ. A contract deployed via CREATE on L1 at nonce N will have a specific address. On L2, if the deployer’s nonce is different, deploying “the same contract” produces a different address. Worse, an attacker can replay the deployer’s historical transactions on the new chain to match the nonce and deploy a malicious contract at the expected address. This is exactly what happened in the Wintermute/Optimism $20M OP token loss.
-
Pre-funding computed addresses. ETH can be sent to an address before any contract exists there. An attacker who knows the deployment address can pre-fund it, and the newly deployed contract will have an unexpected balance. If the init code or contract logic relies on
address(this).balance == 0at deployment, this assumption is violated.
Why it matters: CREATE’s address derivation is deterministic and public. Any security assumption based on “only I know the address before deployment” is false.
T4: Metamorphic Contracts via CREATE + SELFDESTRUCT (Critical)
By combining CREATE with SELFDESTRUCT, an attacker can deploy, destroy, and redeploy different code at the same address (prior to EIP-6780/Dencun restrictions on SELFDESTRUCT):
-
The metamorphic pattern. A deployer contract uses CREATE to deploy a child. The child self-destructs. The deployer’s nonce has already incremented past the child’s slot, but the deployer can reset its own state by self-destructing and being redeployed (via a CREATE2 wrapper), resetting its nonce to 1. A new CREATE from the redeployed deployer at nonce 1 produces the same address as the original child — but with entirely different bytecode.
-
Governance hijacking (Tornado Cash, May 2023). The attacker submitted a governance proposal that deployed a benign-looking contract via CREATE. After the proposal passed, the attacker destroyed the deployed contract and redeployed malicious code at the same address. Since governance had already whitelisted the address, the malicious contract inherited full governance privileges, allowing the attacker to grant themselves 1.2M TORN tokens and seize control of governance.
-
Post-Dencun limitations. EIP-6780 (Dencun, March 2024) restricts SELFDESTRUCT to only clearing code and storage when called within the same transaction as contract creation. This significantly limits metamorphic contract attacks but does not eliminate them entirely — same-transaction create-destroy-redeploy is still possible.
Why it matters: Metamorphic contracts undermine the fundamental assumption that a contract’s code is immutable. Any system that trusts a contract based on its address (governance, access control, token approvals) is vulnerable if the code can change.
T5: Code Size Limit and Gas Exhaustion (Medium)
EIP-170 limits deployed contract code to 24,576 bytes (0x6000), and EIP-3860 limits init code to 49,152 bytes (2 * 24,576). These limits create failure modes:
-
Code deposit failure. If the init code’s
RETURNproduces bytecode exceeding 24,576 bytes, the CREATE fails silently (returns 0). The 200-gas-per-byte code deposit cost also means large contracts require substantial gas — a 24,576-byte contract costs 4,915,200 gas just for code deposit, plus the 32,000 base cost and init code execution gas. -
Gas griefing via insufficient gas. An attacker who controls the gas supplied to a CREATE call (e.g., by calling a factory function with carefully metered gas) can cause the init code to run out of gas. The CREATE returns 0, but the parent contract continues execution. If the parent doesn’t check for failure, the attacker has caused a silent deployment failure while the parent proceeds with
address(0). -
Init code size rejection (EIP-3860). Since Shanghai, CREATE with init code exceeding 49,152 bytes immediately fails. Additionally, init code is charged 2 gas per 32-byte word for jumpdest analysis. Contracts that dynamically construct large init code blobs may unexpectedly exceed this limit.
Why it matters: Gas and size limits create predictable failure modes that attackers can trigger on demand. Combined with the silent failure behavior (T1), these limits become exploitation vectors rather than just resource constraints.
Protocol-Level Threats
P1: State Bloat from Contract Creation (Medium)
Every successful CREATE persists new code and a new account entry in the Ethereum state trie. Unlike storage slots, deployed code cannot be deleted (post-Dencun SELFDESTRUCT only clears code in the creation transaction). This creates permanent state growth:
-
Dust contract spam. An attacker can deploy thousands of minimal contracts (e.g., 1-byte runtime code) at 32,000 + 200 gas each. At current gas prices, mass contract creation is a cost-effective way to bloat the state trie, increasing sync times and disk requirements for full nodes.
-
No rent mechanism. Ethereum has no storage rent — once a contract is deployed, its code persists forever at zero ongoing cost. Proposals for state expiry have not been implemented, making contract creation an irreversible state commitment.
-
Nonce inflation. Each CREATE increments the sender’s nonce, even on failure. While nonces are cheap to store (part of the account object), extremely high nonces from mass-creation activity can theoretically approach overflow limits (though nonces are 64-bit, making overflow impractical).
P2: EIP-3860 Initcode Size Limit and Gas Metering (Low)
EIP-3860 (Shanghai, March 2023) introduced a 49,152-byte limit on init code and charges 2 gas per 32-byte word for jumpdest analysis:
-
DoS prevention. Before EIP-3860, init code had no size limit. An attacker could submit a CREATE transaction with megabytes of init code, forcing validators to perform expensive jumpdest analysis on arbitrarily large bytecode. EIP-3860 bounds this cost and makes it proportional to init code size.
-
Interaction with dynamic init code. Contracts that generate init code dynamically (e.g., by concatenating runtime bytecode with constructor arguments in memory) must ensure the total length stays under 49,152 bytes. Exceeding the limit causes CREATE to fail with an out-of-gas error.
P3: Code Deposit Cost as Economic Constraint (Low)
The 200-gas-per-byte code deposit cost (charged after init code execution) serves as an economic brake on deploying large contracts:
-
Prevents unbounded code deployment. Without code deposit costs, an attacker could deploy maximally-sized contracts cheaply, exacerbating state bloat.
-
Interacts with the 63/64 gas rule. The code deposit cost is charged to the parent context, not the child’s gas allocation. If the parent retains insufficient gas after forwarding 63/64 to the child, the code deposit charge can cause the parent to run out of gas, reverting the entire transaction (not just the CREATE).
Edge Cases
| Edge Case | Behavior | Security Implication |
|---|---|---|
| CREATE with value = 0 | Contract is created with 0 wei balance (unless pre-funded) | Valid operation; no ETH transfer but init code still executes. Pre-funded addresses will have unexpected balance. |
| CREATE failure (returns 0) | Pushes address(0) on stack; sender nonce still increments; all init code state changes revert | Callers that don’t check for 0 proceed with address(0), leading to lost funds or broken state. Nonce increment means retry produces a different address. |
| CREATE with insufficient gas | Init code runs out of gas; CREATE returns 0 | Silent failure exploitable by gas griefing. The 63/64 rule means the parent retains 1/64 of gas, which may be enough to continue past an unchecked failure. |
| Nonce overflow | Nonces are 64-bit unsigned integers; overflow at 2^64 is practically unreachable | Not a realistic threat. Even at 1 million creates per second, overflow would take ~584,942 years. |
| CREATE in STATICCALL context | EVM reverts immediately; CREATE is a state-modifying operation | Correct behavior. No security implication beyond ensuring STATICCALL contexts are truly read-only. |
| CREATE at max call depth (1024) | CREATE fails; returns 0 | An attacker can intentionally consume call depth before invoking a contract that uses CREATE, forcing silent failure. Rarely exploitable since EIP-150’s 63/64 rule makes deep call stacks expensive. |
| CREATE deploying empty code | Init code returns 0 bytes; contract is created with empty code (EOA-like) | The contract exists as an account but has no code. EXTCODESIZE returns 0. Can receive ETH but cannot execute logic. |
| Init code calls back to parent (reentrancy) | Permitted; init code is a full execution context | Parent’s state is mid-update during callback. Reentrancy guards should protect CREATE-calling functions. |
| CREATE with EIP-170 code size exceeded | Init code returns >24,576 bytes; CREATE fails silently | Returns 0. Must be checked. Contracts generating dynamic bytecode can hit this limit unexpectedly. |
| Init code that SELFDESTRUCTs | Post-Dencun: SELFDESTRUCT in same-tx creation clears code/storage. Pre-Dencun: contract destroyed immediately after creation. | Enables metamorphic patterns (pre-Dencun). Post-Dencun, same-tx SELFDESTRUCT still allows create-destroy-redeploy within one transaction. |
Real-World Exploits
Exploit 1: Wintermute — $20M OP Tokens Lost via CREATE Nonce Replay on Optimism (June 2022)
Root cause: Wintermute provided an Ethereum L1 multisig address (deployed via CREATE) as the destination for 20M OP tokens on Optimism L2, but the multisig had not been deployed on L2. An attacker replayed the deployer’s transactions on Optimism to match the nonce and claim the address.
Details: In May 2022, the Optimism Foundation sent 20M OP tokens to Wintermute’s Ethereum mainnet Gnosis Safe address on Optimism. Wintermute confirmed receipt of test transactions (1 OP and 1M OP) but did not verify they could actually control the address on Optimism — the Safe contract had never been deployed on L2.
The attacker exploited the fact that Wintermute’s Safe was created in 2020 using the old Gnosis Safe Proxy Factory, which used CREATE (not CREATE2) for proxy deployment. With CREATE, the deployed address depends solely on keccak256(rlp([factory_address, nonce])). The attacker:
- Replayed the Safe Proxy Factory deployment transaction on Optimism (possible because the original deployment used non-EIP-155 transactions, replayable on any chain).
- Called
createProxy()on the factory 162 times per batch across 62 batches (~10,000 total deployments) to increment the factory’s nonce to match the nonce at which Wintermute’s Safe was originally created on mainnet. - At the matching nonce, deployed a proxy with themselves as the owner, producing the exact same address as Wintermute’s L1 Safe.
- Drained the 20M OP tokens from the now-attacker-controlled address.
CREATE’s role: The entire attack was possible because CREATE’s address derivation (keccak256(rlp([sender, nonce]))) is deterministic and reproducible across chains. If the original deployment had used CREATE2 (which includes a salt and the init code hash), nonce manipulation would not have produced a matching address.
Impact: 20M OP tokens (~$27.6M at the time) lost. Wintermute later negotiated return of the tokens after providing a bounty.
References:
Exploit 2: Tornado Cash Governance Takeover via Metamorphic Contract (~$1M TORN, May 2023)
Root cause: An attacker used the CREATE + SELFDESTRUCT + CREATE2 metamorphic pattern to deploy benign code at an address, get it approved by governance, then replace it with malicious code at the same address.
Details: On May 20, 2023, an attacker submitted a governance proposal to the Tornado Cash DAO that appeared identical to a previously approved and trusted proposal. The proposal deployed a contract via CREATE from a deployer that was itself deployed via CREATE2. The deployed contract’s code was inspected during the governance vote and appeared benign.
After the proposal passed and the contract address was granted governance privileges:
- The attacker called
SELFDESTRUCTon the deployer contract, destroying it. - Using the same CREATE2 salt and bytecode, the attacker redeployed the deployer contract at the identical address — but with a reset nonce (nonce = 1).
- The redeployed deployer called CREATE at nonce 1, producing the same address as the original child contract — but this time deploying malicious bytecode.
- The malicious contract, now at the governance-whitelisted address, granted the attacker 1.2M TORN tokens (worth ~$1M) and full control over Tornado Cash governance.
CREATE’s role: The attack fundamentally relied on CREATE’s nonce-based address derivation. By resetting the deployer’s nonce (via SELFDESTRUCT + CREATE2 redeployment), the attacker could deploy different bytecode to the same CREATE-derived address. The governance system trusted the address, not the code — a critical assumption that CREATE’s deterministic-but-mutable addressing violates.
Impact: ~$1M in TORN tokens minted. Full governance control seized. The attacker later submitted a proposal to restore governance, returning some control, but the attack demonstrated the fundamental unsoundness of trusting addresses derived from CREATE.
References:
- pcaversaccio: Tornado Cash Exploit PoC
- CoinsBench: Tornado Cash DAO Hack Explained
- Medium: Tornado Cash Governance Hack
Exploit 3: Unchecked CREATE Return Values in Factory Contracts (Recurring, 2018-Present)
Root cause: Factory contracts and deployment scripts that use inline assembly create() without verifying the returned address is non-zero, leading to silent deployment failures that persist broken state.
Details: This is a recurring vulnerability class rather than a single exploit. Multiple audit firms (Trail of Bits, OpenZeppelin, Code4rena) have flagged unchecked CREATE return values as critical findings across dozens of protocols:
-
Minimal proxy factories (EIP-1167). Contracts that clone minimal proxies using
create(0, ptr, size)in assembly often omit therequire(addr != address(0))check. If creation fails (e.g., due to insufficient gas), the factory recordsaddress(0)as a valid clone, and users who interact with it lose funds. -
Deterministic deployment wrappers. Contracts that wrap CREATE with pre-computed address validation sometimes skip the zero-check because they “know” the address should be correct. But gas griefing or code size limits can still cause failure, and the pre-computed address is never assigned.
-
Token factory contracts. Several DeFi protocols have had audit findings where their token or pool factory used assembly-level CREATE without checking the return value. In one Code4rena contest (2024), a pool factory’s unchecked CREATE2 return was flagged as enabling complete pool drainage through address collision.
CREATE’s role: CREATE’s design choice to return 0 on failure rather than reverting makes this vulnerability class possible. Every assembly-level use of CREATE requires an explicit zero-check that is easy to forget.
Impact: No single massive exploit attributed solely to unchecked CREATE, but the pattern appears in dozens of audit reports as a high/critical severity finding. The MixBytes analysis and OWASP Smart Contract Security (SCWE-048) both classify unchecked return values as a top vulnerability.
References:
- MixBytes: Pitfalls of Using CREATE, CREATE2 and EXTCODESIZE
- OWASP: SCWE-048 Unchecked Call Return Value
Attack Scenarios
Scenario A: Unchecked CREATE Return Value in Factory
contract VulnerableFactory {
mapping(uint256 => address) public deployments;
uint256 public deployCount;
function deploy(bytes memory initCode) external payable returns (address) {
address addr;
assembly {
addr := create(callvalue(), add(initCode, 0x20), mload(initCode))
}
// VULNERABLE: No check for addr == address(0)
// If CREATE fails (out of gas, code too large, etc.),
// address(0) is stored and returned
deployments[deployCount] = addr;
deployCount++;
return addr;
}
function fundDeployment(uint256 id) external payable {
// Sends ETH to address(0) if deployment failed silently
payable(deployments[id]).transfer(msg.value);
}
}
// Attack: Call deploy() with insufficient gas so CREATE fails.
// Factory stores address(0). Other users call fundDeployment()
// and lose their ETH to the zero address.Scenario B: Nonce Replay Across Chains
// On L1: Factory deploys a vault using CREATE
contract FactoryL1 {
function deployVault(address owner) external returns (address) {
// Address = keccak256(rlp([address(this), nonce]))
Vault v = new Vault(owner);
return address(v);
}
}
// L1 deployment at factory nonce=5 creates vault at 0xABC...
// User sends tokens to 0xABC... on L2, assuming same address
// Attack on L2:
// 1. Replay factory deployment transaction on L2
// 2. Call deployVault() 4 times with attacker-controlled owners (nonces 1-4)
// 3. Call deployVault(attacker) at nonce=5
// 4. Vault at 0xABC... on L2 is owned by attacker
// 5. Attacker drains all tokens sent to 0xABC... on L2Scenario C: Init Code Reentrancy
contract VulnerableDeployer {
address public lastDeployed;
mapping(address => uint256) public deposits;
function deployAndFund() external payable {
// Deploy new child -- init code can call back!
bytes memory initCode = getInitCode();
address child;
assembly {
child := create(0, add(initCode, 0x20), mload(initCode))
}
require(child != address(0), "deploy failed");
// State update AFTER create -- vulnerable window
lastDeployed = child;
deposits[child] = msg.value;
}
function withdraw(address target) external {
uint256 amount = deposits[target];
require(amount > 0);
deposits[target] = 0;
payable(msg.sender).transfer(amount);
}
}
// MaliciousInitCode: during construction, calls back to
// deployer.withdraw(lastDeployed) to drain deposits of the
// PREVIOUS deployment before lastDeployed is updated.
// lastDeployed still points to the old child during init code execution.Scenario D: Metamorphic Contract via CREATE + SELFDESTRUCT (Pre-Dencun)
// Step 1: Deploy a deployer via CREATE2 (deterministic address)
contract MetamorphicDeployer {
function deploy(bytes memory code) external returns (address) {
address child;
assembly {
child := create(0, add(code, 0x20), mload(code))
}
return child;
}
function destroy() external {
selfdestruct(payable(msg.sender));
}
}
// Step 2: Deploy benign code via deployer.deploy() at nonce=1
// Address = keccak256(rlp([deployer_addr, 1]))
// Submit this address for governance approval
// Step 3: After governance approval, call deployer.destroy()
// Deployer is destroyed, nonce is reset
// Step 4: Redeploy deployer at same address via CREATE2 (same salt)
// New deployer has nonce=1 again
// Step 5: Call deployer.deploy(malicious_code)
// CREATE at nonce=1 produces THE SAME ADDRESS as step 2
// but with completely different bytecode
// Governance-whitelisted address now runs attacker's codeScenario E: Gas Griefing to Force Silent CREATE Failure
contract GriefableFactory {
address[] public children;
function createChild() external {
// CREATE forwards 63/64 of remaining gas
Child c = new Child();
// If gas is insufficient, c == address(0) and Solidity 0.8
// reverts. But in assembly or older Solidity versions:
children.push(address(c));
}
}
// Griefing attack: Call createChild() with gas = G where
// G is enough for the function preamble and push, but
// 63/64 * G_remaining is not enough for Child's constructor.
// In Solidity < 0.8 or assembly: CREATE silently fails,
// address(0) is pushed to children array.Mitigations
| Threat | Mitigation | Implementation |
|---|---|---|
| T1: Unchecked CREATE return | Always verify returned address is non-zero | require(addr != address(0), "CREATE failed") after every assembly-level create(). Solidity’s new keyword does this automatically in >= 0.8.x. |
| T2: Init code reentrancy | Reentrancy guards on CREATE-calling functions | Apply OpenZeppelin ReentrancyGuard to any function that calls CREATE. Update state before CREATE, not after. |
| T2: EXTCODESIZE == 0 during construction | Do not use code size as a reliable contract check | EXTCODESIZE returns 0 for contracts mid-construction. Use msg.sender == tx.origin (with account abstraction caveats) or other identity checks. |
| T3: Nonce prediction / cross-chain replay | Use CREATE2 for deterministic, chain-independent deployments | CREATE2 address depends on salt and bytecode, not nonce. Safe Singleton Factory (EIP-7955) ensures consistent cross-chain addresses. |
| T3: Pre-funding attack | Do not assume address(this).balance == 0 at deployment | Check actual balance conditions rather than assuming a clean initial state. |
| T4: Metamorphic contracts | Verify bytecode hash, not just address | require(keccak256(addr.code) == expectedHash) to detect code changes at a trusted address. Post-Dencun, SELFDESTRUCT restrictions reduce but don’t eliminate this risk. |
| T4: Governance address trust | Governance should validate code, not just addresses | Check EXTCODEHASH of approved contracts before executing proposals. Use timelocks to allow bytecode verification. |
| T5: Code size / gas exhaustion | Validate gas requirements and code size constraints | Ensure sufficient gas for init code + code deposit (200 * code_size) + 32,000 base. Test with maximum expected code sizes. |
| T5: Gas griefing | Ensure minimum gas for CREATE operations | Use require(gasleft() >= MIN_CREATE_GAS) before calling CREATE. Set MIN_CREATE_GAS based on expected init code cost. |
| General: Factory safety | Use Solidity >= 0.8.x new keyword with salt where possible | new Contract{salt: salt}() uses CREATE2 with automatic revert-on-failure. Avoid assembly CREATE unless necessary. |
Compiler/EIP-Based Protections
- Solidity >= 0.8.0: The
newkeyword automatically reverts if contract creation fails (generates a revert if the returned address is zero). This eliminates T1 for high-level Solidity usage, but assemblycreate()still requires manual checks. - EIP-170 (Spurious Dragon, 2016): Limits deployed contract code to 24,576 bytes. Prevents unbounded code deployment but creates a predictable failure mode exploitable via T5.
- EIP-3860 (Shanghai, 2023): Limits init code to 49,152 bytes and charges 2 gas per 32-byte word for jumpdest analysis. Prevents DoS via oversized init code.
- EIP-6780 (Dencun, 2024): Restricts SELFDESTRUCT to only clearing code/storage when called in the same transaction as creation. Significantly reduces metamorphic contract attacks (T4) but does not eliminate same-transaction create-destroy patterns.
- EIP-150 (63/64 gas rule): Prevents a parent from forwarding all gas to CREATE, ensuring the parent retains enough gas to handle failure. Also limits call depth attacks.
Severity Summary
| Threat ID | Category | Severity | Likelihood | Real-World Precedent |
|---|---|---|---|---|
| T1 | Smart Contract | High | High | Recurring audit finding across dozens of protocols (OWASP SCWE-048) |
| T2 | Smart Contract | High | Medium | Constructor callback exploits in factory patterns |
| T3 | Smart Contract | Critical | Medium | Wintermute/Optimism $20M OP token loss (June 2022) |
| T4 | Smart Contract | Critical | Low (post-Dencun) | Tornado Cash governance takeover (~$1M TORN, May 2023) |
| T5 | Smart Contract | Medium | Medium | Gas griefing in factory contracts; EIP-170/3860 limit failures |
| P1 | Protocol | Medium | Medium | Ongoing state bloat concerns; no rent mechanism |
| P2 | Protocol | Low | N/A | EIP-3860 prevents init code DoS (Shanghai) |
| P3 | Protocol | Low | N/A | Economic constraint on code deployment; interacts with 63/64 rule |
Related Opcodes
| Opcode | Relationship |
|---|---|
| CREATE2 (0xF5) | Deterministic contract creation using keccak256(0xFF, sender, salt, initCodeHash). Eliminates nonce dependency, preventing cross-chain nonce replay (T3). However, enables its own class of address collision and front-running attacks. |
| CALL (0xF1) | External call that can be made from init code during CREATE execution. The vector for init code reentrancy (T2). Also shares the silent-failure return pattern (returns 0 on failure, does not revert). |
| SELFDESTRUCT (0xFF) | Destroys a contract, clearing code and storage (subject to EIP-6780 restrictions). Combined with CREATE, enables metamorphic contract attacks (T4). Post-Dencun, SELFDESTRUCT only clears state when called in the same transaction as creation. |
| CODECOPY (0x39) | Copies the current contract’s code to memory. Often used in init code to construct runtime bytecode that is then returned to be deployed by CREATE. |
| RETURN (0xF3) | In an init code context, RETURN specifies the runtime bytecode to be deployed. The returned bytes become the contract’s permanent code, subject to EIP-170 size limits. |