Skip to content

Instantly share code, notes, and snippets.

@CQBinh
Last active October 25, 2022 03:38
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save CQBinh/839391083b9af3c7eddd2f63b858d545 to your computer and use it in GitHub Desktop.
Save CQBinh/839391083b9af3c7eddd2f63b858d545 to your computer and use it in GitHub Desktop.
Airdrop contract distributor by merkle tree vs ECDSA signature
import merkleTree from './merkleTree'
import signatureGenerator from './signatureGenerator'
merkleTree.genRootHash()
const address = '0x0737BEf0f49abCf4A62d480A4fFcE1681f90daEE'
const path = merkleTree.getMerklePath(address)
console.log('path', path)
const { signature } = await signatureGenerator.getSignatureAirdrop(address)
console.log('signature', signature)
import Web3 from 'web3'
import { whiteList } from './whiteList'
const web3Provider = new Web3.providers.HttpProvider('https://bsc-dataseed1.binance.org:443')
const web3 = new Web3(web3Provider)
const merkleTree = {
leaves: null,
root: null
}
function genRootHash() {
const leaves = genLeaveHashes(whiteList)
merkleTree.leaves = leaves
merkleTree.root = buildMerkleTree(leaves)
console.log('rootHash', merkleTree.root.hash)
return merkleTree.root.hash
}
function genLeaveHashes(chunks) {
const leaves = []
chunks.forEach((data) => {
const hash = buildHash(data)
const node = {
hash,
parent: null,
}
leaves.push(node)
})
return leaves
}
function buildMerkleTree(leaves) {
const numLeaves = leaves.length
if (numLeaves === 1) {
return leaves[0]
}
const parents = []
let i = 0
while (i < numLeaves) {
const leftChild = leaves[i]
const rightChild = i + 1 < numLeaves ? leaves[i + 1] : leftChild
parents.push(createParent(leftChild, rightChild))
i += 2
}
return buildMerkleTree(parents)
}
function createParent(leftChild, rightChild) {
const hash = leftChild.hash < rightChild.hash ? buildHash(leftChild.hash, rightChild.hash) : buildHash(rightChild.hash, leftChild.hash)
const parent = {
hash,
parent: null,
leftChild,
rightChild
}
leftChild.parent = parent
rightChild.parent = parent
return parent
}
function buildHash(...data) {
return web3.utils.soliditySha3(...data)
}
function getMerklePath(data) {
const hash = buildHash(data)
for (let i = 0; i < merkleTree.leaves.length; i += 1) {
const leaf = merkleTree.leaves[i]
if (leaf.hash === hash) {
return generateMerklePath(leaf)
}
}
}
function generateMerklePath(node, path = []) {
if (node.hash === merkleTree.root.hash) {
return path
}
const isLeft = (node.parent.leftChild === node)
if (isLeft) {
path.push(node.parent.rightChild.hash)
} else {
path.push(node.parent.leftChild.hash)
}
return generateMerklePath(node.parent, path)
}
function verifyPath(data, path) {
let hash = buildHash(data)
for (let i = 0; i < path.length; i += 1) {
hash = hash < path[i] ? buildHash(hash, path[i]) : buildHash(path[i], hash)
}
return hash === merkleTree.root.hash
}
export default {
genRootHash,
getMerklePath,
verifyPath
}
import Web3 from 'web3'
import theAirdropABI from '../cross-env/abis/theAirdrop'
const web3Provider = new Web3.providers.HttpProvider('https://bsc-dataseed1.binance.org:443')
const web3 = new Web3(web3Provider)
const airdropAddress = '0x...'
const airdropContract = new web3.eth.Contract(theAirdropABI, airdropAddress)
function signMessageAirdrop(message) {
const params = [
{ name: 'user', type: 'address' },
{ name: 'nonce', type: 'uint256' }
]
const typedData = {
types: {
EIP712Domain: [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' }
],
Airdrop: params,
},
primaryType: 'Airdrop',
domain: {
name: 'Binh CAO',
version: '1',
chainId: '1',
verifyingContract: airdropAddress
},
message
}
const privateKey = Buffer.from(config.accounts.contractAdmin.key, 'hex')
const messageFromData = getMessage(typedData, true)
const { r, s, v } = ecsign(messageFromData, privateKey)
return `0x${r.toString('hex')}${s.toString('hex')}${v.toString(16)}`
}
async function getSignatureAirdrop(user) {
const nonce = await getNonceOfAirdropContract(user)
const signature = signMessageAirdrop({
user,
nonce,
})
return {
signature
}
}
async function getNonceOfAirdropContract(userAddress) {
return parseInt(await airdropContract.methods.nonces(userAddress).call())
}
export default {
getSignatureAirdrop
}
[
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "address",
"name": "_address",
"type": "address"
}
],
"name": "Claimed",
"type": "event"
},
{
"inputs": [],
"name": "admin",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "nonces",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"inputs": [],
"name": "rootHash",
"outputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"inputs": [
{
"internalType": "string",
"name": "_name",
"type": "string"
},
{
"internalType": "string",
"name": "_version",
"type": "string"
}
],
"name": "initialize",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32[]",
"name": "_path",
"type": "bytes32[]"
}
],
"name": "claimByMerklePath",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes",
"name": "_signature",
"type": "bytes"
}
],
"name": "claimBySignature",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]
// SPDX-License-Identifier: MIT
pragma solidity 0.8.0;
import "@openzeppelin/contracts-upgradeable/utils/cryptography/ECDSAUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/utils/cryptography/draft-EIP712Upgradeable.sol";
import "../libs/fota/MerkelProof.sol";
contract TheAirdropDistributor is EIP712Upgradeable {
bytes32 public rootHash;
mapping(address => bool) claimMerkleTreeMarker;
address public admin;
mapping (address => uint) public nonces;
mapping(address => bool) claimSignatureMarker;
event Claimed(address _address);
function initialize(
string memory _name,
string memory _version
) public initializer {
admin = msg.sender;
rootHash = 0x4d690d7133a91f3a87725794a4532041be28f4eedb8e4305eafe73c6d732b390;
EIP712Upgradeable.__EIP712_init(_name, _version);
}
function updateRootHash(bytes32 _rootHash) external {
require(msg.sender == admin, "401");
rootHash = _rootHash;
}
function claimByMerklePath(bytes32[] calldata _path) external {
require(!claimMerkleTreeMarker[msg.sender], 'AirdropDistributor: Drop already claimed.');
bytes32 hash = keccak256(abi.encodePacked(msg.sender));
require(MerkleProof.verify(_path, rootHash, hash), 'AirdropDistributor: 400');
claimMerkleTreeMarker[msg.sender] = true;
// TODO: distribute the airdrop to user
emit Claimed(msg.sender);
}
function claimBySignature(bytes memory _signature) external {
require(!claimSignatureMarker[msg.sender], 'AirdropDistributor: Drop already claimed.');
bytes32 digest = _hashTypedDataV4(keccak256(abi.encode(
keccak256("Airdrop(address user,uint256 nonce)"),
msg.sender,
nonces[msg.sender]
)));
nonces[msg.sender]++;
address signer = ECDSAUpgradeable.recover(digest, _signature);
require(signer == admin, "MessageVerifier: invalid signature");
require(signer != address(0), "ECDSAUpgradeable: invalid signature");
claimSignatureMarker[msg.sender] = true;
// TODO: distribute the airdrop to user
emit Claimed(msg.sender);
}
}
export const whiteList = [
'0xA1A2EE28Ef70A03864824866b6919c8E6B90c3cD',
'0xDd1c91fB83412966068E502B289b4AF2eF5362Df',
'0xd1880fB67cDbB27cE14BC4B1A1f718e308be4aDf',
'0x2C11506fdc4729914272EB3a5CAf41Ac217Ed2bF',
'0x0737BEf0f49abCf4A62d480A4fFcE1681f90daEE'
]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment