Santa's List

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

`Status.NICE` at enum index 0 makes the naughty-or-nice list vacuous — any address collects a present without ever being checked

Root + Impact

Description

### Summary
`Status.NICE` is declared first in the enum, so it is the zero value that every unwritten mapping
entry returns. Both grading lists are `mapping(address => Status)`, which means every address Santa
has never touched already reads `NICE` on both lists — exactly the condition `collectPresent`
checks. Any address mints a present for gas alone, with no grading, no role, no capital and no
prior transaction. The allowlist confers nothing.
### Vulnerability Details
```solidity
enum Status {
NICE, // index 0
EXTRA_NICE,
NAUGHTY,
NOT_CHECKED_TWICE
}
```
`s_theListCheckedOnce` (`:79`) and `s_theListCheckedTwice` (`:80`) are both
`mapping(address => Status)`. Solidity initialises every unwritten value to the zero value of its
type, so for any address never passed to `checkList` or `checkTwice`, both mappings return
`Status.NICE`.
`collectPresent` at `:154` asks precisely the question that default satisfies:
```solidity
if (s_theListCheckedOnce[msg.sender] == Status.NICE && s_theListCheckedTwice[msg.sender] == Status.NICE) {
_mintAndIncrement();
return;
}
```
The contract declares a `NOT_CHECKED_TWICE` member, so an "ungraded" sentinel was intended. It sits
at index 3, where nothing can default to it. The sentinel exists in the source and is unreachable as
a default — that is the root cause, rather than a missing check.
This contradicts stated rule **SL-1**: *"In order for someone to be considered `NICE` or
`EXTRA_NICE` they must be first 'checked twice' by Santa."* No address is required to be checked at
all.
`EXTRA_NICE` is index 1 and is not reachable by default, so this path mints NFTs only, not
`SantaToken`.// Root cause in the codebase with @> marks to highlight the relevant section

Risk

### Impact
The core access decision of the protocol is inverted: the default state of an unknown address is
"approved". Any address mints a Santa NFT for **74,969 gas** measured, negligible on Arbitrum, with
zero capital. Because a fresh address costs nothing, NFT supply is bounded by the attacker's gas
budget rather than by the protocol.
Measured in the PoC: Santa grades a 5-address allowlist through both passes and those 5 collect. 26
addresses Santa never touched then collect as well — **31 NFTs minted against 5 owed, 520% supply
inflation**. The mints are permanent and the contract is immutable, so there is no recovery path.

Proof of Concept

**Proof of Concept** — `test/Claude/F01_EnumZeroValue.t.sol`
```bash
forge test --match-test test_F01 -vvv
```
```solidity
// Santa has never written to this address. The contract already reports it NICE, twice.
assertEq(uint8(list.getNaughtyOrNiceOnce(attacker)), uint8(SantasList.Status.NICE));
assertEq(uint8(list.getNaughtyOrNiceTwice(attacker)), uint8(SantasList.Status.NICE));
vm.prank(attacker);
list.collectPresent(); // one call, no capital, no prior transaction
assertEq(list.balanceOf(attacker), 1, "ungraded address now owns a Santa NFT");
```
```
[PASS] test_F01_UngradedPublicDrainsTheEntireAllowlist()
addresses Santa graded: 5
NFTs owed by the spec: 5
NFTs actually minted: 31
minted to addresses never graded: 26
attacker capital required (wei): 0
supply inflation vs spec (%): 520
[PASS] test_F01_FixBlocksTheUngradedPublic()
graded users who collected: 1
ungraded addresses that collected: 0
attacker NFT balance: 0
```

Recommended Mitigation

To fix this vulnerability, move the enum sentinel value to index 0. This ensures that unwritten mapping entries default to an uninitialized state rather than `Status.NICE`, preventing ungraded addresses from passing the check and collecting presents.
```diff
enum Status {
- NICE,
+ NOT_CHECKED_TWICE,
+ NICE,
EXTRA_NICE,
- NAUGHTY,
- NOT_CHECKED_TWICE
+ NAUGHTY
}
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!