AirDropper

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

`claimFees` uses unrestricted `.call` to `owner()` and can permanently lock accumulated ETH fees

Description

  • The owner calls claimFees to withdraw ETH collected from claim fees. The implementation forwards the entire contract ETH balance to owner() via a low-level call with empty calldata.

  • When owner() is a contract without a payable receive/fallback, or a contract that deliberately reverts on receive, claimFees always reverts and fees remain stuck. The tagged @audit-info concern about gas: .call forwards most remaining gas to the callee (EIP-150 63/64 rule), increasing griefing surface for malicious owner implementations.

function claimFees() external onlyOwner {
// @> Forwards all gas + full balance; fails when owner cannot accept ETH
(bool succ,) = payable(owner()).call{ value: address(this).balance }("");
if (!succ) {
revert MerkleAirdrop__TransferFailed();
}
}

Risk

Likelihood:

  • The deployer sets Ownable owner to a multisig or module contract on zkSync Era without a payable receive path.

  • Claims occur repeatedly, accumulating FEE per claim in the contract balance.

Impact:

  • All ETH fees are permanently unwithdrawable until ownership is transferred to an EOA that can accept ETH.

  • Owner contract callbacks can consume excessive gas on each withdrawal attempt (operational DoS on fee collection).

Proof of Concept

contract NonPayableOwner {
receive() external payable {
revert();
}
}
function test_PoC_claimFeesRevertsWhenOwnerCannotReceive() public {
NonPayableOwner badOwner = new NonPayableOwner();
// transferOwnership(address(badOwner)) on airdrop
vm.deal(address(airdrop), 10 * airdrop.getFee());
vm.prank(/* owner */);
vm.expectRevert(MerkleAirdrop.MerkleAirdrop__TransferFailed.selector);
airdrop.claimFees();
}

Recommended Mitigation

+ import { Address } from "@openzeppelin/contracts/utils/Address.sol";
+ address private immutable i_feeRecipient;
function claimFees() external onlyOwner {
- (bool succ,) = payable(owner()).call{ value: address(this).balance }("");
- if (!succ) {
- revert MerkleAirdrop__TransferFailed();
- }
+ uint256 balance = address(this).balance;
+ Address.sendValue(payable(i_feeRecipient), balance);
}

Set i_feeRecipient to a known EOA at construction, or document that owner must accept plain ETH transfers.

Updates

Lead Judging Commences

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