Snowman Merkle Airdrop

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

Strict Equality in `SnowmanAirdrop::claimSnowman` Check Prevents Valid Claims.

Root + Impact

Description

The `claimSnowman()` function uses a strict equality check `if (i_snow.balanceOf(receiver) == 0)` to validate that the `receiver` has `Snow` tokens. This creates a vulnerability where users who have spent or transferred even a small portion of their Snow tokens after the Merkle tree snapshot was created will be unable to claim their Snowman NFT, even though they legitimately should be able to claim based on their balance at snapshot time.
function claimSnowman(address receiver, bytes32[] calldata merkleProof, uint8 v, bytes32 r, bytes32 s)
external
nonReentrant
{
if (receiver == address(0)) {
revert SA__ZeroAddress();
}
//p use of strict equality
@> if (i_snow.balanceOf(receiver) == 0) { //Strict equality check
revert SA__ZeroAmount();
}
// ... signature and merkle proof validation ...
uint256 amount = i_snow.balanceOf(receiver); // Uses current balance, not snapshot balance
// ... merkle proof uses current balance ...
i_snow.safeTransferFrom(receiver, address(this), amount);
}

Risk

Likelihood:


Impact:

- Denial of service: Users who have spent any Snow tokens after snapshot cannot claim their rightful Snowman NFTs
- Unfair exclusion: Legitimate users lose access to rewards they earned
- Race condition: Users must avoid spending tokens between snapshot and claim, creating artificial constraints
- Logic inconsistency: The function tries to transfer the current balance but validates against a snapshot-based Merkle proof

Proof of Concept

Run the test ` forge test --match-test testStrictEqualityPreventsValidClaim -vvv` in `TestSnowmanAirdrop.t.sol`
This is the expected test output:
```ngnix
forge test --match-test testStrictEqualityPreventsValidClaim -vvv
[⠒] Compiling...
No files changed, compilation skipped
Ran 1 test for test/TestSnowmanAirdrop.t.sol:TestSnowmanAirdrop
[PASS] testStrictEqualityPreventsValidClaim() (gas: 386778)
Logs:
Alice blocked from claiming due to spending 1 token
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 37.68ms (5.36ms CPU time)
Ran 1 test suite in 48.19ms (37.68ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
```
function testStrictEqualityPreventsValidClaim() public {
uint256 snapshotBalance = 1000 ether;
deal(address(snow),alice, snapshotBalance);
vm.prank(alice);
snow.transfer(bob, 1 ether);
assertEq(snow.balanceOf(alice), 999 ether);
bytes32 messageHash = airdrop.getMessageHash(alice);
(uint8 v , bytes32 r, bytes32 s) = vm.sign(alKey,messageHash);
vm.prank(alice);
vm.expectRevert();
airdrop.claimSnowman(alice, AL_PROOF, v, r, s);
console2.log("Alice blocked from claiming due to spending 1 token");
}

Recommended Mitigation

1. Store snapshot balances: Instead of using current balances, store the snapshot balances that the Merkle tree was built with
2. Use minimum balance check: Change to a minimum balance requirement:
// Include snapshot amount as a parameter
function claimSnowman(address receiver, bytes32
+ uint256 snapshotAmount
[] calldata merkleProof,
uint8 v, bytes32 r, bytes32 s)
external
nonReentrant
{
if (receiver == address(0)) {
revert SA__ZeroAddress();
}
//p use of strict equality
- if (i_snow.balanceOf(receiver) == 0) {revert SA__ZeroAmount();}
+ if (i_snow.balanceOf(receiver) < snapshotAmount) {revert SA__InsufficientBalanc();}
// ..rest of function logic
}
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.