Santa's List

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

F-5: `buyPresent()` Mints NFT to Caller Instead of Present Receiver

Root + Impact

Description

  • The private function _mintAndIncrement() at line 180-182 always mints the
    NFT to msg.sender via _safeMint(msg.sender, s_tokenCounter++).

  • When called from buyPresent(presentReceiver) at line 174, the NFT should
    go to presentReceiver (the person receiving the gift), but instead it goes
    to msg.sender (the buyer). The present receiver never receives the NFT
    that was supposedly bought for them.

// SantasList.sol:180-182
function _mintAndIncrement() private {
_safeMint(msg.sender, s_tokenCounter++);
// @> Always mints to msg.sender — presentReceiver is never the recipient
}

Risk

Likelihood:

  • Every single call to buyPresent() mints the NFT to the wrong address.
    There is no code path where the receiver gets the NFT.

Impact:

  • The present-buying feature is entirely non-functional. Combined with F-4,
    the receiver pays (their tokens are burned) but gets nothing, while the
    buyer pays nothing and gets the NFT. This is a complete inversion of the
    intended behavior.

Proof of Concept

  • A buyer calls buyPresent(receiver) to gift an NFT to the receiver.
    After execution, we check the NFT balances: the buyer has 1 NFT and the
    receiver has 0, proving the NFT was minted to the wrong address. The
    receiver — who was supposed to receive the gift — gets nothing.

function test_F5_mintsToCallerNotReceiver() public {
address buyer = makeAddr("buyer");
address receiver = makeAddr("receiver");
// Setup: receiver has 1e18 SantaTokens (burned per F-4 bug)
vm.prank(buyer);
santasList.buyPresent(receiver);
assertEq(santasList.balanceOf(buyer), 1); // Buyer got the NFT
assertEq(santasList.balanceOf(receiver), 0); // Receiver got nothing
}

Recommended Mitigation

  • Modify _mintAndIncrement to accept a recipient parameter so it can mint
    to different addresses depending on the caller. Update collectPresent() to
    pass msg.sender and buyPresent() to pass presentReceiver, ensuring
    each function mints to the correct destination.

- function _mintAndIncrement() private {
- _safeMint(msg.sender, s_tokenCounter++);
+ function _mintAndIncrement(address recipient) private {
+ _safeMint(recipient, s_tokenCounter++);
}

Update callers:

// In collectPresent() (both NICE and EXTRA_NICE paths):
- _mintAndIncrement();
+ _mintAndIncrement(msg.sender);
// In buyPresent():
- _mintAndIncrement();
+ _mintAndIncrement(presentReceiver);
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 1 hour ago
Submission Judgement Published
Validated
Assigned finding tags:

[L-03] `buyPresent()` function mints to wrong address

## Description The external function `buyPresent()` mints a Present NFT to the `msg.sender` address. This provides opportunity for a malicious actor to mint extra NFTs for themselves. \*This finding is related to other finding titled "`buyPresent()` function burns from wrong address". ## Vulnerability Details The external function `buyPresent()` calls `_mintAndIncrement()`, which mints to the `msg.sender` address, not to the intended `presentReceiver` address, as shown below: ``` function buyPresent(address presentReceiver) external { // ... _mintAndIncrement(); } function _mintAndIncrement() private { // incorrectly mints NFT to msg.sender _safeMint(msg.sender, s_tokenCounter++); } ``` This introduces a new attack vector, since a related bug (as mentioned in summary) causes the incorrect burning of the SANTA ERC20 token immediately before the aforementioned minting bug, shown below: ``` function buyPresent(address presentReceiver) external { // incorrectly burns from `presentReceiver` i_santaToken.burn(presentReceiver); // incorrectly mints to `msg.sender` _mintAndIncrement(); } ``` With the existence of these two related bugs, a user with the SANTA ERC20 token may bypass the rule of only owning one NFT (as implied by line 151 balance check and line 152 revert message below). ``` if (balanceOf(msg.sender) > 0) { revert SantasList__AlreadyCollected(); } ``` To bypass this rule, an `EXTRA_NICE` user, aka malicious actor, may do the following steps 1. call the `collectPresent()` function, acquiring both a SANTA ERC20 token and an NFT. 2. transfer the SANTA ERC20 token to a different "friend" address 3. call the `buyPresent` function, passing the "friend" address as the parameter, burning the ERC20 token in the "friend" wallet, and minting an NFT for themself. The below code block is a PoC (written as a forge test) demonstrating the above steps in action, ultimately allowing the attacker to hold 2 NFTs. ``` function test_buyPresentDoubleMint() public { SantasList.Status extraNice = SantasList.Status.EXTRA_NICE; address santa = makeAddr("santa"); address thief = makeAddr("thief"); address friend = makeAddr("friend"); vm.warp(santasList.CHRISTMAS_2023_BLOCK_TIME() + 1); vm.startPrank(santa); santasList.checkList(thief, extraNice); santasList.checkTwice(thief, extraNice); vm.stopPrank(); vm.startPrank(thief); santasList.collectPresent(); assert(santasList.balanceOf(thief) == 1); assert(santaToken.balanceOf(thief) == 1e18); santaToken.transfer(friend, 1e18); santasList.buyPresent(friend); assert(santasList.balanceOf(thief) == 2); } ``` ## Impact A malicious actor may mint more than one NFT for themselves. ## Recommendations - pass `msg.sender` as a param into the `i_santaToken.burn()` call - add an `address to` param to the private function `_mintAndIncrement()`, passing `to` to `_safeMint`, instead of `msg.sender` by default. See corrected code recommendations below: ``` function buyPresent(address presentReceiver) external { i_santaToken.burn(msg.sender); _mintAndIncrement(presentReceiver); } function _mintAndIncrement(address to) private { _safeMint(to, s_tokenCounter++); } ``` By adding the `address to` param to `_mintAndIncrement`, be sure to pass the correct param value to all other calls to `_mintAndIncrement` in the contract. (this should often be `msg.sender`.) Example of `_mintAndIncrement()` calls in `collectPresent()` function: ``` function collectPresent() external { // ... other checks if (s_theListCheckedOnce[msg.sender] == Status.NICE && s_theListCheckedTwice[msg.sender] == Status.NICE) { _mintAndIncrement(msg.sender); // add `to` param here return; } else if ( s_theListCheckedOnce[msg.sender] == Status.EXTRA_NICE && s_theListCheckedTwice[msg.sender] == Status.EXTRA_NICE ) { _mintAndIncrement(msg.sender); // add `to` param here i_santaToken.mint(msg.sender); return; } revert SantasList__NotNice(); } ```

Support

FAQs

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

Give us feedback!