Puppy Raffle

AI First Flight #1
Beginner FriendlyFoundrySolidityNFT
EXP
View results
Submission Details
Severity: high
Valid

M-4: Front-running refund before selectWinner() corrupts prize pool accounting

Description

Severity: Medium

The selectWinner() function at PuppyRaffle.sol:131-132 calculates totalAmountCollected based on the current players.length:

uint256 totalAmountCollected = players.length * entranceFee;
uint256 prizePool = (totalAmountCollected * 80) / 100;
uint256 fee = (totalAmountCollected * 20) / 100;

However, refund() sets a player's slot to address(0) without reducing players.length. This means totalAmountCollected includes refunded players who already withdrew their entrance fee. The contract calculates prizePool and fee based on ETH it no longer holds.

A malicious actor can watch the mempool for a selectWinner() transaction and front-run it with a refund() call. This causes totalAmountCollected to overcount the actual ETH balance, and the fee deducted from the pool includes ETH that was already refunded. The result is that protocol fees (totalFees) are inflated beyond the actual ETH available, and the winner receives less than expected or the transaction reverts.

Proof of Concept

function testFrontrunRefundStealsFromPrizePool() public {
// 4 players enter at 1 ETH each — contract holds 4 ETH
address[] memory entrants = new address[](4);
entrants[0] = playerOne;
entrants[1] = playerTwo;
entrants[2] = playerThree;
entrants[3] = playerFour;
puppyRaffle.enterRaffle{value: entranceFee * 4}(entrants);
vm.warp(block.timestamp + duration + 1);
// Player front-runs selectWinner with refund — gets 1 ETH back
// Contract now holds 3 ETH
vm.prank(playerOne);
puppyRaffle.refund(0);
// selectWinner() still calculates: totalAmountCollected = 4 * 1 ETH = 4 ETH
// prizePool = 3.2 ETH, fee = 0.8 ETH
// But contract only has 3 ETH — math is wrong
// totalFees += 0.8 ETH of fees from ETH that doesn't exist
puppyRaffle.selectWinner();
}

Risk

  • Impact: Medium — protocol fees are inflated beyond actual balance. Over time this creates an accounting mismatch that can lock funds or cause withdrawFees() to fail.

  • Likelihood: Medium — requires monitoring the mempool and front-running, but is straightforward for any MEV bot.

Recommended Mitigation

Track the actual number of active (non-refunded) players separately at PuppyRaffle.sol:131:

uint256 public activePlayerCount;
function enterRaffle(address[] memory newPlayers) public payable {
// ... existing logic ...
activePlayerCount += newPlayers.length;
}
function refund(uint256 playerIndex) public {
// ... existing logic ...
activePlayerCount--;
}
function selectWinner() external {
uint256 totalAmountCollected = activePlayerCount * entranceFee;
// ... rest of function
}

This ensures totalAmountCollected reflects the actual ETH held by the contract.

Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 4 days ago
Submission Judgement Published
Validated
Assigned finding tags:

[H-07] Potential Front-Running Attack in `selectWinner` and `refund` Functions

## Description Malicious actors can watch any `selectWinner` transaction and front-run it with a transaction that calls `refund` to avoid participating in the raffle if he/she is not the winner or even to steal the owner fess utilizing the current calculation of the `totalAmountCollected` variable in the `selectWinner` function. ## Vulnerability Details The PuppyRaffle smart contract is vulnerable to potential front-running attacks in both the `selectWinner` and `refund` functions. Malicious actors can monitor transactions involving the `selectWinner` function and front-run them by submitting a transaction calling the `refund` function just before or after the `selectWinner` transaction. This malicious behavior can be leveraged to exploit the raffle in various ways. Specifically, attackers can: 1. **Attempt to Avoid Participation:** If the attacker is not the intended winner, they can call the `refund` function before the legitimate winner is selected. This refunds the attacker's entrance fee, allowing them to avoid participating in the raffle and effectively nullifying their loss. 2. **Steal Owner Fees:** Exploiting the current calculation of the `totalAmountCollected` variable in the `selectWinner` function, attackers can execute a front-running transaction, manipulating the prize pool to favor themselves. This can result in the attacker claiming more funds than intended, potentially stealing the owner's fees (`totalFees`). ## Impact - **Medium:** The potential front-running attack might lead to undesirable outcomes, including avoiding participation in the raffle and stealing the owner's fees (`totalFees`). These actions can result in significant financial losses and unfair manipulation of the contract. ## Recommendations To mitigate the potential front-running attacks and enhance the security of the PuppyRaffle contract, consider the following recommendations: - Implement Transaction ordering dependence (TOD) to prevent front-running attacks. This can be achieved by applying time locks in which participants can only call the `refund` function after a certain period of time has passed since the `selectWinner` function was called. This would prevent attackers from front-running the `selectWinner` function and calling the `refund` function before the legitimate winner is selected.

Support

FAQs

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

Give us feedback!