AirDropper

AI First Flight #5
Beginner FriendlyDeFiFoundry
EXP
View results
Submission Details
Impact: low
Likelihood: low
Invalid

claimFees pushes ETH to owner via raw call with no pull fallback; a reverting owner permanently traps collected fees

claimFees() pushes ETH to the owner with a raw call, trapping collected fees if the owner cannot receive ETH

Description

claimFees() forwards the entire ETH balance to owner() via a raw .call. If the owner is a contract that reverts on receiving ETH (no receive/payable fallback, or a reverting one), the sweep always reverts and the collected fees become permanently trapped, since there is no pull-based alternative.

function claimFees() external onlyOwner {
(bool succ,) = payable(owner()).call{ value: address(this).balance }(""); // @> push to owner; traps fees if owner reverts on receive
if (!succ) {
revert MerkleAirdrop__TransferFailed();
}
}

(src/MerkleAirdrop.sol:42-47)

Risk

Likelihood: Low

It requires the owner to be (or to become, via ownership transfer) a contract that cannot accept ETH. With an EOA owner this never triggers.

Impact: Low

If it does occur, all accumulated FEE proceeds are locked in the contract with no way to withdraw them, since the only withdrawal path always reverts. The amounts are small (1 gwei per claim), bounding the loss.

Proof of Concept

Transfer ownership to a contract with no payable receiver, then claimFees() reverts and the fees cannot be withdrawn.

function test_claimFeesTrappedWithNonPayableOwner() public {
NonPayable badOwner = new NonPayable();
vm.prank(owner);
airdrop.transferOwnership(address(badOwner));
vm.prank(address(badOwner));
vm.expectRevert(MerkleAirdrop.MerkleAirdrop__TransferFailed.selector);
airdrop.claimFees(); // balance stuck
}

Recommended Mitigation

Use a pull-payment pattern, or let the owner specify an arbitrary fee recipient address.

- function claimFees() external onlyOwner {
- (bool succ,) = payable(owner()).call{ value: address(this).balance }("");
+ function claimFees(address to) external onlyOwner {
+ (bool succ,) = payable(to).call{ value: address(this).balance }("");
if (!succ) {
revert MerkleAirdrop__TransferFailed();
}
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 3 hours 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!