Proxy Initialization Attacks

How the attack works and how to prevent it

JohnnyTime
JohnnyTime · Updated September 06, 2026
5 min read
Total Stolen $371,000
Last Attack Aug 25, 2026
Latest Victim Enjin

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.

  1. The deployer creates a proxy but does not initialize it atomically.

  2. The attacker calls initialize(attacker) before the deployer does.

  3. The proxy records the attacker as owner or upgrader.

  4. 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 initializer or reinitializer(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.

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 .

Incident database

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.

Exploit setup Root-cause tracing Patch review
Practice Proxy Initialization Attacks Free Trial