Snowman Merkle Airdrop

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

Missing Signature Expiration Mechanism in `SonwmanAirdrop.sol` Allowing Indefinite Signature Validity

Missing Signature Expiration Mechanism in SonwmanAirdrop.sol Allowing Indefinite Signature Validity

Description

  • Digital signatures should have limited validity periods to prevent abuse and reduce the attack window, especially in time-sensitive operations like airdrops where market conditions and user circumstances can change.

  • The current signature mechanism does not include any expiration time or deadline, allowing signatures to remain valid indefinitely until the user claims or the contract is upgraded.

// Root cause in the codebase with @> marks to highlight the relevant section
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 deadline/expiration included in the signature
}

Risk

Likelihood:

  • This occurs when signatures are generated but not immediately used, creating a window where old signatures remain valid

  • Project administrators or users may want to invalidate old signatures due to changed circumstances, but cannot do so

Impact:

  • Signatures remain valid indefinitely, increasing the attack surface over time

  • Potential for misuse of old signatures if private keys are compromised later

  • Difficulty in managing signature lifecycle and revocation

  • Reduced operational flexibility for project administrators

Proof of Concept

// Scenario: User gets signature on Day 1 but doesn't claim immediately
// Day 30: User's private key is compromised
// Day 45: Attacker can still use the old signature to claim, as it never expires
function claimSnowman(address receiver, bytes32[] calldata merkleProof, uint8 v, bytes32 r, bytes32 s)
external
nonReentrant
{
// ... existing checks ...
// Signature from 6 months ago is still valid
if (!_isValidSignature(receiver, getMessageHash(receiver), v, r, s)) {
revert SA__InvalidSignature();
}
// ... rest of function ...
}

Recommended Mitigation

// Update the struct to include deadline
struct SnowmanClaim {
address receiver;
uint256 amount;
+ uint256 deadline;
}
- bytes32 private constant MESSAGE_TYPEHASH = keccak256("SnowmanClaim(address receiver, uint256 amount)");
+ bytes32 private constant MESSAGE_TYPEHASH = keccak256("SnowmanClaim(address receiver,uint256 amount,uint256 deadline)");
- function getMessageHash(address receiver) public view returns (bytes32) {
+ function getMessageHash(address receiver, uint256 deadline) 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})))
+ keccak256(abi.encode(MESSAGE_TYPEHASH, SnowmanClaim({receiver: receiver, amount: amount, deadline: deadline})))
);
}
- function claimSnowman(address receiver, bytes32[] calldata merkleProof, uint8 v, bytes32 r, bytes32 s)
+ function claimSnowman(address receiver, bytes32[] calldata merkleProof, uint256 deadline, uint8 v, bytes32 r, bytes32 s)
external
nonReentrant
{
+ if (block.timestamp > deadline) {
+ revert SA__SignatureExpired();
+ }
// ... existing checks ...
- if (!_isValidSignature(receiver, getMessageHash(receiver), v, r, s)) {
+ if (!_isValidSignature(receiver, getMessageHash(receiver, deadline), v, r, s)) {
revert SA__InvalidSignature();
}
// ... rest of function ...
}
Updates

Lead Judging Commences

yeahchibyke Lead Judge
18 days ago
yeahchibyke Lead Judge 17 days ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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