Puppy Raffle

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

Missing Event for Winner Selection

Root + Impact

Description

  • * The `selectWinner()` function performs critical state changes including selecting a winner, minting an NFT, distributing prize funds, and resetting the raffle, but does not emit any event to notify off-chain systems of these changes.

    * Events are essential for off-chain monitoring, frontend updates, and building integrations. Without an event, applications must poll the contract or rely on transaction logs, which is inefficient and unreliable.

    ```solidity:125:154:src/PuppyRaffle.sol

    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;

    address winner = players[winnerIndex];

    uint256 totalAmountCollected = players.length * entranceFee;

    uint256 prizePool = (totalAmountCollected * 80) / 100;

    uint256 fee = (totalAmountCollected * 20) / 100;

    totalFees = totalFees + uint64(fee);

    uint256 tokenId = totalSupply();

    // We use a different RNG calculate from the winnerIndex to determine rarity

    uint256 rarity = uint256(keccak256(abi.encodePacked(msg.sender, block.difficulty))) % 100;

    if (rarity <= COMMON_RARITY) {

    tokenIdToRarity[tokenId] = COMMON_RARITY;

    } else if (rarity <= COMMON_RARITY + RARE_RARITY) {

    tokenIdToRarity[tokenId] = RARE_RARITY;

    } else {

    tokenIdToRarity[tokenId] = LEGENDARY_RARITY;

    }

    delete players;

    raffleStartTime = block.timestamp;

    previousWinner = winner;

    (bool success,) = winner.call{value: prizePool}("");

    require(success, "PuppyRaffle: Failed to send prize pool to winner");

    _safeMint(winner, tokenId);

    }

    ```


Risk

Likelihood:

  • * This occurs on every call to `selectWinner()` - the function always completes without emitting an event

    * Frontend applications and monitoring systems need to track winner selections

    * Integration with other contracts or services requires event-based notifications

Impact:

  • * Reduced observability - off-chain systems cannot easily track raffle outcomes

    * Poor user experience - frontends cannot react in real-time to winner selections

    * Harder to build integrations and analytics

    * Difficult to audit raffle history without parsing transaction logs

Proof of Concept

```solidity
// selectWinner() is called
puppyRaffle.selectWinner();
// No event emitted
// Frontend must poll previousWinner variable or parse transaction logs
// No real-time notification possible
```

Recommended Mitigation

```diff
// Events
event RaffleEnter(address[] newPlayers);
event RaffleRefunded(address player);
event FeeAddressChanged(address newFeeAddress);
+ event WinnerSelected(address indexed winner, uint256 indexed tokenId, uint256 prizePool, uint256 rarity, uint256 totalPlayers);
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;
address winner = players[winnerIndex];
uint256 totalAmountCollected = players.length * entranceFee;
uint256 prizePool = (totalAmountCollected * 80) / 100;
uint256 fee = (totalAmountCollected * 20) / 100;
totalFees = totalFees + uint64(fee);
uint256 tokenId = totalSupply();
// We use a different RNG calculate from the winnerIndex to determine rarity
uint256 rarity = uint256(keccak256(abi.encodePacked(msg.sender, block.difficulty))) % 100;
+ uint256 rarityValue;
if (rarity <= COMMON_RARITY) {
tokenIdToRarity[tokenId] = COMMON_RARITY;
+ rarityValue = COMMON_RARITY;
} else if (rarity <= COMMON_RARITY + RARE_RARITY) {
tokenIdToRarity[tokenId] = RARE_RARITY;
+ rarityValue = RARE_RARITY;
} else {
tokenIdToRarity[tokenId] = LEGENDARY_RARITY;
+ rarityValue = LEGENDARY_RARITY;
}
delete players;
raffleStartTime = block.timestamp;
previousWinner = winner;
(bool success,) = winner.call{value: prizePool}("");
require(success, "PuppyRaffle: Failed to send prize pool to winner");
_safeMint(winner, tokenId);
+ emit WinnerSelected(winner, tokenId, prizePool, rarityValue, players.length);
}
```
This enables real-time tracking and better integration with off-chain systems.
Updates

Lead Judging Commences

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

[L-02] Missing `WinnerSelected`/`FeesWithdrawn` event emition in `PuppyRaffle::selectWinner`/`PuppyRaffle::withdrawFees` methods

## Description Events for critical state changes (e.g. owner and other critical parameters like a winner selection or the fees withdrawn) should be emitted for tracking this off-chain ## Recommendations Add a WinnerSelected event that takes as parameter the currentWinner and the minted token id and emit this event in `PuppyRaffle::selectWinner` right after the call to [`_safeMing_`](https://github.com/Cyfrin/2023-10-Puppy-Raffle/blob/07399f4d02520a2abf6f462c024842e495ca82e4/src/PuppyRaffle.sol#L153) Add a FeesWithdrawn event that takes as parameter the amount withdrawn and emit this event in `PuppyRaffle::withdrawFees` right at the end of [the method](https://github.com/Cyfrin/2023-10-Puppy-Raffle/blob/07399f4d02520a2abf6f462c024842e495ca82e4/src/PuppyRaffle.sol#L162)

Support

FAQs

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

Give us feedback!