The README says "buyPresent: A function that trades 2e18 of SantaToken for an NFT". The contract declares PURCHASED_PRESENT_COST = 2e18 under the comment "The cost of santa tokens for naughty people to buy presents". One EXTRA_NICE reward is 1e18, so a purchased present should cost two rewards.
PURCHASED_PRESENT_COST is never referenced. buyPresent calls SantaToken.burn(address), which takes no amount and always burns a hardcoded 1e18, so every purchase costs half the specified price.
This is independent of whose balance is burned. Changing the burn source to msg.sender (a separate submission) still charges 1e18, because burn has no amount parameter.
Likelihood:
It occurs on every buyPresent call by any holder, with no precondition.
Integrators and users who read the public PURCHASED_PRESENT_COST() see 2e18 on every call, while every purchase charges 1e18.
Impact:
Presents sell at 50% of the specified price.
One EXTRA_NICE reward of 1e18 buys a present, although at the documented price it should not.
In the PoC, 2e18, the documented price of one present, buys two. NFT issuance per SantaToken is double the design.
The public constant misreports the price. No user funds are lost by this defect alone; the protocol's only token sink is mispriced on every call.
alice and bob are checked EXTRA_NICE twice and collect after Christmas, so each holds 1 NFT and 1e18 SantaToken.
test_baseline_documentedPriceExceedsOneReward: PURCHASED_PRESENT_COST() is 2e18, one reward is 1e18, and debiting 2e18 from alice's 1e18 under the token's checked arithmetic reverts.
test_buyPresent_chargesHalfTheDocumentedPrice: alice holds exactly 1e18 and calls buyPresent(alice). The call succeeds and emits Transfer(alice, 0x0, 1e18). alice is charged 1e18, total supply falls by 1e18, which is exactly half of PURCHASED_PRESENT_COST, and alice gains an NFT.
test_twoE18_buysTwoPresentsInsteadOfOne: holding 2e18, alice buys two presents. A third purchase reverts.
test_burnAmountHardcodedIndependentOfCaller: called by SantasList, SantaToken.burn(alice) removes exactly 1e18. Fixing the burn source therefore would not fix the price.
File test/PoC_SL-06.t.sol:
Run from the repository root:
Result:
Give burn an amount and pass the declared price. This diff changes only the amount. The burn source should also become msg.sender, which is the separate buyPresent burn-source submission.
## 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(); } ```
The contest is live. Earn rewards by submitting a finding.
Submissions are being reviewed by our AI judge. Results will be available in a few minutes.
View all submissionsThe contest is complete and the rewards are being distributed.