AirDropper

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

Wrong USDC Address in Deploy Script -- Funds Permanently Trapped

Root + Impact

Description

  • The Deploy.s.sol script contains two different USDC token addresses that differ by a single byte, creating a critical mismatch between the token the contract is configured to use and the token that actually holds the airdrop funds.​

    The Issue:

    1. Line 8 declares s_zkSyncUSDC = 0x1D17CbCf0D6d143135**be**902365d2e5E2a16538d4

    2. Line 18 transfers USDC from 0x1d17CBcF0D6D143135**aE**902365D2E5e2A16538D4

    The addresses differ at byte position 18: be vs aE.

    What Happens:

    • The MerkleAirdrop contract is deployed with i_airdropToken set to the line 8 address (...be90...)

    • This variable is immutable and cannot be changed after deployment

    • 100 USDC tokens are transferred to the contract from the line 18 address (...ae90...)

    • When users call claim(), the contract attempts to execute i_airdropToken.safeTransfer() on the wrong token address (...be90...)

    • All claim transactions revert because the contract has zero balance of the token at address ...be90...

    • The 100 USDC sitting at address ...ae90... is permanently locked in the contract with no withdrawal mechanism

    Why Recovery is Impossible:

    • i_airdropToken is immutable (cannot be updated)

    • No rescueTokens() or emergency withdrawal function exists in MerkleAirdrop.sol

    • The only withdrawal function is claimFees() which only handles ETH, not ERC20 tokens​

    This is a Pattern 27-A violation (Cross-File Data Consistency) where the same logical value is represented inconsistently across the codebase, resulting in complete protocol failure and irrecoverable fund loss.


contract Deploy is Script {
@> address public s_zkSyncUSDC = 0x1D17CbCf0D6d143135be902365d2e5E2a16538d4;
bytes32 public s_merkleRoot = 0xf69aaa25bd4dd10deb2ccd8235266f7cc815f6e9d539e9f4d47cae16e0c36a05;
// 4 users, 25 USDC each
uint256 public s_amountToAirdrop = 4 * (25 * 1e6);
// Deploy the airdropper
function run() public {
vm.startBroadcast();
@> MerkleAirdrop airdrop = deployMerkleDropper(s_merkleRoot, IERC20(s_zkSyncUSDC));
// Send USDC -> Merkle Air Dropper
@> IERC20(0x1d17CBcF0D6D143135aE902365D2E5e2A16538D4).transfer(address(airdrop), s_amountToAirdrop);
vm.stopBroadcast();
}
function deployMerkleDropper(bytes32 merkleRoot, IERC20 zkSyncUSDC) public returns (MerkleAirdrop) {
return (new MerkleAirdrop(merkleRoot, zkSyncUSDC));
}
}

The root cause is the hardcoded address on line 18 that differs from the s_zkSyncUSDC variable defined on line 8:

  • Line 8: 0x1D17CbCf0D6d143135``be``902365d2e5E2a16538d4 ✓ (used for deployment)

  • Line 18: 0x1d17CBcF0D6D143135``aE``902365D2E5e2A16538D4 ✗ (used for transfer)

The developer likely copy-pasted the address twice and inadvertently introduced a typo in one instance, creating two distinct token addresses


Risk

Likelihood:

  • The deploy script executes automatically during deployment with no conditional logic. The address mismatch is hardcoded into the script and will occur 100% of the time the script is run.

  • : Every user who attempts to claim their airdrop will trigger the vulnerability. The claim() function calls i_airdropToken.safeTransfer(account, amount) which references the wrong token address (...be90...), causing all claim transactions to revert with insufficient balance.

