Snowman Merkle Airdrop

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

Dynamic Balance Creates Invalid Merkle Proofs and Front-Running in SnowmanAirdrop.sol

Root + Impact

Description

  • Normal Behavior: The claimSnowman function should verify eligibility using a Merkle tree with fixed Snow token amounts, ensuring only eligible users claim NFTs.

  • Specific Issue: The function uses i_snow.balanceOf(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.

// Root cause in the codebase with @> marks to highlight the relevant section
function claimSnowman(address receiver, bytes32[] calldata merkleProof, uint8 v, bytes32 r, bytes32 s)
external
nonReentrant
{
uint256 amount = i_snow.balanceOf(receiver);// @> Dynamic balance
bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(receiver, amount))));// @> Invalid if balance changes
if (!MerkleProof.verify(merkleProof, i_merkleRoot, leaf)) {
revert SA__InvalidProof();
}
}

Risk

Likelihood:

  • Occurs whenever a user transfers Snow tokens, altering their balance post-Merkle tree generation.

  • Mempool front-running allows attackers to adjust balances before claims are processed.

Impact:

  • Eligible users fail to claim NFTs due to invalid proofs, disrupting the airdrop.

  • Attackers can manipulate balances to claim unauthorized NFTs, potentially draining allocations.Proof of Concept


Proof of Concept

// Merkle tree: (attacker, 100 SNOW), attacker has 50 SNOW
Snow snow = Snow(snowAddress);
snow.transferFrom(someone, attacker, 50);// Adjust to 100 SNOW
SnowmanAirdrop airdrop = SnowmanAirdrop(airdropAddress);
airdrop.claimSnowman(attacker, merkleProof, v, r, s);// Succeeds

Recommended Mitigation

+ mapping(address => uint256) public s_claimableAmounts;
+ function setClaimableAmounts(address[] calldata users, uint256[] calldata amounts) external onlyOwner {
+ if (users.length != amounts.length) revert("Array length mismatch");
+ for (uint i = 0; i < users.length; i++) {
+ s_claimableAmounts[users[i]] = amounts[i];
+ }
+ }
- uint256 amount = i_snow.balanceOf(receiver);
+ uint256 amount = s_claimableAmounts[receiver];
+ if (amount == 0) revert SA__ZeroAmount();
bytes32 leaf = keccak256(abi.encodePacked(receiver, 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.