SantasList::checkTwice() — Default Enum Value Allows Bypassing First Check RequirementSantasList::checkTwice() is intended to only be callable for addresses that have already been processed through checkList() first. The natspec clearly states:
"Do a second pass on someone if they are naughty or nice. Only if they pass this are they eligible for a present."
However, the validation logic in checkTwice() only checks whether the status in s_theListCheckedOnce matches the input status — it does not verify whether the address has ever been added to the first list at all:
The root cause lies in Solidity's default storage behavior: all uninitialized mapping values default to 0, and since Status.NICE is the first member of the enum (index 0), any address that has never been passed through checkList() will have s_theListCheckedOnce[person] == Status.NICE by default.
This means Santa can — intentionally or accidentally — call checkTwice(randomAddress, Status.NICE) for any address that was never checked once, and the call will succeed without reverting.
Likelihood: MEDIUM
Requires Santa to be the caller (access controlled)
However, Santa could be socially engineered, make an honest mistake, or act maliciously
Combined with the missing access control bug on checkList(), any attacker can first self-register via checkList() and then push Santa to confirm via checkTwice()
Impact: HIGH
Addresses that were never legitimately reviewed can be placed on the second list as NICE or EXTRA_NICE
These addresses become eligible to call collectPresent() and receive NFTs and SantaToken they were never entitled to
Breaks the core two-step verification invariant of the entire protocol
Introduce a separate boolean mapping to explicitly track whether an address has been processed through checkList(), independent of the status value:
Additionally, consider placing UNKNOWN or NOT_CHECKED_TWICE as the first member of the enum so the default value is a neutral, non-privileged state:
## 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(); } ```
The contest is live. Earn rewards by submitting a finding.
Submissions are being reviewed by our AI judge. Results will be available in a few minutes.
View all submissionsThe contest is complete and the rewards are being distributed.