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 {
require(!claimed[msg.sender], "Already claimed");
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:
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);
}