Santa's List

AI First Flight #3
Beginner FriendlyFoundry
EXP
View results
Submission Details
Impact: medium
Likelihood: low
Invalid

[L-01] `collectPresent()` does not check `s_theListCheckedTwice` for `NOT_CHECKED_TWICE` — single-check users collect presents

Description

collectPresent() checks if both s_theListCheckedOnce and s_theListCheckedTwice equal NICE or EXTRA_NICE. The second mapping defaults to NICE (0) for addresses that were only checked once but never checked twice. Combined with the default enum issue (H-03), there is no explicit check that checkTwice() was actually called. A user checked once as NICE by Santa but never confirmed via checkTwice() still passes both conditions because s_theListCheckedTwice defaults to NICE.

Vulnerability Details

// src/SantasList.sol, lines 154-156
if (s_theListCheckedOnce[msg.sender] == Status.NICE
&& s_theListCheckedTwice[msg.sender] == Status.NICE) {
// @> s_theListCheckedTwice defaults to NICE for never-checked-twice addresses
_mintAndIncrement();
return;
}

The protocol's design requires two checks before collecting. Santa calls checkList() first, then checkTwice(). But if Santa only calls checkList(user, NICE) and never follows up with checkTwice(), the user can still collect because the second mapping defaults to NICE.

This is related to H-03 (default enum) but represents a distinct logical flaw: even if the enum were reordered so that 0 = NOT_CHECKED_TWICE, the contract still lacks an explicit verification that checkTwice() was called for the address.

Risk

Likelihood:

  • Requires Santa to check a user once but not follow through with checkTwice. This is a normal operational scenario, especially if Santa is still processing the list.

Impact:

  • Users who were only partially vetted can collect presents early, before their second check.

Proof of Concept

function testExploit_SingleCheckCollects() public {
address user = makeAddr("user");
// Santa checks once as NICE, but hasn't done checkTwice yet
vm.prank(santa);
santasList.checkList(user, SantasList.Status.NICE);
// checkTwice was NEVER called for this user
// But s_theListCheckedTwice[user] defaults to NICE (0)
vm.prank(user);
santasList.collectPresent();
// User collected despite never being checked twice
assertEq(santasList.balanceOf(user), 1);
}

Recommendations

Track whether checkTwice() was actually called:

+ mapping(address => bool) private s_checkedTwice;
function checkTwice(address person, Status status) external onlySanta {
if (s_theListCheckedOnce[person] != status) {
revert SantasList__SecondCheckDoesntMatchFirst();
}
s_theListCheckedTwice[person] = status;
+ s_checkedTwice[person] = true;
emit CheckedTwice(person, status);
}
function collectPresent() external {
// ...
+ if (!s_checkedTwice[msg.sender]) {
+ revert SantasList__NotNice();
+ }
// ... existing NICE/EXTRA_NICE checks
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 4 hours ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!