AirDropper

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

Deploy::run funds the airdrop contract from a hardcoded address that does not match s_zkSyncUSDC, leaving the contract with zero tokens on deployment

Root + Impact

Description

  • Deploy::run is intended to deploy the MerkleAirdrop contract and fund it with 100 USDC so eligible addresses can claim their allocation.

The contract is initialized with s_zkSyncUSDC as the airdrop token. However, the subsequent transfer() call uses a different hardcoded address. The two addresses differ at byte 10: 0xbe vs 0xae. The transfer sends tokens to address(airdrop) from the wrong token contract, meaning the MerkleAirdrop contract receives zero tokens of the correct token. Every subsequent claim() call will revert on safeTransfer.

// @> Token address used to initialize the contract
address public s_zkSyncUSDC = 0x1D17CbCf0D6d143135be902365d2e5E2a16538d4;
// ^^
function run() public {
vm.startBroadcast();
MerkleAirdrop airdrop = deployMerkleDropper(s_merkleRoot, IERC20(s_zkSyncUSDC));
// @> Different address used to fund — 0xaE ≠ 0xbE at byte 10
IERC20(0x1d17CBcF0D6D143135aE902365D2E5e2A16538D4).transfer(address(airdrop), s_amountToAirdrop);
// ^^
vm.stopBroadcast();
}

Risk

Likelihood:

  • The mismatch is a typo that survives compilation — Solidity has no way to detect that two addresses were intended to be the same

The deploy script has 0% test coverage, so no test ever executes run() or verifies the post-deployment token balance

  • The bug will trigger on every deployment without exception

Impact:

  • The deployed MerkleAirdrop contract holds zero tokens of s_zkSyncUSDC

Every claim() call reverts at safeTransfer — the airdrop is completely non-functional

  • All FEE payments made by users attempting to claim are permanently locked in the contract, recoverable only by the owner via claimFees()

Proof of Concept

The addresses can be compared byte-by-byte directly from the source. No test is required to prove the mismatch — it is statically verifiable from the deploy script itself.

To confirm the runtime consequence, deploy using deployMerkleDropper with the correct s_zkSyncUSDC address and verify the balance:

s_zkSyncUSDC (used to initialize contract):
0x1D17CbCf0D6d143135 BE 902365d2e5E2a16538d4
^^
hardcoded transfer address:
0x1d17CBcF0D6D143135 AE 902365D2E5e2A16538D4
^^
0xBE0xAE — these are two distinct contract addresses on zkSync.
function testDeployScript_ContractFundedCorrectly() public {
Deploy deploy = new Deploy();
MerkleAirdrop deployedAirdrop = deploy.deployMerkleDropper(
deploy.s_merkleRoot(),
IERC20(deploy.s_zkSyncUSDC())
);
// Contract is initialized with s_zkSyncUSDC
assertEq(address(deployedAirdrop.getAirdropToken()), deploy.s_zkSyncUSDC());
// run() would fund from a different address — balance would be zero
// This assertion would FAIL if run() is used instead of manual funding:
assertEq(
IERC20(deploy.s_zkSyncUSDC()).balanceOf(address(deployedAirdrop)),
deploy.s_amountToAirdrop()
);
}

Recommended Mitigation

Replace the hardcoded address in run() with the s_zkSyncUSDC state variable so there is a single source of truth for the token address.

function run() public {
vm.startBroadcast();
MerkleAirdrop airdrop = deployMerkleDropper(s_merkleRoot, IERC20(s_zkSyncUSDC));
- IERC20(0x1d17CBcF0D6D143135aE902365D2E5e2A16538D4).transfer(address(airdrop), s_amountToAirdrop);
+ IERC20(s_zkSyncUSDC).transfer(address(airdrop), s_amountToAirdrop);
vm.stopBroadcast();
}
Updates

Lead Judging Commences

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

[H-01] Address of USDC token in `Deploy.s.sol` is wrong causing the claiming process to fail

## Description The `s_zkSyncUSDC` address in `Deploy.s.sol` is incorrectly set, leading to a failure in the claiming process. This error results in funds being stuck in the `MerkleAirdrop` contract due to the immutability of the token address. ## Impact All funds become permanently trapped in the `MerkleAirdrop` contract, rendering them inaccessible for claiming or transfer. **Proof of Concept:** To demonstrate the issue, a test contract can be added and executed using the following command: `forge test --zksync --rpc-url $RPC_ZKSYNC --mt testDeployOnZkSync` Use the RPC URL `https://mainnet.era.zksync.io` for testing. <details> <summary>Proof Of Code</summary> ```javascript // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import { MerkleAirdrop, IERC20 } from "../src/MerkleAirdrop.sol"; import { Test, console2 } from "forge-std/Test.sol"; contract MerkleAirdropTest is Test { MerkleAirdrop public s_airdrop; uint256 s_amountToCollect = (25 * 1e6); // 25.000000 address s_collectorOne = 0x20F41376c713072937eb02Be70ee1eD0D639966C; bytes32 s_proofOne = 0x32cee63464b09930b5c3f59f955c86694a4c640a03aa57e6f743d8a3ca5c8838; bytes32 s_proofTwo = 0x8ff683185668cbe035a18fccec4080d7a0331bb1bbc532324f40501de5e8ea5c; bytes32[] s_proof = [s_proofOne, s_proofTwo]; address public deployer; // From Deploy.t.sol bytes32 public s_merkleRoot = 0x3b2e22da63ae414086bec9c9da6b685f790c6fab200c7918f2879f08793d77bd; address public s_zkSyncUSDC = 0x1d17CBcF0D6D143135aE902365D2E5e2A16538D4; uint256 public s_amountToAirdrop = 4 * (25 * 1e6); function setUp() public { deployer = makeAddr("deployer"); deal(0x1D17CbCf0D6d143135be902365d2e5E2a16538d4, deployer, 100 * 1e6); vm.deal(s_collectorOne, 100 ether); } function testDeployOnZkSync() public { if (block.chainid != 324) { return; } vm.startPrank(deployer); // From here there is the code from run() s_airdrop = deployMerkleDropper(s_merkleRoot, IERC20(s_zkSyncUSDC)); // Send USDC -> Merkle Air Dropper IERC20(0x1d17CBcF0D6D143135aE902365D2E5e2A16538D4).transfer(address(s_airdrop), s_amountToAirdrop); // end code from run vm.stopPrank(); vm.startPrank(s_collectorOne); s_airdrop.claim{ value: s_airdrop.getFee() }(s_collectorOne, s_amountToCollect, s_proof); vm.stopPrank(); } function deployMerkleDropper(bytes32 merkleRoot, IERC20 zkSyncUSDC) public returns (MerkleAirdrop) { return (new MerkleAirdrop(merkleRoot, zkSyncUSDC)); } } ``` </details> ## Recommendations To resolve the issue, update the s_zkSyncUSDC address in Deploy.s.sol to the correct value: ```diff - address public s_zkSyncUSDC = 0x1D17CbCf0D6d143135be902365d2e5E2a16538d4; + address public s_zkSyncUSDC = 0x1d17CBcF0D6D143135aE902365D2E5e2A16538D4; ```

Support

FAQs

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

Give us feedback!