Santa's List

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

SantaToken.burn() hardcodes 1e18 but buyPresent() should burn 2e18 (PURCHASED_PRESENT_COST), letting users buy presents at half price and for themselves

Root + Impact

SantaToken.burn() takes no amount parameter and always calls _burn(from, 1e18). The documented cost for buyPresent() is PURCHASED_PRESENT_COST = 2e18, so every purchase burns only half the intended tokens. Additionally, buyPresent() places no restriction on presentReceiver == msg.sender, allowing users to buy presents for themselves — which the docs explicitly prohibit.

Description

  • SantasList.buyPresent() calls i_santaToken.burn(presentReceiver) to destroy the caller's SantaToken before minting a present NFT:

// src/SantasList.sol
uint256 public constant PURCHASED_PRESENT_COST = 2e18;
function buyPresent(address presentReceiver) external {
i_santaToken.burn(presentReceiver); // @> burns presentReceiver's tokens, not msg.sender's
_mintAndIncrement(); // @> mints to msg.sender, not presentReceiver
}
  • SantaToken.burn() ignores the intended cost entirely:

// src/SantaToken.sol
function burn(address from) external {
if (msg.sender != i_santasList) { revert SantaToken__NotSantasList(); }
_burn(from, 1e18); // @> hardcoded 1e18 — should be PURCHASED_PRESENT_COST (2e18)
}
  • Result: a user with 1e18 SantaToken can call buyPresent(themselves), burn their own 1e18 (half price), and receive a present NFT — bypassing both the price check and the "only buy for others" rule.

Risk

Likelihood:

  • Every buyPresent() call is affected from day one. Any user who earned 1e18 SantaToken via collectPresent() can immediately exploit both issues.

Impact:

  • The token burn acts as the economic brake on present minting. At half price, twice as many NFTs can be minted per token supply. Combined with the self-buy path, an EXTRA_NICE user can: collect one NFT + 1e18 token via collectPresent(), then call buyPresent(self) to get a second NFT — doubling their allocation at half the intended cost.

Proof of Concept

Alice collects her present (receives 1 NFT + 1e18 SantaToken). She then calls buyPresent(alice) — burning only 1e18 token (half the documented price) and minting a second NFT for herself, violating both the price and the self-purchase prohibition.

function test_buyPresentHalfPriceAndSelf() public {
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());
vm.prank(user);
santasList.collectPresent(); // user gets 1 NFT + 1e18 SantaToken
uint256 balanceBefore = santaToken.balanceOf(user);
assertEq(balanceBefore, 1e18);
vm.prank(user);
santasList.buyPresent(user); // self-purchase — docs say this should be forbidden
// Only 1e18 burned instead of PURCHASED_PRESENT_COST (2e18)
assertEq(santaToken.balanceOf(user), balanceBefore - 1e18);
// User now holds 2 NFTs
assertEq(santasList.balanceOf(user), 2);
}

User ends up with 2 NFTs after spending only 1e18 tokens — half the documented cost, with the purchase made for themselves.

Recommended Mitigation

Add an amount parameter to burn() and block self-purchases in buyPresent():

// SantaToken.sol
- function burn(address from) external {
+ function burn(address from, uint256 amount) external {
if (msg.sender != i_santasList) { revert SantaToken__NotSantasList(); }
- _burn(from, 1e18);
+ _burn(from, amount);
}
// SantasList.sol
+ error SantasList__ReceiverIsCaller();
function buyPresent(address presentReceiver) external {
+ if (msg.sender == presentReceiver) { revert SantasList__ReceiverIsCaller(); }
- i_santaToken.burn(presentReceiver);
+ i_santaToken.burn(msg.sender, PURCHASED_PRESENT_COST);
_mintAndIncrement();
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 2 hours ago
Submission Judgement Published
Validated
Assigned finding tags:

[M-01] Cost to buy NFT via SantasList::buyPresent is 2e18 SantaToken but it burns only 1e18 amount of SantaToken

## Description - The cost to buy NFT as mentioned in the docs is 2e18 via the `SantasList::buyPresent` function but in the actual implementation of buyPresent function it calls the SantaToken::burn function which doesn't take any parameter for amount and burns a fixed 1e18 amount of SantaToken, thus burning only half of the actual amount that needs to be burnt, and hence user can buy present for their friends at cheaper rates. - Along with this the user is able to buy present for themselves but the docs mentions that present can be bought only for other users. ## Vulnerability Details The vulnerability lies in the code in the function `SantasList::buyPresent` at line 173 and in `SantaToken::burn` at line 28. The function `burn` burns a fixed amount of 1e18 SantaToken whenever `buyPresent` is called but the true value of SantaToken that was expected to be burnt to mint an NFT as present is 2e18. ```cpp function buyPresent(address presentReceiver) external { @> i_santaToken.burn(presentReceiver); _mintAndIncrement(); } ``` ```cpp function burn(address from) external { if (msg.sender != i_santasList) { revert SantaToken__NotSantasList(); } @> _burn(from, 1e18); } ``` ## PoC Add the test in the file: `test/unit/SantasListTest.t.sol`. Run the test: ```cpp forge test --mt test_UsersCanBuyPresentForLessThanActualAmount ``` ```cpp function test_UsersCanBuyPresentForLessThanActualAmount() public { 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 present vm.prank(user); santasList.collectPresent(); // balance after collecting present uint256 userInitBalance = santaToken.balanceOf(user); // now the user holds 1e18 SantaToken assertEq(userInitBalance, 1e18); vm.prank(user); santaToken.approve(address(santasList), 1e18); vm.prank(user); // user buy present // docs mention that user should only buy present for others, but they can buy present for themselves santasList.buyPresent(user); // only 1e18 SantaToken is burnt instead of the true price (2e18) assertEq(santaToken.balanceOf(user), userInitBalance - 1e18); } ``` ## Impact - Protocol mentions that user should be able to buy NFT for 2e18 amount of SantaToken but users can buy NFT for their friends by burning only 1e18 tokens instead of 2e18, thus NFT can be bought at much cheaper rate which is half of the true amount that was expected to buy NFT. - User can buy a present for themselves but docs strictly mentions that present can be bought for someone else. ## Recommendations Include an argument inside the `SantaToken::burn` to specify the amount of token to burn and also update the `SantasList::buyPresent` function with updated parameter for `burn` function to pass correct amount of tokens to burn. - Update the `SantaToken::burn` function ```diff -function burn(address from) external { +function burn(address from, uint256 amount) external { if (msg.sender != i_santasList) { revert SantaToken__NotSantasList(); } - _burn(from, 1e18); + _burn(from, amount); } ``` - Update the `SantasList::buyPresent` function ```diff + error SantasList__ReceiverIsCaller(); function buyPresent(address presentReceiver) external { + if (msg.sender == presentReceiver) { + revert SantasList__ReceiverIsCaller(); + } - i_santaToken.burn(presentReceiver); + i_santaToken.burn(presentReceiver, PURCHASED_PRESENT_COST); _mintAndIncrement(); } ```

Support

FAQs

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

Give us feedback!