AirDropper

AI First Flight #5
Beginner FriendlyDeFiFoundry
EXP
View results
Submission Details
Impact: low
Likelihood: low
Invalid

Deploy::deployMerkleDropper() is public instead of internal, allowing anyone to deploy arbitrary MerkleAirdrop instances from the script

Description

  • The deployMerkleDropper() function is a helper intended to be called only internally by run() during the deployment process.

  • However, the function is declared public instead of internal, making it callable by any external account or contract. This allows anyone to invoke it directly with arbitrary merkleRoot and zkSyncUSDC parameters, deploying unintended MerkleAirdrop instances that share no relation to the intended deployment. In a testing or staging environment this inflates the deployment footprint with spurious contract instances; in a production context it could be used to deploy lookalike contracts under the deployer's script address to mislead users.

// @> public visibility — callable by any external account or contract
// @> should be internal since it is only used by run()
function deployMerkleDropper(bytes32 merkleRoot, IERC20 zkSyncUSDC)
public
returns (MerkleAirdrop) {
return (new MerkleAirdrop(merkleRoot, zkSyncUSDC));
}

Risk

Likelihood:

  • Any external account calls deployMerkleDropper() directly with a crafted merkleRoot and a fake token address, deploying a malicious MerkleAirdrop lookalike that appears to originate from the same deploy script

  • An automated deployment pipeline or test suite invokes the helper function directly outside of run(), producing unintended contract instances that pollute the deployment registry

Impact:

  • Arbitrary MerkleAirdrop instances can be deployed by anyone using the script as a factory, with no access control on the parameters — a malicious actor deploys a lookalike airdrop contract with a controlled merkle root to phish eligible users into claiming from the wrong contract

  • The unintended public surface area violates the principle of least privilege — a function that is only ever called from one place inside the same contract should never be externally accessible

Proof of Concept

Because deployMerkleDropper() is public, it is part of the contract's ABI and can be called directly by any account without going through run(). The caller has full control over both parameters, meaning the deployed instance has no relationship to the intended airdrop configuration:

// Conceptual misuse flow:
//
// 1. Attacker obtains the Deploy contract address (or deploys their own copy)
//
// 2. Attacker calls deployMerkleDropper() directly with controlled parameters:
// deploy.deployMerkleDropper(
// attackerControlledMerkleRoot, // attacker is the only eligible address
// IERC20(fakeTokenAddress) // attacker-controlled token
// );
//
// 3. A new MerkleAirdrop is deployed with:
// - i_merkleRoot = attacker's root (attacker can claim anything)
// - i_airdropToken = fake/malicious token contract
//
// 4. Attacker promotes the new contract address as the "real" airdrop
// to phish users into approving or interacting with the malicious instance
//
// 5. The legitimate run() function is never involved —
// the public visibility alone enables this entire flow

Recommended Mitigation

Change the visibility of deployMerkleDropper() from public to internal. Since the function is only called from run() within the same contract, internal is the correct and sufficient visibility. This removes the function from the ABI entirely, eliminating the external call surface with a one-word change.

function deployMerkleDropper(bytes32 merkleRoot, IERC20 zkSyncUSDC)
- public
+ internal
returns (MerkleAirdrop) {
return (new MerkleAirdrop(merkleRoot, zkSyncUSDC));
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 2 hours ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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

Give us feedback!