Snowman Merkle Airdrop

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

_isValidSignature() ignores ECDSA error codes — invalid signatures silently treated as wrong signer

Root + Impact

Description

  • Normal behavior: _isValidSignature() should validate that a signature was produced by the expected receiver address and revert clearly on malformed input.

  • The issue: ECDSA.tryRecover() returns three values: (address signer, RecoverError error, bytes32 errorArg). The function discards the error code entirely. When tryRecover() fails due to a malformed signature it returns address(0) with a non-zero error code. The function only compares the address, making it impossible to distinguish a wrong signer from a completely invalid signature format.

```solidity
// src/SnowmanAirdrop.sol#102-109
function _isValidSignature(address receiver, bytes32 digest, uint8 v, bytes32 r, bytes32 s)
internal pure returns (bool)
{
// @> error and errorArg return values silently discarded
(address actualSigner,,) = ECDSA.tryRecover(digest, v, r, s);
return actualSigner == receiver;
}
```

Risk

Likelihood:

  • Any caller can submit a malformed signature tryRecover returns address(0) with no error propagation to the caller.

  • The signature verification path is the primary security gate for airdrop claims.

Impact:

  • Signature validation errors are silently swallowed, no distinction between wrong signer and invalid signature format.

  • Combined with the MESSAGE_TYPEHASH typo, signature verification is broken at two independent levels simultaneously.

Proof of Concept

The following test demonstrates that _isValidSignature() silently discards ECDSA error information. A completely malformed signature returns address(0) from tryRecover() with a non-zero error code, but the function only checks the address. The caller receives false with no way to determine the failure cause.

```solidity
function testInvalidSignatureIgnored() public {
uint8 v = 27;
bytes32 r = bytes32(0);
bytes32 s = bytes32(0);
bytes32 digest = airdrop.getMessageHash(receiver);
(address recovered,,) = ECDSA.tryRecover(digest, v, r, s);
assertEq(recovered, address(0));
// Error type invisible to caller
bool isValid = airdrop._isValidSignature(receiver, digest, v, r, s);
assertFalse(isValid);
}
```

Recommended Mitigation

Option A (recommended) uses ECDSA.recover() which reverts automatically on any invalid signature. This eliminates ambiguity between wrong signer and malformed input. Option B preserves tryRecover() but explicitly checks the error code before trusting the recovered address. Option A is recommended since claimSnowman() already reverts on invalid signatures via SA__InvalidSignature. Note the function signature changes from pure to view with Option A.

```diff
function _isValidSignature(address receiver, bytes32 digest, uint8 v, bytes32 r, bytes32 s)
internal
- pure
+ view
returns (bool)
{
- (address actualSigner,,) = ECDSA.tryRecover(digest, v, r, s);
- return actualSigner == receiver;
+ // Option A: recover() reverts on invalid signatures
+ address actualSigner = ECDSA.recover(digest, v, r, s);
+ return actualSigner == receiver;
+ // Option B: explicit error check
+ // (address actualSigner, ECDSA.RecoverError err,) = ECDSA.tryRecover(digest, v, r, s);
+ // return err == ECDSA.RecoverError.NoError && actualSigner == receiver;
}
```
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 7 hours ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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

Give us feedback!