Snowman Merkle Airdrop

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

Reentrancy vulnerability risk in claim() function of SnowmanAirdrop.sol

Root + Impact

Description

  • The claim() function in SnowmanAirdrop.sol allows users to claim NFTs via Merkle proofs. However, the function does not follow the Checks-Effects-Interactions pattern and performs the external NFT transfer before updating internal state. This could expose the contract to reentrancy attacks, where an attacker repeatedly calls claim() before the state is updated, potentially allowing multiple claims.

In the current implementation:
function claim(bytes32[] calldata proof) external {
// Verify Merkle proof
require(!claimed[msg.sender], "Already claimed");
// External call before state update
nft.safeTransferFrom(address(this), msg.sender, tokenId);
claimed[msg.sender] = true;
}
Because the state update `claimed[msg.sender] = true;` happens after the external call, an attacker can trigger a reentrant call during `safeTransferFrom` and claim multiple times.

Risk

Likelihood:

  • Reason 1 // Describe WHEN this will occur (avoid using "if" statements)

  • Reason 2

Impact:

  • Unauthorized multiple claims of NFTs, inflating rewards unfairly.

  • Potential economic loss or unfair distribution.

  • Damage to contract’s reputation and trustworthiness.

Proof of Concept

An attacker can create a malicious contract that calls claim(), and in the fallback function triggered by the NFT transfer, call claim() again before `claimed[msg.sender]` is set to true. This would allow multiple claims:
contract Attack {
SnowmanAirdrop airdrop;
constructor(address _airdrop) {
airdrop = SnowmanAirdrop(_airdrop);
}
fallback() external {
if (!claimed) {
airdrop.claim(proof);
}
}
function attack(bytes32[] calldata proof) external {
airdrop.claim(proof);
}
}

Recommended Mitigation

function claim(bytes32[] calldata proof) external {
- require(!claimed[msg.sender], "Already claimed");
-
- nft.safeTransferFrom(address(this), msg.sender, tokenId);
-
- claimed[msg.sender] = true;
+ require(!claimed[msg.sender], "Already claimed");
+
+ claimed[msg.sender] = true;
+
+ nft.safeTransferFrom(address(this), msg.sender, tokenId);
}
Updates

Lead Judging Commences

yeahchibyke Lead Judge
3 months ago
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.