Beginner FriendlyFoundry
100 EXP
View results
Submission Details
Severity: high
Valid

Anyone can use the `SantasList::buyPresent` function to buy himself a present and burn a `SantaToken` from the `presentReceiver` address

Summary

The logic in SantasList::buyPresent function is wrong and it burns a SantaToken from the presentReceiver instead of the caller (msg.sender) and the new NFT is present for the caller not for the presentReceiver. Also, the amount of SantaToken::burn function is hardcoded to 1e18 that may be in conflict with the intended behaviour of the protocol, if the NAUGHTY or all people should pay 2e18 of SantaToken for an NFT.

Vulnerability Details

In the notice for SantasList::buyPresent is written: @notice Buy a present for someone else. This should only be callable by anyone with SantaTokens. and in the documentation for this functions is written: A function that trades 2e18 of SantaToken for an NFT. This function can be called by anyone. The people which have SantaToken are these that have status EXTRA_NICE. The function buyPresent can be called by anyone and does not check if the caller has SantaToken or he is EXTRA_NICE or NAUGHTY. Additionally, the function burns a SantaToken from the presentReceiver instead of the caller (msg.sender) and the new NFT is present for the caller not for the presentReceiver. This is likely not the intended behaviour, as the intention seems to be to allow people to buy presents for others at the cost of their own tokens and maybe the NAUGHTY people to pay more because ot these lines which contradict to the docs:

// The cost of santa tokens for naughty people to buy presents
uint256 public constant PURCHASED_PRESENT_COST = 2e18;
function buyPresent(address presentReceiver) external {
@> i_santaToken.burn(presentReceiver);
@> _mintAndIncrement();
}

Also, in SantaToken::burn function the amount in _burn function is hardcoded to 1e18. Therefore, the cost for the present will be for all people 1e18. If the comments in the codebase are the intended behaviour the NAUGHTY people should pay more than the other - 2e18. If the documentation is the intended behaviout all people should pay 2e18.

function burn(address from) external {
if (msg.sender != i_santasList) {
revert SantaToken__NotSantasList();
}
//@audit the cost for naughty people is not 1e18
@> _burn(from, 1e18);
}

Impact

The logic in SantasList::buyPresent is wrong. The caller of the function should pay with his SantaToken and the input parameter presentReceiver should receive NFT. But the implementation of the function lets the presentReceiver to pay for the present for the caller of the function. That allows people without SantaToken to call the function and buy theirself NFT using the address of person with SantaToken as presentReceiver.

Let's consider the following scenario:

  1. Bob is a naughty person and has no SantaToken.

  2. Alice is a extra nice person and has SantaToken.

  3. Bob calls SantasList::buyPresent function with input argument Alice's address.

  4. The buyPresent function is executed and Alice pays with her SantaToken for the Bob's NFT.

The following test function demonstrates this scenario:

function testBuyPresentFromSomeoneElse() public {
vm.startPrank(santa);
santasList.checkList(alice, SantasList.Status.EXTRA_NICE);
santasList.checkTwice(alice, SantasList.Status.EXTRA_NICE);
vm.stopPrank();
vm.warp(santasList.CHRISTMAS_2023_BLOCK_TIME() + 1);
santaToken.approve(address(santasList), 1e18);
vm.startPrank(alice);
santasList.collectPresent();
vm.startPrank(bob);
// Bob calls the function with alice address as receivePresent input argument
santasList.buyPresent(alice);
assertEq(santasList.balanceOf(alice), 1);
// Alice has no more santaTokens
assertEq(santaToken.balanceOf(alice), 0);
// Bob receives present payed from the alice
assertEq(santasList.balanceOf(bob), 1);
vm.stopPrank();
}

You can add the testBuyPresentFromSomeoneElse function to the file SantasListTest.t.sol and execute it using the following command: forge test --match-test testBuyPresentFromSomeoneElse.

Also, the hardoceded amount in SantaToken::burn function allows the NAUGHTY people to pay less than the intended by the code behaviour.

Tools Used

VS Code, Foundry

Recommendations

If the intended behaviour is someone with SantaToken to buy NFT for someone else, add a check if the caller has SantaToken and rewrite the function to burn SantaToken from the caller address and add the NFT to the receivePresent address. Also, rewrite the documentation and comments in the codebase. In the current version there are several places with contradictions and that leads to confusion.
Additionally, if we consider that the NAUGHTY person has SantaToken and should pay more than the other add a check that the caller is NAUGHTY and change the function SantaToken::burn to accept amount of 2e18, not to be hardcoded. For the other people it should be 1e18. If all people should pay 2e18 as the documentation said, the amount in SantaToken::burn function is wrong again and should be modified.

Updates

Lead Judging Commences

inallhonesty Lead Judge over 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

buyPresent should use msg.sender

Current implementation allows a malicious actor to burn someone else's tokens as the burn function doesn't actually check for approvals.

Price is not enforced in buyPresent

This line indicates that the intended cost of presents for naughty people should be 2e18: https://github.com/Cyfrin/2023-11-Santas-List/blob/6627a6387adab89ae2ba2e82b38296723261c08a/src/SantasList.sol#L87 PURCHASE_PRESENT_COST should be implemented to enforce the cost of presents.

Support

FAQs

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