The PuppyRaffle::enterRaffle()
, PuppyRaffle::getActivePlayerId
, and PuppyRaffle::_isActivePlayer()
methods loop over the players
array. The players
array does not have an upper bound on its length. Each time a player joins the raffle by calling enterRaffle()
, the array is looped over twice to check for duplicates before adding the player(s) to the array. As the number of players increases, the gas cost for entering, getting the player's ID, and whether they are active becomes more costly and can result in a gas cost that surpasses the block gas limit. This will mean that the transaction will revert leaving the contract's core functionalities (entering the raffle, getting the player ID, and whether they are active) in a state of denial of service (DoS).
In enterRaffle()
on line 86-90, there are two loops over the players[]
array when checking that the addresses are not duplicated:
In getActivePlayer()
on line 111-115, the players
array is looped over to check if the address provided as argument matches any of the addresses in the players
array and returns the index of the player if it does:
In _isActivePlayer
, on line 173-180 the players
array is looped over to check if the caller address matches any of the addresses in the players
array:
All these functions loop over the players
array. As there are no restrictions on the number of players in the lottery, if the players
array surpasses a certain threshold length, the gas limit will be breached leading to a state of DoS for the current lottery.
When the players
array increases past the threshold length, there will be a DoS when calling the following functions:
enterRaffle()
getActivePlayerIndex()
_isActivePlayer()
Prospective players will not be able to enter the raffle or retrieve their index, required to be able to call refund()
. This is a core functionality of the contract but this state of DoS will only exist for the duration of a single raffle, therefore this is a medium-severity vulnerability.
The following test demonstrates that as the number of players
increases, the gas cost to call enterRaffle()
to enter a single player into the lottery increases.
This test passes if the cost for the eighth player is higher than for the first player.
Running this test yields the following output:
As observed above, the gas cost for the first player to enter was 126616
whereas the gas cost for the eighth player was 131240
.
As the number of players
increases, the gas cost will increase and eventually surpass the gas limit resulting in a state of DoS as expected.
Removing unbounded array issues can be achieved in a few ways:
Limit the number of players who can join a raffle.
Modify the logic to avoid looping over unbounded arrays.
One way to achieve this is by storing a mapping of address
to bool
to determine if a player is active or not. _isActivePlayer()
can be removed and enterRaffle()
modified:
To refund()
, the activityStatus
will be set to false
for the refunding address.
Since getActivePlayerIndex()
is only used externally so that the returned value can be used to call refund()
, getActivePlayerIndex()
can be removed and refund()
modified to take an address:
When drawing the winner, the players
activity status can be checked, and only allow active players to be drawn as the winner
.
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.