Initialization & Proxy Vulnerability Patterns
Distilled from 300+ Code4rena audit findings. Proxy and initialization patterns are foundational to modern DeFi — nearly every major protocol uses upgradeability or factory deployment. This class of vulnerabilities is high-severity because it often leads to complete protocol takeover, not merely fund loss.
Delegatecall Vulnerabilities
1. Caller-Controlled Function Selector
Pattern: A contract exposes a function that accepts an arbitrary function selector and delegates to a module:
function execute(bytes4 functionId, bytes calldata data) external {
(bool ok,) = module.delegatecall(abi.encodeWithSelector(functionId, data));
}
delegatecall executes in the calling contract's storage context. An attacker who can control functionId can call any function on the module contract with the caller's storage — including selfdestruct, transferOwnership, or any function that writes to storage slot 0 (which typically holds the owner).
Real-world context: Tapioca DAO's modular architecture allowed callers to select the delegated function ID, enabling attackers to execute arbitrary module functions in the context of the main contract.
Fix:
- Use a fixed whitelist mapping:
mapping(bytes4 => bool) allowedSelectors - Never pass function selectors from user input into delegatecall
- Consider clone patterns (EIP-1167) instead of delegatecall for modular code
2. Storage Layout Collision in Proxies
Pattern: In a proxy contract, the proxy's own state variables occupy the same storage slots as the implementation contract's variables. If the implementation is upgraded and new variables are inserted at the beginning of the layout, existing variables shift, corrupting state.
Fix:
- Follow OpenZeppelin's storage gap pattern: reserve unused storage slots in base contracts
- Use EIP-1967 standard proxy storage slots for proxy-specific variables (e.g., implementation address at
0x360894...) - Validate storage layout compatibility before every upgrade
3. Uninitialized Implementation Contract
Pattern: In UUPS or Transparent proxies, the implementation contract itself (not the proxy) can be separately called if it is never initialized. An attacker who calls initialize() on the implementation contract directly becomes the owner of that implementation, can selfdestruct it, and permanently brick all proxies pointing to it.
Fix: Call _disableInitializers() (OpenZeppelin v4.6+) in the implementation's constructor to prevent direct initialization.
Initialization Function Vulnerabilities
4. Front-Runnable Initialization
Pattern: A initialize(address admin, ...) function has no access control — only an initialized flag to prevent double initialization. An attacker watching the mempool can submit the same transaction with malicious parameters (their own address as admin) at a higher gas price.
Vulnerable:
bool private initialized;
function initialize(address admin) external {
require(!initialized);
initialized = true;
_admin = admin; // ← attacker front-runs with their own address
}
Fix options:
- Use the constructor (non-upgradeable contracts)
- Use
msg.senderas the admin (whoever deploys is admin) - Combine
Ownable+Initializableso only the deployer can callinitialize - Include a commitment scheme or use CREATE2 with known deployer address
5. Re-Initialization via Upgrade
Pattern: When a new implementation is deployed, if the new version has a new initialize function (or reinitializer) and the upgrade doesn't immediately call it, an attacker can call the reinitializer before the protocol does, injecting malicious configuration.
Fix: Always call the new initialize/reinitialize function atomically in the same transaction as the upgrade. Use upgradeToAndCall() instead of upgradeTo() + separate call.
6. Missing Initialization Guard in Abstract Base Contracts
Pattern: An abstract base contract has an initialize function that sets critical storage. A derived contract's initialize forgets to call super.initialize(), leaving the base contract's state uninitialized. This can result in owner == address(0) or other critical defaults.
Fix: Use OpenZeppelin's Initializable consistently across the inheritance hierarchy. Verify with _initialized checks.
CREATE2 / Factory Patterns
7. Predictable Deployment Address Front-Running
Pattern: A factory deploys contracts via CREATE2 with a user-controlled salt. Because CREATE2 addresses are deterministic (hash(0xFF, deployer, salt, initcodeHash)), an attacker can compute the address and deploy a malicious contract to that address before the legitimate deployment.
Real-world context: A protocol computed a deterministic vault address and sent tokens to it before deploying. An attacker deployed their own contract to the same address (using the same salt on a different chain or in a different context), gaining control of the tokens.
Fix:
- Include additional entropy in the salt:
keccak256(abi.encode(msg.sender, userSalt, block.timestamp)) - Do not fund or configure a CREATE2 address before verifying it contains the expected code
- Consider CREATE3 (uses a fixed deployer contract, making the address depend only on salt + deployer address, not initcode)
8. Cloned Contract Lacks Its Own Storage Initialization
Pattern: EIP-1167 minimal proxies (clones) share implementation logic but have separate storage. If the clone is not immediately initialized after creation, anyone can call initialize() on the new clone and take ownership.
Fix: Always initialize in the same transaction as the clone deployment. Use Clones.cloneDeterministic + initialize call atomically.
Cross-Module / Cross-Contract Trust
9. Implicit Trust Between Modules
Pattern: In a multi-module architecture, Module A calls Module B via delegatecall or inter-contract call with elevated privileges (e.g., acting as owner). If Module B's interface is upgradeable or can be swapped, an attacker who controls the upgrade can make Module B's next call drain Module A's funds.
Fix: Treat all module calls as untrusted. Validate module addresses against a whitelist. Apply access control checks within each module, not just at the entry point.
Audit Checklist
- Any
delegatecallwith user-controlled function selector → whitelist required - Storage layout: are proxy and implementation slot layouts validated for each upgrade?
- Is
_disableInitializers()called in every implementation constructor? -
initialize()functions: is there protection against mempool front-running? - Upgrades: does
upgradeToAndCallatomically invoke new initializer? - CREATE2 deployments: is the salt user-controllable? Is funding deferred until after deployment?
- EIP-1167 clones: are they initialized in the same transaction as deployment?
- Are abstract base contract
initializefunctions always called viasuper.initialize()?