Puppy Raffle

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

# H-02 Refund tombstones desync accounting and can brick `selectWinner()`

Description

  • Normal behavior: draw payout should be derived from actual active entrants and available pot, so selectWinner() can always settle completed rounds.

  • Issue: refunded players are replaced with address(0) but remain in players.length. selectWinner() derives totalAmountCollected from players.length * entranceFee, which can exceed real balance after refunds and make winner payout revert.

function refund(uint256 playerIndex) public {
// ...
payable(msg.sender).sendValue(entranceFee);
players[playerIndex] = address(0); // @> tombstone entry remains counted in length
}
function selectWinner() external {
// ...
uint256 totalAmountCollected = players.length * entranceFee; // @> stale nominal accounting
uint256 prizePool = (totalAmountCollected * 80) / 100;
(bool success,) = winner.call{value: prizePool}(""); // @> can exceed actual contract balance
require(success, "PuppyRaffle: Failed to send prize pool to winner");
}

Risk

Likelihood:

  • Accounting desynchronization appears as soon as any participant successfully refunds.

  • Settlement attempts continue to use array length rather than active-player/pot accounting.

Impact:

  • Draw settlement can revert indefinitely, creating a liveness DoS for raffle completion.

  • Legitimate participants are unable to receive timely payout and protocol operation is disrupted.

Proof of Concept

function testPoC_RefundAccountingCanBrickSelectWinner() public {
vm.deal(address(0x201), ENTRANCE_FEE);
vm.deal(address(0x202), ENTRANCE_FEE);
vm.deal(address(0x203), ENTRANCE_FEE);
vm.deal(address(0x204), ENTRANCE_FEE);
_enter(address(0x201));
_enter(address(0x202));
_enter(address(0x203));
_enter(address(0x204));
uint256 p1Index = raffle.getActivePlayerIndex(address(0x201));
vm.prank(address(0x201));
raffle.refund(p1Index);
vm.warp(block.timestamp + DURATION + 1);
vm.roll(block.number + 1);
vm.expectRevert("PuppyRaffle: Failed to send prize pool to winner");
raffle.selectWinner();
}

Recommended Mitigation

- uint256 totalAmountCollected = players.length * entranceFee;
+ uint256 totalAmountCollected = activePlayerCount * entranceFee;
+ // or use an explicit pot variable updated on enter/refund
Updates

Lead Judging Commences

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