Skip to content

Commit

Permalink
Merge pull request #21 from javier123454321/feature/#20-airdrop-liqui…
Browse files Browse the repository at this point in the history
…dity

Feature/#20 airdrop liquidity
  • Loading branch information
javier123454321 authored Jan 3, 2022
2 parents f3cb864 + 0ffd82a commit 924986d
Show file tree
Hide file tree
Showing 3 changed files with 201 additions and 36 deletions.
54 changes: 47 additions & 7 deletions contracts/SimpleToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,14 @@ contract SimpleToken is ERC20, AccessControl {

struct Airdrop {
bytes32 merkleRoot;
bool isComplete;
uint256 claimPeriodEnds;
BitMaps.BitMap claimed;
}
event MerkleRootChanged(bytes32 merkleRoot);
event NewAirdrop(uint256 index, bytes32 merkleRoot, uint256 claimPeriod);
event Claimed(address claimant, uint256 amount);
event AirdropComplete(uint256 index);
event Sweep(address destination, uint256 amount);

uint256 public numberOfAirdrops = 0;
mapping (uint => Airdrop) airdrops;
Expand Down Expand Up @@ -54,22 +58,32 @@ contract SimpleToken is ERC20, AccessControl {
}

function getInitialSupply() public view returns (uint256) {
return initialSupply;
return initialSupply;
}

function newAirdrop(bytes32 _merkleRoot) public onlyRole(DEFAULT_ADMIN_ROLE) returns (uint256 airdropId) {
function newAirdrop(bytes32 _merkleRoot, uint256 _timeLimit) public onlyRole(DEFAULT_ADMIN_ROLE) returns (uint256 airdropId) {
airdropId = numberOfAirdrops;
if(numberOfAirdrops > 0) {
require(airdrops[numberOfAirdrops - 1].isComplete, "Airdrop currently active, creation failed");
}
Airdrop storage _drop = airdrops[airdropId];
_drop.merkleRoot = _merkleRoot;
emit MerkleRootChanged(_merkleRoot);
_drop.claimPeriodEnds = block.timestamp + _timeLimit;
emit NewAirdrop(airdropId, _merkleRoot, _drop.claimPeriodEnds);
numberOfAirdrops += 1;
}

function isClaimed(uint256 airdropIndex, uint256 claimIndex) public view returns (bool) {
return airdrops[airdropIndex].claimed.get(claimIndex);
}

function claimTokens(uint256 airdropIndex, uint256 claimAmount, bytes32[] calldata merkleProof) external {
/**
* @dev Uses merkle proofs to verify that the amount is equivalent to the user's claim
* @param claimAmount this must be calculated off chain and can be verified with the merkleProof
* @param merkleProof calculated using MerkleProof.js
*/
function claimTokens(uint256 claimAmount, bytes32[] calldata merkleProof) external {
uint256 airdropIndex = numberOfAirdrops - 1;
bytes32 leaf = keccak256(abi.encodePacked(msg.sender, claimAmount));
(bool valid, uint256 claimIndex) = MerkleProof.verify(merkleProof, airdrops[airdropIndex].merkleRoot, leaf);
require(valid, "Failed to verify proof");
Expand All @@ -81,7 +95,33 @@ contract SimpleToken is ERC20, AccessControl {
_transfer(address(this), msg.sender, claimAmount);
}

function getAirdropInfo(uint256 _index) public view returns (bytes32) {
return airdrops[_index].merkleRoot;
function getAirdropInfo(uint256 _index) public view returns (bytes32 root, uint256 claimPeriodEnds, bool isComplete) {
root = airdrops[_index].merkleRoot;
isComplete = airdrops[_index].isComplete;
claimPeriodEnds = airdrops[_index].claimPeriodEnds;
}


/**
* @dev Requires claimPeriod of airdrop to have finished
*/
function completeAirdrop() external {
require(numberOfAirdrops > 0, "No airdrops active");
uint256 claimPeriodEnds = airdrops[numberOfAirdrops - 1].claimPeriodEnds;
require(block.timestamp > claimPeriodEnds, "Airdrop claim period still active");
airdrops[numberOfAirdrops - 1].isComplete = true;
emit AirdropComplete(numberOfAirdrops - 1);
}

/**
* @dev Requires last airdrop to have finished
* @param _destination to sweep funds in the contract to
*/
function sweepTokens(address _destination) external onlyRole(DEFAULT_ADMIN_ROLE) {
require(numberOfAirdrops > 0, "No airdrops active");
require(airdrops[numberOfAirdrops - 1].isComplete, "Cannot sweep until airdrop is finished");
uint256 amountToSweep = balanceOf(address(this));
_transfer(address(this), _destination, amountToSweep);
emit Sweep(_destination, amountToSweep);
}
}
47 changes: 47 additions & 0 deletions contracts/utils/MerkleProof.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// SPDX-License-Identifier: MIT
// Modified from https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.3.0/contracts/utils/cryptography/MerkleProof.sol

pragma solidity ^0.8.0;

/**
* @dev These functions deal with verification of Merkle Trees proofs.
*
* The proofs can be generated using the JavaScript library
* https://github.com/miguelmota/merkletreejs[merkletreejs].
* Note: the hashing algorithm should be keccak256 and pair sorting should be enabled.
*
* See `test/utils/cryptography/MerkleProof.test.js` for some examples.
*/
library MerkleProof {
/**
* @dev Returns true if a `leaf` can be proved to be a part of a Merkle tree
* defined by `root`. For this, a `proof` must be provided, containing
* sibling hashes on the branch from the leaf to the root of the tree. Each
* pair of leaves and each pair of pre-images are assumed to be sorted.
*/
function verify(
bytes32[] memory proof,
bytes32 root,
bytes32 leaf
) internal pure returns (bool, uint256) {
bytes32 computedHash = leaf;
uint256 index = 0;

for (uint256 i = 0; i < proof.length; i++) {
index *= 2;
bytes32 proofElement = proof[i];

if (computedHash <= proofElement) {
// Hash(current computed hash + current element of the proof)
computedHash = keccak256(abi.encodePacked(computedHash, proofElement));
} else {
// Hash(current element of the proof + current computed hash)
computedHash = keccak256(abi.encodePacked(proofElement, computedHash));
index += 1;
}
}

// Check if the computed hash (root) is equal to the provided root
return (computedHash == root, index);
}
}
136 changes: 107 additions & 29 deletions test/SimpleToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,77 +84,155 @@ describe("SimpleToken", () => {
].map((baseNode: (String | BigNumber)[]) => ethers.utils.solidityKeccak256(['address', 'uint256'], [baseNode[0], baseNode[1]]))
merkleTree = new MerkleTree(leaves, keccak_256, { sort: true })
})
const setMerkleRoot = async () => {
const root = merkleTree.getHexRoot()
await simpleToken.connect(admin1).newAirdrop(root, BigNumber.from("100000000000"))
}

it("should allow admin to set merkle Tree Root for airdrops", async () => {
const root = merkleTree.getHexRoot()
await expect(
simpleToken.connect(addresses[8]).newAirdrop(root)
simpleToken.connect(addresses[8]).newAirdrop(root, BigNumber.from("100000000000"))
).to.be.revertedWith(
"AccessControl: account 0xfabb0ac9d68b0b445fb7357272ff202c5651694a is missing role 0x0000000000000000000000000000000000000000000000000000000000000000"
);
await expect(await simpleToken.connect(admin1).newAirdrop(root)).to.be.ok;
})

it("should create an airdrop with a new index each time 'newAirdrop' is called", async () => {
const root = merkleTree.getHexRoot()
await expect(await simpleToken.numberOfAirdrops()).to.equal(BigNumber.from("0"));
await simpleToken.connect(admin1).newAirdrop(root)
await expect(await simpleToken.numberOfAirdrops()).to.equal(BigNumber.from("1"));
await simpleToken.connect(admin2).newAirdrop(root)
await expect(await simpleToken.numberOfAirdrops()).to.equal(BigNumber.from("2"));
expect(await simpleToken.connect(admin1).newAirdrop(root, BigNumber.from("100000000000"))).to.be.ok;
})

it("should emit a MerkleRootChanged Event on setting", async () => {
const root = merkleTree.getHexRoot()
await ethers.provider.send("evm_mine", [200000000000]);
await expect(
simpleToken.connect(admin1).newAirdrop(root))
.to.emit(simpleToken, 'MerkleRootChanged')
.withArgs(root);
simpleToken.connect(admin1).newAirdrop(root, BigNumber.from("100000000000")))
.to.emit(simpleToken, 'NewAirdrop')
.withArgs(0, root, BigNumber.from("300000000001"));
})

it("should allow people to claim their alloted tokens", async () => {
const root = merkleTree.getHexRoot()
await simpleToken.connect(admin1).newAirdrop(root)
await setMerkleRoot();

const expectedBalance = BigNumber.from("1000000000000000000000")
const leaf = ethers.utils.solidityKeccak256(['address', 'uint256'], [await addresses[0].getAddress(), expectedBalance])
const proof = merkleTree.getHexProof(leaf)
await simpleToken.connect(addresses[0]).claimTokens(0, expectedBalance, proof);
await expect(await simpleToken.balanceOf(await addresses[0].getAddress())).to.equal(expectedBalance);
await simpleToken.connect(addresses[0]).claimTokens(expectedBalance, proof);
expect(await simpleToken.balanceOf(await addresses[0].getAddress())).to.equal(expectedBalance);
})

it("should emit a Claimed event when claiming airdropped tokens", async () => {
const root = merkleTree.getHexRoot()
await simpleToken.connect(admin1).newAirdrop(root)
await setMerkleRoot();

const expectedBalance = BigNumber.from("1000000000000000000000")
const leaf = ethers.utils.solidityKeccak256(['address', 'uint256'], [await addresses[0].getAddress(), expectedBalance])
const proof = merkleTree.getHexProof(leaf)
await expect(simpleToken.connect(addresses[0]).claimTokens(0, expectedBalance, proof))
await expect(simpleToken.connect(addresses[0]).claimTokens(expectedBalance, proof))
.to.emit(simpleToken, 'Claimed')
.withArgs(await addresses[0].getAddress(), expectedBalance);
})

it("should only allow an address to claim their alloted tokens once", async () => {
const root = merkleTree.getHexRoot()
await simpleToken.connect(admin1).newAirdrop(root)
await setMerkleRoot();

const expectedBalance = BigNumber.from("1000000000000000000000")
const leaf = ethers.utils.solidityKeccak256(['address', 'uint256'], [await addresses[0].getAddress(), expectedBalance])
const proof = merkleTree.getHexProof(leaf)
await simpleToken.connect(addresses[0]).claimTokens(0, expectedBalance, proof);
await simpleToken.connect(addresses[0]).claimTokens(expectedBalance, proof);
await expect(
simpleToken.connect(addresses[0]).claimTokens(0, expectedBalance, proof)
simpleToken.connect(addresses[0]).claimTokens(expectedBalance, proof)
).to.be.revertedWith("Tokens already claimed for this airdrop");
await expect(await simpleToken.balanceOf(await addresses[0].getAddress())).to.equal(expectedBalance);
expect(await simpleToken.balanceOf(await addresses[0].getAddress())).to.equal(expectedBalance);
})

it("should allow you to get the airdrop information", async () => {
await setMerkleRoot()

const { root } = await simpleToken.connect(addresses[0]).getAirdropInfo(0);
expect(root).to.equal(merkleTree.getHexRoot());
})
})
describe("Sweep", async () => {
let merkleTree: MerkleTree
beforeEach(async () => {
await setupSimpleToken()
const leaves = [
[await addresses[0].getAddress(), BigNumber.from("1000000000000000000000")],
[await addresses[1].getAddress(), BigNumber.from("2000000000000000000000")],
[await addresses[2].getAddress(), BigNumber.from("2000000000000000000000")],
].map((baseNode: (String | BigNumber)[]) => ethers.utils.solidityKeccak256(['address', 'uint256'], [baseNode[0], baseNode[1]]))
merkleTree = new MerkleTree(leaves, keccak_256, { sort: true })
const root = merkleTree.getHexRoot()
await simpleToken.connect(admin1).newAirdrop(root)
await simpleToken.connect(admin1).newAirdrop(root, BigNumber.from("100000000000"))
})
it("should set isComplete to false on creation", async () => {
const { isComplete } = await simpleToken.connect(addresses[0]).getAirdropInfo(0);
expect(isComplete).to.equal(false);
})

const airdropInfo = await simpleToken.connect(addresses[0]).getAirdropInfo(0);
await expect(airdropInfo).to.equal(root);
it("should not allow you to create a new airdrop if the previous one is not finished", async () => {
await expect(
simpleToken.connect(admin1).newAirdrop(merkleTree.getHexRoot(), BigNumber.from("100000000000"))
).to.be.revertedWith("Airdrop currently active, creation failed");
})

it("should only allow you to finish an airdrop if the claimperiod has ended", async () => {
await expect(
simpleToken.connect(admin1).completeAirdrop()
).to.be.revertedWith("Airdrop claim period still active");

await ethers.provider.send("evm_increaseTime", [100000000010])
await ethers.provider.send("evm_mine", [])
await expect(
simpleToken.connect(admin1).completeAirdrop()
).to.be.ok
})

it("should emit an AirdropComplete event when completing an airdrop", async () => {
await ethers.provider.send("evm_increaseTime", [100000000010])
await ethers.provider.send("evm_mine", [])
await expect(
simpleToken.connect(admin1).completeAirdrop()
).to.emit(simpleToken, 'AirdropComplete')
.withArgs(0);
})

it("should create an airdrop with a new index each time 'newAirdrop' is called", async () => {
const root = merkleTree.getHexRoot()
expect(await simpleToken.numberOfAirdrops()).to.equal(BigNumber.from("1"));

await ethers.provider.send("evm_increaseTime", [100000000010])
await ethers.provider.send("evm_mine", [])
await simpleToken.connect(admin1).completeAirdrop()

await simpleToken.connect(admin1).newAirdrop(root, BigNumber.from("0"))
expect(await simpleToken.numberOfAirdrops()).to.equal(BigNumber.from("2"));

await simpleToken.connect(admin1).completeAirdrop()

await simpleToken.connect(admin2).newAirdrop(root, BigNumber.from("0"))
await expect(await simpleToken.numberOfAirdrops()).to.equal(BigNumber.from("3"));
})

it("should allow you to sweep the leftover funds if no airdrop is running", async () => {
await expect(
simpleToken.connect(admin1).sweepTokens(await admin1.getAddress())
).to.be.revertedWith("Cannot sweep until airdrop is finished");

await ethers.provider.send("evm_increaseTime", [100000000010])
await ethers.provider.send("evm_mine", [])
await simpleToken.connect(admin1).completeAirdrop()

expect(await simpleToken.balanceOf(await admin1.getAddress())).to.equal(BigNumber.from(0));
await simpleToken.connect(admin1).sweepTokens(await admin1.getAddress());
expect(await simpleToken.balanceOf(await admin1.getAddress())).to.equal(AIRDROP_SUPPLY);
})

it("should emit a Sweep event when sweeping funds to an address", async () => {
await ethers.provider.send("evm_increaseTime", [100000000010])
await ethers.provider.send("evm_mine", [])
await simpleToken.connect(admin1).completeAirdrop()
await expect(
simpleToken.connect(admin1).sweepTokens(await admin1.getAddress())
).to.emit(simpleToken, 'Sweep')
.withArgs(await admin1.getAddress(), AIRDROP_SUPPLY);
})
})
})

0 comments on commit 924986d

Please sign in to comment.