Description
The protocol declares PURCHASED_PRESENT_COST = 2e18 as the cost in SantaTokens for calling buyPresent, and EXTRA_NICE
users receive SantaTokens as their reward for being on the nice list.
PURCHASED_PRESENT_COST is never referenced in any executable code path. SantaToken.burn hardcodes 1e18 regardless of
the constant, meaning the actual cost is 1 token (not 2), the constant is dead code, and the protocol's own
documentation contradicts its on-chain behavior. EXTRA_NICE users receive exactly 1e18 from mint — enough for one
purchase — but only because the burn also happens to be 1e18, not because the constant is used.
// SantasList.sol
// @> Declared as 2e18 but referenced nowhere in logic
uint256 public constant PURCHASED_PRESENT_COST = 2e18;
function buyPresent(address presentReceiver) external {
// @> No reference to PURCHASED_PRESENT_COST anywhere in this function
i_santaToken.burn(presentReceiver);
_mintAndIncrement();
}
// SantaToken.sol
function burn(address from) external {
if (msg.sender != i_santasList) revert SantaToken__NotSantasList();
// @> Hardcoded 1e18 — half of PURCHASED_PRESENT_COST, never reads the constant
_burn(from, 1e18);
}
function mint(address to) external {
if (msg.sender != i_santasList) revert SantaToken__NotSantasList();
// @> Also hardcoded 1e18 — if cost were corrected to 2e18 without updating mint,
// @> EXTRA_NICE users would never have enough tokens to buy a present
_mint(to, 1e18);
}
Risk
Likelihood:
Every buyPresent call burns 1e18 tokens instead of 2e18 — the discrepancy is active in 100% of calls from the moment
of deployment
Any front-end or integration reading PURCHASED_PRESENT_COST to display cost or set approvals will mislead users with
the wrong amount
Impact:
Presents cost half the intended price, inflating NFT supply beyond the protocol's economic design
If the constant were ever enforced in an upgrade, EXTRA_NICE users would be permanently locked out of buyPresent since
their 1e18 mint allocation would be insufficient
Proof of Concept
function testPurchasedPresentCostNeverEnforced() 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() + 1);
vm.prank(user);
santasList.collectPresent();
// User has 1e18 — PURCHASED_PRESENT_COST says they need 2e18
assertEq(santaToken.balanceOf(user), 1e18);
assertEq(santasList.PURCHASED_PRESENT_COST(), 2e18);
// buyPresent succeeds anyway — only 1e18 is burned
vm.prank(user);
santasList.buyPresent(user);
assertEq(santaToken.balanceOf(user), 0);
assertEq(santasList.balanceOf(user), 2); // second NFT at half the documented price
}
Recommended Mitigation
Pass the amount through the constant and align the mint so EXTRA_NICE users receive enough for one purchase:
// SantaToken.sol
function mint(address to) external {
function mint(address to, uint256 amount) external {
if (msg.sender != i_santasList) revert SantaToken__NotSantasList();
_mint(to, 1e18);
_mint(to, amount);
}
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 — collectPresent EXTRA_NICE branch
i_santaToken.mint(msg.sender);
i_santaToken.mint(msg.sender, PURCHASED_PRESENT_COST);
// SantasList.sol — buyPresent (after applying fix for finding 3)
i_santaToken.burn(msg.sender);
i_santaToken.burn(msg.sender, PURCHASED_PRESENT_COST);
## 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.