Snowman Merkle Airdrop

First Flight #42
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Impact: low
Likelihood: low
Invalid

Airdrop Eligibility Spoofing in SnowmanAirdrop

Root + Impact

Description

  • The airdrop contract lacks robust eligibility checks, allowing repeated or unauthorized claims.

  • If threshold is low or zero, any address—even freshly created ones—can claim once. Worse, if hasClaimed tracking is flawed (e.g., stored in a non‐unique mapping key), attackers can reset or collide entries to claim multiple times.

function claim() external {
require(!hasClaimed[msg.sender], "Already claimed");
require(snow.balanceOf(msg.sender) > threshold, "Insufficient Snow");
hasClaimed[msg.sender] = true;
_mintNFT(msg.sender);
}

Risk

Likelihood:

  • Likely – Without on‑chain identity or Merkle‐proof gating, any address meets the simple balance check once. Attackers can spin up many addresses to claim en masse.

Impact:

  • High – Attackers can drain all NFT airdrops, leaving legitimate users without rewards and invalidating the mechanism.

Proof of Concept

// Spawning multiple EOA in a loop
for (let i = 0; i < 100; i++) {
const attacker = ethers.Wallet.createRandom().connect(provider);
await ethers.provider.send("hardhat_setBalance", [attacker.address, "0x1000000000000000"]);
await snow.connect(attacker).earnSnow(); // meet balance threshold
await airdrop.connect(attacker).claim(); // claim NFT
}
console.log("NFTs minted by bots:", await nft.balanceOf(attacker.address));

Recommended Mitigation

- remove this code
function claim() external {
require(!hasClaimed[msg.sender], "Already claimed");
require(snow.balanceOf(msg.sender) > threshold, "Insufficient Snow");
hasClaimed[msg.sender] = true;
_mintNFT(msg.sender);
}
+ add this code
import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
bytes32 public merkleRoot;
mapping(address => bool) public hasClaimed;
constructor(bytes32 _merkleRoot) {
merkleRoot = _merkleRoot;
}
function claim(bytes32[] calldata proof) external {
require(!hasClaimed[msg.sender], "Already claimed");
bytes32 leaf = keccak256(abi.encodePacked(msg.sender));
require(MerkleProof.verify(proof, merkleRoot, leaf), "Invalid proof");
hasClaimed[msg.sender] = true;
_mintNFT(msg.sender);
}
Updates

Lead Judging Commences

yeahchibyke Lead Judge 3 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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