low

It Can Be Economically Impractical for the Contract Owner to Claim Airdrop Fees

Selected Submission

It Can Be Economically Impractical for the Contract Owner to Claim Airdrop Fees

Severity

Low Risk

Relevant GitHub Links

https://github.com/Cyfrin/2024-04-airdropper/blob/main/src/MerkleAirdrop.sol#L15

https://github.com/Cyfrin/2024-04-airdropper/blob/main/src/MerkleAirdrop.sol#L42-L47

Summary

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.

POC

MerkleAirdropTest.t.sol

    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

forge test --match-test test_GasExeceedsFeeClaimAmount --gas-report -vvvv

Example Output

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)

Impact

There exists an economic disinsentive for the owner to claim fees from the contract.

Tools Used

Manual Analysis, Foundry Tests & Gas Report

Recommendations

Either remove the need for a fee to be paid during a claim or increase the claim fee to make it economically practical.