PuppyRaffle::withdrawFees
checks if any players have entered using the assumption that if the contract balance is equal to the accumulated totalFees
then no players are active. If this assumption passes, the feeAddress
can withdraw their fees. If any funds have been locked in the contract either due to a bug or by being force-sent to the contract, the call to withdrawFees()
will revert as the contract balance is higher than totalFees
, even if no players have entered and the assumption breaks. This results in a state of DoS and the fees are locked in the contract.
withdrawFees()
checks if the contract balance is greater than totalFees
, on line 158, using the following require statement:
This check is used to determine if there are any currently active players. The assumption is that the contract balance will be equal to totalFees
if no players are active however this assumption can be broken. The balance of the contract can be different from totalFees
for numerous other reasons including:
If an overflow has occurred.
Precision loss leading to the remainder of the funds not being transferred (see issues: "Precision loss when calculating fee
due to unsafe math" and "Precision Loss when calculating prizePool
due to unsafe math" for more details).
If an attacking contract self-destructs and the funds are sent to the PuppyRaffle
contract.
A reentrancy attack has reduced the contract balance (see issue: "Reentracy attack when calling PuppyRaffle::refund
means an attacker can steal entire contract balance")
If the contract balance is higher than expected due to any of the above reasons, withdrawFunds()
will be in a permanent state of DoS and the feeAddress
will not be able to withdraw the totalFees
.
withdrawFunds()
will revert if called if the contract balance is not equal to the totalFees
(detailed above). This is a permanent state of DoS and the feeAddress
will not be able to withdraw the totalFees
.
An attacker can maliciously send funds to the contract by adding a fallback function to an attacking contract which calls selfdistruct()
. When the fallback function is called, sends it's funds to the PuppyRaffle
contract. The attacker would add a balance to the contract before self-destructing and stop the feeAddress
from withdrawing their funds. Since there is a reentrancy vulnerability in refund()
, where the attacker can steal the entire contract balance, the attacker can prevent the feeAddress
from withdrawing and then steal the entirety of totalFees
that has accumulated as well as all of the prizePool
(see issue: "Reentracy attack when calling PuppyRaffle::refund
means an attacker can steal entire contract balance").
Since the feeAddress
being able to take a 20% portion of the raffle winnings is a core capability of this contract, this is a high-severity vulnerability.
The following contract implements a fallback function which, when called, calls selfdistruct()
and sends its balance to PuppyRaffle
:
This test initially runs a raffle to accumulate a totalFee
balance to attempt to withdraw. Funds are then sent to the attacking contract and attack()
is called to send the funds to the PuppyRaffle
contract. withdrawFees()
is called and the test passes if the function call reverts and the contract balance is non-zero meaning that the feeAddress
cannot withdraw their fees.
When run, the test yields the following output:
As observed, the test passes meaning that despite no players being active, withdrawFunds()
reverts and is therefore in a state of DoS as expected.
The PuppyRaffle
contract balance is higher than totalFees
as the Attacking
contract has successfully forced its funds into the PuppyRaffle
contract.
To mitigate this issue, allow the feeAddress
to withdraw the funds at any time, even if players are active:
Alternatively, send the funds to the feeAddress
during the selectWinner()
call:
Note that the case where feeAddress
cannot receive funds will need to be handled to avoid a DoS calling selectWinner()
.
Root cause: bad RNG Impact: manipulate 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.