Snowman Merkle Airdrop

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

_isValidSignature() ignores ECDSA error codes and invalid signatures accepted as valid

Root + Impact

Description

  • Normal behavior: _isValidSignature() should validate that a signature was produced by the expected receiver address.

  • The issue: ECDSA.tryRecover() returns three values: (address signer, RecoverError error, bytes32 errorArg). The function only captures the first value and ignores the error code:

    (address actualSigner,,) = ECDSA.tryRecover(digest, v, r, s);

    If tryRecover() fails due to an invalid signature (malformed, wrong length, etc.), it returns address(0) as the signer and a non-zero error code. The function then checks actualSigner == receiver. If receiver is somehow address(0) (which is guarded against), this would pass. More critically, the error state is completely invisible and the caller has no way to distinguish a valid recovery failure from a genuine signer mismatch.

```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 and tryRecover returns address(0) with no error propagation.

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

Impact:

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

  • Combined with the MESSAGE_TYPEHASH typo, signature verification is doubly broken.

Proof of Concept

The following demonstrates that _isValidSignature() silently ignores
ECDSA error codes. When tryRecover() fails due to a malformed signature,
it returns address(0) with a non-zero error code. The function only checks
the returned address, making it impossible to distinguish between a wrong
signer and a completely invalid signature. Combined with the MESSAGE_TYPEHASH
typo, this means signature verification is broken at two independent levels.

function testInvalidSignatureIgnored() public {
// Craft a malformed signature (invalid length)
uint8 v = 27;
bytes32 r = bytes32(0);
bytes32 s = bytes32(0);
bytes32 digest = airdrop.getMessageHash(receiver);
// tryRecover returns (address(0), RecoverError.InvalidSignatureLength, 0)
// _isValidSignature only checks the address, ignores the error
(address recovered,,) = ECDSA.tryRecover(digest, v, r, s);
assertEq(recovered, address(0)); // returns zero address silently
// _isValidSignature returns false here — but for wrong reason
// Caller cannot tell if signature was wrong signer or completely malformed
// No error is surfaced — silent failure
bool isValid = airdrop._isValidSignature(receiver, digest, v, r, s);
assertFalse(isValid); // fails silently, no error code propagated
}

Recommended Mitigation

The fix has two options depending on the desired behavior. Option A uses
ECDSA.recover() which reverts on any invalid signature — this is the
simplest and most secure approach, as invalid signatures immediately halt
execution rather than returning a potentially misleading false. Option B
preserves tryRecover() but explicitly checks the error code before trusting
the recovered address and this is appropriate if the caller needs to handle
invalid signatures gracefully without reverting. Either option eliminates
the silent failure. Option A is recommended for this use case since
claimSnowman() already reverts on invalid signatures via SA__InvalidSignature.

function _isValidSignature(address receiver, bytes32 digest, uint8 v, bytes32 r, bytes32 s)
internal
- pure
+ view
returns (bool)
{
- // @> error and errorArg silently discarded
- (address actualSigner,,) = ECDSA.tryRecover(digest, v, r, s);
- return actualSigner == receiver;
+ // Option A (recommended): recover() reverts on invalid signatures
+ // No silent failures — invalid signatures immediately halt execution
+ address actualSigner = ECDSA.recover(digest, v, r, s);
+ return actualSigner == receiver;
+ // Option B: explicit error code check if graceful handling needed
+ // (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 2 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!