The intended behavior is to refund the entrance fee to the caller of the function whose address is stored in the players array. The refund()
function logic facilitates the refund of the entrance fee to each player.
In addition, the selectWinner()
function is designed to provide the winner with an NFT and 80% of the total collected amount, while the remaining 20% is allocated to the totalFee
variable for the protocol's use.
The refund()
function employs the following array manipulation:
Issues arise when players are refunded. Their addresses in the players array are set to the zero address, but this doesn't affect the length of the players array. The erroneous array length is then used in the calculations of totalAmountCollected
, prizePool
, and totalFees
, as shown in the code below:
This causes at least 2 critical issues in the protocol.
There are two critical issues that arise from the aforementioned problems:
When only one player is refunded, and their index is set to the zero address, the length of the players array remains one more than the actual number of players.
Assuming there were initially four players in the raffle, and one of them is refunded, the actual number of players is three, but the length of the players array remains at four. Consequently, the totalAmountCollected
will be miscalculated due to the erroneous length of the players array.
This miscalculation affects both the prize pool and the totalFees
. As a result, the selectWinner()
function is reverted with the message "PuppyRaffle: Failed to send the prize pool to the winner"
due to insufficient funds. Additionally, the totalFees
amount is impacted due to the incorrect calculation of the players array length.
The totalAmountCollected
will be miscalculated due to wrong length of the players array:
Based on the provided details and calculations, the prize pool for the winner is expected to be 3.2 ether. However, due to the refunding of one of the players, only 3 ether remains in the contract. Consequently, when the selectWinner()
function attempts to execute, it will be reverted with the message "PuppyRaffle: Failed to send prize pool to winner"
due to the insufficient funds.
Furthermore, the totalFees
variable is also impacted as a result of the miscalculation arising from the manipulation of the players array. This can potentially lead to further issues within the smart contract.
At this point, if a new player is added by callingenterRaffle()
it will be possible to call selectWinner()
and the prizepool will be send to the winner but the prizepool
amount will be wrong.
The length of the players array is now calculated as 5 and according to the formula prizepool
should be 4 ether. Since the balance of the contract is 4 there is enough ether to send to the winner.
On the other hand, the totalFees
amount supposed to be 1 ether but since the whole 4 ether is sent to the winner the actual balance of the protocol is 0.
Considering the recalculated length of the players array as 5 and the corresponding prizePool
calculation of 4 ether, the balance of the contract would indeed be sufficient to send to the winner in this scenario.
However, it's crucial to note that the totalFees
amount is expected to be 1 ether, as determined by the calculation. Due to the entire 4 ether being sent to the winner, the actual balance of the protocol would be reduced to 0, resulting in a discrepancy between the expected totalFees
and the actual balance.
The second critical issue occurs when two players are refunded, and their indices are both set to the zero address, resulting in the inability for new players to enter the raffle. This is due to the prevention mechanism in the enterRaffle()
function, which checks for duplicate entries in the players array. With two zero addresses present, the function reverts, blocking new entries.
The logic in the enterRaffle()
function prevents new players to enter the raffle. There are 2 zero addresses in the players array. PoC in Test3
in the scenario where there were initially 10 or more players in the players array, the selectWinner()
function could indeed be called, enabling the winner to receive the NFT and the prize pool, but with inaccurate calculations due to the previously highlighted critical issues.
While the winner may still be able to claim the NFT and the prize pool, these discrepancies can have broader implications for the fairness and transparency of the raffle system, as the actual allocation of funds may not align with the expected distribution based on the flawed calculations. Therefore, rectifying these issues is crucial to ensuring the integrity and reliability of the raffle protocol.
If initially there were 10 or more players in the players array, selectWinner()
function can be called and winner can get the NFT and the prizepool although the calculations will be wrong due to detailed explanations provided in the previous critical case.
In cases where the number of initial players is less than ten and two players are refunded, the selectWinner()
function reverts due to a shortage of funds compared to the protocol's balance.
Assume there were 9 players initially and 2 of them refunded. The balance of the contract will be 7 ether but the prizepool will be 7.2:
selectWinner()
reverts due to above explanation.
withdrawFees()
also reverts because:
The only action can be to call refund()
by all players.
Below are the tests used as PoCs:
Manual code review
Foundry
Implement an if loop to verify the existence of the player in the players array before executing operations. This additional validation can help prevent issues stemming from the manipulation of the players array.
Funds are locked to no one. If someone gets the refund issue, they also got this issue. IMPACT: High Likelihood: High
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.