Santa's List

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

Unlimited mint via balanceOf check bypass in collectPresent - can transfer NFT to bypass one-per-wallet restriction

Root + Impact-In src/SantasList.sol:collectPresent() the contract uses if (balanceOf(msg.sender) > 0) revert to enforce 1 present per user. This is flawed because ERC721 balance is not persistent - owner can transfer token to another address and reset balance to 0.

Impact: Breaks invariant of 1 present per NICE user. Any NICE user can mint unlimited NFTs by looping mint->transfer->mint. Causes infinite supply, devalues collection.

Description:Normal behavior: Each address checked NICE twice by Santa should collect exactly ONE present.Issue: balanceOf check can be bypassed. After first mint, attacker transfers NFT to dump address via transferFrom, balance becomes 0 again, and can call collectPresent() again. Can be automated in attacker contract to mint hundreds


// Root cause in the codebase with @> marks to highlight the relevant section
PoC Steps:
1. Santa marks victim as NICE twice
2. Victim calls collectPresent() -> balance 1
3. Victim transfers NFT to dump address -> balance 0
4. Victim calls collectPresent() again -> bypasses check, mints again
​
Code:
​
function test_PoC_UnlimitedMint() public {
address niceUser = makeAddr("niceUser");
address dump = makeAddr("dump");
vm.prank(santasList.getSanta());
santasList.checkList(niceUser, SantasList.Status.NICE);
vm.prank(santasList.getSanta());
santasList.checkTwice(niceUser, SantasList.Status.NICE);
vm.prank(niceUser);
santasList.collectPresent();
vm.prank(niceUser);
santasList.transferFrom(niceUser, dump, 0);
vm.prank(niceUser);
santasList.collectPresent(); // second mint - BUG, should revert but doesn't
}

Risk

Likelihood: High - Any user marked NICE twice can execute it. No privileges required. transferFrom is standard public ERC721 function.

Impact:High - Infinite mint, breaks tokenomics, allows draining value if present is valuable, DOS of legitimate supply.


Proof of Concept-Normal behavior: Each address checked NICE twice by Santa should collect exactly ONE present.Issue: balanceOf check can be bypassed. After first mint, attacker transfers NFT to dump address via transferFrom, balance becomes 0 again, and can call collectPresent() again. Can be automated in attacker contract to mint hundreds.

function test_PoC_UnlimitedMint() public {
address niceUser = makeAddr("niceUser");
address dump = makeAddr("dump");
// Santa marks niceUser as NICE twice
vm.prank(santasList.getSanta());
santasList.checkList(niceUser, SantasList.Status.NICE);
vm.prank(santasList.getSanta());
santasList.checkTwice(niceUser, SantasList.Status.NICE);
// First collect
vm.prank(niceUser);
santasList.collectPresent();
assertEq(santasList.balanceOf(niceUser), 1);
// Transfer away to bypass balance check
vm.prank(niceUser);
santasList.transferFrom(niceUser, dump, 0);
assertEq(santasList.balanceOf(niceUser), 0);
// Second collect - BUG: should revert but mints again
vm.prank(niceUser);
santasList.collectPresent();
}

Recommended Mitigation-Do not use balanceOf for anti-sybil. Use a dedicated mapping that cannot be reset by transfer.

- if (balanceOf(msg.sender) > 0) { revert AlreadyCollected(); }
+ mapping(address => bool) private s_hasCollected;
+ if (s_hasCollected[msg.sender]) revert AlreadyCollected();
+ s_hasCollected[msg.sender] = true;
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 1 hour 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!