Puppy Raffle

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

Front-running attacks in `selectWinner()` and `refund()` allow MEV extraction and griefing

Description

  • Normal: Raffle outcomes should be unpredictable and ungameable. Players should not be able to selectively exit based on pending results.

  • Bug: An attacker monitoring the mempool can see a pending selectWinner() transaction, simulate its outcome locally, and front-run with a refund() if they would lose. This gives the attacker a risk-free participation: win the raffle or get a refund, never lose.

Additionally, selectWinner() uses msg.sender as a random seed — the caller can choose their own address or use a contract to influence the outcome.

// src/PuppyRaffle.sol:128-129
uint256 winnerIndex = uint256(keccak256(abi.encodePacked(
msg.sender, //@> Caller can be chosen to influence outcome
block.timestamp, //@> Known in advance
block.difficulty //@> Known in advance
))) % players.length;

Risk

Likelihood:

  • MEV searchers actively monitor mempools for profitable opportunities

  • Front-running infrastructure (Flashbots) is readily available

  • No special tools needed — a simple script can monitor and front-run

Impact:

  • Honest players always lose — attackers have an asymmetric advantage

  • Raffle fairness is fundamentally broken

  • Protocol loses credibility and users

Proof of Concept

Attack scenario:

  1. Attacker enters raffle with N addresses

  2. Attacker monitors mempool for selectWinner() transactions

  3. On seeing selectWinner():
    a. Simulate: winnerIndex = keccak256(msg.sender, block.timestamp, block.difficulty) % players.length
    b. If players[winnerIndex] is one of attacker's addresses → let it through
    c. If not → front-run with refund() on all attacker positions

  4. Net result: attacker either wins the prize or gets a full refund. Never loses.

Recommended Mitigation

+ // Use a commit-reveal scheme or time-locked refunds
+ uint256 public selectWinnerDeadline;
+
function selectWinner() external {
+ selectWinnerDeadline = block.timestamp;
// ... existing logic ...
}
function refund(uint256 playerIndex) public {
+ require(block.timestamp > selectWinnerDeadline + 1 hours || selectWinnerDeadline == 0,
+ "Refunds locked during winner selection");
// ... existing logic ...
}

For the randomness, use Chainlink VRF instead of on-chain block data.

Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 35 minutes 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!