Stratax Contracts

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

Centralized beacon upgrade with no timelock: beacon owner can instantly upgrade and rug user funds

Author Revealed upon completion

Root + Impact

Description

  • Normal behavior: The protocol uses an UpgradeableBeacon so that many BeaconProxy instances share one implementation and can be upgraded in one place. The beacon owner is expected to upgrade only to non-malicious implementations. Stratax proxy owners (position owners) control their own position via onlyOwner (open, unwind, recoverTokens).

  • Specific issue: Centralized upgradeability via UpgradeableBeacon—the beacon owner can unilaterally upgrade the implementation for all BeaconProxy instances and introduce malicious logic to drain user funds. If the beacon owner key is compromised or malicious (especially if it’s the same key as a position owner), all user positions are at risk. There is no timelock on UpgradeableBeacon upgrades: the beacon owner can upgrade the implementation instantly. A malicious or compromised owner can point to a new implementation that steals or redirects user funds (e.g. sweep tokens, abuse Aave), enabling an immediate rug pull with no on-chain delay for users to exit.

// OpenZeppelin UpgradeableBeacon (lib/.../UpgradeableBeacon.sol)
// @> No timelock: owner can upgrade in one tx; no delay, no schedule
function upgradeTo(address newImplementation) public virtual onlyOwner {
_setImplementation(newImplementation);
}
// Deployment pattern (test/unit/StrataxBeaconProxy.t.sol): beacon owner set at construction
// @> Beacon owner has full control; if same key as position owner or compromised, all proxies at risk
beacon = new UpgradeableBeacon(address(strataxImplementation), beaconOwner);

Risk

Likelihood:

  • The beacon is deployed with an EOA or contract as owner; that owner can call upgradeTo at any time. In production the same key may control both the beacon and one or more proxy owners, or the beacon owner key may be compromised.

  • There is no timelock or delay—the upgrade takes effect in the same transaction as upgradeTo, so users have no on-chain window to exit before the new implementation is active.

Impact:

  • A malicious or compromised beacon owner can deploy a malicious implementation (e.g. with a sweep() or modified recoverTokens / Aave logic) and call beacon.upgradeTo(maliciousImplementation), causing all proxies to execute the new code and enabling theft of user funds (contract-held tokens and/or Aave positions).

  • All user positions backed by proxies that use this beacon are at risk in a single upgrade; there is no per-user or per-proxy isolation from the beacon owner’s upgrade power.

Proof of Concept

The PoC shows that (1) the beacon owner can upgrade the implementation in one transaction with no delay (no timelock), and (2) the upgrade immediately affects all proxies using that beacon. There is no timelock—upgradeTo is a single call that changes the implementation used by every BeaconProxy on the next invocation. Run: forge test --match-contract PoC_BeaconUpgradeNoTimelock -vvv.

// test/unit/PoC_BeaconUpgradeNoTimelock.t.sol
contract PoC_BeaconUpgradeNoTimelock is Test, ConstantsEtMainnet {
Stratax public strataxImplementation;
UpgradeableBeacon public beacon;
BeaconProxy public proxy;
Stratax public stratax;
StrataxOracle public strataxOracle;
address public beaconOwner;
function setUp() public {
beaconOwner = address(0x1);
// ... deploy implementation, beacon, proxy (same as StrataxBeaconProxy.t.sol)
strataxImplementation = new Stratax();
vm.prank(beaconOwner);
beacon = new UpgradeableBeacon(address(strataxImplementation), beaconOwner);
proxy = new BeaconProxy(address(beacon), initData);
stratax = Stratax(address(proxy));
}
/// @dev PoC: Beacon owner can upgrade in one tx; no timelock
function test_PoC_BeaconOwnerCanUpgradeInstantly_NoTimelock() public {
address beforeImpl = beacon.implementation();
Stratax newImplementation = new Stratax();
vm.prank(beaconOwner);
beacon.upgradeTo(address(newImplementation));
address afterImpl = beacon.implementation();
assertEq(afterImpl, address(newImplementation), "Implementation changed in one tx");
assertTrue(beforeImpl != afterImpl, "PoC: Upgrade is instantaneous; no timelock");
}
/// @dev PoC: Single upgrade affects all proxies; no exit window
function test_PoC_MaliciousUpgradeWouldAffectAllProxies() public {
BeaconProxy proxy2 = new BeaconProxy(address(beacon), initData);
Stratax maliciousImpl = new Stratax();
vm.prank(beaconOwner);
beacon.upgradeTo(address(maliciousImpl));
assertEq(beacon.implementation(), address(maliciousImpl), "PoC: Single upgrade affects all proxies");
}
}

Recommended Mitigation

Use a timelock (e.g. OpenZeppelin TimelockController) as the beacon owner so that upgradeTo only takes effect after a scheduled delay (e.g. 24–48 hours), giving users time to exit or the community to react. Deployment change: create the beacon with the timelock as owner instead of an EOA.
Alternative: If upgrades are not required, renounce beacon ownership after deployment (beacon.transferOwnership(address(0)) or beacon.renounceOwnership() if available), at the cost of losing the ability to fix bugs via upgrade.

// Deployment / factory (conceptual; actual file may be in scripts or a factory)
+ import {TimelockController} from "@openzeppelin/contracts/governance/TimelockController.sol";
// 1. Deploy implementation
strataxImplementation = new Stratax();
+ // 2. Deploy timelock (e.g. 48h delay, single proposer/executor for simplicity)
+ uint256 minDelay = 2 days;
+ address[] memory proposers = new address[](1);
+ proposers[0] = beaconAdmin;
+ address[] memory executors = new address[](1);
+ executors[0] = beaconAdmin;
+ TimelockController timelock = new TimelockController(minDelay, proposers, executors, address(0));
// 2. Deploy beacon
- beacon = new UpgradeableBeacon(address(strataxImplementation), beaconOwner);
+ beacon = new UpgradeableBeacon(address(strataxImplementation), address(timelock));
// 3. Deploy proxy
proxy = new BeaconProxy(address(beacon), initData);

Support

FAQs

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

Give us feedback!