Snowman Merkle Airdrop

First Flight #42
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Impact: medium
Likelihood: high
Invalid

Missing nonce and deadline in signature enables replay vulnerabilities

Description:

The SnowmanAirdrop contract's signature mechanism lacks critical replay protection fields. The SnowmanClaim struct only contains address receiver and uint256 amount, missing both a nonce field for preventing signature reuse and a deadline field for signature expiration. This creates a scenario where signatures remain valid indefinitely and can potentially be replayed under specific circumstances.

Attack path:

  1. User signs a message containing only their address and token amount, with no nonce or expiration time

  2. The signature remains cryptographically valid forever, as there's no deadline field to enforce expiration

  3. Anyone can re-use the signature to claim more NFTs than intended

Impact:

Signatures remain valid indefinitely with no expiration mechanism

Risk of signature replay

Recommended Mitigation:

Update the signature structure to include replay protection mechanisms:

struct SnowmanClaim {
address receiver;
uint256 amount;
uint256 nonce;
uint256 deadline;
}
mapping(bytes32 => bool) public usedSignatures;
function claimSnowman(
address receiver,
uint256 nonce,
uint256 deadline,
bytes32[] calldata merkleProof,
uint8 v, bytes32 r, bytes32 s
) external nonReentrant {
require(block.timestamp <= deadline, "Signature expired");
require(!s_hasClaimedSnowman[receiver], "Already claimed");
bytes32 signatureHash = keccak256(abi.encodePacked(receiver, nonce, deadline));
require(!usedSignatures[signatureHash], "Signature already used");
usedSignatures[signatureHash] = true;
// rest of the function
}

Update getMessageHash() accordingly:

function getMessageHash(address receiver, uint256 nonce, uint256 deadline) public view returns (bytes32) {
uint256 amount = i_snow.balanceOf(receiver);
return _hashTypedDataV4(
keccak256(abi.encode(
MESSAGE_TYPEHASH,
SnowmanClaim({
receiver: receiver,
amount: amount,
nonce: nonce,
deadline: deadline
})
))
);
}

Update the MESSAGE_TYPEHASH accordingly:

bytes32 private constant MESSAGE_TYPEHASH = keccak256("SnowmanClaim(address receiver,uint256 amount,uint256 nonce,uint256 deadline)");
Updates

Lead Judging Commences

yeahchibyke Lead Judge
3 months ago
yeahchibyke Lead Judge 3 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.