Puppy Raffle

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

Missing access control on withdrawFees allows anyone to trigger fee withdrawal

# Missing access control on `PuppyRaffle::withdrawFees` allows anyone to trigger fee withdrawal
## Description
The `withdrawFees` function sends accumulated protocol fees to the `feeAddress` but has no access control modifier. Any external account or contract can call this function at any time, as long as the balance invariant check passes.
While the funds are always sent to `feeAddress` (not the caller), the lack of access control means any third party can force a withdrawal at a time that may not be desired by the protocol owner -- for example, triggering a withdrawal before enough fees have accumulated, or front-running an owner's call.
```solidity
@> function withdrawFees() external {
require(address(this).balance == uint256(totalFees), "PuppyRaffle: There are currently players active!");
uint256 feesToWithdraw = totalFees;
totalFees = 0;
(bool success,) = feeAddress.call{value: feesToWithdraw}("");
require(success, "PuppyRaffle: Failed to withdraw fees");
}
```
## Risk
**Likelihood**:
* The function is `external` with no access control, so any EOA or contract can call it at any time
* The only requirement is that `address(this).balance == uint256(totalFees)`, which holds true whenever there are no active raffle players
**Impact**:
* Anyone can trigger fee withdrawals at arbitrary times, removing the protocol owner's control over treasury management
* An attacker can front-run the owner's withdrawal transaction for griefing purposes
## Proof of Concept
1. Four players enter the raffle and the raffle duration elapses. `selectWinner` is called, distributing the prize and recording 20% as `totalFees`.
2. At this point, any random address -- not the owner, not the `feeAddress` -- can call `withdrawFees` and it succeeds, because there is no `onlyOwner` or similar modifier.
3. The test demonstrates that a completely unrelated address (`address(0xdead)`) can trigger the fee withdrawal to `feeAddress`.
```solidity
function testAnyoneCanWithdrawFees() public playersEntered {
vm.warp(block.timestamp + duration + 1);
vm.roll(block.number + 1);
puppyRaffle.selectWinner();
uint256 expectedFees = (entranceFee * 4 * 20) / 100;
// A random unrelated address calls withdrawFees
vm.prank(address(0xdead));
puppyRaffle.withdrawFees();
// Fees were successfully sent to feeAddress, triggered by a stranger
assertEq(address(feeAddress).balance, expectedFees);
assertEq(puppyRaffle.totalFees(), 0);
}
```
## Recommended Mitigation
Restrict `withdrawFees` to the contract owner:
```diff
- function withdrawFees() external {
+ function withdrawFees() external onlyOwner {
require(address(this).balance == uint256(totalFees), "PuppyRaffle: There are currently players active!");
uint256 feesToWithdraw = totalFees;
totalFees = 0;
(bool success,) = feeAddress.call{value: feesToWithdraw}("");
require(success, "PuppyRaffle: Failed to withdraw fees");
}
```
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 1 day 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!