File: src/MerkleAirdrop.sol, claimFees() (lines 44–48)
The claimFees() function sends ETH to the owner using a low-level .call{}(""). If the owner is a smart contract with a receive() or fallback() function that reverts, the claimFees() call will always revert and accumulated fees are permanently locked. This is a well-known "push payment" vulnerability.
The function pushes all ETH to owner(). If:
The owner is a multisig or smart contract that temporarily cannot receive ETH (e.g., during an upgrade or if the multisig logic reverts),
The owner deliberately transfers ownership to a contract that always reverts on receive,
A malicious actor socially engineers an ownership transfer,
...then succ == false, and MerkleAirdrop__TransferFailed is thrown. All ETH fees accumulated in the contract are permanently inaccessible.
Additionally, Ownable.transferOwnership() (inherited from OZ v5) does NOT check if the new owner can receive ETH before the transfer completes, making it trivial to lock fees accidentally.
Note: In Solidity 0.8.x, owner() can also theoretically return address(0) after renounceOwnership() is called (OZ Ownable). Sending ETH to address(0) via .call returns true (ETH is burned) — but the fee is permanently lost.
Accumulated ETH fees permanently locked or burned.
Protocol owner cannot recover funds.
Severity: High (permanent loss of ETH, no recovery path)
Manual analysis
OZ Ownable v5 behavior analysis
Push-payment vulnerability pattern
Use a pull-payment pattern instead:
Or at minimum, add a to parameter to avoid dependency on owner():
The contest is live. Earn rewards by submitting a finding.
Submissions are being reviewed by our AI judge. Results will be available in a few minutes.
View all submissionsThe contest is complete and the rewards are being distributed.