Stratax Contracts

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

Missing `_disableInitializers` In Implementation Constructor

Author Revealed upon completion

The Stratax contract is an upgradeable implementation designed to be used behind a Beacon Proxy pattern. It inherits from OpenZeppelin's Initializable and uses an initialize() function with the initializer modifier to set up state, including setting owner = msg.sender. However, the contract does not define a constructor that calls _disableInitializers(). This means the implementation contract itself (as opposed to the proxy) remains uninitialised after deployment. Because proxy initialisation only affects proxy storage, the implementation's _initialized flag remains at zero, leaving initialize() callable by anyone on the bare implementation address.

When a caller invokes initialize() on the implementation, the initializer modifier passes because _initialized == 0, and the caller becomes the owner on the implementation's storage. The attacker then has access to all onlyOwner functions on the implementation, including recoverTokens(), setStrataxOracle(), setFlashLoanFee(), and transferOwnership().

// File: src/Stratax.sol
contract Stratax is Initializable {
// @audit No constructor calling _disableInitializers() exists
// @audit The implementation contract's initialize() remains callable by anyone
// ... snip ...
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 Attacker becomes owner of the implementation
flashLoanFeeBps = 9;
}
// ... snip ...
function recoverTokens(address _token, uint256 _amount) external onlyOwner {
IERC20(_token).transfer(owner, _amount); // @audit Attacker drains tokens
}
}

The deployment script deploys the implementation without any initialiser lock:

// File: script/DeployStrataxBeacon.s.sol
Stratax implementation = new Stratax();
// @audit No _disableInitializers() called in constructor
// @audit Implementation's initialize() remains open for anyone to call

OpenZeppelin's official documentation for Initializable explicitly warns about this scenario, stating that an uninitialised contract can be taken over by an attacker and recommending that _disableInitializers() be invoked in the constructor. Real-world precedent exists: the Wormhole bridge exploit (February 2022) involved an uninitialised implementation contract, and OpenZeppelin disclosed CVE-2021-41264, a UUPS vulnerability arising from the same class of issue.

This issue has a medium impact as the primary damage is limited to tokens accidentally sent to the implementation address and control over the implementation's state. Proxy users' funds and state are not directly at risk because the Beacon Proxy pattern does not allow the attacker to upgrade or destroy the proxy through the implementation.

This issue has a high likelihood as the attack requires no special privileges, no preconditions, and is trivially executable. The implementation address is publicly visible onchain, and automated bots actively scan for this pattern.

recommendation

Add a constructor to the Stratax contract that calls _disableInitializers() to permanently lock the implementation contract against direct initialisation. This sets _initialized to type(uint64).max on the implementation's own storage during deployment, preventing any future calls to initialize() or reinitializer() on the implementation directly. The proxy's storage is unaffected because the constructor runs in the implementation's storage context, not the proxy's.

Support

FAQs

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

Give us feedback!