Puppy Raffle

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

withdrawFees() requires address(this).balance == totalFees, which an attacker can permanently break by force-sending ETH via selfdestruct

Root + Impact

Description

  • withdrawFees() only pays out when the contract's ETH balance exactly equals the recorded totalFees, intended as a guard that no raffle is mid-flight.

  • ETH can be force-sent to any contract (e.g. selfdestruct, or a pre-computed address funded before deployment), bypassing receive/fallback. A single wei of forced ETH makes address(this).balance permanently exceed totalFees, so the strict-equality check never holds again and all fees are locked.

function withdrawFees() external {
@> require(address(this).balance == uint256(totalFees), "PuppyRaffle: There are currently players active!");
...
}

Risk

Likelihood:

  • Occurs whenever any party force-feeds ETH to the contract — a low-cost, permissionless action (1 wei plus a self-destructing helper).

Impact:

  • All accumulated protocol fees become permanently unwithdrawable, with no rescue path.

Proof of Concept

The test below runs a normal round so totalFees is set and the balance equals it, then force-sends 1 wei via a self-destructing contract. After that, address(this).balance permanently exceeds totalFees, so withdrawFees() reverts on its strict-equality check and the fees are locked.

contract ForceFeeder {
constructor(address payable target) payable {
selfdestruct(target);
}
}
function test_force_feed_bricks_withdrawFees() public {
// ... run a normal round so totalFees > 0 and balance == totalFees ...
new ForceFeeder{value: 1 wei}(payable(address(puppyRaffle)));
// now balance == totalFees + 1 wei
vm.expectRevert("PuppyRaffle: There are currently players active!");
puppyRaffle.withdrawFees();
}

Recommended Mitigation

Do not tie withdrawal to exact contract balance; track fees with internal accounting and withdraw that amount, or use >=.

- require(address(this).balance == uint256(totalFees), "PuppyRaffle: There are currently players active!");
+ // pay out exactly the internally tracked fee total, independent of contract balance
+ uint256 feesToWithdraw = totalFees;
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 22 hours 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!