AirDropper

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

Deployment funds a different token contract, permanently preventing claims

Root+Impact

Description

  • The deployment script deploys MerkleAirdrop with the intended zkSync USDC token address. The airdrop subsequently needs to receive that same token so it can transfer the allocated USDC to eligible recipients.

  • The script configures the airdrop with s_zkSyncUSDC, but transfers funds using a different hardcoded address. The two addresses differ at one hexadecimal character (...135b... versus ...135a...). Consequently, the configured token contract receives no funds, and every claim reverts when the airdrop tries to transfer its zero token balance.

contract Deploy is Script {
address public s_zkSyncUSDC = 0x1D17CbCf0D6d143135be902365d2e5E2a16538d4;
function run() public {
vm.startBroadcast();
// @> The airdrop is configured to distribute tokens from s_zkSyncUSDC.
MerkleAirdrop airdrop = deployMerkleDropper(s_merkleRoot, IERC20(s_zkSyncUSDC));
// @> Funds are transferred using a distinct token address (...135a...).
IERC20(0x1d17CBcF0D6D143135aE902365D2E5e2A16538D4).transfer(
address(airdrop),
s_amountToAirdrop
);
vm.stopBroadcast();
}
}

Risk

Likelihood:

  • The production deployment executes Deploy.run().

  • An eligible recipient submits a valid Merkle proof to claim their allocation.

Impact:

  • The MerkleAirdrop contract receives no balance of its configured airdrop token.

  • Every claim reverts during the token transfer, permanently preventing the intended airdrop distribution.

Proof of Concept

function testClaimsFailWhenDeploymentFundsDifferentToken() public {
AirdropToken configuredToken = new AirdropToken();
AirdropToken incorrectlyFundedToken = new AirdropToken();
MerkleAirdrop localAirdrop = new MerkleAirdrop(merkleRoot, configuredToken);
incorrectlyFundedToken.mint(address(this), amountToSend);
incorrectlyFundedToken.transfer(address(localAirdrop), amountToSend);
// The airdrop has only the incorrectly funded token, not the token it distributes.
assertEq(configuredToken.balanceOf(address(localAirdrop)), 0);
assertEq(incorrectlyFundedToken.balanceOf(address(localAirdrop)), amountToSend);
vm.deal(collectorOne, localAirdrop.getFee());
vm.prank(collectorOne);
vm.expectRevert();
localAirdrop.claim{value: localAirdrop.getFee()}(
collectorOne,
amountToCollect,
proof
);
}

Recommended Mitigation

function run() public {
vm.startBroadcast();
MerkleAirdrop airdrop = deployMerkleDropper(s_merkleRoot, IERC20(s_zkSyncUSDC));
- IERC20(0x1d17CBcF0D6D143135aE902365D2E5e2A16538D4).transfer(
+ IERC20(s_zkSyncUSDC).transfer(
address(airdrop),
s_amountToAirdrop
);
vm.stopBroadcast();
}
Updates

Lead Judging Commences

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