Puppy Raffle

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

Refund Creates “Ghost Players” That Break Prize Accounting and Raffle Integrity

Root + Impact

Description

Normal behavior:

The raffle tracks participants using the players array.
Each address in this array represents a valid ticket holder and contributes exactly entranceFee to the prize pool.

When a participant calls refund(), their ticket should be removed such that:

  • they are no longer eligible to win

  • they no longer contribute to prize or fee calculations

Issue
The refund() function does not remove the refunded player from the players array.
Instead, it replaces the player’s address with address(0):

function refund(uint256 playerIndex) public {
address playerAddress = players[playerIndex];
require(playerAddress == msg.sender, "PuppyRaffle: Only the player can refund");
require(playerAddress != address(0), "PuppyRaffle: Player already refunded");
payable(msg.sender).sendValue(entranceFee);
players[playerIndex] = address(0); // @> refunded player remains counted in players.length
}

However, the protocol continues to rely on players.length as the source of truth for:

  • prize pool calculation

  • protocol fee calculation

  • winner selection range

function selectWinner() external {
// @> players.length assumed to represent active players
uint256 totalAmountCollected = players.length * entranceFee;
uint256 prizePool = (totalAmountCollected * 80) / 100;
uint256 fee = (totalAmountCollected * 20) / 100;
uint256 winnerIndex =
uint256(keccak256(abi.encodePacked(msg.sender, block.timestamp, block.difficulty)))
% players.length;
address winner = players[winnerIndex]; // @> may resolve to address(0)
}

As a result, inactive refunded players (“ghost players”) are still counted as active participants, breaking the raffle’s economic and logical integrity.

Risk

Likelihood:

  • Reason 1: Refunds are a documented and intended feature of the protocol.

  • Reason 2: Any participant can refund at any time before winner selection.

Impact:

  • Impact 1: Prize pool and fee calculations become incorrect, overstating the amount of ETH collected.

  • Impact 2: The raffle completes successfully without reverting, silently violating fairness guarantees.

Proof of Concept

1. 10 players enter the raffle.
2. 6 players call `refund()`.
3. The `players` array becomes:
[A, B, C, D, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0]
4. `players.length == 10`
5. Prize pool and fee calculations assume 10 paid entries.
6. Random winner index lands on an empty slot.

Result:

  • ETH prize is sent to address(0) or NFT is minted to address(0)

  • Funds and NFTs are permanently lost

  • No revert, no recovery path

Recommended Mitigation

Ensure refunded players are fully removed from raffle accounting.

Instead of relying on players.length, track only active participants and update this value on entry and refund.

- uint256 totalAmountCollected = players.length * entranceFee;
+ uint256 totalAmountCollected = activePlayers * entranceFee;

Update activePlayers when users enter or refund to prevent inactive players from affecting prize and fee calculations.

Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 1 day 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!