Each selectWinner call should add 20% of the round's entrance fees to totalFees. Between rounds the contract balance should then equal totalFees, so withdrawFees can send the full accumulated fee amount to feeAddress.
totalFees is a uint64, which holds at most 2^64 - 1 wei (about 18.4467 ETH). L134 adds each round's uint256 fee to it through an explicit uint64(fee) cast, under pragma ^0.7.6 with no SafeMath. A single-round fee of 2^64 wei or more is therefore silently truncated, and a running sum past 2^64 silently wraps. The ETH stays in the contract, so the balance exceeds totalFees by a multiple of 2^64 wei (exactly 2^64 in the PoC). From then on the strict equality check in withdrawFees (L158) fails in normal operation, and no other function can send the fees to feeAddress.
Likelihood:
Single round: the overflow happens when one round collects about 92.23 ETH or more in entrance fees, because the 20% fee then reaches 2^64 wei. At the deploy script's entranceFee of 1 ETH, that is 93 entrants. enterRaffle is permissionless, and the PoC enters 93 distinct addresses in one call for about 5.8M gas, well under the block gas limit.
Across rounds: the counter wraps when fees left unwithdrawn pass 2^64 wei in total. The PoC shows this after 24 four-player rounds at 1 ETH (0.8 ETH fee each) with no withdrawFees call in between. Anyone can call withdrawFees between rounds, so this path also needs the fees to go unwithdrawn for 23 or more rounds.
Both paths need much more volume than the 4-player minimum, either a large raffle or one deliberately large entry. A successful raffle could plausibly reach that volume, but it is not guaranteed, so likelihood is Medium.
Impact:
The whole fee balance becomes unwithdrawable. In the PoC the contract holds 18.6 ETH of fees but records totalFees = 153,255,926,290,448,384 wei (about 0.153 ETH). withdrawFees reverts with "There are currently players active!" even though no players are active, and feeAddress receives nothing.
The loss covers later rounds too. Each later round adds its fee to both the balance and totalFees, so the 2^64 gap stays and every future protocol fee joins the locked amount. In the PoC, two more rounds add 1.6 ETH and withdrawFees still reverts.
Nothing can recover the funds. The contract has no sweep or rescue function. The owner's only related function, changeFeeAddress, does not affect the equality check: the PoC calls it and withdrawFees still reverts.
The PoC (test/PoC_PR-05.t.sol) deploys PuppyRaffle with entranceFee = 1 ETH and a 1-day duration. A baseline test shows that a 4-player round records 0.8 ETH and withdraws correctly. The single-round test enters 93 players, calls selectWinner and asserts that totalFees == 153255926290448384 (18.6e18 - 2^64) while the contract holds 18.6 ETH. It then asserts that withdrawFees reverts both before and after changeFeeAddress, and that the 2^64 gap remains after two more rounds. The cumulative test asserts that the counter wraps to 753255926290448384 after 24 four-player rounds while 19.2 ETH is held; run all three with forge test --match-path test/PoC_PR-05.t.sol -vv.
File test/PoC_PR-05.t.sol; run FOUNDRY_FFI=false forge test --match-path test/PoC_PR-05.t.sol -vv.
## Description ## Vulnerability Details The type conversion from uint256 to uint64 in the expression 'totalFees = totalFees + uint64(fee)' may potentially cause overflow problems if the 'fee' exceeds the maximum value that a uint64 can accommodate (2^64 - 1). ```javascript totalFees = totalFees + uint64(fee); ``` ## POC <details> <summary>Code</summary> ```javascript function testOverflow() public { uint256 initialBalance = address(puppyRaffle).balance; // This value is greater than the maximum value a uint64 can hold uint256 fee = 2**64; // Send ether to the contract (bool success, ) = address(puppyRaffle).call{value: fee}(""); assertTrue(success); uint256 finalBalance = address(puppyRaffle).balance; // Check if the contract's balance increased by the expected amount assertEq(finalBalance, initialBalance + fee); } ``` </details> In this test, assertTrue(success) checks if the ether was successfully sent to the contract, and assertEq(finalBalance, initialBalance + fee) checks if the contract's balance increased by the expected amount. If the balance didn't increase as expected, it could indicate an overflow. ## Impact This could consequently lead to inaccuracies in the computation of 'totalFees'. ## Recommendations To resolve this issue, you should change the data type of `totalFees` from `uint64` to `uint256`. This will prevent any potential overflow issues, as `uint256` can accommodate much larger numbers than `uint64`. Here's how you can do it: Change the declaration of `totalFees` from: ```javascript uint64 public totalFees = 0; ``` to: ```jasvascript uint256 public totalFees = 0; ``` And update the line where `totalFees` is updated from: ```diff - totalFees = totalFees + uint64(fee); + totalFees = totalFees + fee; ``` This way, you ensure that the data types are consistent and can handle the range of values that your contract may encounter.
The contest is live. Earn rewards by submitting a finding.
Submissions are being reviewed by our AI judge. Results will be available in a few minutes.
View all submissionsThe contest is complete and the rewards are being distributed.