NFT Dealers

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

Missing Max Supply Cap Allows Unlimited Token Minting

Author Revealed upon completion

Root + Impact

Description

  • In standard ERC20 token contracts, particularly those meant to mimic stablecoins like USDC, there is typically a maximum supply cap. This cap ensures that the total number of tokens in circulation cannot exceed a predefined limit, preserving the token's scarcity and economic model.

    The MockUSDC contract contains a mint function that lacks any validation against a maximum supply. Furthermore, because the mint function has no access control, any external user can invoke it. This allows any actor to increase the totalSupply arbitrarily, bypassing the intended economic constraints of the token.

function mint(address to, uint256 amount) external {
@> _mint(to, amount); // No check against a max supply variable
}

Risk

Likelihood:High

  • Reason 1: The mint function is completely unrestricted (external with no modifiers); therefore, any wallet or contract interacting with the blockchain can call it.

  • Reason 2: The lack of a supply cap means the function will always succeed as long as the caller has enough gas, regardless of how many tokens have already been minted.

Impact:High

  • Economic Collapse: An attacker can mint trillions of tokens, causing hyperinflation. This renders the token valueless and destroys the trust of any users holding the token.

  • Protocol Exploitation: If this token is used as a currency or collateral in other DeFi protocols (e.g., lending markets, DEXs), an attacker can mint a massive supply to manipulate prices, drain liquidity pools, or borrow against fabricated value.

Proof of Concept

The following Foundry test demonstrates that there is no maximum supply. It shows that:

  1. Any user (Alice) can mint a huge amount.

  2. The total supply increases cumulatively with no upper bound.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "../src/MockUSDC.sol";
contract MockUSDCNoMaxSupplyTest is Test {
MockUSDC public token;
address public alice = address(0x1);
address public bob = address(0x2);
function setUp() public {
token = new MockUSDC();
}
// A test that anyone can mint a very large quantity
function testAnyoneCanMintHugeAmount() public {
uint256 hugeAmount = 1_000_000_000_000 * 10**6;
// Alice is talking to herself
vm.prank(alice);
token.mint(alice, hugeAmount);
// Check balance and total display
assertEq(token.balanceOf(alice), hugeAmount);
assertEq(token.totalSupply(), hugeAmount);
}
// Testing that there is no maximum, and that the process can be repeated several times
function testNoMaxSupplyMultipleMints() public {
uint256 amount1 = 1000 * 10**6;
uint256 amount2 = 2000 * 10**6;
uint256 amount3 = 3000 * 10**6;
// Alice تسك 1000
vm.prank(alice);
token.mint(alice, amount1);
assertEq(token.totalSupply(), amount1);
// Bob 2000
vm.prank(bob);
token.mint(bob, amount2);
assertEq(token.totalSupply(), amount1 + amount2);
// Alice sends 3000 to Bob
vm.prank(alice);
token.mint(bob, amount3);
assertEq(token.totalSupply(), amount1 + amount2 + amount3);
// Each user's balance
assertEq(token.balanceOf(alice), amount1);
assertEq(token.balanceOf(bob), amount2 + amount3);
}
// Fuzzing test: For any random quantity (except 0), it can be minted
function testFuzzMintAnyAmount(uint256 amount) public {
vm.assume(amount > 0);
token.mint(alice, amount);
assertEq(token.balanceOf(alice), amount);
assertEq(token.totalSupply(), amount);
}
}

Test Result:

[⠒] Compiling...
No files changed, compilation skipped
Ran 3 tests for test/NoMaxSupply.sol:MockUSDCNoMaxSupplyTest
[PASS] testAnyoneCanMintHugeAmount() (gas: 61277)
[PASS] testFuzzMintAnyAmount(uint256) (runs: 256, μ: 61304, ~: 61304)
[PASS] testNoMaxSupplyMultipleMints() (gas: 100481)
Suite result: ok. 3 passed; 0 failed; 0 skipped; finished in 62.91ms (56.23ms CPU time)
Ran 1 test suite in 74.23ms (62.91ms CPU time): 3 tests passed, 0 failed, 0 skipped (3 total tests)

Recommended Mitigation

To fix this issue, introduce a constant maximum supply and add a validation check inside the mint function. Additionally, restrict access to the mint function to prevent unauthorized minting.

+ import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
- contract MockUSDC is ERC20 {
+ contract MockUSDC is ERC20, Ownable {
+ uint256 public constant MAX_SUPPLY = 1_000_000_000 * 10**6; // Example: 1 Billion
constructor() ERC20("MockUSDC", "mUSDC") {}
- function mint(address to, uint256 amount) external {
+ function mint(address to, uint256 amount) external onlyOwner {
+ require(totalSupply() + amount <= MAX_SUPPLY, "Max supply exceeded");
_mint(to, amount);
}
}

Support

FAQs

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

Give us feedback!