Santa's List

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

`SantaToken.burn()` Has No Allowance Check — Anyone Can Burn Any User's Tokens via SantasList

Description

Root + Impact

  • The SantaToken.burn() function only checks that the caller is i_santasList. It does NOT check any ERC20 allowance or that the from address has approved the burn.

  • Combined with the buyPresent() bug (Finding #2), this means SantasList can burn tokens from any address without any approval mechanism. Even if buyPresent() were fixed to burn from msg.sender, the burn() function itself lacks the fundamental ERC20 safety of requiring allowance.

function burn(address from) external {
if (msg.sender != i_santasList) {
revert SantaToken__NotSantasList();
}
_burn(from, 1e18); // @> No allowance check — burns from ANY address unconditionally
}

The NatSpec on buyPresent() says "You'll first need to approve the SantasList contract to spend your SantaTokens" — but no approval is ever checked anywhere. The burn() ignores all allowances.


Risk

Likelihood:

  • The burn path is directly triggered via buyPresent(), which is callable by anyone

  • No approval or consent is required from the token holder

Impact:

  • Directly enables Finding #2: any attacker can burn any user's SantaTokens

  • Violates the ERC20 allowance model — the approve mentioned in the NatSpec is never enforced

  • Any future function added to SantasList that calls burn() inherits this vulnerability


Proof of Concept

function test_BurnNoAllowanceCheck() public {
// Give victim SantaTokens via EXTRA_NICE flow
vm.startPrank(santa);
santasList.checkList(user, SantasList.Status.EXTRA_NICE);
santasList.checkTwice(user, SantasList.Status.EXTRA_NICE);
vm.stopPrank();
vm.warp(santasList.CHRISTMAS_2023_BLOCK_TIME() + 1);
vm.prank(user);
santasList.collectPresent();
assertEq(santaToken.balanceOf(user), 1e18);
// User has NOT approved SantasList to spend their tokens
assertEq(santaToken.allowance(user, address(santasList)), 0);
// Yet buyPresent burns their tokens anyway — no allowance check
address anyone = makeAddr("anyone");
vm.prank(anyone);
santasList.buyPresent(user); // Burns user's tokens with zero allowance
assertEq(santaToken.balanceOf(user), 0); // Tokens gone without approval
}

Recommended Mitigation

Add an allowance check in the burn function, or restructure buyPresent() to use transferFrom with the standard allowance pattern:

function burn(address from) external {
if (msg.sender != i_santasList) {
revert SantaToken__NotSantasList();
}
+ // Require that the SantasList contract has been approved by `from`
+ uint256 allowed = allowance[from][msg.sender];
+ if (allowed != type(uint256).max) allowance[from][msg.sender] = allowed - 1e18;
_burn(from, 1e18);
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 3 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!