Puppy Raffle

AI First Flight #1
Beginner FriendlyFoundrySolidityNFT
EXP
View results
Submission Details
Impact: medium
Likelihood: medium
Invalid

Medium 2 form Function Withdraw Fee

Root + Impact

Description

withdrawFees() gates the fee payout on an exact whole-balance equality check (line 158):

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}("");

The contract has no receive() or fallback(), but that does not stop forced ETH: selfdestruct credits the target address regardless of whether it can receive — including from a constructor, which still works post-EIP-6780. The attack prerequisites are minimal: anyone deploys a contract that selfdestructs 1 wei into the raffle.

Once balance exceeds totalFees by even 1 wei, the == check can never pass again — there is no way to remove the extra wei — so withdrawFees() reverts forever and the fee recipient's funds are locked permanently. This is the well-known strict-balance-equality anti-pattern.

Risk

Likelihood: Medium — the griefing costs 1 wei and needs no interaction with other users, but the payoff is denial of someone else's fee rather than direct theft.

Impact: Medium — the fee recipient is permanently denied their funds; the locked fee has no recovery path.

Proof of Concept

The Foundry test below demonstrates the permanent bricking:

contract ForceFeeder {
constructor(address target) payable {
selfdestruct(payable(target)); // force-feeds ETH, no receive() needed
}
}
​
function test_PoC_L2_WithdrawFeesForceFeed() public {
// ... 4 players enter, warp, selectWinner() ...
assertEq(address(puppyRaffle).balance, 0.8 ether); // == totalFees, withdrawable
​
new ForceFeeder{value: 1 wei}(address(puppyRaffle)); // +1 wei, no entry point needed
​
vm.expectRevert("PuppyRaffle: There are currently players active!");
puppyRaffle.withdrawFees(); // bricked forever: balance != totalFees
}

Test assertions and measured result (forge test --match-test test_PoC_L2_WithdrawFeesForceFeed):

Recommended Mitigation

Drop the strict equality gate and pay out exactly the accounted fee amount. The extra force-fed wei then sits in the contract harmlessly instead of bricking the withdrawal — the fee recipient can always claim what the books say they are owed, regardless of stray balance.

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}("");
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 1 hour ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!