The variable totalFees is responsible for accumulating fees for the owner across different raffles, but since it's not protected from overflowing, it will reset to 0 after accumulating approximately 18.45 ether.
When a user calls PuppyRaffle::selectWinner, the function will choose a winner, calculate the total entrance fees and then cut 20% of them and send them to feeAddress, which we can consider as the owner of the raffle contract. The other 80% goes to the winner.
The 20% cut is added to an uint64 totalFees variable, which accumulated the 20% fee from all raffles since last time withdrawFees() was called.
Since totalFees is an uint64, it means it can hold a maximum of about 18.45 ether. Addition of extra fees will cause totalFees to reset to 0 and start over.
This means that from the 93rd participant registering, totalFees will reset and start over, reducing the fees to ~0.35 (this is also assuming we ignore the unsafe casting of fee, which is another issue).
This overflow cycle will continue as more players join, but anyway the 20% cut for the owner is capped at ~18.45 ether, and may be as low as 0 ether.
This vulnerability means a big loss in fee payments for the owner. Also, it doesn't require to craft any special attack. This overflow will occur once enough players join, which is a pretty small number of only 93.
Here is a PoC showing the value of totalFees before and after an overflow:
Foundry
Use larger type. The contract actually uses an uint256 for the fee variable which accumulates the 20% cut for a single raffle, while totalFees accumulates across multiple raffles, yet it has a smaller data type. It doesn't make much sense, so use uint256 totalFees.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.