_disableInitializers() Allows Implementation Contract TakeoverIn the upgradeable proxy pattern, the implementation contract's initialize() function should be permanently disabled to prevent anyone from calling it directly on the implementation. OpenZeppelin provides _disableInitializers() for this purpose.
Stratax inherits from Initializable but has no constructor that calls _disableInitializers(). The implementation contract is deployed in an uninitialized state, allowing anyone to call initialize() on it directly and become its owner.
Likelihood:
The attack is trivial — any address can call initialize() on the implementation contract with a single transaction
The implementation address is publicly discoverable on-chain (via proxy storage slots or deployment records)
Requires no special permissions, timing, or setup
Impact:
The attacker becomes owner of the implementation contract
As owner, the attacker can call recoverTokens() to drain any tokens accidentally sent to the implementation address (a common user error — users often send tokens to the implementation instead of the proxy)
The attacker can call setStrataxOracle() to point to a malicious oracle on the implementation
While this does not directly compromise the proxy's storage, it creates a risk vector if the implementation ever holds value or is involved in upgrade logic
Real-World Precedent:
Wormhole (2022-02-02) — $326,000,000 lost: Uninitialized implementation contract was exploited to bypass authorization checks
Audius (2022-07-23) — $6,000,000 lost: Attacker initialized unprotected implementation contract and used it to manipulate governance
OpenZeppelin explicitly warns about this: "To prevent the implementation contract from being used, you should invoke the _disableInitializers function in the constructor"
How the attack works:
Stratax implementation contract is deployed at address 0xImpl
A proxy is deployed pointing to 0xImpl and initialized — the proxy's storage has owner = deployer
Attacker calls initialize(attackerAavePool, attackerDataProvider, attackerRouter, attackerUSDC, attackerOracle) directly on 0xImpl (not the proxy)
The initializer modifier passes because 0xImpl's own storage has never been initialized
owner in 0xImpl's storage is now set to the attacker's address
Attacker calls recoverTokens(tokenAddress, amount) on 0xImpl — any tokens at the implementation address are sent to the attacker
PoC code:
Expected outcome: The attacker successfully calls initialize() on the bare implementation contract and becomes its owner, gaining access to all onlyOwner functions on the implementation.
The root cause is that the implementation contract's initialize() function is not disabled. OpenZeppelin's recommended pattern is to call _disableInitializers() in the constructor, which permanently marks the implementation as initialized and prevents anyone from calling initialize() on it.
Primary fix — Add a constructor that disables initializers:
Why this works:
_disableInitializers() sets the initialization state to the maximum value (type(uint64).max), preventing any future initializer or reinitializer calls on the implementation contract
The /// @custom:oz-upgrades-unsafe-allow constructor comment is required by OpenZeppelin's upgrade safety tooling to acknowledge that the constructor is intentional
The proxy's storage is unaffected — initialize() still works when called through the proxy because the proxy has its own storage
This is a zero-cost defense that permanently closes the attack vector
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.
The contest is complete and the rewards are being distributed.