Santa's List

AI First Flight #3
Beginner FriendlyFoundry
EXP
View results
Submission Details
Severity: high
Valid

Default Status enum value is NICE, allowing anyone to free-mint NFTs without being checked by Santa

Description

  • Only addresses that Santa has explicitly marked as NICE on both s_theListCheckedOnce and s_theListCheckedTwice should be eligible to mint a Christmas NFT via collectPresent(). The intent of the protocol is that an unchecked address has no claim on any present.

  • The Status enum declares NICE as the first variant. In Solidity, the default value of any uninitialized mapping entry of an enum type is the first variant — i.e. Status.NICE (uint8(0)). Because s_theListCheckedOnce and s_theListCheckedTwice both return Status.NICE for every never-touched address, the collectPresent NICE branch evaluates to true for every address Santa has never seen. Combined with the fact that CHRISTMAS_2023_BLOCK_TIME = 1_703_480_381 (Dec 25 2023) is already in the past, any address on Arbitrum can call collectPresent() once and mint a free NFT with no involvement from Santa.

// src/SantasList.sol
@> enum Status {
@> NICE, // index 0 — DEFAULT value of every mapping entry
EXTRA_NICE,
NAUGHTY,
NOT_CHECKED_TWICE
}
mapping(address => Status) private s_theListCheckedOnce;
mapping(address => Status) private s_theListCheckedTwice;
function collectPresent() external {
if (block.timestamp < CHRISTMAS_2023_BLOCK_TIME) revert SantasList__NotChristmasYet();
if (balanceOf(msg.sender) > 0) revert SantasList__AlreadyCollected();
@> if (s_theListCheckedOnce[msg.sender] == Status.NICE
@> && s_theListCheckedTwice[msg.sender] == Status.NICE) {
_mintAndIncrement(); // <-- reached by ANY unchecked address
return;
}
...
}

Risk

