Stratax Contracts

First Flight #57
Beginner FriendlyDeFi
100 EXP
Submission Details
Impact: high
Likelihood: high

Missing `_disableInitializers()` Allows Implementation Contract Takeover

Author Revealed upon completion

Missing _disableInitializers() Allows Implementation Contract Takeover

Description

  • In 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.

contract Stratax is Initializable {
// @audit No constructor — implementation contract left uninitialized and vulnerable
// Missing:
// /// @custom:oz-upgrades-unsafe-allow constructor
// constructor() { _disableInitializers(); }
function initialize(
address _aavePool,
address _aaveDataProvider,
address _oneInchRouter,
address _usdc,
address _strataxOracle
) external initializer {
aavePool = IPool(_aavePool);
aaveDataProvider = IProtocolDataProvider(_aaveDataProvider);
oneInchRouter = IAggregationRouter(_oneInchRouter);
USDC = _usdc;
strataxOracle = _strataxOracle;
owner = msg.sender; // @audit Whoever calls initialize() becomes owner
flashLoanFeeBps = 9;
}
}

Risk

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"

Proof of Concept

How the attack works:

  1. Stratax implementation contract is deployed at address 0xImpl

  2. A proxy is deployed pointing to 0xImpl and initialized — the proxy's storage has owner = deployer

  3. Attacker calls initialize(attackerAavePool, attackerDataProvider, attackerRouter, attackerUSDC, attackerOracle) directly on 0xImpl (not the proxy)

  4. The initializer modifier passes because 0xImpl's own storage has never been initialized

  5. owner in 0xImpl's storage is now set to the attacker's address

  6. Attacker calls recoverTokens(tokenAddress, amount) on 0xImpl — any tokens at the implementation address are sent to the attacker

PoC code:

function testExploit_UninitializedImplementation() public {
// Deploy implementation (no proxy)
Stratax impl = new Stratax();
// Attacker initializes the implementation directly
address attacker = makeAddr("attacker");
vm.prank(attacker);
impl.initialize(
address(0x1), // fake aavePool
address(0x2), // fake dataProvider
address(0x3), // fake oneInchRouter
address(0x4), // fake usdc
address(0x5) // fake oracle
);
// Attacker is now owner of the implementation
assertEq(impl.owner(), attacker);
}

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.

Recommended Mitigation

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:

/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}

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

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!