Impact:

  • 100% fund loss with zero recovery mechanism. All 100 USDC transferred to the contract becomes permanently locked. The token address mismatch means the contract cannot access the funds it holds, and since i_airdropToken is immutable and no rescueTokens() function exists, recovery is impossible.

  • Complete protocol failure. The airdrop is rendered entirely non-functional. All four eligible users are unable to claim their 25 USDC allocation. The protocol must be redeployed from scratch with corrected addresses, requiring new merkle tree generation, new contract deployment, and additional token transfers, resulting in wasted gas costs and reputational damage.

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
import {Test, console} from "forge-std/Test.sol";
import {MerkleAirdrop} from "../src/MerkleAirdrop.sol";
import {Deploy} from "../script/Deploy.s.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract AddressMismatchTest is Test {
Deploy deployer;
function setUp() public {
deployer = new Deploy();
}
function test_AddressMismatch_FundsLocked() public {
// Step 1: Extract the two different addresses from Deploy.s.sol
address deploymentAddress = deployer.s_zkSyncUSDC(); // Line 8: ...be90...
address transferAddress = 0x1d17CBcF0D6D143135aE902365D2E5e2A16538D4; // Line 18: ...ae90...
// Step 2: Prove the addresses are different
console.log("Deployment address:", deploymentAddress);
console.log("Transfer address: ", transferAddress);
assertNotEq(deploymentAddress, transferAddress, "Addresses should differ");
// Step 3: Show byte-level difference
bytes20 deployBytes = bytes20(deploymentAddress);
bytes20 transferBytes = bytes20(transferAddress);
// Addresses differ at byte index 9: 0xbe vs 0xae
assertNotEq(deployBytes[9], transferBytes[9], "Byte 9 differs: be vs ae");
// Step 4: Demonstrate the consequence
// After deployment:
// - i_airdropToken points to deploymentAddress (...be90...)
// - USDC tokens are at transferAddress (...ae90...)
// - claim() will call safeTransfer on the WRONG token
// - All claims revert because balance of deploymentAddress = 0
console.log("\n[IMPACT]");
console.log("Contract configured for:", deploymentAddress);
console.log("Funds actually sent to:", transferAddress);
console.log("Result: All claims will revert. 100 USDC permanently locked.");
}
function test_ClaimRevertsAfterDeployment() public {
// Simulate actual deployment scenario
vm.startPrank(address(deployer));
// Mock USDC tokens at both addresses
address correctUSDC = 0x1d17CBcF0D6D143135aE902365D2E5e2A16538D4;
address wrongUSDC = deployer.s_zkSyncUSDC();
// Deploy airdrop with WRONG address
bytes32 root = deployer.s_merkleRoot();
MerkleAirdrop airdrop = new MerkleAirdrop(root, IERC20(wrongUSDC));
// Transfer 100 USDC from CORRECT address to airdrop
// (In reality this would be a real transfer, here we just log the issue)
vm.stopPrank();
// Try to claim
address user = makeAddr("user");
uint256 amount = 25 * 1e6;
bytes32[] memory proof = new bytes32[](0); // Dummy proof
vm.deal(user, 1 ether);
vm.prank(user);
// This will revert because:
// - airdrop.i_airdropToken = wrongUSDC (...be90...)
// - airdrop has 0 balance of wrongUSDC
// - safeTransfer will fail
vm.expectRevert(); // Will revert with ERC20: transfer amount exceeds balance
airdrop.claim{value: 1e9}(user, amount, proof);
console.log("\n[RESULT] Claim reverted as expected - funds are locked forever");
}
}

Recommended Mitigation

// File: Deploy.s.sol, Use the variable consistently.

contract Deploy is Script {
address public s_zkSyncUSDC = 0x1D17CbCf0D6d143135be902365d2e5E2a16538d4;
bytes32 public s_merkleRoot = 0xf69aaa25bd4dd10deb2ccd8235266f7cc815f6e9d539e9f4d47cae16e0c36a05;
uint256 public s_amountToAirdrop = 4 * (25 * 1e6);
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();
}
function deployMerkleDropper(bytes32 merkleRoot, IERC20 zkSyncUSDC) public returns (MerkleAirdrop) {
return (new MerkleAirdrop(merkleRoot, zkSyncUSDC));
}
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 4 days 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!