AirDropper

AI First Flight #5
Beginner FriendlyDeFiFoundry
EXP
View results
Submission Details
Severity: low
Valid

Hardcoded Fee Provides No Economic Deterrent and Cannot Be Updated

Root + Impact

Description

  • MerkleAirdrop charges a fixed FEE of 1e9 wei (1 gwei) per claim() call, collected in the contract and withdrawable by the owner via claimFees(). The intent is to create a small cost barrier per claim.

  • The fee is hardcoded as a constant with no setter, making it impossible to adjust after deployment. At 1 gwei, the cost is economically insignificant
    on any network — including zkSync where the contract is intended to deploy — and provides no meaningful Sybil resistance, spam deterrent, or replay
    deterrent. Combined with the absence of a hasClaimed mapping, the fee does not prevent any attacker from calling claim() in a loop to drain the
    contract.

uint256 private constant FEE = 1e9; // @> 1 gwei — hardcoded, not updatable
function claim(address account, uint256 amount, bytes32[] calldata merkleProof) external payable {
if (msg.value != FEE) { // @> 1 gwei check — trivially satisfied
revert MerkleAirdrop__InvalidFeeAmount();
}
bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(account, amount))));
if (!MerkleProof.verify(merkleProof, i_merkleRoot, leaf)) {
revert MerkleAirdrop__InvalidProof();
}
emit Claimed(account, amount);
i_airdropToken.safeTransfer(account, amount);
}

Risk

Likelihood:

  • Every claim interaction pays only 1 gwei regardless of the token value being claimed — the fee will always be economically irrelevant relative to any
    real airdrop amount.

  • Gas price fluctuations or network upgrades render the hardcoded value stale with no on-chain recourse, as the owner has no mechanism to update it
    post-deployment

Impact:

  • The fee accumulates a negligible ETH amount for the owner — on zkSync, 1 gwei per claim can produces essentially zero revenue even across thousands of
    claims.

Proof of Concept

// 1 gwei = 0.000000001 ETH
// At ETH = $3,000: cost per claim = $0.000003
// Airdrop amount per user = 25 USDC
// Attacker profit per repeated claim (ignoring gas) = ~$24.999997
// An attacker with a valid proof loops claims at 1 gwei each:
for (uint i = 0; i < 100; i++) {
merkleAirdrop.claim{value: 1e9}(account, amount, proof);
}
// Total fee paid: 100 gwei (~$0.0003)
// Total tokens drained: 100 * 25 USDC = $2,500

Recommended Mitigation

Replace the constant with an owner-updatable state variable so the fee can be adjusted post-deployment to match network conditions:

- uint256 private constant FEE = 1e9;
+ uint256 private s_fee = 1e9;
+ event FeeUpdated(uint256 newFee);
+ function setFee(uint256 newFee) external onlyOwner {
+ s_fee = newFee;
+ emit FeeUpdated(newFee);
+ }
function getFee() external view returns (uint256) {
- return FEE;
+ return s_fee;
}
function claim(...) external payable {
- if (msg.value != FEE) {
+ if (msg.value != s_fee) {
revert MerkleAirdrop__InvalidFeeAmount();
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 6 hours ago
Submission Judgement Published
Validated
Assigned finding tags:

[L-01] It Can Be Economically Impractical for the Contract Owner to Claim Airdrop Fees

## 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.

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!