Santa's List

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

PURCHASED_PRESENT_COST Constant Defined But Never Used in Contract Logic

Root + Impact

Description

  • The contract defines a public constant PURCHASED_PRESENT_COST = 2e18 suggesting that buying a present should cost 2 SantaTokens. However, this constant is never referenced anywhere in the contract logic.

  • The buyPresent function calls i_santaToken.burn() which has a hardcoded value of 1e18 tokens. This creates a significant discrepancy between the documented intent and actual behavior, potentially confusing users and integrators who rely on the public constant for pricing information.

// Root cause in the codebase with @> marks to highlight the relevant section
uint256 public constant PURCHASED_PRESENT_COST = 2e18; // Suggests 2 tokens required
function buyPresent(address presentReceiver) external {
@> i_santaToken.burn(presentReceiver); // Calls burn with hardcoded 1e18
_mintAndIncrement();
}
// In SantaToken.sol
function burn(address from) external {
if (msg.sender != i_santasList) {
revert SantaToken__NotSantasList();
}
@> _burn(from, 1e18); // Hardcoded to burn only 1 token, ignoring the constant
}

Risk

Likelihood: (High)

  • Reason 1 // The constant is publicly visible and suggests a 2 token cost

  • Reason 2 // Any external integration reading this constant will get incorrect pricing

  • Reason 3 // The mismatch between constant and implementation is guaranteed to occur

Impact: (Low):

  • Documentation and code are out of sync, reducing code clarity

  • External protocols integrating with SantasList may calculate incorrect costs

  • Users checking the constant before buying will be misinformed about the actual price

  • Wasted deployment gas for storing an unused constant in the contract bytecode

Proof of Concept

Demonstrates that the PURCHASED_PRESENT_COST constant indicates a 2 token requirement, but users with only 1 token can successfully purchase presents because the constant is never enforced in the actual burn logic.

function test_PurchasedPresentCostNotUsed() public {
// Constant publicly advertises 2e18 token cost
assertEq(santasList.PURCHASED_PRESENT_COST(), 2e18);
// Setup: User gets EXTRA_NICE and collects their 1e18 tokens
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 exactly 1e18, which is LESS than PURCHASED_PRESENT_COST
assertEq(santaToken.balanceOf(user), 1e18);
assertLt(santaToken.balanceOf(user), santasList.PURCHASED_PRESENT_COST());
// Despite having insufficient tokens per the constant, purchase succeeds
vm.prank(user);
santasList.buyPresent(user);
// User got the present with only half the advertised cost
assertEq(santasList.balanceOf(user), 2);
}

Recommended Mitigation

Either enforce the constant in the burn logic or remove the unused constant to prevent confusion. The recommended approach is to use the constant as originally intended.

// Option 1: Update SantaToken to accept amount parameter and use the constant
// In 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);
}
// In SantasList.sol
function buyPresent(address presentReceiver) external {
- i_santaToken.burn(presentReceiver);
+ i_santaToken.burn(msg.sender, PURCHASED_PRESENT_COST);
_mintAndIncrement();
}
// Option 2: Remove the misleading constant if 1e18 is the intended cost
- uint256 public constant PURCHASED_PRESENT_COST = 2e18;
Updates

Lead Judging Commences

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