The withdrawFees function in PuppyRaffle.sol uses a strict equality check (address(this).balance == uint256(totalFees)) to ensure no active players exist before withdrawing fees. If any ETH is sent directly to the contract (e.g., via a standard transfer or selfdestruct), the balance exceeds totalFees, causing the check to fail permanently and locking all accumulated fees inside the contract.
The contract relies on the contract's total ETH balance to verify that all players have been paid out or refunded. However, Ethereum allows anyone to force-send ETH to any contract address without executing any code. When this happens, address(this).balance becomes greater than totalFees. The require statement in withdrawFees will then revert on every subsequent attempt, creating a permanent Denial of Service (DoS) on the fee withdrawal mechanism.
File: src/PuppyRaffle.sol (line 153)
Severity: Medium
Likelihood: Medium
Impact: Medium
❌ Fee withdrawal is permanently blocked if extra ETH is sent
❌ Accumulated protocol fees become trapped forever
❌ Owner loses access to their revenue
✅ Can be triggered accidentally or maliciously by anyone
Scenario: After a raffle completes and fees are ready to be withdrawn, an external user sends 1 wei directly to the contract. The owner then attempts to withdraw fees, but the transaction reverts.
Test Output:
What This Proves:
✅ Force-sending ETH increases contract balance
✅ Strict equality check fails
✅ Fee withdrawal is permanently DoS'd
Change the equality check to a greater-than-or-equal check, or better yet, track active players using a counter:
Why This Fixes It:
✅ >= allows withdrawal even if extra ETH was force-sent
✅ activePlayerCount completely removes reliance on contract balance
✅ Prevents permanent DoS on fee collection
SWC-132: Unchecked Return Value For Low Level Call (Related to force-send)
CWE-391: Unchecked Error Condition
Ethereum Force-Feed vulnerability patterns
## 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.