AirDropper

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

Deploy.s.sol funds the airdrop from a different token address than it deploys with (one-nibble typo), and neither is the real zkSync USDC — the airdrop can never pay a claim

Description

Deploy.s.sol (explicitly in scope) deploys the airdrop with one token address but funds it from a different one:

address public s_zkSyncUSDC = 0x1D17CbCf0D6d143135be902365d2e5E2a16538d4;
...
function run() public {
vm.startBroadcast();
MerkleAirdrop airdrop = deployMerkleDropper(s_merkleRoot, IERC20(s_zkSyncUSDC)); // airdrop's token
// Send USDC -> Merkle Air Dropper
IERC20(0x1d17CBcF0D6D143135aE902365D2E5e2A16538D4).transfer(address(airdrop), s_amountToAirdrop); // funded token
vm.stopBroadcast();
}

The two addresses are not the same token. Lower-cased for comparison:

  • deployed with (s_zkSyncUSDC): 0x1d17cbcf0d6d143135 be 902365d2e5e2a16538d4

  • funded from (transfer target): 0x1d17cbcf0d6d143135 ae 902365d2e5e2a16538d4

They differ by a single nibble (b vs a, the 19th byte) — a copy-paste typo that makes them two distinct contracts. The airdrop is configured to distribute the token at ...be..., but the script transfers s_amountToAirdrop of the token at ...ae... into it. So after deployment the airdrop holds 100e6 of the ...ae... token and zero of the ...be... token it actually pays out.

Every legitimate claim therefore reverts on i_airdropToken.safeTransfer (insufficient balance), and the ...ae... tokens that were sent in are stranded — the contract has no function to move a non-airdrop token back out.

Compounding this: neither address is the real zkSync Era USDC (0x3355df6D4c9C3035724Fd0e3914dE96A5a83aaf4). So even without the typo, the airdrop would be pointed at a non-USDC contract on the target chain.

Risk

Impact: High. The deployed airdrop is dead on arrival and the funding is lost: it holds the wrong token, cannot satisfy a single claim, and the transferred tokens are stranded with no recovery path.

Likelihood: High. It is deterministic on the in-scope deployment script — every deployment reproduces it.

Proof of Concept

function test_deployFundsWrongMismatchedToken() public {
address deployedWith = 0x1D17CbCf0D6d143135be902365d2e5E2a16538d4; // s_zkSyncUSDC
address fundedFrom = 0x1d17CBcF0D6D143135aE902365D2E5e2A16538D4; // transfer(...) target
assertTrue(deployedWith != fundedFrom); // two different token contracts
// Run Deploy: airdrop.getAirdropToken() == deployedWith, but the funding went in as `fundedFrom`,
// so the airdrop's balance of its OWN token is 0:
assertEq(IERC20(deployedWith).balanceOf(address(airdrop)), 0);
// Any legitimate claim then reverts on safeTransfer (insufficient balance of the airdrop token):
vm.expectRevert();
airdrop.claim{value: 1e9}(eligible, 25e6, proof);
}

Expected: the airdrop holds the USDC it distributes and can pay every eligible claim. Actual: it holds a different token, pays nobody, and the funds are stranded.

Recommended Mitigation

Use a single, correct token address — the real zkSync Era USDC — for both the deployment and the funding, referenced from the same variable so they cannot diverge:

address public s_zkSyncUSDC = 0x3355df6D4c9C3035724Fd0e3914dE96A5a83aaf4; // real zkSync Era USDC
...
MerkleAirdrop airdrop = deployMerkleDropper(s_merkleRoot, IERC20(s_zkSyncUSDC));
IERC20(s_zkSyncUSDC).transfer(address(airdrop), s_amountToAirdrop); // fund from the SAME address

Never hardcode the funding token as a second literal; always reuse s_zkSyncUSDC, and verify it against the official zkSync USDC before deploying.

Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 2 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!