The refund mechanism in case of cancelled raffle has bad accounting which leads to funds stuck.
At refundPlayers()
funds previously accounted as _lockedETH
are not substracted from the state when withdrawn from the system.
Does not apply.
Does not apply.
- Admin creates a raffle.
- Anyone participate via
buyTickets()
andmsg.value
sent is locked from withdrawals at_lockedETH
. (lets saymsg.value = 10
) - Raffle ends up being cancelled for any valid reasons, like minimum tickets not reached.
- Client calls
refundPlayers()
to get the funds back. He gets them, balance of contract == 0. - Yet the
_lockedETH
value is the same. - Another raffle is created.
- A bucnh of people call
buyTickets()
and sendmsg.value
to the contract. Increase by 50 lets say. Total value earned by the raffle to the protocol is 50.TotalRaised==50, _lockedETH==10+50==60, address(this).balance==50
. - Raffle ends and
propagateRaffleWinner()
is called, unlocking raised funds._lockedETH==60-50==10
. - Admin calls
withdrawETH()
, andbalance=address(this).balance - _lockedETH
is calculated.balance==50-10==40
. 40 is withdrawn yet the value raised is 50 and all of it should be withdrawable.
Every refunded amount will end up stucking in the contract that amount and actually the loss is taken by the protocol from future raffles revenue.
_lockedETH
is only altered in 3 parts of the code: buyTickets()
(here), propagateRaffleWinner()
(here), withdrawETH()
(here).
Follow the attack path calls and see the links to easily see the execution path is correct.
I will add a PoC executable code if I got enough time later.
Substract at refundPlayers()
the amounts refunded from the _lockedETH
state.