AirDropper

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

Ethereum L1 Addresses in Merkle Tree Cannot Be Used by Account Abstraction Wallet Holders on zkSync Era

Description

  • The protocol selects four recipients based on their Ethereum L1 activity and encodes their Ethereum addresses into the Merkle tree. The airdrop is then
    deployed on zkSync Era, where users call claim(account, amount, proof) with their address.

  • zkSync Era supports native Account Abstraction (AA). AA wallet contracts — unlike standard EOAs — are deployed at addresses determined by the factory and
    initialization parameters of each chain independently. An AA wallet user who holds address 0xABC on Ethereum Mainnet will have a different address on zkSync Era.
    Since the Merkle tree encodes the Ethereum L1 addresses, any recipient using an AA wallet cannot produce a valid leaf for their zkSync Era address — their claim
    will always revert with InvalidProof.

// makeMerkle.js — uses Ethereum L1 addresses
const values = [
@> ["0x20F41376c713072937eb02Be70ee1eD0D639966C", amount], // @> ETH L1 address
@> ["0x277D26a45Add5775F21256159F089769892CEa5B", amount], // @> ETH L1 address
@> ["0x0c8Ca207e27a1a8224D1b602bf856479b03319e7", amount], // @> ETH L1 address
@> ["0xf6dBa02C01AF48Cf926579F77C9f874Ca640D91D", amount], // @> ETH L1 address
]

// src/MerkleAirdrop.sol — deployed on zkSync Era
function claim(address account, uint256 amount, bytes32[] calldata merkleProof) external payable {
bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(account, amount))));
@> if (!MerkleProof.verify(merkleProof, i_merkleRoot, leaf)) {
revert MerkleAirdrop__InvalidProof();
@> // @> If account = user's zkSync AA address (≠ their ETH L1 address),
@> // @> the leaf will not match → always reverts
}
}

Risk

Likelihood:

  • zkSync Era was specifically designed with native AA as a first-class feature — a significant portion of its active user base uses AA wallets (Argent, ZKsync
    native wallets, etc.) rather than standard EOAs, making this a probable rather than hypothetical scenario for at least some of the four recipients.

  • The protocol README explicitly states recipients were selected based on Ethereum L1 activity, confirming their known addresses are L1 EOA/contract addresses
    that may differ from their zkSync counterparts.

Impact:

  • Any recipient using an AA wallet on zkSync Era is permanently unable to claim their 25 USDC allocation — the Merkle proof for their Ethereum address will
    always fail verification against their zkSync address.

  • There is no on-chain recovery path: i_merkleRoot is immutable and cannot be updated without redeployment, so the tokens intended for AA wallet holders remain
    permanently locked unless the contract is redeployed with zkSync-specific addresses.

Proof of Concept

Scenario:

  1. 0x20F41376...966C is an AA wallet on Ethereum L1

  2. The same user's AA wallet on zkSync Era is deployed at 0xDEAD...BEEF
    (different address because zkSync factory uses different parameters)

  3. User calls claim(0xDEAD...BEEF, 25e6, proof_for_0x20F4..._on_L1)
    → leaf = keccak256(keccak256(abi.encode(0xDEADBEEF, 25e6)))
    → stored leaf = keccak256(keccak256(abi.encode(0x20F4...966C, 25e6)))
    → MerkleProof.verify() returns false
    → reverts with MerkleAirdrop__InvalidProof

  4. User's 25 USDC is permanently locked

Recommended Mitigation

// makeMerkle.js — replace ETH L1 addresses with the recipients' zkSync Era addresses

const values = [
  • ["0x20F41376c713072937eb02Be70ee1eD0D639966C", amount],

  • ["<recipient_1_zksync_era_address>", amount],

  • ["0x277D26a45Add5775F21256159F089769892CEa5B", amount],

  • ["<recipient_2_zksync_era_address>", amount],

  • ["0x0c8Ca207e27a1a8224D1b602bf856479b03319e7", amount],

  • ["<recipient_3_zksync_era_address>", amount],

  • ["0xf6dBa02C01AF48Cf926579F77C9f874Ca640D91D", amount],

  • ["<recipient_4_zksync_era_address>", amount],
    ]

// Before deployment: confirm with each recipient whether they use an AA wallet
// on zkSync Era and obtain their chain-specific address.

Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 4 hours ago
Submission Judgement Published
Validated
Assigned finding tags:

[H-04] Unable to receive airdrop due to account abstraction

## Description The users that use account abstraction wallets have different addresses across chains for the same account. ## Vulnerability Details In the docs is said: ```javascript "Our team is looking to airdrop 100 USDC tokens on the zkSync era chain to 4 lucky addresses based on their activity on the Ethereum L1. The Ethereum addresses are: 0x20F41376c713072937eb02Be70ee1eD0D639966C 0x277D26a45Add5775F21256159F089769892CEa5B 0x0c8Ca207e27a1a8224D1b602bf856479b03319e7 0xf6dBa02C01AF48Cf926579F77C9f874Ca640D91D" ``` The user can claim his/her USDC tokens through the `MerkleAirdrop::claim` function. This function requires `account, amount and proof array`. With the help of this three arguments the merkle proof will ensure that the caller is eligible to claim. But in the generated merkle root are used the Ethereum addresses of the lucky users. But the protocol will be deployed on the zkSync era chain. If any of them uses account abstraction wallet, this lucky user will not be able to claim his/her tokens. The account abstraction wallets have different addresses in the different chains for the same account. ## Impact The users that use account abstraction wallets have different addresses on the zkSync era chain. That means these users will not be able to claim their USDC tokens, because the merkle root will require another account address (this on Ethereum). ## Recommendations Ensure that the addresses in `makeMerkle` file for the lucky users are their addresses for the zkSync era chain.

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!