Description
The deploy script creates the MerkleAirdrop contract by passing s_zkSyncUSDC as the airdrop token address, then separately transfers USDC into the new contract
to fund it for distribution.
Two different addresses are used: s_zkSyncUSDC (0x1D17CbCf0D6d143135be902365d2e5E2a16538d4) is stored as i_airdropToken in the constructor, but the funding
transfer() call uses 0x1d17CBcF0D6D143135ae902365D2E5e2A16538D4 — a different contract at byte 9 of the address. The deployed MerkleAirdrop holds zero
balance of i_airdropToken, so every claim() call reverts on safeTransfer.
// script/Deploy.s.sol
@> address public s_zkSyncUSDC = 0x1D17CbCf0D6d143135be902365d2e5E2a16538d4;
// ^^
// 'b' here
function run() public {
vm.startBroadcast();
MerkleAirdrop airdrop = deployMerkleDropper(s_merkleRoot, IERC20(s_zkSyncUSDC));
// i_airdropToken = 0x1D17CbCf0D6d143135be902365d2e5E2a16538d4
@> IERC20(0x1d17CBcF0D6D143135aE902365D2E5e2A16538D4).transfer(address(airdrop), s_amountToAirdrop);
// ^^
// 'a' here — DIFFERENT ADDRESS
// Contract is funded with Token B, but i_airdropToken is Token A
vm.stopBroadcast();
}
Risk
Likelihood:
Every deployment using this script results in a broken contract — the address mismatch is hardcoded and triggers on every run() execution with no conditional
path that avoids it.
Because i_airdropToken is immutable, there is no on-chain recovery mechanism; the contract must be redeployed and the incorrectly-funded tokens manually
recovered from the wrong address.
Impact:
All four eligible addresses are permanently locked out of their airdrop — every claim() call reverts with insufficient balance because the contract holds none
of the token it is configured to distribute.
The 100e6 USDC sent via the funding transfer() is deposited into the contract but belongs to a different token contract than i_airdropToken, making it
unrecoverable through the normal claim() or claimFees() paths.
Proof of Concept
function test_H01_DeployAddressMismatch() public pure {
address configuredToken = 0x1D17CbCf0D6d143135bE902365d2e5E2a16538d4;
address fundingToken = 0x1d17CBcF0D6D143135aE902365D2E5e2A16538D4;
// These are provably different addresses
assert(configuredToken != fundingToken);
// Consequence:
// MerkleAirdrop is deployed with i_airdropToken = configuredToken
// Contract is funded with fundingToken tokens
// configuredToken.balanceOf(address(airdrop)) == 0
// Every claim() → safeTransfer(configuredToken, account, amount) → reverts
}
Recommended Mitigation
// script/Deploy.s.sol
address public s_zkSyncUSDC = 0x1D17CbCf0D6d143135be902365d2e5E2a16538d4;
address public s_zkSyncUSDC = 0x1d17CBcF0D6D143135aE902365D2E5e2A16538D4;
## 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; ```
The contest is live. Earn rewards by submitting a finding.
Submissions are being reviewed by our AI judge. Results will be available in a few minutes.
View all submissionsThe contest is complete and the rewards are being distributed.