Free snapshot slots open this week info@inversez.com
DetectionVulnerabilitiesToolsAuditsPricingBlogFree monitoring assessmentContact
Home/Vulnerabilities/Uninitialised proxy implementation
— · Typically critical

Uninitialised proxy implementation

Upgradeable contracts cannot use constructors, because a constructor runs in the implementation's context, not the proxy's. Instead they use an initialize() function. If that function is left callable on the implementation contract itself, anyone can call it and become its owner.

Vulnerable code

Do not ship this
contract VaultV1 is Initializable, OwnableUpgradeable {

    function initialize() public initializer {
        __Ownable_init();     // sets msg.sender as owner
    }

    function upgradeTo(address impl) external onlyOwner { /* ... */ }

    function kill() external onlyOwner {
        selfdestruct(payable(msg.sender));
    }
}

Why it breaks

The proxy is initialised at deployment, so the proxy is safe. But the implementation contract sits on-chain with its own untouched storage, and its initializer has never run. An attacker calls initialize() directly on the implementation address, becomes its owner, then calls kill(). The implementation self-destructs. Every proxy delegating to it now points at empty code, and all funds behind those proxies are stranded.

Watch the attack run

The same bug as a stepped sequence. Play it, or walk through with the arrow keys.

The fix

Corrected
contract VaultV1 is Initializable, OwnableUpgradeable, UUPSUpgradeable {

    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() {
        _disableInitializers();   // the fix — one line
    }

    function initialize(address owner_) public initializer {
        __Ownable_init(owner_);
        __UUPSUpgradeable_init();
    }

    function _authorizeUpgrade(address) internal override onlyOwner {}
}

The invariant

The fix above is what an audit gives you. This is what monitoring gives you: the thing that should always be true once the contract is live, stated precisely enough to check every block.

InvariantEVM · uninitialized proxy

The implementation contract has a non-zero owner from the block it was deployed.

invariant  implementation.owner() != address(0)
  from deployment block onward

watch  Initialized event on the implementation
Backtest: not yet run. This entry carries the reasoning, not the evidence. When the invariant has been replayed against the real transactions around the exploit, the firing block, how far ahead it fired, and its false-positive rate over a clean control period will be published here, along with the recording of the run.

How to test for it

A finding you cannot reproduce is an opinion. Write the test before you write the fix, watch it fail, then make it pass.

function testImplementationCannotBeInitialised() public {
    VaultV1 impl = new VaultV1();

    vm.prank(attacker);
    vm.expectRevert();          // InvalidInitialization
    impl.initialize(attacker);
}

function testProxyInitialisedOnce() public {
    vm.expectRevert();
    vault.initialize(attacker);   // already initialised
}

Where it has caused real losses

An uninitialised implementation behind a widely-used bridge was reported through a bug bounty in 2022; the finder was paid $10M, then the largest bounty ever awarded. Had it been found by an attacker instead, the loss would have been substantially larger. The fix was a single line in the constructor.

What we check during review

The full checklist we run on any upgradeable deployment: is _disableInitializers() called in the implementation's constructor? Is initialize() guarded by initializer, and are re-initialisers guarded by reinitializer(n)? Is _authorizeUpgrade actually restricted? Does the new implementation's storage layout append rather than reorder? And is there a storage gap (uint256[50] private __gap) in each inherited contract so future versions can add variables safely?

Want us to check your contract for this?

A free monitoring assessment covers one contract up to 200 lines, manually reviewed, findings back within 72 hours. This class is on the checklist for every review we run.

Free monitoring assessment

Found this useful?

We publish these because the alternative, asking you to trust us, is worth less. Send a contract and we’ll apply the same thinking to your code.

  • Reply written by the auditor who read your code
  • No sales sequence, no drip campaign, no retargeting
  • We’ll tell you if you don’t need a paid audit yet
  • Report published only with your written permission

Request your snapshot

We read every submission. If it doesn’t fit the free tier we’ll say so and quote you instead, no obligation.