Skip to content
Quantix Security Labs

Unguarded Initializer

Explain how an outside account can become the owner of the rewards distributor and sweep its tokens, then fix the deployment and the contract.
IntroductorySolidity · EthereumUnsafe proxy upgrade20–30 minutes

Briefing

Contracts behind a proxy cannot use constructors to set state, because the constructor runs in the implementation’s storage, not the proxy’s. Initialisation moves into an ordinary function that must run exactly once.

The team deploys the proxy in one transaction and calls `initialize` in a second transaction from their deployment script.

RewardsDistributorV1.solsolidity
// Quantix Security Research Network — training challenge.// Intentionally vulnerable. Written for education; do not deploy.// SPDX-License-Identifier: MITpragma solidity 0.8.24; interface IERC20 {    function transfer(address to, uint256 amount) external returns (bool);} contract RewardsDistributorV1 {    address public owner;    address public rewardToken;    bool private initialized;     function initialize(address _owner, address _rewardToken) external {        require(!initialized, "already initialized");        owner = _owner;        rewardToken = _rewardToken;        initialized = true;    }     function sweep(address token, address to, uint256 amount) external {        require(msg.sender == owner, "not owner");        require(IERC20(token).transfer(to, amount), "transfer failed");    }}
deploy.s.sol (excerpt)solidity
// Transaction 1: deploy implementation and proxy without init dataRewardsDistributorV1 impl = new RewardsDistributorV1();ERC1967Proxy proxy = new ERC1967Proxy(address(impl), ""); // Transaction 2: initialise from the deployerRewardsDistributorV1(address(proxy)).initialize(TEAM_MULTISIG, REWARD_TOKEN);

Hints

Reveal one at a time. Each moves a step closer to the answer.

  1. H1

    Hidden

  2. H2

    Hidden

  3. H3

    Hidden

Solution

Try writing your own explanation and fix before opening it.

References