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.
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:
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:
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.
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
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:
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.
## 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.