Santa's List

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

`PURCHASED_PRESENT_COST` is never read — `SantaToken.burn` hardcodes `1e18`, halving the documented price

Root + Impact

Description

  • Describe the normal behavior in one or more sentences

  • Explain the specific issue or problem in one or more sentences

### Summary
The purchase price is published as `PURCHASED_PRESENT_COST = 2e18` but is dead code — grepping the
in-scope sources at the audited commit returns exactly one hit, its own declaration. The amount
actually charged is a `1e18` literal inside `SantaToken.burn`, which takes no amount parameter.
Presents sell at 50% of the documented price and the protocol issues twice the NFTs its token supply
was designed to support.
### Vulnerability Details
```solidity
// The cost of santa tokens for naughty people to buy presents
uint256 public constant PURCHASED_PRESENT_COST = 2e18; // :88
```
No function reads it and it is never passed to anything. The amount actually charged is a literal
inside the token:
```solidity
function burn(address from) external {
if (msg.sender != i_santasList) { revert SantaToken__NotSantasList(); }
_burn(from, 1e18); // SantaToken.sol:32
}
```
`burn` takes no amount parameter, so `SantasList` has no way to charge the price it publishes even if
it tried. Stated rule **SL-6** — `buyPresent` *"trades `2e18` of SantaToken for an NFT"*is
contradicted by the implementation, and the declared constant is the protocol's own evidence of the
intended value.
The unit economics compound the error: `collectPresent` grants exactly `1e18` per `EXTRA_NICE` user
(`:162` → `SantaToken.mint`). Under the documented price one grant buys half a present. As
implemented, one grant buys a whole one.

Risk


### Impact
Every present is sold at a **50% discount to the documented price**, and the protocol issues **100%
more NFTs** than its token supply was designed to support. In the PoC six `EXTRA_NICE` holders
produce a 6e18 supply; the design sells 3 presents for that, the implementation sells 6.
No individual user is robbed and no attacker gains an edge unavailable to everyone else — the
discount is uniform. What is lost is the protocol's scarcity model and the economic relationship
between the reward grant and the purchase price. Rated Medium on that basis: a broken economic
parameter with deterministic effect and no direct fund loss.

Proof of Concept

**Proof of Concept** — `test/Claude/F07_WrongBurnAmount.t.sol`
```bash
forge test --match-test test_F07 -vvv
```
The buyer is marked `NAUGHTY` on both lists, so purchasing is his only legitimate route to a present
this isolates the pricing path from the collect path.
```
[PASS] test_F07_TokenSupplyBuysTwiceTheIntendedPresents()
PURCHASED_PRESENT_COST declared (wei): 2000000000000000000
effective price charged per NFT (wei): 1000000000000000000
SANTA supply (wei): 6000000000000000000
presents the design sells for that: 3
presents actually sold: 6
excess NFTs issued: 3
overissuance (%): 100
discount to the documented price (%): 50
[PASS] test_F07_OneGrantBuysAWholePresentInsteadOfHalf()
SANTA per EXTRA_NICE grant (wei): 1000000000000000000
SANTA the docs require per NFT (wei): 2000000000000000000
presents one grant SHOULD buy: 0 (half a present)
presents one grant DOES buy: 1
```

Recommended Mitigation

The fix spans both contracts, because the token cannot currently express a price:
```diff
// 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
function buyPresent(address presentReceiver) external {
- i_santaToken.burn(presentReceiver);
+ i_santaToken.burn(msg.sender, PURCHASED_PRESENT_COST);
_safeMint(presentReceiver, s_tokenCounter++);
}
```
Updates

Lead Judging Commences

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