Proxy Initialization Attacks
How the attack works and how to prevent it
Proxy Initialization Attacks: Upgradeability Vulnerabilities in Solidity
Proxy initialization attacks happen when an upgradeable contract leaves its initializer callable by the wrong party. The attacker does not need to break cryptography or bypass Solidity. They simply call the setup function first and become the owner, upgrader, guardian, or privileged operator.
This page focuses on the upgradeability bugs auditors see in real reviews: uninitialized proxies, exposed implementations, unsafe reinitializers, bad deployment scripts, and weak UUPS authorization.
What Is A Proxy Initialization Attack?
Upgradeable contracts usually cannot rely on constructors for proxy state. The constructor runs on the implementation contract, while runtime state lives in the proxy. That is why upgradeable contracts use external initializer functions.
The problem is that an initializer is just a function. If it is not protected and called at the right time, someone else can call it.
| Variant | What Goes Wrong | Impact |
|---|---|---|
| Uninitialized proxy | Proxy deployed without initializer calldata | Attacker becomes owner |
| Uninitialized implementation | Logic contract leaves initializer open | UUPS or implementation takeover risk |
| Unsafe reinitializer | New version exposes setup again | Attacker claims new privileged module |
| Bad inheritance order | Parent initializer skipped or duplicated | Broken roles or storage state |
| Weak upgrade authorization | _authorizeUpgrade() is missing or public |
Malicious implementation upgrade |
How The Attack Works
The basic exploit sequence is short.
-
The deployer creates a proxy but does not initialize it atomically.
-
The attacker calls
initialize(attacker)before the deployer does. -
The proxy records the attacker as owner or upgrader.
-
The attacker calls privileged functions or upgrades to malicious logic.
The bug often lives in deployment, not in the contract file auditors are reading. A source-level review that ignores deployment scripts can miss the entire issue.
Vulnerable Code
// VULNERABLE CONTRACT - DO NOT USE IN PRODUCTION
contract VaultV1 is UUPSUpgradeable {
address public owner;
IERC20 public asset;
function initialize(address _asset, address _owner) external {
// Missing initializer modifier.
asset = IERC20(_asset);
owner = _owner;
}
function withdraw(address to, uint256 amount) external {
require(msg.sender == owner, "not owner");
asset.transfer(to, amount);
}
function _authorizeUpgrade(address) internal override {
require(msg.sender == owner, "not owner");
}
}
This contract has multiple problems. initialize() can be called more than once. The implementation does not disable initializers. If deployment forgets initializer calldata, the first caller controls ownership.
Safer Pattern
// SECURE PATTERN
contract VaultV1 is Initializable, UUPSUpgradeable, OwnableUpgradeable {
IERC20 public asset;
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
function initialize(address _asset, address initialOwner)
external
initializer
{
require(_asset != address(0), "asset zero");
require(initialOwner != address(0), "owner zero");
__Ownable_init(initialOwner);
__UUPSUpgradeable_init();
asset = IERC20(_asset);
}
function withdraw(address to, uint256 amount) external onlyOwner {
asset.transfer(to, amount);
}
function _authorizeUpgrade(address newImplementation)
internal
override
onlyOwner
{
require(newImplementation.code.length > 0, "not contract");
}
}
The deployment should initialize the proxy in the same transaction that creates it.
bytes memory initData = abi.encodeCall(
VaultV1.initialize,
(asset, owner)
);
ERC1967Proxy proxy = new ERC1967Proxy(address(implementation), initData);
Separate "deploy now, initialize later" flows create a takeover window.
Real-World Mappings
Proxy initialization risk has appeared in multiple high-impact patterns.
| Pattern | Lesson |
|---|---|
| Parity wallet library | Initialization and shared-library control can brick or capture systems |
| UUPS implementation advisories | Exposed implementation initializers can matter even when users call proxies |
| Factory-created proxies | Every clone or proxy instance must be initialized immediately |
| Upgrade migrations | New versions can accidentally re-open setup paths |
Do not oversimplify these incidents into one bug type. The shared lesson is that initialization is a security boundary.
How Auditors Should Review Proxies
Review three things together: source code, deployment scripts, and upgrade process.
-
Every proxy is initialized atomically at deployment.
-
Every implementation disables initializers in the constructor.
-
Every initializer uses
initializerorreinitializer(n). -
Parent initializers are called exactly once.
-
_authorizeUpgrade()is implemented and access-controlled. -
Proxy admin, owner, and guardian are expected addresses.
-
Factories cannot deploy uninitialized instances.
-
New implementations preserve storage layout.
-
Reinitializer functions cannot be called by arbitrary users.
-
Tests include "attacker initializes first" and "attacker initializes implementation" cases.
Storage Layout Still Matters
Initialization is only one upgradeability risk. The next question is whether the new implementation writes to the same storage layout as the old implementation.
contract V1 {
address public owner; // slot 0
uint256 public totalAssets; // slot 1
}
contract V2 {
uint256 public totalAssets; // slot 0, collision
address public owner; // slot 1, collision
}
That layout change can corrupt ownership or accounting. Review storage layout diffs before every upgrade.
Related Vulnerabilities
Proxy initialization attacks usually combine with access control attacks, because the end result is privileged control. They also depend on delegatecall and call attack mechanics, because proxy logic executes implementation code against proxy storage.
If a malicious upgrade can destroy code or brick behavior, also review self-destruct attacks.
FAQ
What is a proxy initialization attack?
A proxy initialization attack is when an attacker calls an upgradeable contract's initializer before the legitimate deployer or initializes an exposed implementation contract to gain privileged control.
Why do proxies use initialize() instead of constructors?
Constructor code runs on the implementation contract. Proxy state is stored in the proxy, so setup must happen through a function called via the proxy.
Is the initializer modifier enough?
No. It prevents repeat initialization, but it does not help if the attacker is first. The proxy must be initialized atomically during deployment.
Why disable initializers on the implementation?
The implementation should not become a live privileged contract. Disabling initializers reduces the risk of UUPS or implementation-level takeover paths.
Practice Upgradeability Review
Proxy bugs are where architecture, deployment, and access control meet. If you want to practice this in realistic audit scenarios, the Smart Contract Hacking course covers upgradeability, delegatecall, ownership takeover, and exploit validation.
Sources and editorial notes
Reviewed by JohnnyTime. Last updated .
Real Proxy Initialization Attacks hacks to study
A stable selection of high-signal incidents linked to this attack class, ordered by reported loss and recency.
Master Proxy Initialization Attacks in a safe lab
Practice the exploit path, debug the vulnerable code, and learn the prevention workflow auditors use in real reviews.