pragma solidity ^0.8.24;
contract SnowmanAirdrop is EIP712, ReentrancyGuard {
@>
struct SnowmanClaim {
address receiver;
uint256 amount;
@>
}
bytes32 private constant MESSAGE_TYPEHASH =
@> keccak256("SnowmanClaim(address receiver, uint256 amount)");
function claimSnowman(
address receiver,
bytes32[] calldata merkleProof,
uint8 v,
bytes32 r,
bytes32 s
) external nonReentrant {
@>
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);
}
function getMessageHash(address receiver) public view returns (bytes32) {
if (i_snow.balanceOf(receiver) == 0) {
revert SA__ZeroAmount();
}
uint256 amount = i_snow.balanceOf(receiver);
@> return _hashTypedDataV4(
keccak256(
abi.encode(
MESSAGE_TYPEHASH,
SnowmanClaim({
receiver: receiver,
amount: amount
@>
})
)
)
);
}
}
pragma solidity ^0.8.24;
import "./test/TestSnowmanAirdrop.t.sol";
contract SnowmanAirdropReplayTest is Test {
SnowmanAirdrop public airdrop;
Snow public snowToken;
Snowman public snowmanNFT;
address public alice = address(0x1);
uint256 public alicePrivateKey = 0x1;
bytes32[] public merkleProof;
function setUp() public {
snowToken = new Snow();
snowmanNFT = new Snowman();
bytes32 merkleRoot = bytes32(0x123...);
airdrop = new SnowmanAirdrop(merkleRoot, address(snowToken), address(snowmanNFT));
vm.deal(alice, 100 ether);
snowToken.transfer(alice, 1000);
merkleProof = generateMerkleProof(alice, 1000);
}
function testSignatureReplay() public {
bytes32 messageHash = airdrop.getMessageHash(alice);
(uint8 v, bytes32 r, bytes32 s) = vm.sign(alicePrivateKey, messageHash);
vm.startPrank(alice);
snowToken.approve(address(airdrop), 1000);
airdrop.claimSnowman(alice, merkleProof, v, r, s);
assertTrue(airdrop.getClaimStatus(alice));
assertEq(snowmanNFT.balanceOf(alice), 1000);
bytes memory replayCalldata = abi.encodeWithSelector(
airdrop.claimSnowman.selector,
alice,
merkleProof,
v,
r,
s
);
for (uint256 i = 0; i < 3; i++) {
(bool success,) = address(airdrop).call(replayCalldata);
assertFalse(success);
}
vm.stopPrank();
}
function testReplayGasWaste() public {
uint256 gasStart = gasleft();
airdrop.claimSnowman(alice, merkleProof, v, r, s);
uint256 gasUsed = gasStart - gasleft();
gasStart = gasleft();
try airdrop.claimSnowman(alice, merkleProof, v, r, s) {
} catch {
uint256 replayGasUsed = gasStart - gasleft();
assertTrue(replayGasUsed > 30000);
}
}
function testMempoolReplay() public {
vm.startPrank(address(0xATTACKER));
vm.createSelectFork(block.number);
bytes memory pendingTx = vm.getTransactionByHash(txHash);
(address receiver, bytes32[] memory proof, uint8 v, bytes32 r, bytes32 s) =
abi.decode(pendingTx[4:], (address, bytes32[], uint8, bytes32, bytes32));
airdrop.claimSnowman(receiver, proof, v, r, s);
vm.stopPrank();
}
}
// In SnowmanAirdrop.sol
// Add nonce mapping
+ mapping(address => uint256) private s_nonces;
// Update struct
struct SnowmanClaim {
address receiver;
uint256 amount;
- // No nonce field
+ uint256 nonce;
}
// Update message type hash
- bytes32 private constant MESSAGE_TYPEHASH = keccak256("SnowmanClaim(address receiver, uint256 amount)");
+ bytes32 private constant MESSAGE_TYPEHASH = keccak256("SnowmanClaim(address receiver, uint256 amount, uint256 nonce)");
// Update claim function signature
- function claimSnowman(address receiver, bytes32[] calldata merkleProof, uint8 v, bytes32 r, bytes32 s)
+ function claimSnowman(
+ address receiver,
+ bytes32[] calldata merkleProof,
+ uint8 v,
+ bytes32 r,
+ bytes32 s,
+ uint256 nonce
+ )
external
nonReentrant
{
+ // Verify nonce
+ if (nonce != s_nonces[receiver]) revert SA__InvalidNonce();
if (receiver == address(0)) {
revert SA__ZeroAddress();
}
// Rest of validations...
i_snow.safeTransferFrom(receiver, address(this), amount);
s_hasClaimedSnowman[receiver] = true;
+ s_nonces[receiver]++;
emit SnowmanClaimedSuccessfully(receiver, amount);
i_snowman.mintSnowman(receiver, amount);
}
// Update message hash generation
function getMessageHash(address receiver) public view returns (bytes32) {
if (i_snow.balanceOf(receiver) == 0) {
revert SA__ZeroAmount();
}
uint256 amount = i_snow.balanceOf(receiver);
return _hashTypedDataV4(
keccak256(
abi.encode(
MESSAGE_TYPEHASH,
SnowmanClaim({
receiver: receiver,
amount: amount,
- // No nonce
+ nonce: s_nonces[receiver]
})
)
)
);
}
// Add new error
+ error SA__InvalidNonce();
// Add new event for nonce updates
+ event NonceIncremented(address indexed user, uint256 newNonce);