Snowman Merkle Airdrop

First Flight #42
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Severity: high
Valid

`MESSAGE_TYPEHASH` typo causing encoding error which breaks signature verification.

[H-1] Incorrect Signature Hash Generation - getMessageHash(address reciever) dependent on MESSAGE_TYPEHASH which contains a typo that will provide a faulty bytes32 representation, thus causing SnowmanAirdrop::_isValidSignature() to fail signature validation everytime

Description:
In SnowmanAirdrop.sol on line 49 there is the following statement with a typo in "addres":

bytes32 private constant MESSAGE_TYPEHASH = keccak256("SnowmanClaim(addres receiver, uint256 amount)");

So when MESSAGE_TYPEHASH is used in getMessageHash() as shown below it encodes the misspelling into bytes32, this bytes 32 value is used later to verify the signature, this incorrectly encoded value will not match a valid signature (unless that valid signature was also encoded with the typo intentionally)

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})))
);
}

getMessageHash() is called on line 80 of SnowmanAirdrop.sol as shown below:

if (!_isValidSignature(receiver, getMessageHash(receiver), v, r, s)) { // is the signature valid
revert SA__InvalidSignature();
}

The issue is that the keccak256 hash of the misspelled line is not the same as the corrected version of the same line:

- bytes32 private constant MESSAGE_TYPEHASH = keccak256("SnowmanClaim(addres receiver, uint256 amount)");
+ bytes32 private constant MESSAGE_TYPEHASH = keccak256("SnowmanClaim(address receiver, uint256 amount)");

These two unencoded statments will result in different encoded bytes32 values:

- Misspelled: ff59e96f4a12fdaf4e417a1440b578f822f6cb542be1a2a7c196280bec54f9ab
+ Corrected: 6cedfa9a039e959a5fe5a6e4224acac28bb707e70fb8ad28e3ac109c88b280d6

Meaning that in this instance, the user would not be able to claim their snowman via SnowmanAirdrop::claimSnowman()


Impact:
MESSAGE_TYPEHASH is required when the call to getMessageHash() is made on line 80 in SnowmanAirdrop.sol for the _isValidSignature check. When the misspelled line (49) is encoded on line 120 in SnowmanAirdrop.sol it gets encoded with the misspelling into bytes32 format, this bytes32 value is then used to verify the validity of the signature.

Thus the message hash will be generated incorrectly everytime, meaning that no user will be able to claim their snowman.

A comparison of the corrected and misspelled hash are above.


Proof of Concept:

Proof of Concept (Code Block) ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20;
import {MerkleProof} from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {Base64} from "@openzeppelin/contracts/utils/Base64.sol";
contract testSnowmanAirdrop{
MockSnowToken public i_snow; //Placeholder snow token for testing
struct SnowmanClaim { // Placeholder struct for testing
address receiver;
uint256 amount;
}
// >>> ERRORS
error SA__InvalidProof(); // Thrown when the provided Merkle proof is invalid
error SA__InvalidSignature(); // Thrown when the provided ECDSA signature is invalid
error SA__ZeroAddress();
error SA__ZeroAmount();
//Not accessing Snow or Snowman for this test, only testing encoding in `getMessageHash`
// >>> VARIABLES
address[] private s_claimers; // array to store addresses of claimers
bytes32 private immutable i_merkleRoot; // Merkle root used to validate airdrop claims
//Snow private immutable i_snow; // Snow token to be staked for the airdrop
//Snowman private immutable i_snowman; // Snowman nft to be claimed
mapping(address => bool) private s_hasClaimedSnowman;
constructor(address snowToken) { // Placeholder constructor for deploying test contract
i_snow = MockSnowToken(snowToken);
}
// BROKEN LINE
bytes32 private constant MESSAGE_TYPEHASH = keccak256("SnowmanClaim(addres receiver, uint256 amount)");
// CORRECTED LINE
bytes32 private constant CORRECT_MESSAGE_TYPEHASH = keccak256("SnowmanClaim(address receiver, uint256 amount)");
// Prepare CORRECT_MESSAGE_TYPEHASH for comparison
function getCorrectMessageHash(address receiver) internal view returns (bytes32) {
if (i_snow.balanceOf(receiver) == 0) {
revert SA__ZeroAmount();
}
uint256 amount = i_snow.balanceOf(receiver);
// get encoded version of CORRECT_MESSAGE_TYPEHASH
return keccak256(abi.encode(CORRECT_MESSAGE_TYPEHASH, SnowmanClaim({receiver: receiver, amount: amount})));
}
// Prepare MESSAGE_TYPEHASH for comparison
function getMessageHash(address receiver) internal view returns (bytes32) {
if (i_snow.balanceOf(receiver) == 0) {
revert SA__ZeroAmount();
}
uint256 amount = i_snow.balanceOf(receiver);
// get encoded version of the mispelled line
return keccak256(abi.encode(MESSAGE_TYPEHASH, SnowmanClaim({receiver: receiver, amount: amount})));
}
// compare bytes32 representation of MESSAGE_TYPEHASH and CORRECT_MESSAGE_TYPEHASH
function isPassing(address receiver) public view {
require(keccak256(abi.encodePacked(getMessageHash(receiver))) == keccak256(abi.encodePacked(getCorrectMessageHash(receiver))), "Values Do Not Match");
}
}
// Placeholder mock token to pass nonzero balance checks
contract MockSnowToken {
mapping(address => uint256) private balances;
function balanceOf(address account) external view returns (uint256) {
return balances[account];
}
function setBalance(address account, uint256 amount) external {
balances[account] = amount;
}
}
```

PROCESS:
To compare the Correct and incorrect typehash I made a function isPassing() which takes the correct and incorrect typehash and identically keccak256 encodes them into a bytes32 representation, this bytes32 representation is then compared in the require() statement which will throw an error and cancel the request if the two typehash's don't match.

Even if i rebuilt the merkle tree from the provided flakes I would still have a discrepency between any bytes32 values that we're encoded from MESSAGE_TYPEHASH and those that were encoded from CORRECT_MESSAGE_TYPEHASH

Recommended Mitigation:

Fix the typo in the line below, replace:

-bytes32 private constant MESSAGE_TYPEHASH = keccak256("SnowmanClaim(addres receiver, uint256 amount)");
With:
+bytes32 private constant MESSAGE_TYPEHASH = keccak256("SnowmanClaim(address receiver, uint256 amount)");
Updates

Lead Judging Commences

yeahchibyke Lead Judge 3 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Inconsistent MESSAGE_TYPEHASH with standard EIP-712 declaration

A typo in the `MESSAGE_TYPEHASH` variable of the `SnowmanAirdrop` contract will prevent signature verification claims. Used `addres` instead of `address`

Support

FAQs

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