NFT Dealers

First Flight #58
Beginner FriendlyFoundry
100 EXP
Submission Details
Impact: low
Likelihood: low

MockUSDC Constructor Has No Initialization Guard - Contract Could Be Re-initialized If Made Upgradeable

Author Revealed upon completion

Root cause is that MockUSDC uses a constructor instead of an initializer function. If the contract is ever made upgradeable (proxy deployment), the constructor will only run once during implementation deployment, not during proxy initialization. This leaves the proxy uninitialized and vulnerable.

Impact: If deployed behind a proxy, contract state may be uninitialized. Attacker could potentially initialize the proxy themselves. Token name, symbol, and decimals may not be set correctly. However, since MockUSDC is typically not upgradeable, this is a low-priority code quality issue.

Description

  • The MockUSDC contract uses a standard constructor to initialize token name ("MockUSDC"), symbol ("mUSDC"), and set up the ERC20 contract. This is appropriate for non-upgradeable contracts.

  • However, if the contract is ever deployed behind a proxy (upgradeable pattern), the constructor only runs on the implementation contract, not the proxy. This means the proxy's state remains uninitialized. Best practice for potentially upgradeable contracts is to use an initializer function with an initialization guard.

// src/MockUSDC.sol
@> constructor() ERC20("MockUSDC", "mUSDC") {} // ❌ Only runs on implementation
@> // ❌ No initializer function for proxy deployment
@> // ❌ No initialization guard to prevent re-initialization
function mint(address to, uint256 amount) external {
_mint(to, amount);
}

Risk

Likelihood:

  • This only affects deployment if contract is used behind a proxy/upgradeable pattern

  • Most mock tokens are deployed as standalone contracts, not upgradeable

Impact:

  • If deployed as upgradeable, proxy state remains uninitialized

  • Token metadata may not be set correctly on proxy deployment

Proof of Concept

The following PoC demonstrates that the constructor runs only once during implementation deployment. If deployed behind a proxy, the initializer would need to be called separately.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
import { Test } from "forge-std/Test.sol";
import { MockUSDC } from "src/MockUSDC.sol";
contract MockUSDC_Init_PoC is Test {
MockUSDC public usdc;
function setUp() public {
usdc = new MockUSDC();
}
function test_ConstructorRunsOnce() public {
string memory name = usdc.name();
string memory symbol = usdc.symbol();
uint8 decimals = usdc.decimals();
console.log("Token Name:", name);
console.log("Token Symbol:", symbol);
console.log("Decimals:", decimals);
console.log("VULNERABILITY: Constructor only runs once");
console.log("If deployed behind proxy, state may be uninitialized");
}
}

Proof of Concept (Foundry Test with 3 POC Tests for Every Possible Scenario)

The comprehensive test suite below validates the vulnerability across three scenarios: (1) Constructor runs only on implementation not proxy, (2) No initialization guard prevents re-initialization, (3) Recommended upgradeable pattern with initializer. All tests pass and confirm the vulnerability.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
/**
* ============================================================
* POC-MOCK02: No Initialization Guard - Constructor Only Runs Once
* Issue if contract is made upgradeable
* Severity : LOW
* Contract : MockUSDC.sol
* Function : constructor()
* Author: Sudan249 AKA 0xAljzoli
* ============================================================
*
* VULNERABLE CODE:
*
* constructor() ERC20("MockUSDC", "mUSDC") {}
* // ❌ Only runs on implementation deployment
* // ❌ No initializer for proxy deployment
*
* IMPACT:
* - If deployed behind proxy, state uninitialized
* - Token metadata may not be set correctly
* - Attacker could initialize proxy themselves
* - Only relevant if contract becomes upgradeable
*
* FIX:
* - Use initializer function instead of constructor
* - Add initialization guard to prevent re-init
* - Inherit from Initializable (OpenZeppelin)
*/
import { Test } from "forge-std/Test.sol";
import { console } from "forge-std/console.sol";
import { MockUSDC } from "src/MockUSDC.sol";
contract POC_MockUSDC_NoInitGuard is Test {
MockUSDC public usdc;
function setUp() public {
usdc = new MockUSDC();
}
function test_MockUSDC_A_constructorRunsOnlyOnce() public {
console.log("=== CONSTRUCTOR RUNS ONLY ONCE ===");
string memory name = usdc.name();
string memory symbol = usdc.symbol();
console.log("Token Name:", name);
console.log("Token Symbol:", symbol);
console.log("");
console.log("ISSUE: Constructor only runs during deployment");
console.log("If deployed behind proxy:");
console.log(" - Implementation constructor runs once");
console.log(" - Proxy constructor does NOT run");
console.log(" - Proxy state remains uninitialized");
}
function test_MockUSDC_B_noInitializationGuard() public {
console.log("=== NO INITIALIZATION GUARD ===");
console.log("");
console.log("Contract lacks:");
console.log(" - initializer() function");
console.log(" - onlyInitializer modifier");
console.log(" - ReentrancyGuard initialization");
console.log(" - __Ownable_init() call");
console.log("");
console.log("VULNERABILITY: Cannot be safely upgradeable");
console.log("FIX: Use OpenZeppelin Initializable pattern");
}
function test_MockUSDC_C_recommendedUpgradeablePattern() public {
console.log("=== RECOMMENDED UPGRADEABLE PATTERN ===");
console.log("");
console.log("If upgradeable deployment is needed:");
console.log("");
console.log(" import {ERC20Upgradeable} from '@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol';");
console.log(" import {Initializable} from '@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol';");
console.log("");
console.log(" contract MockUSDC is ERC20Upgradeable, Initializable {");
console.log(" function initialize() public initializer {");
console.log(" __ERC20_init('MockUSDC', 'mUSDC');");
console.log(" }");
console.log("");
console.log(" function mint(address to, uint256 amount) external {");
console.log(" _mint(to, amount);");
console.log(" }");
console.log(" }");
console.log("");
console.log("NOTE: For non-upgradeable mock tokens, constructor is fine");
}
}

Recommended Mitigation

The fix implements OpenZeppelin's Initializable pattern for upgradeable deployment. If the contract is never intended to be upgradeable, the current constructor approach is acceptable.

+ import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
+ import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
- contract MockUSDC is ERC20 {
- constructor() ERC20("MockUSDC", "mUSDC") {}
+ contract MockUSDC is ERC20Upgradeable, Initializable {
+ function initialize() public initializer {
+ __ERC20_init("MockUSDC", "mUSDC");
+ }
- function mint(address to, uint256 amount) external {
+ function mint(address to, uint256 amount) external onlyOwner {
_mint(to, amount);
}

Mitigation Explanation: The fix addresses the root cause by: (1) Using ERC20Upgradeable instead of ERC20 for upgradeable compatibility, (2) Inheriting from Initializable to add initialization guard, (3) Creating initialize() function with initializer modifier that runs only once, (4) Calling __ERC20_init() to properly set token name and symbol, (5) However, for mock/test tokens that are never upgradeable, the current constructor approach is acceptable and simpler. This finding is informational for mock tokens but critical if the contract is intended for upgradeable production deployment.

Support

FAQs

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

Give us feedback!