Santa's List

AI First Flight #3
Beginner FriendlyFoundry
EXP
View results
Submission Details
Impact: low
Likelihood: medium
Invalid

buyPresent has no per-address NFT cap, allowing unlimited NFT minting with sufficient tokens

Root + Impact

Description

collectPresent correctly enforces a one-NFT-per-address rule via balanceOf(msg.sender) > 0. However buyPresent has no equivalent check. Any address holding enough SantaToken (or benefiting from the F-2 bug) can call buyPresent repeatedly to accumulate unlimited NFTs, breaking the intended supply model.

Risk

// collectPresent — has the guard
function collectPresent() external {
if (balanceOf(msg.sender) > 0) {
revert SantasList__AlreadyCollected(); // protected
}
// ...
}
// buyPresent — no guard
function buyPresent(address presentReceiver) external {
i_santaToken.burn(presentReceiver);
_mintAndIncrement(); // no balanceOf check on msg.sender
}

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity 0.8.22;
import {Test} from "forge-std/Test.sol";
import {SantasList} from "../src/SantasList.sol";
import {SantaToken} from "../src/SantaToken.sol";
contract BuyPresentNoCap is Test {
SantasList santasList;
SantaToken santaToken;
address santa = makeAddr("santa");
address buyer = makeAddr("buyer");
address tokenSource1 = makeAddr("tokenSource1");
address tokenSource2 = makeAddr("tokenSource2");
uint256 constant CHRISTMAS = 1_703_480_381;
function setUp() public {
vm.prank(santa);
santasList = new SantasList();
santaToken = SantaToken(santasList.getSantaToken());
// Give tokenSource addresses EXTRA_NICE status and collect tokens
address[2] memory sources = [tokenSource1, tokenSource2];
for (uint i = 0; i < 2; i++) {
vm.startPrank(santa);
santasList.checkList(sources[i], SantasList.Status.EXTRA_NICE);
santasList.checkTwice(sources[i], SantasList.Status.EXTRA_NICE);
vm.stopPrank();
}
vm.warp(CHRISTMAS + 1);
vm.prank(tokenSource1);
santasList.collectPresent(); // gets 1e18 tokens
vm.prank(tokenSource2);
santasList.collectPresent(); // gets 1e18 tokens
}
function test_buyerReceivesMultipleNFTs() public {
// buyer calls buyPresent twice, draining two victims
// (note: this also demonstrates F-2 — buyer pays nothing)
vm.startPrank(buyer);
santasList.buyPresent(tokenSource1); // buyer gets NFT #1
santasList.buyPresent(tokenSource2); // buyer gets NFT #2
vm.stopPrank();
// buyer holds 2 NFTs despite never being on the list
assertEq(santasList.balanceOf(buyer), 2);
}
}

Recommended Mitigation

Add a balanceOf check inside buyPresent for the recipient, mirroring collectPresent. After fixing F-2 (so the NFT goes to presentReceiver), the check should target presentReceiver:

// AFTER fixing F-2 and adding cap
function buyPresent(address presentReceiver) external {
if (balanceOf(presentReceiver) > 0) {
revert SantasList__AlreadyCollected();
}
i_santaToken.burn(msg.sender);
_safeMint(presentReceiver, s_tokenCounter++);
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 1 day ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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

Give us feedback!