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.
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");
}
}
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.
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") {}
*
*
*
* 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");
}
}
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);
}