Santa's List

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

Unused constant "PURCHASED_PRESENT_COST"

Root + Impact

Description

  • The contract declares a constant PURCHASED_PRESENT_COST = 2e18 which appears to define the cost in SantaTokens for purchasing additional NFT presents. This constant should be used when burning tokens from users who want to buy presents through the buyPresent() function.

  • The constant is never actually used in the codebase. Instead, the SantaToken.burn() function has a hardcoded value of 1e18 tokens to burn, which is half of the declared cost. This creates a mismatch between the documented intent (2e18 cost) and the actual implementation (1e18 cost), indicating either incomplete implementation or a configuration error.

// In SantasList.sol
uint256 public constant CHRISTMAS_2023_BLOCK_TIME = 1_703_480_381;
@> uint256 public constant PURCHASED_PRESENT_COST = 2e18; // Declared but never used
function buyPresent(address presentReceiver) external {
@> i_santaToken.burn(presentReceiver); // Doesn't reference PURCHASED_PRESENT_COST
_mintAndIncrement();
}
// In SantaToken.sol
function burn(address from) external {
if (msg.sender != i_santasList) {
revert SantaToken__NotSantasList();
}
@> _burn(from, 1e18); // Hardcoded 1e18 instead of using PURCHASED_PRESENT_COST (2e18)
}

Risk

Likelihood:

  • Every call to buyPresent() burns exactly 1e18 tokens regardless of the declared PURCHASED_PRESENT_COST constant, making the discrepancy occur on every purchase transaction

  • Users and auditors reading the contract see the PURCHASED_PRESENT_COST = 2e18 constant and reasonably expect presents to cost 2e18 tokens, creating a gap between documentation and actual behavior

Impact:

  • Presents cost half the intended price (1e18 instead of 2e18), potentially breaking the economic model and allowing users to purchase twice as many NFTs as intended with the same token supply

  • Code maintainability suffers as future developers may attempt to change the constant expecting it to affect the burn amount, only to discover the hardcoded value requires changes in the separate SantaToken contract

  • Creates confusion and reduces trust in the codebase when the declared constant doesn't match actual implementation, indicating lack of attention to detail or incomplete development

Proof of Concept

// Declared constant says 2e18
uint256 constant = santasList.PURCHASED_PRESENT_COST();
console.log(constant); // 2e18
// Actual burn amount is hardcoded to 1e18
// In SantaToken.sol: _burn(from, 1e18);
// User with 1e18 tokens can purchase, even though constant says 2e18 required
santasList.buyPresent(user); // Success, only burns 1e18

Recommended Mitigation

// Option 1: Use the constant in SantaToken.sol (Recommended)
+
+ // Add interface to access the constant
+ interface ISantasList {
+ function PURCHASED_PRESENT_COST() external view returns (uint256);
+ }
contract SantaToken is ERC20 {
error SantaToken__NotSantasList();
uint8 private constant DECIMALS = 18;
address private immutable i_santasList;
function burn(address from) external {
if (msg.sender != i_santasList) {
revert SantaToken__NotSantasList();
}
- _burn(from, 1e18);
+ uint256 cost = ISantasList(i_santasList).PURCHASED_PRESENT_COST();
+ _burn(from, cost);
}
}
// Option 2: If 1e18 is the correct cost, remove the unused constant
// In SantasList.sol
- uint256 public constant PURCHASED_PRESENT_COST = 2e18;
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 14 days 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!