Santa's List

AI First Flight #3
Beginner FriendlyFoundry
EXP
View results
Submission Details
Impact: high
Likelihood: high
Invalid

H-3] `Deploy.s.sol` funds a different token address than the one passed to the `MerkleAirdrop` constructor, so the deployed airdrop is never funded

Description

  • The deploy script should construct MerkleAirdrop with the USDC token address and then move the airdrop funds into the contract using that same token.

  • The constructor receives s_zkSyncUSDC = 0x1D17...be9..., but the funding transfer is called on a different hardcoded literal 0x1d17...ae9.... The two addresses differ by one hex digit (b vs a), so they are different tokens. The MerkleAirdrop is configured for token A while the funds are sent in token B, leaving the airdrop contract with a zero balance of its configured token — every claim then reverts.

// script/Deploy.s.sol
@> address public s_zkSyncUSDC = 0x1D17CbCf0D6d143135be902365d2e5E2a16538d4; // token A
​
function run() public {
vm.startBroadcast();
MerkleAirdrop airdrop = deployMerkleDropper(s_merkleRoot, IERC20(s_zkSyncUSDC)); // constructor uses token A
@> IERC20(0x1d17CBcF0D6D143135aE902365D2E5e2A16538D4).transfer(address(airdrop), s_amountToAirdrop); // token B (different!)
vm.stopBroadcast();
}

Risk

Likelihood: HIGH

  • The two hardcoded addresses differ by one character and are both used unconditionally in run(); the mismatch happens on every deployment, with no attacker or special condition required.

Impact: HIGH

  • The airdrop contract is never funded in its configured token, so all four recipients' claims revert and the intended distribution never happens.

  • If token B exists and the deployer holds a balance of it, the funding transfer moves real value into a contract that can never distribute it, locking those funds.

Severity: HIGH (High likelihood × High impact)

Proof of Concept

The two address literals used in Deploy.s.sol are not equal, proving the constructor token and the funding token differ (test/PoC_H3_Address.t.sol). A chisel/cast comparison of the lowercased addresses shows the difference at the be9… vs ae9… position.

function test_H3_MismatchedUsdcAddresses() public pure {
address ctorToken = 0x1D17CbCf0D6d143135be902365d2e5E2a16538d4; // passed to constructor
address fundToken = 0x1d17CBcF0D6D143135aE902365D2E5e2A16538D4; // used in transfer()
assert(ctorToken != fundToken); // two DIFFERENT tokens -> airdrop never receives funds
}
$ forge test --match-path test/PoC_H3_Address.t.sol -vv
[PASS] test_H3_MismatchedUsdcAddresses() (gas: 313)
​
# lowercased for comparison:
# token A (constructor): 0x1d17cbcf0d6d143135 be9 02365d2e5e2a16538d4
# token B (transfer): 0x1d17cbcf0d6d143135 ae9 02365d2e5e2a16538d4 -> differ

Recommended Mitigation

Use the single, verified USDC address consistently — reuse the s_zkSyncUSDC variable instead of a second hardcoded literal, and confirm it is the real USDC on zkSync Era. Also check the transfer return value (or use SafeERC20), since the compiler flags this line for an unchecked ERC20 transfer.

- IERC20(0x1d17CBcF0D6D143135aE902365D2E5e2A16538D4).transfer(address(airdrop), s_amountToAirdrop);
+ IERC20(s_zkSyncUSDC).safeTransfer(address(airdrop), s_amountToAirdrop);
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 1 hour 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!