Puppy Raffle

AI First Flight #1
Beginner FriendlyFoundrySolidityNFT
EXP
View results
Submission Details
Severity: high
Valid

PuppyRaffle uses Solidity 0.7.6 which has no built-in overflow/underflow protection, allowing arithmetic in selectWinner() and enterRaffle() to silently produce wrong results

Root + Impact

Solidity versions before 0.8.0 do not revert on integer overflow or underflow — arithmetic wraps silently. PuppyRaffle.sol uses pragma solidity ^0.7.6 and performs unchecked multiplication and addition in selectWinner() and enterRaffle(), enabling prize pool miscalculation, fee miscalculation, and fee counter overflow that permanently lock accumulated fees.

Description

  • In selectWinner(), totalAmountCollected, prizePool, fee, and totalFees are computed with plain arithmetic. With enough players, players.length * entranceFee can overflow, producing a smaller-than-expected total and thus a smaller prize and fee than the contract actually holds. The totalFees accumulator is declared uint64, making overflow even more likely as fees accumulate across rounds.

// src/PuppyRaffle.sol — selectWinner()
uint256 totalAmountCollected = players.length * entranceFee; // @> overflow if players.length is large
uint256 prizePool = (totalAmountCollected * 80) / 100;
uint256 fee = (totalAmountCollected * 20) / 100;
totalFees = totalFees + uint64(fee); // @> uint64 overflow truncates accumulated fees
  • In enterRaffle(), entranceFee * newPlayers.length is used in a require check — if this overflows, the required payment wraps to a small number, allowing attackers to enter with near-zero ETH.

Risk

Likelihood:

  • Any raffle with a large number of players or enough rounds of fee accumulation will trigger this. The uint64 totalFees overflow is especially likely over time since fees accumulate across every completed raffle.

Impact:

  • Overflow in totalFees permanently locks ETH in the contract — withdrawFees() uses the underflowed value to transfer, leaving the remainder inaccessible. Overflow in totalAmountCollected miscalculates the winner's prize and the owner's fee. Overflow in entranceFee * newPlayers.length allows free or near-free raffle entry.

Proof of Concept

With a uint8 stand-in for the overflow-prone calculation, the problem is directly demonstrable. The same issue exists in production for uint64 totalFees and for large players.length:

function test_uint64FeesOverflow() public {
// Fill raffle enough times so totalFees (uint64) overflows
// uint64 max = 18446744073709551615 (~18.4 ETH at 1e18)
// After overflow, totalFees < actual ETH balance → withdrawFees transfers less than held
uint256 playersNeeded = type(uint64).max / entranceFee / 20 * 100 + 1;
// Demonstrate: cast fee to uint64 wraps
uint256 bigFee = uint256(type(uint64).max) + 1;
uint64 truncated = uint64(bigFee);
assertEq(truncated, 0); // overflows to 0
}

After overflow totalFees becomes 0 or a small number; withdrawFees() transfers that amount while the contract retains the real accumulated ETH — permanently locked.

Recommended Mitigation

Import and apply OpenZeppelin's SafeMath for all arithmetic, or upgrade the compiler to ^0.8.0 which reverts on overflow natively:

+ import "@openzeppelin/contracts/math/SafeMath.sol";
contract PuppyRaffle {
+ using SafeMath for uint256;
- uint256 totalAmountCollected = players.length * entranceFee;
+ uint256 totalAmountCollected = players.length.mul(entranceFee);
- uint256 prizePool = (totalAmountCollected * 80) / 100;
+ uint256 prizePool = totalAmountCollected.mul(80).div(100);
- uint256 fee = (totalAmountCollected * 20) / 100;
+ uint256 fee = totalAmountCollected.mul(20).div(100);
- totalFees = totalFees + uint64(fee);
+ totalFees = totalFees.add(fee); // also change totalFees to uint256
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 5 hours ago
Submission Judgement Published
Validated
Assigned finding tags:

[H-06] Overflow/Underflow vulnerabilty for any version before 0.8.0

## Description The PuppyRaffle.sol uses Solidity compiler version 0.7.6. Any Solidity version before 0.8.0 is prone to Overflow/Underflow vulnerability. Short example - a `uint8 x;` can hold 256 values (from 0 - 255). If the calculation results in `x` variable to get 260 as value, the extra part will overflow and we will end up with 5 as a result instead of the expected 260 (because 260-255 = 5). ## Vulnerability Details I have two example below to demonstrate the problem of overflow and underflow with versions before 0.8.0, and how to fix it using safemath: Without `SafeMath`: ``` function withoutSafeMath() external pure returns (uint256 fee){ uint8 totalAmountCollected = 20; fee = (totalAmountCollected * 20) / 100; return fee; } // fee: 1 // WRONG!!! ``` In the above code,`without safeMath`, 20x20 (totalAmountCollected \* 20) was 400, but 400 is beyond the limit of uint8, so after going to 255, it went back to 0 and started counting from there. So, 400-255 = 145. 145 was the result of 20x20 in this code. And after dividing it by 100, we got 1.45, which the code showed as 1. With `SafeMath`: ``` function withSafeMath() external pure returns (uint256 fee){ uint8 totalAmountCollected = 20; fee = totalAmountCollected.mul(20).div(100); return fee; } // fee: 4 // CORRECT!!!! ``` This code didnt suffer from Overflow problem. Because of the safeMath, it was able to calculate 20x20 as 400, and then divided it by 100, to get 4 as result. ## Impact Depending on the bits assigned to a variable, and depending on whether the value assigned goes above or below a certain threshold, the code could end up giving unexpected results. This unexpected OVERFLOW and UNDERFLOW will result in unexpected and wrong calculations, which in turn will result in wrong data being used and presented to the users. ## Recommendations Modify the code to include SafeMath: 1. First import SafeMath from openzeppelin: ``` import "@openzeppelin/contracts/math/SafeMath.sol"; ``` 2. then add the following line, inside PuppyRaffle Contract: ``` using SafeMath for uint256; ``` (can also add safemath for uint8, uint16, etc as per need) 3. Then modify the `require` inside `enterRaffle() function`: ```diff - require(msg.value == entranceFee * newPlayers.length, "PuppyRaffle: Must send enough to enter raffle"); + uint256 totalEntranceFee = newPlayers.length.mul(entranceFee); + require(msg.value == totalEntranceFee, "PuppyRaffle: Must send enough to enter raffle"); ``` 3. Then modify variables (`totalAmountCollected`, `prizePool`, `fee`, and `totalFees`) inside `selectWinner()` function: ```diff - uint256 totalAmountCollected = players.length * entranceFee; + uint256 totalAmountCollected = players.length.mul(entranceFee); - uint256 prizePool = (totalAmountCollected * 80) / 100; + uint256 prizePool = totalAmountCollected.mul(80).div(100); - uint256 fee = (totalAmountCollected * 20) / 100; + uint256 fee = totalAmountCollected.mul(20).div(100); - totalFees = totalFees + uint64(fee); + totalFees = totalFees.add(fee); ``` This way, the code is now safe from Overflow/Underflow vulnerabilities.

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!