Beginner FriendlyFoundryNFT
100 EXP
View results
Submission Details
Severity: high
Valid

`totalFees` is a `uint64` allowing for integer overflow and loss of fees

Summary

totalFees is stored as uint64 which can lead to an integer overflow and loss of funds.

Vulnerability Details

type(uint64).max is 18446744073709551615 which is the maximum value a uint64 can store. Since fees are denominated in Wei, this value translates to approximately 18.5 ether. Given that the used compiler version of pragma solidity ^0.7.6 has no overflow checks, this leads to an overflow of totalFees once the fees exceed this limit, which leads to a irreversible loss of fees. Additionally, given that the withdrawFees function checks whether the contract balance is equal to totalFees, this will revert the withdrawFees function.

// We do some storage packing to save gas
address public feeAddress;
@> uint64 public totalFees = 0;
// rest of code of `PuppyRaffle`
function selectWinner() external {
require(block.timestamp >= raffleStartTime + raffleDuration, "PuppyRaffle: Raffle not over");
require(players.length >= 4, "PuppyRaffle: Need at least 4 players");
uint256 winnerIndex =
uint256(keccak256(abi.encodePacked(msg.sender, block.timestamp, block.difficulty))) % players.length;
address winner = players[winnerIndex];
uint256 totalAmountCollected = players.length * entranceFee;
uint256 prizePool = (totalAmountCollected * 80) / 100;
uint256 fee = (totalAmountCollected * 20) / 100;
@> totalFees = totalFees + uint64(fee);
uint256 tokenId = totalSupply();
// We use a different RNG calculate from the winnerIndex to determine rarity
uint256 rarity = uint256(keccak256(abi.encodePacked(msg.sender, block.difficulty))) % 100;
if (rarity <= COMMON_RARITY) {
tokenIdToRarity[tokenId] = COMMON_RARITY;
} else if (rarity <= COMMON_RARITY + RARE_RARITY) {
tokenIdToRarity[tokenId] = RARE_RARITY;
} else {
tokenIdToRarity[tokenId] = LEGENDARY_RARITY;
}
delete players;
raffleStartTime = block.timestamp;
previousWinner = winner;
(bool success,) = winner.call{value: prizePool}("");
require(success, "PuppyRaffle: Failed to send prize pool to winner");
_safeMint(winner, tokenId);
}
/// @notice this function will withdraw the fees to the feeAddress
function withdrawFees() external {
@> require(address(this).balance == uint256(totalFees), "PuppyRaffle: There are currently players active!");
uint256 feesToWithdraw = totalFees;
totalFees = 0;
(bool success,) = feeAddress.call{value: feesToWithdraw}("");
require(success, "PuppyRaffle: Failed to withdraw fees");
}

Impact

Change the code in PuppyRaffleTest.t.sol and run forge test --mt testUint64FeeCastingCanLeadToOverflow and forge test --mt testUint64FeeCastingCanLeadToLossOfFees. The first test fails while the second passes.

PuppyRaffle puppyRaffle;
ReentrancyTest reentrancyTest;
- uint256 entranceFee = 1e18;
+ uint256 entranceFee = 100e18;
address playerOne = address(1);
address playerTwo = address(2);
address playerThree = address(3);
address playerFour = address(4);
address feeAddress = address(99);
uint256 duration = 1 days;
// rest of code of `PuppyRaffleTest`
+ function testUint64FeeCastingCanLeadToOverflow() public playersEntered {
+ uint256 expectedPrizeAmount = ((entranceFee * 4) * 20) / 100;
+ vm.warp(block.timestamp + duration + 1);
+ vm.roll(block.number + 1);
+ puppyRaffle.selectWinner();
+ assertEq(puppyRaffle.totalFees(), expectedPrizeAmount);
+ }
+ function testUint64FeeCastingCanLeadToLossOfFees() public playersEntered {
+ vm.warp(block.timestamp + duration + 1);
+ vm.roll(block.number + 1);
+ puppyRaffle.selectWinner();
+ vm.expectRevert();
+ puppyRaffle.withdrawFees();
+ }

Tools Used

  • Foundry

Recommendations

Store totalFees as uint256. Additionally, consider importing SafeMath.sol from OpenZeppelin. Alternatively, use at least pragma solidity 0.8.0 as compiler as it comes with built-in overflow protection.

// We do some storage packing to save gas
address public feeAddress;
- uint64 public totalFees = 0;
+ uint256 public totalFees;
// rest of code of `PuppyRaffle`
function selectWinner() external {
require(block.timestamp >= raffleStartTime + raffleDuration, "PuppyRaffle: Raffle not over");
require(players.length >= 4, "PuppyRaffle: Need at least 4 players");
uint256 winnerIndex =
uint256(keccak256(abi.encodePacked(msg.sender, block.timestamp, block.difficulty))) % players.length;
address winner = players[winnerIndex];
uint256 totalAmountCollected = players.length * entranceFee;
uint256 prizePool = (totalAmountCollected * 80) / 100;
uint256 fee = (totalAmountCollected * 20) / 100;
- totalFees = totalFees + uint64(fee);
+ totalFees += fee;
uint256 tokenId = totalSupply();
// We use a different RNG calculate from the winnerIndex to determine rarity
uint256 rarity = uint256(keccak256(abi.encodePacked(msg.sender, block.difficulty))) % 100;
if (rarity <= COMMON_RARITY) {
tokenIdToRarity[tokenId] = COMMON_RARITY;
} else if (rarity <= COMMON_RARITY + RARE_RARITY) {
tokenIdToRarity[tokenId] = RARE_RARITY;
} else {
tokenIdToRarity[tokenId] = LEGENDARY_RARITY;
}
delete players;
raffleStartTime = block.timestamp;
previousWinner = winner;
(bool success,) = winner.call{value: prizePool}("");
require(success, "PuppyRaffle: Failed to send prize pool to winner");
_safeMint(winner, tokenId);
}
Updates

Lead Judging Commences

Hamiltonite Lead Judge over 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

overflow-uint64

Support

FAQs

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