pragma solidity ^0.8.24;
import {Test, console2} from "forge-std/Test.sol";
import {Snow} from "../src/Snow.sol";
import {Snowman} from "../src/Snowman.sol";
import {SnowmanAirdrop} from "../src/SnowmanAirdrop.sol";
import {MockWETH} from "../src/mock/MockWETH.sol";
import {Helper} from "../script/Helper.s.sol";
import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
* @title CorrectMessageHash Contract
* @notice This contract is used to demonstrate the hashing vulnerability in SnowmanAirdrop.
* It generates the CORRECT EIP-712 hash for comparison.
*/
contract CorrectMessageHash is EIP712 {
bytes32 private constant CORRECT_MESSAGE_TYPEHASH = keccak256("SnowmanClaim(address receiver,uint256 amount)");
struct SnowmanClaim {
address receiver;
uint256 amount;
}
constructor() EIP712("Snowman Airdrop", "1") {}
function getCorrectMessageHash(address receiver, uint256 amount) public view returns (bytes32) {
return _hashTypedDataV4(
keccak256(abi.encode(CORRECT_MESSAGE_TYPEHASH, SnowmanClaim({receiver: receiver, amount: amount})))
);
}
}
contract TestSnowmanAirdrop is Test {
Snow snow;
Snowman nft;
SnowmanAirdrop airdrop;
MockWETH weth;
CorrectMessageHash correctMessageHash;
Helper deployer;
bytes32 public ROOT = 0xc0b6787abae0a5066bc2d09eaec944c58119dc18be796e93de5b2bf9f80ea79a;
bytes32 alProofA = 0xf99782cec890699d4947528f9884acaca174602bb028a66d0870534acf241c52;
bytes32 alProofB = 0xbc5a8a0aad4a65155abf53bb707aa6d66b11b220ecb672f7832c05613dba82af;
bytes32 alProofC = 0x971653456742d62534a5d7594745c292dda6a75c69c43a6a6249523f26e0cac1;
bytes32[] AL_PROOF = [alProofA, alProofB, alProofC];
bytes32 bobProofA = 0x51c4b9a3cc313d7d7325f2d5d9e782a5a484e56a38947ab7eea7297ec86ff138;
bytes32 bobProofB = 0xbc5a8a0aad4a65155abf53bb707aa6d66b11b220ecb672f7832c05613dba82af;
bytes32 bobProofC = 0x971653456742d62534a5d7594745c292dda6a75c69c43a6a6249523f26e0cac1;
bytes32[] BOB_PROOF = [bobProofA, bobProofB, bobProofC];
bytes32 clProofA = 0x0065f7c9c934093ee1c4d51b77e77ad69d1c21351298d21cc720df18a39412f5;
bytes32 clProofB = 0xe4f70a2d0da3e6c29810b3eb84deeae82d06479d602b0e64225458c968f98cc1;
bytes32 clProofC = 0x971653456742d62534a5d7594745c292dda6a75c69c43a6a6249523f26e0cac1;
bytes32[] CL_PROOF = [clProofA, clProofB, clProofC];
bytes32 danProofA = 0xc7c84a70b50ff4103e9a8b3a716b446a138a507fc1b65ebdfae38439e52b2612;
bytes32 danProofB = 0xe4f70a2d0da3e6c29810b3eb84deeae82d06479d602b0e64225458c968f98cc1;
bytes32 danProofC = 0x971653456742d62534a5d7594745c292dda6a75c69c43a6a6249523f26e0cac1;
bytes32[] DAN_PROOF = [danProofA, danProofB, danProofC];
bytes32 eliProofA = 0x0000000000000000000000000000000000000000000000000000000000000000;
bytes32 eliProofB = 0x0000000000000000000000000000000000000000000000000000000000000000;
bytes32 eliProofC = 0xd7ed3892547c15a926b49d400e13fefe2c9f08de658f08b09925d5790383e978;
bytes32[] ELI_PROOF = [eliProofA, eliProofB, eliProofC];
address alice;
uint256 alKey;
address satoshi;
function setUp() public {
deployer = new Helper();
(airdrop, snow, nft, weth) = deployer.run();
(alice, alKey) = makeAddrAndKey("alice");
satoshi = makeAddr("gas_payer");
correctMessageHash = new CorrectMessageHash();
}
function testExploit_ClaimFailsWithCorrectHashAndSucceedsWithFlawedHash() public {
assert(nft.balanceOf(alice) == 0);
vm.prank(alice);
snow.approve(address(airdrop), 1);
bytes32 correctDigest = correctMessageHash.getCorrectMessageHash(alice, snow.balanceOf(alice));
(uint8 correctV, bytes32 correctR, bytes32 correctS) = vm.sign(alKey, correctDigest);
vm.prank(satoshi);
vm.expectRevert(SnowmanAirdrop.SA__InvalidSignature.selector);
airdrop.claimSnowman(alice, AL_PROOF, correctV, correctR, correctS);
assert(nft.balanceOf(alice) == 0);
bytes32 flawedDigest = airdrop.getMessageHash(alice);
(uint8 flawedV, bytes32 flawedR, bytes32 flawedS) = vm.sign(alKey, flawedDigest);
vm.prank(satoshi);
airdrop.claimSnowman(alice, AL_PROOF, flawedV, flawedR, flawedS);
assert(nft.balanceOf(alice) == 1);
assert(nft.ownerOf(0) == alice);
}
}