Closed code423n4 closed 2 years ago
withdraw()
is a single external function with one purpose. It does not matter if it reverts or not.
Refundable voting is a nicety. The comments also show this was deliberate: * Voting takes place regardless of refund success.
They do not want voting to revert even if the refund fails.
Lines of code
https://github.com/code-423n4/2022-08-nounsdao/blob/main/contracts/governance/NounsDAOLogicV2.sol#L789 https://github.com/code-423n4/2022-08-nounsdao/blob/main/contracts/governance/NounsDAOLogicV2.sol#L983
Vulnerability details
Impact
It's considered a best practice to always check the return of the transaction when sending Ether with
.call
, since it's possible for a tx failure due to external factors out of the contract control.Currently, the contract emits an event with the result but it doesn't throw in case of failure.
I would recommend to ensure the transaction throws in case of failure instead of emitting an event with the boolean result of false.
Proof of Concept
There's two functions sending ether:
_withdraw()
and_refundGas()
.For the case of the external function
withdraw()
, failing to send Ether will be a successful transaction with theWithdraw
event having thesent
value asfalse
.And in the case of
refundGas()
, the function is called with this flow:castRefundableVote()
->castRefundableVoteInternal()
->_refundGas()
.Even if the function fails to send ether (refund the gas)storage variables will still be updated (the
proposal
andreceipt
insidecastVoteInternal()
called bycastRefundableVoteInternal()
) and, similarly towithdraw()
, it will be a succesful transaction with anRefundableVote
having therefundSent
as false.Tools used
Manual review
Recommended Mitigation Steps
Check the return of
.call
inside_withdraw()
and inside_refundGas()
.Alternatively, for
_refundGas()
, if the idea is to update the storage variables even if no gas is refunded, the contract could save the gas transaction variables into the state and add a separate function used exclusively to refund the gas (decoupled fromcastRefundableVoteInternal()
).