Snowman Merkle Airdrop

First Flight #42
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Severity: medium
Valid

Merkle Tree Based on Token Balance (Dynamic State Mismatch)

Description: SnowmanAirdrop::claimSnowman() calculates the Merkle leaf using the 'balanceOf(receiver)' at claim time, instead of using the static amount from the original Merkle snapshot.

Impact: If the user’s balance changes after the Merkle tree is built, their Merkle proof will no longer be valid, and the claim will fail.

Proof of Concept: Include the following test in the TestSnowmanAirdrop.t.sol file:

function testBalanceChangeBreaksMerkleClaim() public {
vm.prank(alice);
snow.approve(address(airdrop), 1);
// Alice claims after the Merkle tree is generated
vm.warp(block.timestamp + 1 weeks);
vm.prank(alice);
snow.earnSnow();
bytes32 alDigest = airdrop.getMessageHash(alice);
(uint8 alV, bytes32 alR, bytes32 alS) = vm.sign(alKey, alDigest);
// Alice tries to claim, but her balance not matching the Merkle proof
vm.expectRevert(SnowmanAirdrop.SA__InvalidProof.selector);
airdrop.claimSnowman(alice, AL_PROOF, alV, alR, alS);
}

Recommended Mitigation: Use amount from Merkle input (passed via signature or as an argument), not balanceOf:

- function claimSnowman(address receiver, bytes32[] calldata merkleProof, uint8 v, bytes32 r, bytes32 s)
+ function claimSnowman(uint256 amount, address receiver, bytes32[] calldata merkleProof, uint8 v, bytes32 r, bytes32 s)
external
nonReentrant
{
// ...
- if (!_isValidSignature(receiver, getMessageHash(receiver), v, r, s)) {
+ if (!_isValidSignature(amount, receiver, getMessageHash(amount, receiver), v, r, s)) {
revert SA__InvalidSignature();
}
- uint256 amount = i_snow.balanceOf(receiver); // -
// ...
}
- function getMessageHash(address receiver) public view returns (bytes32) {
+ function getMessageHash(uint256 amount, address receiver) public view returns (bytes32) {
if (i_snow.balanceOf(receiver) == 0) {
revert SA__ZeroAmount();
}
- uint256 amount = i_snow.balanceOf(receiver);
return _hashTypedDataV4(
keccak256(abi.encode(MESSAGE_TYPEHASH, SnowmanClaim({receiver: receiver, amount: amount})))
);
}
Updates

Lead Judging Commences

yeahchibyke Lead Judge 3 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Invalid merkle-proof as a result of snow balance change before claim action

Claims use snow balance of receiver to compute the merkle leaf, making proofs invalid if the user’s balance changes (e.g., via transfers). Attackers can manipulate balances or frontrun claims to match eligible amounts, disrupting the airdrop.

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.