Santa's List

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

Anyone can collect a present without ever being checked — status mappings default to NICE

Root + Impact

Description

The Status enum starts with NICE = 0, so both status mappings default to NICE for any address Santa never touched. collectPresent() only checks that both mappings read NICE, so a complete stranger passes both checks and collects an NFT — Santa's list is never consulted at all.

@> enum Status {
@> NICE,
@> EXTRA_NICE,
@> NAUGHTY,
@> NOT_CHECKED_TWICE
@> }
@> if (s_theListCheckedOnce[msg.sender] == Status.NICE && s_theListCheckedTwice[msg.sender] == Status.NICE) {
@> _mintAndIncrement();
@> return;
@> }

For an address never checked by Santa, s_theListCheckedOnce[addr] and s_theListCheckedTwice[addr] both return the default enum value 0, which is NICE. The NOT_CHECKED_TWICE variant exists in the enum but is never used as a default, so there is no "unchecked" state — every stranger reads as NICE. The same default also makes getNaughtyOrNiceOnce() / getNaughtyOrNiceTwice() report NICE for unchecked addresses.

Risk

Likelihood: High — the bug triggers for every address that was never checked, with no special setup; anyone can call collectPresent() after Christmas and receive an NFT.

Impact: High — the protocol's core invariant is broken: presents are supposed to go only to people Santa checked twice as NICE or EXTRA_NICE, but anyone can mint an NFT without Santa's involvement, so the naughty/nice list is meaningless and the NFT supply is unlimited.

Proof of Concept

A never-checked address calls collectPresent() after Christmas and receives an NFT. Note the two getter calls returning NICE for an address Santa never touched.

function test_H1_anyoneCanCollectWithoutBeingChecked() public {
// `attacker` was never checked by Santa in either mapping.
assertEq(uint256(santasList.getNaughtyOrNiceOnce(attacker)), uint256(SantasList.Status.NICE));
assertEq(uint256(santasList.getNaughtyOrNiceTwice(attacker)), uint256(SantasList.Status.NICE));
​
vm.prank(attacker);
santasList.collectPresent();
​
// A complete stranger just collected a present.
assertEq(santasList.balanceOf(attacker), 1);
}

Recommended Mitigation

Make the first enum value an explicit "not checked" state so an unchecked address can never read as NICE, or track checked-ness with a separate boolean.

enum Status {
+ UNKNOWN,
NICE,
EXTRA_NICE,
NAUGHTY,
NOT_CHECKED_TWICE
}
Updates

Lead Judging Commences

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