Santa's List

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

Anyone Can Collect Presents as `NICE` Without Being Checked — Default Enum Value Is `NICE` (index 0)

Description

Root + Impact

  • The Status enum defines NICE as the first member (index 0). In Solidity, uninitialized mapping values default to 0.

  • Both s_theListCheckedOnce and s_theListCheckedTwice are mappings that default to 0 (= Status.NICE) for any address that has never been checked.

  • The collectPresent() function checks if s_theListCheckedOnce[msg.sender] == Status.NICE && s_theListCheckedTwice[msg.sender] == Status.NICE. For any address that was never checked at all, both mappings return 0 = NICE, so the check passes.

enum Status {
NICE, // @> index 0 — this is the DEFAULT value for uninitialized mappings
EXTRA_NICE, // index 1
NAUGHTY, // index 2
NOT_CHECKED_TWICE // index 3
}
mapping(address person => Status naughtyOrNice) private s_theListCheckedOnce; // @> defaults to 0 = NICE
mapping(address person => Status naughtyOrNice) private s_theListCheckedTwice; // @> defaults to 0 = NICE
function collectPresent() external {
if (block.timestamp < CHRISTMAS_2023_BLOCK_TIME) {
revert SantasList__NotChristmasYet();
}
if (balanceOf(msg.sender) > 0) {
revert SantasList__AlreadyCollected();
}
// @> Both checks pass for ANY address that was never checked — defaults to NICE (0)
if (s_theListCheckedOnce[msg.sender] == Status.NICE && s_theListCheckedTwice[msg.sender] == Status.NICE) {
_mintAndIncrement();
return;
}
// ...
}

Risk

Likelihood:

  • Every address in existence that has never interacted with the contract has a default status of NICE in both mappings

  • After Christmas 2023 block time, any address can call collectPresent() and receive an NFT

Impact:

  • The entire naughty/nice list is meaningless — Santa's checking process is completely bypassable

  • Unlimited addresses can mint NFTs without any authorization from Santa

  • The NFT has no scarcity — anyone can mint one


Proof of Concept

function test_UncheckedAddressCollectsAsNice() public {
address stranger = makeAddr("neverChecked");
// Stranger has NEVER been checked by Santa — not in any list
assertEq(uint256(santasList.getNaughtyOrNiceOnce(stranger)), uint256(SantasList.Status.NICE)); // default = 0 = NICE
assertEq(uint256(santasList.getNaughtyOrNiceTwice(stranger)), uint256(SantasList.Status.NICE)); // default = 0 = NICE
// Wait for Christmas
vm.warp(santasList.CHRISTMAS_2023_BLOCK_TIME() + 1);
// Stranger collects present without EVER being checked!
vm.prank(stranger);
santasList.collectPresent();
assertEq(santasList.balanceOf(stranger), 1); // Got NFT without Santa's approval
}

Recommended Mitigation

Reorder the enum so the default (0) is an invalid/unchecked state:

enum Status {
+ NOT_CHECKED_TWICE, // index 0 — safe default for uninitialized mappings
NICE,
EXTRA_NICE,
- NAUGHTY,
- NOT_CHECKED_TWICE
+ NAUGHTY
}

Or add an explicit check that the address was actually verified:

function collectPresent() external {
if (block.timestamp < CHRISTMAS_2023_BLOCK_TIME) {
revert SantasList__NotChristmasYet();
}
if (balanceOf(msg.sender) > 0) {
revert SantasList__AlreadyCollected();
}
+ // Ensure the person was actually checked (not just default 0)
+ if (s_theListCheckedTwice[msg.sender] == Status.NOT_CHECKED_TWICE) {
+ revert SantasList__NotNice();
+ }
if (s_theListCheckedOnce[msg.sender] == Status.NICE ...
Updates

Lead Judging Commences

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