Santa's List

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

Unverified default list state permits unauthenticated NFT claims after Christmas

Root + Impact

Description

  • Addresses are intended to qualify for presents only after Santa records a matching first pass and second pass on-chain (for example NICE / EXTRA_NICE as reflected in both s_theListCheckedOnce and s_theListCheckedTwice), and after the Christmas timestamp gate passes.

  • In Solidity the first enumerator value (NICE) is zero. Any address that Santa has never written still reads as NICE in both mappings, so collectPresent treats them as doubly-checked nice once time has advanced past CHRISTMAS_2023_BLOCK_TIME, allowing a single free mint with no privileged action.

// file: src/SantasList.sol
// @> Impact: uninitialized storage reads as Status.NICE (enum value 0) for BOTH mappings — indistinguishable from "Santa approved NICE twice"
enum Status {
NICE, // @> 0 = default read for unchecked addresses
EXTRA_NICE,
NAUGHTY,
NOT_CHECKED_TWICE
}
// ...
if (s_theListCheckedOnce[msg.sender] == Status.NICE && s_theListCheckedTwice[msg.sender] == Status.NICE) {
// @> Any never-listed EOA qualifies here after the timestamp check
_mintAndIncrement();
return;
}

Risk

Likelihood:

  • Permanent on deployments where collectPresent remains callable after CHRISTMAS_2023_BLOCK_TIME, anytime an address has 0 unset slots (new wallets, scripted sybils).

  • Applies to 100% of addresses that nobody has overwritten on checkList / checkTwice.

Impact:

  • Unrestricted inflation of the present NFT supply against the intended Santa-gated eligibility model.

  • Reputational and economic harm wherever the NFT is treated as proof of being on-chain “verified nice.”

Proof of Concept

// forge test — add to a *.t.sol
function test_finding_defaultNiceDoubleCheck_mints_without_santa() public {
address stranger = makeAddr("strangerNeverOnList");
vm.warp(santasList.CHRISTMAS_2023_BLOCK_TIME() + 1);
vm.prank(stranger);
santasList.collectPresent();
assertEq(santasList.balanceOf(stranger), 1);
assertEq(uint256(santasList.getNaughtyOrNiceOnce(stranger)), uint256(SantasList.Status.NICE));
assertEq(uint256(santasList.getNaughtyOrNiceTwice(stranger)), uint256(SantasList.Status.NICE));
}

Recommended Mitigation

- if (s_theListCheckedOnce[msg.sender] == Status.NICE && s_theListCheckedTwice[msg.sender] == Status.NICE) {
- _mintAndIncrement();
- return;
- }
+ // Representative fix: explicit “Santa completed second check” bit or non-zero sentinel;
+ // never treat raw enum equality with unset storage as eligibility.
+ if (!hasBeenCheckedTwiceBySanta[msg.sender]) {
+ revert SantasList__NotNice();
+ }
+ if (s_theListCheckedTwice[msg.sender] == Status.NICE) {
+ _mintAndIncrement();
+ return;
+ }

(Concrete options: mapping bool processedTwice; require NOT_CHECKED_TWICE semantics with ***NICE*moved off zero ; nonce per user set only in checkTwice.)


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!