Users buying too many tickets will DoS them and the protocol if they are the winner due to OOG
Summary
WinnablesTicket stores nft ownership by setting the first minted nft id ownership to the user minting and all the next minted nfts remain as 0. This means it always costs the same to mint, but the ownerOf() function becomes much more expensive, to the point where it may cause OOG errors. In this case, the user is able to buy tickets via WinnablesTicketManager::buyTickets(), the draw is made in WinnablesTicketManager::drawWinner() and the chainlink request is fulfilled with the winner in WinnablesTicketManager::fulfillRandomWords(). However, in WinnablesTicketManager::propagateWinner(), it reverts due to OOG when calling WinnablesTicket::ownerOf().
Root Cause
In WinnablesTicket:97-99, it may run out of gas if enough tickets were bought.
Internal pre-conditions
- Admin sets max holdings and max tickets to a number of at least 4000 or simillar.
External pre-conditions
None.
Attack Path
- Users by tickets via WinnablesTicketManager::buyTickets()`
- The draw is made in
WinnablesTicketManager::drawWinner() - The chainlink request is fulfilled with the winner in `WinnablesTicketManager::fulfillRandomWords()]
- In
WinnablesTicketManager::propagateWinner(), it reverts due to OOG when callingWinnablesTicket::ownerOf().
Impact
DoSed winner and protocol ETH from the raffle.
PoC
The following test costs 9 million gas, more than the block limit on Avalanche.
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {Test, console} from "forge-std/Test.sol";
import {WinnablesPrizeManager, LinkTokenInterface} from "contracts/WinnablesPrizeManager.sol";
import {WinnablesTicket} from "contracts/WinnablesTicket.sol";
import {IRouterClient} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IRouterClient.sol";
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
contract WinnablesPrizeManagerTest is Test {
WinnablesPrizeManager public winnablesPrizeManager;
WinnablesTicket public winnablesTicket;
LinkTokenInterface public link;
IRouterClient public router;
address public owner;
function setUp() public {
vm.createSelectFork(vm.envString("ETHEREUM_RPC_URL"));
link = LinkTokenInterface(0x514910771AF9Ca656af840dff83E8264EcF986CA);
router = IRouterClient(0x80226fc0Ee2b096224EeAc085Bb9a8cba1146f7D);
owner = makeAddr("owner");
vm.prank(owner);
winnablesPrizeManager = new WinnablesPrizeManager(address(link), address(router));
vm.prank(owner);
winnablesTicket = new WinnablesTicket();
}
function test_POC_OwnerOfDos() public {
vm.prank(owner);
winnablesTicket.setRole(owner, 1, true);
vm.prank(owner);
winnablesTicket.mint(owner, 1, 4000);
winnablesTicket.ownerOf(1, 3999);
}
}Mitigation
Set a reasonable cap for max holdings.