PuppyRaffle::withdrawFees allows anyone to permanently block the function by force-sending ETHDescription: PuppyRaffle::withdrawFees function uses a strict equality between address(this).balance and uint256(totalFees). Any user (whether malicious or not) can force-send ETH to the contract breaking this strict equality and permanently blocking the function. Sending more ETH cannot fix the block: address(this).balance can only increase, so once it exceeds totalFees the equality can never hold again.
Impact: The contract remains without any possibility to withdraw the fees. The rest of the contract functionality will still work but accumulated fees will remain permanently blocked in the contract.
Proof of Concept: In the following test we can see how, after an external contract fires selfdestruct (which resides in SelfDestructiveContract::destroy function) with PuppyRaffle address as the receiver, the PuppyRaffle::withdrawFees function fires the revert from its first require causing a permanent block.
Place the following test into PuppyRaffle.t.sol.
Note that since EIP-6780 (Cancun), selfdestruct no longer deletes the contract unless called in the same transaction as its creation, but it still forwards the balance, which is all that is needed here.
Recommended Mitigation: There are a few recommendations:
Consider changing the strict equality == to checking the players length which correctly enforces that no active player remains in the raffle and removes the ETH mishandling issue.
This solves the ETH mishandling but the force-sent ETH will still be blocked in the contract because totalFees variable does not account for it. If this ETH needs to be withdrawn there are two main changes to apply:
Note: In the code above, totalFees variable is removed as it is not needed anymore, we use address(this).balance instead.
or
The code above should be placed in PuppyRaffle contract to withdraw the exceeded fees.
The last snippet mitigates the bug without changing PuppyRaffle::withdrawFees function. PuppyRaffle::withdrawFees will still get blocked but firing PuppyRaffle::removeExceededETH unlocks it as the exceeded ETH is removed. The downside of this solution is that the block is still possible and every time it happens PuppyRaffle::removeExceededETH needs to be called.
As a conclusion, address(this).balance should never be used in a strict equality. A contract's balance can always be increased by external parties without executing any of its code. This might break some contract functionality as in the PuppyRaffle::withdrawFees case.
## Description An attacker can slightly change the eth balance of the contract to break the `withdrawFees` function. ## Vulnerability Details The withdraw function contains the following check: ``` require(address(this).balance == uint256(totalFees), "PuppyRaffle: There are currently players active!"); ``` Using `address(this).balance` in this way invites attackers to modify said balance in order to make this check fail. This can be easily done as follows: Add this contract above `PuppyRaffleTest`: ``` contract Kill { constructor (address target) payable { address payable _target = payable(target); selfdestruct(_target); } } ``` Modify `setUp` as follows: ``` function setUp() public { puppyRaffle = new PuppyRaffle( entranceFee, feeAddress, duration ); address mAlice = makeAddr("mAlice"); vm.deal(mAlice, 1 ether); vm.startPrank(mAlice); Kill kill = new Kill{value: 0.01 ether}(address(puppyRaffle)); vm.stopPrank(); } ``` Now run `testWithdrawFees()` - ` forge test --mt testWithdrawFees` to get: ``` Running 1 test for test/PuppyRaffleTest.t.sol:PuppyRaffleTest [FAIL. Reason: PuppyRaffle: There are currently players active!] testWithdrawFees() (gas: 361718) Test result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 3.40ms ``` Any small amount sent over by a self destructing contract will make `withdrawFees` function unusable, leaving no other way of taking the fees out of the contract. ## Impact All fees that weren't withdrawn and all future fees are stuck in the contract. ## Recommendations Avoid using `address(this).balance` in this way as it can easily be changed by an attacker. Properly track the `totalFees` and withdraw it. ```diff function withdrawFees() external { -- require(address(this).balance == uint256(totalFees), "PuppyRaffle: There are currently players active!"); uint256 feesToWithdraw = totalFees; totalFees = 0; (bool success,) = feeAddress.call{value: feesToWithdraw}(""); require(success, "PuppyRaffle: Failed to withdraw fees"); } ```
The contest is live. Earn rewards by submitting a finding.
Submissions are being reviewed by our AI judge. Results will be available in a few minutes.
View all submissionsThe contest is complete and the rewards are being distributed.