AirDropper

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

[H-3] `Deploy.s.sol` funds a different token address than the one passed to the `MerkleAirdrop` constructor, so the deployed airdrop is never funded

Description

  • The deploy script should construct MerkleAirdrop with the USDC token address and then move the airdrop funds into the contract using that same token.

  • The constructor receives s_zkSyncUSDC = 0x1D17...be9..., but the funding transfer is called on a different hardcoded literal 0x1d17...ae9.... The two addresses differ by one hex digit (b vs a), so they are different tokens. The MerkleAirdrop is configured for token A while the funds are sent in token B, leaving the airdrop contract with a zero balance of its configured token — every claim then reverts.

// script/Deploy.s.sol
@> address public s_zkSyncUSDC = 0x1D17CbCf0D6d143135be902365d2e5E2a16538d4; // token A
​
function run() public {
vm.startBroadcast();
MerkleAirdrop airdrop = deployMerkleDropper(s_merkleRoot, IERC20(s_zkSyncUSDC)); // constructor uses token A
@> IERC20(0x1d17CBcF0D6D143135aE902365D2E5e2A16538D4).transfer(address(airdrop), s_amountToAirdrop); // token B (different!)
vm.stopBroadcast();
}

Risk

Likelihood: HIGH

  • The two hardcoded addresses differ by one character and are both used unconditionally in run(); the mismatch happens on every deployment, with no attacker or special condition required.

Impact: HIGH

  • The airdrop contract is never funded in its configured token, so all four recipients' claims revert and the intended distribution never happens.

  • If token B exists and the deployer holds a balance of it, the funding transfer moves real value into a contract that can never distribute it, locking those funds.

Severity: HIGH (High likelihood × High impact)

Proof of Concept

The two address literals used in Deploy.s.sol are not equal, proving the constructor token and the funding token differ (test/PoC_H3_Address.t.sol). A chisel/cast comparison of the lowercased addresses shows the difference at the be9… vs ae9… position.

function test_H3_MismatchedUsdcAddresses() public pure {
address ctorToken = 0x1D17CbCf0D6d143135be902365d2e5E2a16538d4; // passed to constructor
address fundToken = 0x1d17CBcF0D6D143135aE902365D2E5e2A16538D4; // used in transfer()
assert(ctorToken != fundToken); // two DIFFERENT tokens -> airdrop never receives funds
}
$ forge test --match-path test/PoC_H3_Address.t.sol -vv
[PASS] test_H3_MismatchedUsdcAddresses() (gas: 313)
​
# lowercased for comparison:
# token A (constructor): 0x1d17cbcf0d6d143135 be9 02365d2e5e2a16538d4
# token B (transfer): 0x1d17cbcf0d6d143135 ae9 02365d2e5e2a16538d4 -> differ

Recommended Mitigation

Use the single, verified USDC address consistently — reuse the s_zkSyncUSDC variable instead of a second hardcoded literal, and confirm it is the real USDC on zkSync Era. Also check the transfer return value (or use SafeERC20), since the compiler flags this line for an unchecked ERC20 transfer.

- IERC20(0x1d17CBcF0D6D143135aE902365D2E5e2A16538D4).transfer(address(airdrop), s_amountToAirdrop);
+ IERC20(s_zkSyncUSDC).safeTransfer(address(airdrop), s_amountToAirdrop);
Updates

Lead Judging Commences

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