Snowman Merkle Airdrop

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

Missing Approval Check Before Token Transfer

Root + Impact

Description

The claimSnowman() function attempts to transfer Snow tokens from the user without first checking if the contract has sufficient allowance. While safeTransferFrom will revert if approval is missing, the lack of an explicit check results in a generic revert message that doesn't clearly communicate the issue to users.

Users must approve the airdrop contract before claiming, but there's no validation or helpful error message if they forget this step.

// src/SnowmanAirdrop.sol:92
function claimSnowman(address receiver, bytes32[] calldata merkleProof, uint8 v, bytes32 r, bytes32 s)
external
nonReentrant
{
// ...
uint256 amount = i_snow.balanceOf(receiver);
// @> No check: if (i_snow.allowance(receiver, address(this)) < amount) revert;
i_snow.safeTransferFrom(receiver, address(this), amount);
// @> Will revert with generic error if not approved
// ...
}

Risk

Likelihood:

  • Users commonly forget to approve tokens before transfers

  • No frontend validation can catch this

  • First-time users especially prone to this mistake

Impact:

  • Poor user experience with unclear error messages

  • Users don't understand why their claim failed

  • Increased support burden

  • May discourage participation in airdrop

Proof of Concept

function testMissingApprovalGivesUnclearError() public {
vm.startPrank(alice);
// Alice has Snow tokens but forgets to approve
assertEq(snow.balanceOf(alice), 10);
assertEq(snow.allowance(alice, address(airdrop)), 0); // No approval!
// Alice tries to claim
// Expected: Clear error like "SA__InsufficientAllowance"
// Actual: Generic SafeERC20 revert
vm.expectRevert(); // Generic revert, not user-friendly
airdrop.claimSnowman(alice, proof, v, r, s);
// Alice doesn't know she needs to approve first
// Error message doesn't guide her to the solution
}

Recommended Mitigation

+ error SA__InsufficientAllowance();
function claimSnowman(address receiver, bytes32[] calldata merkleProof, uint8 v, bytes32 r, bytes32 s)
external
nonReentrant
{
if (s_hasClaimedSnowman[receiver]) {
revert SA__AlreadyClaimed();
}
if (receiver == address(0)) {
revert SA__ZeroAddress();
}
if (i_snow.balanceOf(receiver) == 0) {
revert SA__ZeroAmount();
}
if (!_isValidSignature(receiver, getMessageHash(receiver), v, r, s)) {
revert SA__InvalidSignature();
}
uint256 amount = i_snow.balanceOf(receiver);
+
+ if (i_snow.allowance(receiver, address(this)) < amount) {
+ revert SA__InsufficientAllowance();
+ }
bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(receiver, amount))));
if (!MerkleProof.verify(merkleProof, i_merkleRoot, leaf)) {
revert SA__InvalidProof();
}
i_snow.safeTransferFrom(receiver, address(this), amount);
s_hasClaimedSnowman[receiver] = true;
emit SnowmanClaimedSuccessfully(receiver, amount);
i_snowman.mintSnowman(receiver, amount);
}
Updates

Lead Judging Commences

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