Likelihood: High — every address on the chain meets the precondition with zero setup. CHRISTMAS_2023_BLOCK_TIME is already in the past on every live deployment, so the only gate (Santa's two-step check) is silently auto-passed by the enum default.

Impact: High — the entire access-control model of the NFT mint is bypassed. Santa's checkList / checkTwice calls become decorative. NFT supply is uncapped (every EOA can mint one), and any future logic that gates rewards on collectPresent having been called by genuinely-nice users is broken.

Proof of Concept

Place this test in test/SantasListTest.t.sol (alongside the existing test file) and run with forge test --mt test_AnyoneCanCollectFreeNFT -vvv. The exploit runs against a fresh deployment with no checkList / checkTwice calls — the attacker is a completely random address that Santa has never seen.

// SPDX-License-Identifier: MIT
pragma solidity 0.8.22;
import {Test} from "forge-std/Test.sol";
import {SantasList} from "../src/SantasList.sol";
contract DefaultEnumExploitTest is Test {
SantasList santasList;
address santa = makeAddr("santa");
address attacker = makeAddr("attacker");
function setUp() public {
vm.prank(santa);
santasList = new SantasList();
// Santa NEVER calls checkList or checkTwice for `attacker`.
// Warp to a moment after Christmas 2023 (already in the past on mainnet).
vm.warp(santasList.CHRISTMAS_2023_BLOCK_TIME() + 1);
}
function test_AnyoneCanCollectFreeNFT() public {
// Pre-conditions: attacker is unknown to Santa; both lists default to Status.NICE (index 0).
assertEq(uint(santasList.getNaughtyOrNiceOnce(attacker)), uint(SantasList.Status.NICE));
assertEq(uint(santasList.getNaughtyOrNiceTwice(attacker)), uint(SantasList.Status.NICE));
assertEq(santasList.balanceOf(attacker), 0);
// Exploit: unauthorized address mints itself a Christmas NFT.
vm.prank(attacker);
santasList.collectPresent();
// Result: attacker owns NFT #0 even though Santa never touched its address.
assertEq(santasList.balanceOf(attacker), 1);
assertEq(santasList.ownerOf(0), attacker);
}
}

Run output (expected):

[PASS] test_AnyoneCanCollectFreeNFT() (gas: ~85k)

The exploit can be repeated from every fresh EOA, minting an unbounded number of NFTs to the same human attacker.

Recommended Mitigation

Reorder the enum so the zero / default variant represents the un-checked state, and update collectPresent to require explicit NICE markings:

enum Status {
+ NOT_CHECKED_TWICE, // index 0 — default for any unchecked address
NICE,
EXTRA_NICE,
- NAUGHTY,
- NOT_CHECKED_TWICE
+ NAUGHTY
}

Optionally, harden collectPresent with an explicit guard so any future re-ordering of the enum cannot reintroduce the bug:

function collectPresent() external {
if (block.timestamp < CHRISTMAS_2023_BLOCK_TIME) revert SantasList__NotChristmasYet();
if (balanceOf(msg.sender) > 0) revert SantasList__AlreadyCollected();
+ Status once = s_theListCheckedOnce[msg.sender];
+ Status twice = s_theListCheckedTwice[msg.sender];
+ if (twice == Status.NOT_CHECKED_TWICE) revert SantasList__NotNice();
+ if (once != twice) revert SantasList__NotNice();
- if (s_theListCheckedOnce[msg.sender] == Status.NICE
- && s_theListCheckedTwice[msg.sender] == Status.NICE) {
+ if (twice == Status.NICE) {
_mintAndIncrement();
return;
- } else if (
- s_theListCheckedOnce[msg.sender] == Status.EXTRA_NICE
- && s_theListCheckedTwice[msg.sender] == Status.EXTRA_NICE
- ) {
+ } else if (twice == Status.EXTRA_NICE) {
_mintAndIncrement();
i_santaToken.mint(msg.sender);
return;
}
revert SantasList__NotNice();
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 1 day ago
Submission Judgement Published
Validated
Assigned finding tags:

[H-02] All addresses are considered `NICE` by default and are able to claim a NFT through `collectPresent` function before any Santa check.

## Description `collectPresent` function is supposed to be called by users that are considered `NICE` or `EXTRA_NICE` by Santa. This means Santa is supposed to call `checkList` function to assigned a user to a status, and then call `checkTwice` function to execute a double check of the status. Currently, the enum `Status` assigns its default value (0) to `NICE`. This means that both mappings `s_theListCheckedOnce` and `s_theListCheckedTwice` consider every existent address as `NICE`. In other words, all users are by default double checked as `NICE`, and therefore eligible to call `collectPresent` function. ## Vulnerability Details The vulnerability arises due to the order of elements in the enum. If the first value is `NICE`, this means the enum value for each key in both mappings will be `NICE`, as it corresponds to `0` value. ## Impact The impact of this vulnerability is HIGH as it results in a flawed mechanism of the present distribution. Any unchecked address is currently able to call `collectPresent` function and mint an NFT. This is because this contract considers by default every address with a `NICE` status (or 0 value). ## Proof of Concept The following Foundry test will show that any user is able to call `collectPresent` function after `CHRISTMAS_2023_BLOCK_TIME` : ``` function testCollectPresentIsFlawed() external { // prank an attacker's address vm.startPrank(makeAddr("attacker")); // set block.timestamp to CHRISTMAS_2023_BLOCK_TIME vm.warp(1_703_480_381); // collect present without any check from Santa santasList.collectPresent(); vm.stopPrank(); } ``` ## Recommendations I suggest to modify `Status` enum, and use `UNKNOWN` status as the first one. This way, all users will default to `UNKNOWN` status, preventing the successful call to `collectPresent` before any check form Santa: ``` enum Status { UNKNOWN, NICE, EXTRA_NICE, NAUGHTY } ``` After modifying the enum, you can run the following test and see that `collectPresent` call will revert if Santa didn't check the address and assigned its status to `NICE` or `EXTRA_NICE` : ``` function testCollectPresentIsFlawed() external { // prank an attacker's address vm.startPrank(makeAddr("attacker")); // set block.timestamp to CHRISTMAS_2023_BLOCK_TIME vm.warp(1_703_480_381); // collect present without any check from Santa vm.expectRevert(SantasList.SantasList__NotNice.selector); santasList.collectPresent(); vm.stopPrank(); } ```

Support

FAQs

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

Give us feedback!