Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Ready for Audit] Create MerkleTreeAirdropForERC20 module #16

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions MerkleTreeAirdrop/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# MerkleTreeAirdrop

## Overview

Requires a list of wallet address and root hash which are the leaves and merkle proofs needed to verify that a given data exists for that address in the tree.
Contract acts as Airdrop for ERC20 and only sends out tokens on data verification i.e merkle proof and appropriate arguments used to build the merkle tree for that user.

## How to use

1. Prepare an ERC20 token as an airdrop token. You can use existing token or deploy a new ERC20 token on DApps page.
2. Prepare the whitelist and their drop amount, generate the merkle tree and output to the JSON file. Check JSON file format in `test/resources/airdrop/testMerkle.json`.
It contains the merkle root, total token amount for airdrop, individual token amounts and list of merkle proof.
3. Deploy `MerkleTreeAirdrop` contract with merkle root.
4. Transfer drop assets to this contract.
5. Beneficiaries can claim their drop tokens by `claim` function.
6. Only verified users can claim.

## Functions

### WRITE

#### isClaimed

Used to check if a merkle claim has been claimed from the merkle tree.

| name | type | description |
| :---- | :-----: | ----------------: |
| index | uint256 | merkle tree index |

#### claim

Claim drop tokens to account who would be verified through merkle tree.

| name | type | description |
| :---------- | :------: | ------------------------------: |
| index | uint256 | merkle tree index |
| account | address | account that is claiming tokens |
| amount | uint256 | amount of tokens to claim |
| merkleProof | byte32[] | merkle proofs |

### READ

#### isClaimed

Used to check if a merkle claim has been claimed from the merkle tree.

| name | type | description |
| :---- | :-----: | ---------------------: |
| index | uint256 | The index of the award |
49 changes: 49 additions & 0 deletions MerkleTreeAirdrop/contracts/MerkleDistributor.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./oz/MerkleProofUpgradeable.sol";

abstract contract MerkleDistributor {
bytes32 public merkleRoot;

// This is a packed array of booleans.
mapping(uint256 => uint256) private claimedBitMap;

function __MerkleDistributor_init(bytes32 _merkleRoot) internal {
merkleRoot = _merkleRoot;
}

/**
* @dev Used to check if a merkle claim has been claimed from the merkle tree.
* @param index The index of the award
*/
function isClaimed(uint256 index) public view returns (bool) {
uint256 claimedWordIndex = index / 256;
uint256 claimedBitIndex = index % 256;
uint256 claimedWord = claimedBitMap[claimedWordIndex];
uint256 mask = (1 << claimedBitIndex);
return claimedWord & mask == mask;
}

/**
* @dev Used to set that a merkle claim has been claimed.
* @param index The index of the award
*/
function _setClaimed(uint256 index) internal {
uint256 claimedWordIndex = index / 256;
uint256 claimedBitIndex = index % 256;
claimedBitMap[claimedWordIndex] =
claimedBitMap[claimedWordIndex] |
(1 << claimedBitIndex);
}

function _verifyClaim(
bytes32[] calldata merkleProof,
bytes32 node
) internal view {
require(
MerkleProofUpgradeable.verify(merkleProof, merkleRoot, node),
"MerkleDistributor: Invalid proof"
);
}
}
56 changes: 56 additions & 0 deletions MerkleTreeAirdrop/contracts/MerkleTreeAirdrop.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

import {MerkleDistributor} from "./MerkleDistributor.sol";
import {IBunzz} from "./interfaces/IBunzz.sol";

contract MerkleTreeAirdrop is IBunzz, Ownable, MerkleDistributor {
using SafeERC20 for IERC20;

event Claimed(uint256 index, address account, uint256 amount);

address public token;

constructor(bytes32 merkleRoot_) {
__MerkleDistributor_init(merkleRoot_);
}

function connectToOtherContracts(
address[] calldata contracts
) external override onlyOwner {
require(contracts.length > 0, "No contract to connect");
require(token != address(0x0), "Token address was already set");
token = contracts[0];
}

/**
* @dev Claims a token distribution
* @param index The merkle tree index
* @param account The account that is claiming tokens
* @param amount The amount of tokens to claim
* @param merkleProof The merkle proofs
*/
function claim(
uint256 index,
address account,
uint256 amount,
bytes32[] calldata merkleProof
) external {
require(!isClaimed(index), "Tokens already claimed.");

// Verify the merkle proof.
bytes32 node = keccak256(abi.encodePacked(index, account, amount));
_verifyClaim(merkleProof, node);

// Mark it claimed and send the token.
_setClaimed(index);

IERC20(token).safeTransfer(account, amount);

emit Claimed(index, account, amount);
}
}
6 changes: 6 additions & 0 deletions MerkleTreeAirdrop/contracts/interfaces/IBunzz.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

interface IBunzz {
function connectToOtherContracts(address[] calldata contracts) external;
}
13 changes: 13 additions & 0 deletions MerkleTreeAirdrop/contracts/mock/MockERC20.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract MockERC20 is Ownable, ERC20 {
constructor(string memory name, string memory symbol) ERC20(name, symbol) {}

function mintTo(address account, uint256 amount) public onlyOwner {
_mint(account, amount);
}
}
Loading