Description:
If a user receives extra Snow tokens before claiming their NFT in SnowmanAirdrop
, their claim will fail. This is because the SnowmanAirdrop::claimSnowman
function uses the snow balance of the user to make the leaf for the merkle tree, and how this was set before with a fixed value, the related verification will fails.
function claimSnowman(address receiver, bytes32[] calldata merkleProof, uint8 v, bytes32 r, bytes32 s)
external
nonReentrant
{
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);
@> 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);
}
Impact:
Users who receive additional Snow tokens (e.g., via transfer) before claiming cannot claim their NFT, potentially locking them out of the airdrop.
Proof of Concept:
Add the following after the TestSnowmanAirdrop
test suite:
function testBalanceChangeDenialClaim() public {
vm.prank(bob);
snow.transfer(alice, 1);
vm.prank(alice);
snow.approve(address(airdrop), 2);
bytes32 alDigest = airdrop.getMessageHash(alice);
(uint8 alV, bytes32 alR, bytes32 alS) = vm.sign(alKey, alDigest);
vm.expectRevert(SnowmanAirdrop.SA__InvalidProof.selector);
vm.prank(alice);
airdrop.claimSnowman(alice, AL_PROOF, alV, alR, alS);
}
Recommended Mitigation:
On SnowmanAirdrop::claimSnowman
function allow only one nft per valid user:
function claimSnowman(address receiver, bytes32[] calldata merkleProof, uint8 v, bytes32 r, bytes32 s)
external
nonReentrant
{
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();
}
+ if (s_hasClaimedSnowman[receiver]) {
+ revert("Nft Already Recived");
+ }
- uint256 amount = i_snow.balanceOf(receiver);
+ uint256 amount = 1;
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); // send tokens to contract... akin to burning
s_hasClaimedSnowman[receiver] = true;
emit SnowmanClaimedSuccessfully(receiver, amount);
i_snowman.mintSnowman(receiver, amount);
}