In PuppyRaffle.sol, the withdrawFees() function attempts to ensure that no active raffle players exist before withdrawing protocol revenue by enforcing a strict balance check:
Solidity
However, relying on strict equality (==) with address(this).balance is an unsafe design pattern. An attacker can forcibly send Ether to the PuppyRaffle contract using selfdestruct() from a temporary contract, bypassing all contract functions.
Once the contract's balance is artificially inflated by even 1 wei, address(this).balance becomes permanently strictly greater than totalFees. Because there is no mechanism to extract excess ETH, this statement will fail indefinitely, causing withdrawFees() to revert on every future attempt regardless of active players.Risk
Likelihood: Medium
Regular users cannot trigger this bug accidentally since the contract lacks payable functions. However, an attacker can easily execute this attack at a negligible cost (1 wei + minimal transaction gas for a contract with selfdestruct).
Impact: High
Protocol fees are permanently frozen inside the contract balance. The feeAddress recipient will never be able to collect accumulated revenue.
[PASS] test_MishandalingOfEthWithdrawRefund() (gas: 373993)
Logs:
Total Fees: 800000000000000000
PuppyRaffle balance: 800000000000000000
Total Fees: 800000000000000000
PuppyRaffle balance: 1800000000000000000
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 50.35ms (11.67ms CPU time)
Remove the strict equality check with address(this).balance.
Ensure the contract checks if its current balance is at least equal to totalFees, or simply rely on tracking totalFees separately without strict balance assertions.
## 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.