AirDropper

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

Deploy Script Uses Two Different USDC Addresses, Funding the Contract With the Wrong Token

Root + Impact

Description

The `Deploy.s.sol` script defines a USDC address in the state variable `s_zkSyncUSDC` and uses it to configure the `MerkleAirdrop` contract's `i_airdropToken`. However, when funding the contract with tokens, line 18 hardcodes a different address literal. These two addresses differ at the byte level -- they are not the same address with different EIP-55 checksums, they are entirely different addresses.

// script/Deploy.s.sol
@> 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));
// Send USDC -> Merkle Air Dropper
@> IERC20(0x1d17CBcF0D6D143135aE902365D2E5e2A16538D4).transfer(address(airdrop), s_amountToAirdrop);
// ^^
// Different address! Byte 20: 0xbe vs 0xae
vm.stopBroadcast();
}

Lowercase comparison of the two addresses:

Line 8: 0x1d17cbcf0d6d143135 be 902365d2e5e2a16538d4
Line 18: 0x1d17cbcf0d6d143135 ae 902365d2e5e2a16538d4

Risk

Likelihood:

  • The deploy script will execute exactly as written. Both addresses pass EIP-55 checksum validation (the compiler accepted both), so there is no compilation error to catch this.

  • The mismatch is a typo that is extremely difficult to catch visually since the addresses differ by only one hex character in a 40-character string.

Impact:

  • The `MerkleAirdrop` contract is configured with `i_airdropToken = 0x...be...` but tokens are transferred from `0x...ae...`. The contract holds zero balance of the token it is configured to distribute.

  • All `claim()` calls revert with insufficient balance. 100 USDC is either stuck in the wrong token transfer or the transfer itself fails.

Proof of Concept

Both test Pass:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
import { MerkleAirdrop } from "../src/MerkleAirdrop.sol";
import { AirdropToken } from "./mocks/AirdropToken.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { Test, console } from "forge-std/Test.sol";
/// @title F-003 PoC: Deploy script uses two different USDC addresses
/// @notice Proves the addresses on Deploy.s.sol lines 8 and 18 are different,
/// causing the airdrop contract to hold zero balance of its configured token.
contract F003_AddressMismatchTest is Test {
// Exact addresses from Deploy.s.sol
address constant ADDR_LINE_8 = 0x1D17CbCf0D6d143135be902365d2e5E2a16538d4;
address constant ADDR_LINE_18 = 0x1d17CBcF0D6D143135aE902365D2E5e2A16538D4;
/// @notice Prove the two address literals are not the same address
function test_F003_addressesAreDifferent() public pure {
assert(ADDR_LINE_8 != ADDR_LINE_18);
}
/// @notice Simulate the deploy script flow and prove the airdrop contract
/// ends up with zero balance of its configured airdrop token.
function test_F003_contractFundedWithWrongToken() public {
// Create two separate ERC20 tokens to represent the two different addresses
AirdropToken tokenAtLine8 = new AirdropToken(); // what the constructor receives
AirdropToken tokenAtLine18 = new AirdropToken(); // what the transfer uses
uint256 amountToAirdrop = 4 * (25 * 1e6); // 100 USDC
// --- Simulate Deploy.s.sol run() ---
// Line 16: deploy with s_zkSyncUSDC (line 8 address)
MerkleAirdrop airdrop = new MerkleAirdrop(
bytes32(0), // merkle root irrelevant for this test
IERC20(address(tokenAtLine8))
);
// Line 18: fund from a DIFFERENT address (line 18 address)
tokenAtLine18.mint(address(this), amountToAirdrop);
tokenAtLine18.transfer(address(airdrop), amountToAirdrop);
// --- Verify the problem ---
// The contract's configured airdrop token is tokenAtLine8
IERC20 configuredToken = airdrop.getAirdropToken();
assertEq(address(configuredToken), address(tokenAtLine8));
// But the contract holds zero balance of its configured token
uint256 airdropTokenBalance = tokenAtLine8.balanceOf(address(airdrop));
assertEq(airdropTokenBalance, 0, "Contract has 0 of the token it distributes");
// The contract holds 100 USDC of the WRONG token (line 18) -- unusable
uint256 wrongTokenBalance = tokenAtLine18.balanceOf(address(airdrop));
assertEq(wrongTokenBalance, amountToAirdrop, "100 USDC stuck in wrong token");
console.log("[F-003] PROVEN: airdrop contract configured for token at", address(tokenAtLine8));
console.log("[F-003] but funded with token at", address(tokenAtLine18));
console.log("[F-003] configured token balance:", airdropTokenBalance);
console.log("[F-003] wrong token balance:", wrongTokenBalance);
}
}

Byte-level address comparison:

addr1 = '0x1D17CbCf0D6d143135be902365d2e5E2a16538d4'.lower()
# '0x1d17cbcf0d6d143135be902365d2e5e2a16538d4'
addr2 = '0x1d17CBcF0D6D143135aE902365D2E5e2A16538D4'.lower()
# '0x1d17cbcf0d6d143135ae902365d2e5e2a16538d4'
assert addr1 != addr2 # PASSES -- these are different addresses
# Difference at position 20: 'b' vs 'a'

Recommended Mitigation

Use the `s_zkSyncUSDC` state variable consistently instead of hardcoding a second address literal:

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 3 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!