Puppy Raffle

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

Missing address(0) Validation in changeFeeAddress()

Root + Impact

Description

  • changeFeeAddress() allows the owner to set feeAddress to any address, including address(0), without validation:

function changeFeeAddress(address newFeeAddress) external onlyOwner {
feeAddress = newFeeAddress; // @audit No zero-address check
emit FeeAddressChanged(newFeeAddress);
}
  • If the owner accidentally sets feeAddress to address(0), subsequent calls to withdrawFees() would send ETH to the zero address via feeAddress.call{value: feesToWithdraw}(""). While this technically succeeds (ETH is sent to address(0)), the funds are effectively burned and irrecoverable.

Risk

Likelihood:

  • Low — requires owner error (accidental address(0) input)

  • Only callable by the contract owner (onlyOwner modifier)

Impact:

  • Low-Medium — protocol fees permanently burned if triggered

  • Irrecoverable once withdrawFees() is called with feeAddress == address(0)

Proof of Concept

How the issue manifests:

  1. The contract owner calls changeFeeAddress(address(0)) by mistake (e.g., passing an uninitialized variable)

  2. feeAddress is now set to address(0) — the event FeeAddressChanged(address(0)) is emitted but there is no on-chain prevention

  3. When withdrawFees() is called, ETH is sent to address(0) via a low-level call, which succeeds but the funds are burned

Vulnerable code (src/PuppyRaffle.sol:167-170):

function changeFeeAddress(address newFeeAddress) external onlyOwner {
feeAddress = newFeeAddress; // No validation
emit FeeAddressChanged(newFeeAddress);
}

Expected outcome: Protocol fees are permanently burned by being sent to address(0), with no way to recover them.

Recommended Mitigation

The root cause is that changeFeeAddress() performs no input validation on the newFeeAddress parameter. Since the owner is a human (or multisig), accidental zero-address input is a realistic operational error — especially when interacting via scripts or low-level calls where uninitialized variables default to address(0).

Primary fix — Add zero-address validation:

function changeFeeAddress(address newFeeAddress) external onlyOwner {
require(newFeeAddress != address(0), "PuppyRaffle: Fee address cannot be zero");
feeAddress = newFeeAddress;
emit FeeAddressChanged(newFeeAddress);
}

Why this works: The require check prevents the only dangerous input value. Since address(0) is never a valid fee recipient (ETH sent there is permanently burned), there is no legitimate use case that this check blocks.

Additional hardening — Two-step ownership transfer pattern:

For critical admin functions that change fund destinations, a two-step pattern adds an extra safety layer:

address public pendingFeeAddress;
function proposeFeeAddress(address newFeeAddress) external onlyOwner {
require(newFeeAddress != address(0), "PuppyRaffle: Fee address cannot be zero");
pendingFeeAddress = newFeeAddress;
emit FeeAddressProposed(newFeeAddress);
}
function acceptFeeAddress() external {
require(msg.sender == pendingFeeAddress, "PuppyRaffle: Only proposed address can accept");
feeAddress = pendingFeeAddress;
pendingFeeAddress = address(0);
emit FeeAddressChanged(feeAddress);
}

Why this helps: The two-step pattern requires the new fee address to actively confirm acceptance, which verifies: (1) the address is not zero (a zero address cannot send transactions), (2) the address is controlled by someone (they can call acceptFeeAddress), and (3) there are no typos in the address. This pattern is used by OpenZeppelin's Ownable2Step for ownership transfers.

Updates

Lead Judging Commences

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