Santa's List

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

NICE at position 0 in the Status enum causes all uninitialised addresses to pass the second-pass verification check by default

Root + Impact

Every address in existence has s_theListCheckedTwice set to NICE before Santa ever calls checkTwice(). The second-pass verification — the core security guarantee of the protocol — is pre-satisfied by default for all addresses. Santa only needs to perform one pass for an address to collect a present, and in combination with the unguarded checkList(), Santa does not need to be involved at all.

This is a structural property of the contract, not a conditional edge case. It affects every address on every deployment, permanently and from block zero. No attacker action is required to create the vulnerable state — it exists by default.

Description

  • SantasList uses a two-pass system to verify who is on the nice list. Santa calls checkList() for a first pass, then checkTwice() for a confirmed second pass. Only addresses that pass both checks are eligible to collect a present.

    The Status enum is defined as:

    // @> NICE is at position 0 — this becomes the default value for all uninitialised mappings
    enum Status {
    NICE, // = 0
    EXTRA_NICE, // = 1
    NAUGHTY, // = 2
    NOT_CHECKED_TWICE // = 3
    }
  • In Solidity, every uninitialised mapping value defaults to 0. Because NICE occupies position 0, the following is true for every address before Santa touches anything:

    s_theListCheckedTwice[anyAddress] == Status(0) == Status.NICE // always true by default

  • collectPresent() checks both mappings:

    if (s_theListCheckedOnce[msg.sender] == Status.NICE
    // @> s_theListCheckedTwice already equals NICE for every address — Santa never needs to call checkTwice
    && s_theListCheckedTwice[msg.sender] == Status.NICE) {
    _mintAndIncrement();
    return;
    }
  • The second check provides zero security. It is satisfied for every address in existence before Santa ever calls checkTwice(). The entire second-pass design is a no-op.

    This is independently exploitable: even assuming checkList() were correctly restricted to Santa only, Santa performing only the first pass is sufficient for an address to collect. Santa calling checkList(alice, NICE) once — without ever calling checkTwice — allows Alice to collect a present because the second mapping was already NICE.

Risk

Likelihood:

Affects every address on every deployment from block zero — no attacker setup required
Santa performing only one pass (a likely operational mistake) is sufficient for a user to collect
Combined with the unguarded checkList(), any address can collect with zero Santa involvement
Impact:

The two-pass verification system provides no security guarantee — the second check is always pre-satisfied
Any address that passes the first check (legitimately or via the unguarded checkList()) can collect immediately
The intended design — requiring explicit dual confirmation from Santa — is completely broken

Proof of Concept

The following test demonstrates that Santa calling checkList() once (no checkTwice() call) is sufficient for an address to collect a present. The second check passes purely from the default enum value:

function test_F2_CheckTwiceNeverCalledButSecondCheckPasses() public {
// Santa does ONE pass only — intentionally never calls checkTwice
vm.prank(santa);
santasList.checkList(attacker, SantasList.Status.NICE);
// s_theListCheckedTwice[attacker] == NICE purely from the default enum value
// Santa never called checkTwice — yet the second check is already satisfied
assertEq(
uint256(santasList.getNaughtyOrNiceTwice(attacker)),
uint256(SantasList.Status.NICE)
);
vm.warp(santasList.CHRISTMAS_2023_BLOCK_TIME() + 1);
// collectPresent succeeds — both checks pass despite only one Santa pass
vm.prank(attacker);
santasList.collectPresent();
assertEq(santasList.balanceOf(attacker), 1);
}

Recommended Mitigation

Move NOT_CHECKED_TWICE to position 0 in the enum so that all uninitialised mappings default to the safe, unreviewed state. This ensures an address that has never been through checkTwice() cannot pass the second check.

After this fix, s_theListCheckedTwice[anyAddress] defaults to NOT_CHECKED_TWICE(0) instead of NICE, and the second check in collectPresent() will correctly fail for any address Santa has not explicitly confirmed.

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

Lead Judging Commences

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