Santa's List

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

Enum Default Value Allows Unchecked Users to Pass as NICE

Root + Impact

Description

File: SantasList.sol

  • The Invariant Broken:
    Users should only be able to collect presents if they have been explicitly checked by Santa. Unchecked users should have status NOT_CHECKED_TWICE, but the enum design makes them default to NICE.

enum Status {
NICE, // 0 - DEFAULT VALUE ❌
EXTRA_NICE, // 1
NAUGHTY, // 2
NOT_CHECKED_TWICE // 3
}

Solidity mappings default to 0, which maps to Status.NICE instead of Status.NOT_CHECKED_TWICE.


Risk

Attack Path:

  1. Alice is never checked by Santa (never appears on either list)

  2. Alice's status: s_theListCheckedOnce[alice] = 0 = Status.NICE (default)

  3. Alice's status: s_theListCheckedTwice[alice] = 0 = Status.NICE (default)

  4. Christmas arrives

  5. Alice calls collectPresent()

  6. Check at line 154 passes: Status.NICE == Status.NICE && Status.NICE == Status.NICE

  7. Alice collects NFT without ever being checked by Santa

Additionally - Santa Can Skip checkList:

function checkTwice(address person, Status status) external onlySanta {
if (s_theListCheckedOnce[person] != status) { // Defaults to NICE (0)
revert SantasList__SecondCheckDoesntMatchFirst();
}
s_theListCheckedTwice[person] = status;
}

If Santa calls checkTwice(bob, Status.NICE) without first calling checkList(), the check passes because unchecked bob defaults to NICE.

Economic Impact:

  • Attack Cost: $0.30 (gas only)

  • Attack Gain: 1 NFT per unchecked address

  • Protocol Damage: Unauthorized NFT minting, bypasses Santa's authority

  • Business logic bypass, requires specific timing (Christmas) but no other barriers

Proof of Concept

function testExploitDefaultEnumValue() public {
address uncheckedUser = makeAddr("unchecked");
// User was NEVER checked by Santa
// Verify default values
assertEq(uint256(santasList.getNaughtyOrNiceOnce(uncheckedUser)), 0); // NICE
assertEq(uint256(santasList.getNaughtyOrNiceTwice(uncheckedUser)), 0); // NICE
// Fast forward to Christmas
vm.warp(santasList.CHRISTMAS_2023_BLOCK_TIME());
// Unchecked user collects present - SHOULD REVERT but doesn't!
vm.prank(uncheckedUser);
santasList.collectPresent();
// Exploit successful
assertEq(santasList.balanceOf(uncheckedUser), 1);
}
function testSantaCanSkipCheckList() public {
address user = makeAddr("user");
// Santa skips checkList and goes straight to checkTwice
vm.prank(santa);
santasList.checkTwice(user, SantasList.Status.NICE); // Doesn't revert!
// User can now collect
vm.warp(santasList.CHRISTMAS_2023_BLOCK_TIME());
vm.prank(user);
santasList.collectPresent();
assertEq(santasList.balanceOf(user), 1);
}

Recommended Mitigation

enum Status {
NOT_CHECKED_TWICE, // 0 - Move to first position as default ✅
NICE, // 1
EXTRA_NICE, // 2
NAUGHTY // 3
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 2 days 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!