0

Can anyone help me figure out where I'm going wrong? Using ECDSA, I'm checking whether the address claiming the airdrop is a valid address or not. Using 4 of my addresses in hardhat localhost, I created a merkle tree, and then wrote test for the contract but it is failing most likely because the recovered address from ECDSA and the user's address is not matching.

Here is my Merkle Airdrop contract

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.24;

import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
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";

contract MerkleAirDrop is EIP712 {
    using ECDSA for bytes32;
    using SafeERC20 for IERC20;

    error MerkleAirDrop__InvalidProof();
    error MerkleAirDrop__AlreadyClaimed();
    error MerkleAirDrop__InvalidSignature();

    address[] claimers;
    
    bytes32 private immutable i_merkleRoot;
    IERC20 private immutable i_airdropToken;
    mapping (address claimer => bool claimed) private s_hasclaimed;

    bytes32 private constant MESSAGE_TYPEHASH = keccak256("AirDropClaim(address account,uint256 amount)");

    struct AirDropClaim {
        address account;
        uint256 amount;
    }

    event Claimed(address account, uint256 amount);

    constructor(bytes32 merkleRoot, IERC20 airdropToken) EIP712("MerkleAirDrop", "1") {
        i_merkleRoot = merkleRoot;
        i_airdropToken = airdropToken;
    }

    function claim(address account, uint256 amount, bytes32[] calldata merkleProof, uint8 v, bytes32 r, bytes32 s) external {
        if (s_hasclaimed[account]) {
            revert MerkleAirDrop__AlreadyClaimed();
        }

        if (!_isValidSignature(account, getMessageHash(account, amount), v, r, s)) {
            revert MerkleAirDrop__InvalidSignature();
        }

        bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(account, amount))));
        if (!MerkleProof.verify(merkleProof, i_merkleRoot, leaf)) {
            revert MerkleAirDrop__InvalidProof();
        }
        s_hasclaimed[account] = true;
        emit Claimed(account, amount);
        i_airdropToken.safeTransfer(account, amount);
    }

    function getMessageHash(address account, uint256 amount) public view returns(bytes32){
        return _hashTypedDataV4(
            keccak256(abi.encode(MESSAGE_TYPEHASH, AirDropClaim({account: account, amount: amount})))
        );
    } 

    function getMerkleRoot() external view returns (bytes32) {
        return i_merkleRoot;
    }

    function getAirDropToken() external view returns (IERC20) {
        return i_airdropToken;
    }

    function _isValidSignature(address account, bytes32 digest, uint8 v, bytes32 r, bytes32 s) internal pure returns(bool) {
        (address actualSigner, , ) = ECDSA.tryRecover(digest, v, r, s);
        return (actualSigner == account);
    }
}

Here is my makeMerkle.js

const fs = require('fs');
const keccak256 = require('keccak256');
const { MerkleTree } = require('merkletreejs');
const { ethers } = require("ethers");

async function main() {
    const inputPath = './scripts/target/input.json';
    const outputPath = './scripts/target/output.json';
    
    const inputData = JSON.parse(fs.readFileSync(inputPath, 'utf8'));
    const count = inputData.count;

    const leafs = [];
    const inputs = [];

    for (let i = 0; i < count; i++) {
        const account = inputData.values[i][0];
        const amount = inputData.values[i][1];
        
        const leaf = keccak256(
            ethers.utils.solidityPack(
                ['bytes32'],
                [keccak256(ethers.utils.defaultAbiCoder.encode(['address', 'uint256'], [account, amount]))]
            )
        );
        leafs.push(leaf);
        
        inputs.push(inputData.values[i]);
    }

    const merkleTree = new MerkleTree(leafs, keccak256, { sortPairs: true });
    const root = merkleTree.getRoot().toString('hex');

    console.log(`Merkle Root: 0x${root}`);

    const output = inputs.map((input, i) => {
        const proof = merkleTree.getHexProof(leafs[i]);
        
        return {
            inputs: [input['0'], input['1']],
            proof,
            root: `0x${root}`,
            leaf: `0x${leafs[i].toString('hex')}`
        };
    });

    fs.writeFileSync(outputPath, JSON.stringify(output, null, 2));
    console.log(`Merkle proof data written to: ${outputPath}`);
}

main()
    .then(() => process.exit(0))
    .catch((error) => {
        console.error(error);
        process.exit(1);
    });

and here is the test file

const { expect } = require("chai");
const { ethers } = require("hardhat");

