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)