Description
The SnowmanAirdrop::claimSnowman()
function includes a mapping s_hasClaimedSnowman[receiver] = true
to track which addresses have already claimed the airdrop. However, the function fails to check this mapping is true/false
before allowing a claim. As a result, eligible users can repeatedly call the function and mint multiple NFTs.
function claimSnowman(address user, bytes32[] calldata merkleProof, uint8 v, bytes32 r, bytes32 s) external {
...
@> s_hasClaimedSnowman[receiver] = true;
}
Impact
Any qualified user can call the SnowmanAirdrop::claimSnowman()
function multiple times minting an unlimited amount of nfts till the contract is drained or deflates the price of the Nft.
Proof of Concept
Alice calls the SnowmanAirdrop::claimSnowman()
function.
Alice calls the SnowmanAirdrop::claimSnowman()
function again and succeeds.
Alice now has multiple NFTs claimed from a single intent.
Proof of Code
function testClaimMultipleTimes() public {
assert(nft.balanceOf(alice) == 0);
vm.startPrank(alice);
snow.approve(address(airdrop), type(uint256).max);
bytes32 alDigest = airdrop.getMessageHash(alice);
(uint8 alV, bytes32 alR, bytes32 alS) = vm.sign(alKey, alDigest);
airdrop.claimSnowman(alice, AL_PROOF, alV, alR, alS);
assert(nft.balanceOf(alice) == 1);
assert(nft.ownerOf(0) == alice);
uint256 FEE = snow.s_buyFee();
snow.buySnow{value: FEE}(1);
airdrop.claimSnowman(alice, AL_PROOF, alV, alR, alS);
assert(nft.balanceOf(alice) == 2);
}
Ran 1 test for test/TestSnowmanAirdrop.t.sol:TestSnowmanAirdrop
[PASS] testClaimMultipleTimes() (gas: 265735)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 12.35ms (1.22ms CPU time)
Recommended Mitigation
To fix this, add a check at the top of the SnowmanAirdrop::claimSnowman()
function to check if a user has claimed or not.
+ error SA__UserAlreadyClaimed();
function claimSnowman(address receiver, bytes32[] calldata merkleProof, uint8 v, bytes32 r, bytes32 s)
external
nonReentrant
{
+ if (s_hasClaimedSnowman[receiver]) {
revert SA__UserAlreadyClaimed();
} else {
s_hasClaimedSnowman[receiver] = true;
}
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); // send tokens to contract... akin to burning
- s_hasClaimedSnowman[receiver] = true;
emit SnowmanClaimedSuccessfully(receiver, amount);
i_snowman.mintSnowman(receiver, amount);
}