Santa's List

AI First Flight #3
Beginner FriendlyFoundry
EXP
View results
Submission Details
Impact: low
Likelihood: low
Invalid

`PURCHASED_PRESENT_COST` Constant Is Never Used — `mint` and `burn` Hardcode Half the Documented Price

Description

SantasList declares a public constant that documents the intended cost of buying a present:

// SantasList.sol:89
uint256 public constant PURCHASED_PRESENT_COST = 2e18;

SantaToken implements the two functions that enforce that cost — mint (issued to EXTRA_NICE users as their reward) and burn (collected when anyone calls buyPresent). Both hardcode 1e18 instead of referencing the constant:

// SantaToken.sol:21-33
function mint(address to) external {
if (msg.sender != i_santasList) revert SantaToken__NotSantasList();
_mint(to, 1e18); // ← should be PURCHASED_PRESENT_COST (2e18)
}
function burn(address from) external {
if (msg.sender != i_santasList) revert SantaToken__NotSantasList();
_burn(from, 1e18); // ← should be PURCHASED_PRESENT_COST (2e18)
}

PURCHASED_PRESENT_COST is never referenced anywhere in the codebase. It is dead code. The actual price enforced on-chain is 1e18 — exactly half the documented amount.


Impact

A — Presents are bought at half the documented price.

buyPresent calls i_santaToken.burn(presentReceiver), which burns 1e18. The protocol documentation implies the cost should be 2e18. Any caller can buy a present NFT by consuming only 1e18 SantaTokens.

B — An EXTRA_NICE user can immediately spend their entire reward on a present.

collectPresent mints 1e18 SantaTokens to EXTRA_NICE users. Because burn also consumes exactly 1e18, a user's entire reward covers exactly one buyPresent call in the same session. If the documented 2e18 cost were enforced, a single collection reward would be insufficient — users would need to accumulate tokens across two collection cycles before affording a present.

C — The public constant misleads integrators and auditors.

PURCHASED_PRESENT_COST is public, meaning external contracts and front-ends can query it. Any system that reads this constant to determine the token cost will see 2e18 but observe 1e18 being burned on-chain — a semantic inconsistency that can cause off-chain accounting errors.


Proof of Concept

Added to test/unit/SantasListTest.t.sol:

function testPurchasedPresentCostMismatch() 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.startPrank(user);
santasList.collectPresent();
// EXTRA_NICE user receives 1e18, not PURCHASED_PRESENT_COST (2e18)
uint256 tokenBalance = santaToken.balanceOf(user);
assertEq(tokenBalance, 1e18, "Mint amount is 1e18, not PURCHASED_PRESENT_COST (2e18)");
// User can immediately buy a present with their 1e18 reward,
// because burn() also uses 1e18, not 2e18
santaToken.approve(address(santasList), tokenBalance);
santasList.buyPresent(user);
assertEq(santaToken.balanceOf(user), 0, "Only 1e18 burned, not 2e18");
assertEq(santasList.balanceOf(user), 2, "User holds two NFTs after paying half the documented price");
// Confirm the constant is dead code
assertEq(santasList.PURCHASED_PRESENT_COST(), 2e18, "Constant says 2e18 but code enforces 1e18");
vm.stopPrank();
}

Result: An EXTRA_NICE user collects 1e18 tokens and immediately uses them to buy a second NFT, paying 1e18 — half of PURCHASED_PRESENT_COST. The constant is never consulted.


Recommendation

Determine the intended cost and make the code and constant agree. Two valid resolutions:

Option A — Enforce the documented 2e18 cost (presents cost more, rewards cover half):

// SantaToken.sol
function mint(address to) external {
if (msg.sender != i_santasList) revert SantaToken__NotSantasList();
_mint(to, SantasList(i_santasList).PURCHASED_PRESENT_COST());
}
function burn(address from) external {
if (msg.sender != i_santasList) revert SantaToken__NotSantasList();
_burn(from, SantasList(i_santasList).PURCHASED_PRESENT_COST());
}

Option B — Correct the constant to match the implemented 1e18 cost:

// SantasList.sol
uint256 public constant PURCHASED_PRESENT_COST = 1e18;

Option A preserves the documented economic design (presents cost 2e18) and makes the constant live. Option B is simpler if 1e18 was always the intended price. Either way, hardcoded magic numbers in mint and burn should be replaced with the constant so future changes to the price only require editing one location.


Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 2 hours ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!