The claim() function requires users to pay a 1 gwei (1e9 wei) fee per claim, which the owner can collect via claimFees(). However, with only 4 eligible addresses, the maximum collectible fees (4 gwei) are significantly less than the gas cost required to execute the withdrawal transaction.
The Issue:
Fee per claim: 1 gwei (1e9 wei)
Maximum claims: 4 users
Maximum total fees: 4 gwei (4e9 wei)
Gas cost to call claimFees(): ~30,479 gas
zkSync Era average gas price: ~0.02 gwei
Transaction cost: 30,479 × 0.02 = ~609 gwei
Net loss: 609 - 4 = 605 gwei (152x more expensive to collect than value collected)
What Happens:
Owner calls claimFees() to withdraw accumulated fees
Transaction costs ~609 gwei in gas
Owner receives only 4 gwei in fees
Owner loses 605 gwei net (152x loss)
Economic Incentives:
The fee mechanism creates a perverse incentive where:
Collecting fees is economically irrational
Fees remain locked in the contract forever
The fee serves no practical purpose except adding friction to claims
This is a Revenue Mechanism Design Flaw where the fee structure fails basic economic viability testing, making the feature entirely non-functional in practice.
The root cause is the hardcoded fee amount (1 gwei) combined with the small number of eligible users (4). The constant fee was likely chosen arbitrarily without economic analysis of:
Total addressable fee revenue (4 × 1 gwei = 4 gwei)
Gas cost to withdraw fees (~609 gwei on zkSync Era)
Net economic outcome (605 gwei loss per withdrawal)
The fee mechanism creates deadweight loss with no benefit to either party.
Likelihood:
The economic calculation is deterministic. With exactly 4 eligible users and a fixed 1 gwei fee, the maximum collectible fees are precisely 4 gwei. The gas cost to execute claimFees() on zkSync Era is measurable (~30,479 gas). At any realistic gas price (>0.00013 gwei), calling claimFees() results in a net loss for the owner.
Any rational owner who calculates the economics will never call claimFees(). The 4 gwei in fees will remain permanently locked in the contract. Users pay the fee (adding friction to claims) but the fee serves no economic purpose since it cannot be profitably collected.
Impact:
Dead code with minor user friction. The fee mechanism is economically non-functional, making claimFees() effectively dead code. Users must pay 4 gwei total in fees that benefit no one - the fees are trapped in the contract forever, creating pure deadweight loss. This adds unnecessary complexity and user friction for zero benefit.
Design intent failure. If the fee was intended to generate revenue, prevent spam, or fund operations, it completely fails these goals. The mechanism suggests incomplete economic analysis during development. While the monetary impact is negligible (~4 gwei), it indicates poor design practices that could manifest more severely in scaled versions of this protocol.
// File: MerkleAirdrop.sol **Remove fee mechanism entirely
## Description The low `MerkleAirdrop::FEE` (1 Gwei) makes it economically impractical (ETH-wise) for the owner to claim fees, even with the low gas cost of the zkSync chain. The fee should either be removed or increased to make it economically practical to claim by the owner. ## Vulnerability Details The low `MerkleAirdrop::FEE` (1 Gwei) makes it economically impractical (ETH-wise) for the owner to claim fees, even with the low gas cost of the zkSync chain. The gas cost for the owner to call `MerkleAirdrop::claimFees` is 30,479 gas units. Using the average zkSync gas price of 0.02 Gwei, the effective total gas cost would be ~609 Gwei or 0.000000609 Ether. For it to be economically sensible to claim fees (using the current fee price of 1 Gwei), there would need to be greater than or equal to 609 successful airdrop claims to meet or exceed the gas cost. Compared to the current number of addresses that are a part of the merkle tree, there is a significant discrepancy. <details> <summary>POC</summary> ### `MerkleAirdropTest.t.sol` ```javascript address owner = vm.addr(1); ... // deploy contracts as an EOA instead of contract function setUp() public { vm.startPrank(owner); token = new AirdropToken(); airdrop = new MerkleAirdrop(merkleRoot, token); token.mint(owner, amountToSend); token.transfer(address(airdrop), amountToSend); vm.stopPrank(); } ... function test_GasExeceedsFeeClaimAmount() public { uint256 assumedZksyncGasPrice = 0.00000000002 ether; // 0.02 Gwei uint256 airdropFee = airdrop.getFee(); vm.deal(collectorOne, airdropFee); vm.startPrank(collectorOne); airdrop.claim{ value: airdropFee }(collectorOne, amountToCollect, proof); vm.stopPrank(); // assert the contract and owner have the proper balances assertEq(address(airdrop).balance, airdropFee); assertEq(owner.balance, 0); vm.startPrank(owner); uint256 gasBeforeClaim = gasleft(); airdrop.claimFees(); uint256 gasAfterClaim = gasleft(); vm.stopPrank(); // assert the contract has had its fees claimed by owner assertEq(address(airdrop).balance, 0); // assert that the amount of gas spent is greater than the fees obtained (in wei) uint256 gasDelta = gasBeforeClaim - gasAfterClaim; assertGt((gasDelta * assumedZksyncGasPrice), owner.balance); } ``` ### Run Test ```bash forge test --match-test test_GasExeceedsFeeClaimAmount --gas-report -vvvv ``` #### Example Output ```bash Ran 1 test for test/MerkleAirdropTest.t.sol:MerkleAirdropTest [PASS] test_GasExeceedsFeeClaimAmount() (gas: 129297) Traces: [129297] MerkleAirdropTest::test_GasExeceedsFeeClaimAmount() │ ... ├─ [0] VM::assertGt(620640000000 [6.206e11], 1000000000 [1e9]) [staticcall] │ └─ ← () └─ ← () Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.38ms (690.20µs CPU time) | src/MerkleAirdrop.sol:MerkleAirdrop contract | | | | | | | -------------------------------------------- | --------------- | ----- | ------ | ----- | ------- | | Deployment Cost | Deployment Size | | | | | | 540806 | 2502 | | | | | | Function Name | min | avg | median | max | # calls | | claim | 59686 | 59686 | 59686 | 59686 | 1 | | claimFees | 30479 | 30479 | 30479 | 30479 | 1 | <--- | getFee | 225 | 225 | 225 | 225 | 1 | ... Ran 1 test suite in 5.26ms (2.38ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests) ``` </details> ## Impact There exists an economic disinsentive for the owner to claim fees from the contract. ## Recommendations Either remove the need for a fee to be paid during a claim or increase the claim fee to make it economically practical.
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.