describe("MerkleAirDrop", function () {
  let airdrop, token, owner, gasPayer, user;
  const merkleRoot = '0xfb74e1a6f36e429e034de0ae290ff93edfa336d6e0d431cb241d4d98ceda2e6b';
  const amountToCollect = ethers.utils.parseEther("25");
  const amountToSend = amountToCollect.mul(4);
  const proofOne = '0xf884e61898c71567fd4f44aa020453ed544cb775949e2087043630858aa9e609';
  const proofOneBytes32Hex = ethers.utils.hexZeroPad(proofOne, 32);
  const proofTwo = '0xf19a9e842b5a96e6e829203e375dfae8688610006eff2ecee5b1d5171631c970';
  const proofTwoBytes32Hex = ethers.utils.hexZeroPad(proofTwo, 32);
  const proof = [proofOneBytes32Hex, proofTwoBytes32Hex];  

  before(async function () {
    [owner, gasPayer, user] = await ethers.getSigners();
    
    const OrionToken = await ethers.getContractFactory("OrionToken");
    token = await OrionToken.deploy();
    await token.deployed();

    const MerkleAirDrop = await ethers.getContractFactory("MerkleAirDrop");
    airdrop = await MerkleAirDrop.deploy(merkleRoot, token.address);
    await airdrop.deployed();

    await token.mint(owner.address, amountToSend);
    await token.transfer(airdrop.address, amountToSend);
  });

  it("Users can claim the correct amount of tokens", async function () {
    const startingBalance = await token.balanceOf(user.address);

    const messageHash = await airdrop.getMessageHash(user.address, amountToCollect);
    const messageHashBytes32 = ethers.utils.arrayify(messageHash); // Convert to byte array

    // Sign the hash
    const signature = await user.signMessage(messageHashBytes32);
    const sig = ethers.utils.splitSignature(signature);

    const { v, r, s } = sig;

    await airdrop.connect(gasPayer).claim(user.address, amountToCollect, proof, v, r, s);

    const endingBalance = await token.balanceOf(user.address);
    expect(endingBalance.sub(startingBalance)).to.equal(amountToCollect);
  });
});

And I am getting the following error while running npx hardhat test

1) MerkleAirDrop
       Users can claim the correct amount of tokens:
     Error: VM Exception while processing transaction: reverted with custom error 'MerkleAirDrop__InvalidSignature()'
    at MerkleAirDrop.claim (contracts/MerkleAirDrop.sol:44)       
    at EdrProviderWrapper.request (node_modules\hardhat\src\internal\hardhat-network\provider\provider.ts:428:41)
    at EthersProviderWrapper.send (node_modules\@nomiclabs\hardhat-ethers\src\ethers-provider-wrapper.ts:13:20)

1 Answer 1

0

I think instead of:

if (!_isValidSignature(account, getMessageHash(account, amount), v, r, s)) {
    revert MerkleAirDrop__InvalidSignature();
}

you should try:

bytes32 hash2 = MessageHashUtils.toEthSignedMessageHash(getMessageHash(account, amount));
if (!_isValidSignature(account, hash2, v, r, s)) {
    revert MerkleAirDrop__InvalidSignature();
}

MessageHashUtils is another package from OZ: import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";.

This way you add "\x19Ethereum Signed Message:\n" at the beginning of the message to verify, since ethers does the same: https://docs.ethers.org/v5/api/signer/#Signer-signMessage


Regarding the invalid proof error, this is what I suggest doing:

  1. Add the leaf as argument to the error, so you can see if the error is caused by getting a wrong value for leaf or something else:
    error MerkleAirDrop__InvalidProof(bytes32 leaf);
  1. I think doing a bytes.concat() is wrong here, so you should remove it:
        bytes32 leaf = keccak256((keccak256(abi.encode(account, amount))));
        if (!MerkleProof.verify(merkleProof, i_merkleRoot, leaf)) {
            revert MerkleAirDrop__InvalidProof(leaf);
        }
8
  • that seems to solve my issue but now I'm getting invalid proof error :)
    – Klaus
    Commented Oct 23 at 11:55
  • @Klaus I edited the answer trying to solve the new error. Let me know if now it works :)
    – 0xSanson
    Commented Oct 23 at 14:44
  • nope it's not working, I tried changing the leaf expression in the makemerkle script to const leaf = keccak256(ethers.utils.defaultAbiCoder.encode(['address', 'uint256'], [account, amount])); still no luck
    – Klaus
    Commented Oct 23 at 16:39
  • That leaf should correspond to keccak256(abi.encode(account, amount)) in solidity. Check from the error if it they are the same, cause then the error might be in the proof generation/verification
    – 0xSanson
    Commented Oct 23 at 19:01
  • yes the leaf node in solidity matches with one of the leaf nodes in output.json
    – Klaus
    Commented Oct 24 at 6:10

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.