Stratax Contracts

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

Implementation contract can be initialized: missing _disableInitializers() in constructor

Author Revealed upon completion

Root + Impact

Description

  • Normal behavior: Upgradeable contracts use an initialize() function with the initializer modifier so that each proxy is initialized at most once. The implementation contract is intended to be logic-only and should not be initialized; OpenZeppelin recommends calling _disableInitializers() in the implementation’s constructor so that no one can call initialize() on the implementation contract itself.

  • Specific issue: Stratax uses Initializable and protects initialize() with the initializer modifier (so proxies cannot be re-initialized). However, Stratax has no constructor, so _disableInitializers() is never called on the implementation. Anyone can call initialize() on the implementation contract’s address, initializing the implementation’s own storage (e.g. owner = msg.sender). The implementation is then no longer logic-only and can be taken over or misused; it also violates upgradeable best practices.

// Stratax.sol
contract Stratax is Initializable {
// @> No constructor — _disableInitializers() is never called on the implementation
// ...
function initialize(
address _aavePool,
address _aaveDataProvider,
address _oneInchRouter,
address _usdc,
address _strataxOracle
) external initializer {
// @> initializer prevents re-init on same contract; implementation has no constructor lock
aavePool = IPool(_aavePool);
// ...
owner = msg.sender;
}
}

Risk

Likelihood:

  • The implementation contract is deployed without a constructor that calls _disableInitializers(), so initialize() remains callable on the implementation address.

  • Any address can call Stratax(implementationAddress).initialize(...) and become the implementation contract’s “owner” and set its storage.

Impact:

  • The implementation contract’s own storage can be initialized and taken over (e.g. attacker sets themselves as owner on the implementation). If the implementation were ever used directly (e.g. mistaken call) or for non-proxy use, that state could be abused.

  • Violates OpenZeppelin upgradeable guidance and leaves the implementation in a state that was not intended (logic-only, never initialized).

Proof of Concept

The PoC deploys the Stratax implementation contract (no proxy) and calls initialize() on it. Because there is no constructor that calls _disableInitializers(), the call succeeds and the implementation’s storage is initialized (e.g. owner is set). This shows the implementation contract can be taken over.

function test_PoC_ImplementationCanBeInitializedByAnyone() public {
Stratax impl = new Stratax();
StrataxOracle oracle = new StrataxOracle();
address attacker = address(0xbad);
vm.prank(attacker);
impl.initialize(
AAVE_POOL,
AAVE_PROTOCOL_DATA_PROVIDER,
INCH_ROUTER,
USDC,
address(oracle)
);
assertEq(impl.owner(), attacker, "PoC: Implementation contract taken over; owner set on impl");
}

Recommended Mitigation

// src/Stratax.sol
contract Stratax is Initializable {
+ /// @custom:oz-upgrades-unsafe-allow constructor
+ constructor() {
+ _disableInitializers();
+ }
+
/*//////////////////////////////////////////////////////////////
TYPE DECLARATIONS
//////////////////////////////////////////////////////////////*/

Support

FAQs

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

Give us feedback!