The README says: "In order for someone to be considered NICE or EXTRA_NICE they must be first 'checked twice' by Santa". Only addresses marked NICE or EXTRA_NICE on both lists may collect a present. The README also names an UNKNOWN status for addresses Santa has not classified.
NICE is the first member of Status, so its value is 0. That is also the value both mappings return for any address that was never written. As a result, the NICE branch of collectPresent cannot tell "checked twice and nice" apart from "never checked", and after Christmas any fresh address mints the NFT. The same default lets checkTwice(x, NICE) pass its "already marked on the first list" guard for an address that was never passed to checkList.
Likelihood:
After CHRISTMAS_2023_BLOCK_TIME, any EOA or contract that Santa never touched can call collectPresent in one permissionless transaction.
Fresh addresses cost nothing to create, so the mint scales across any number of addresses, and the NFTs can then be swept into one wallet.
Impact:
For the NICE tier, the protocol's core eligibility gate does not exist. Any address can mint the "checked twice by Santa" NFT, so NFT supply is unbounded. In the PoC, 25 never-checked addresses minted 25 NFTs into one wallet without a single call from Santa.
A user whom Santa marked NAUGHTY on both lists can get the NFT from a fresh address without any SantaToken. That makes the paid buyPresent route pointless.
Scope of this root cause: it reaches only the NICE tier. The EXTRA_NICE branch is never reached through defaults, so no SantaToken is minted.
The PoC deploys SantasList as Santa. It uses no ffi and no repo scripts.
test_baseline_gateWorksOnlyForExplicitlyWrittenStatuses: before Christmas, collecting reverts with NotChristmasYet. After Christmas, a user Santa checked NICE twice collects, and a user Santa checked NAUGHTY twice reverts with NotNice.
test_neverCheckedAddressCollectsNFT: for an address never touched, both getters return NICE and uint(NICE) == 0. collectPresent mints token 0 to it, and its SantaToken balance stays 0.
test_sybil_unboundedSupplyFromFreshAddresses: 25 never-checked addresses each mint one NFT and transfer it to one collector, whose balance ends at 25.
test_naughtyUserBypassesViaFreshAddress_buyPresentPointless: a user checked NAUGHTY twice is rejected, then collects from a fresh alt address and moves the NFT to the main address. SantaToken total supply stays 0.
test_checkTwiceNice_succeedsWithoutFirstCheck: for a never-checked address, checkTwice(x, EXTRA_NICE) reverts but checkTwice(x, NICE) succeeds. Only Santa can call checkTwice, so this part only shows that the README rule is not enforced.
test_firstPassNaughtyOnly_selfResetBecomesEligible: a user Santa marked NAUGHTY on the first pass only still has a default-NICE second list. After rewriting their own first list to NICE through the unguarded checkList, which is a separate submission, they collect. A user Santa marked NAUGHTY on both lists cannot use the same trick.
A single address minting without limit needs the separate balanceOf guard bypass and is not exercised here. Many fresh addresses give the same supply impact without it.
File test/PoC_SL-03.t.sol:
Run from the repository root:
Result:
Make the unchecked state the zero member of the enum, as the README's UNKNOWN status implies. Never-written addresses then read UNKNOWN, which matches neither collect branch. checkTwice(x, NICE) also reverts for an address that was never passed to checkList, because 0 no longer equals NICE.
## 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.