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

Feat/distributors #16

Merged
merged 16 commits into from
Dec 5, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
feat(airdrop): distributors
  • Loading branch information
fakenickels committed Nov 24, 2021
commit 203244d8a2c750a57f6ca02a0663c36d5addfa80
90 changes: 90 additions & 0 deletions contracts/KittenHDRKittenMerkleDistributor.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.4;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
import "./KittensHD.sol";

// Allows anyone to claim a token if they exist in a merkle root.
interface IMerkleDistributor {
// Returns the address of the token distributed by this contract.
function rKittenAddress() external view returns (address);

// Returns the merkle root of the merkle tree containing account balances available to claim.
function merkleRoot() external view returns (bytes32);

// Returns true if the index has been marked claimed.
function isClaimed(uint256 index) external view returns (bool);

// Claim the given amount of the token to the given address. Reverts if the inputs are invalid.
function claim(
uint256 index,
uint256 amount,
bytes32[] calldata merkleProof
) external;

// This event is triggered whenever a call to #claim succeeds.
event Claimed(uint256 index, address account, uint256 amount);
}

contract KittenHDRKittenMerkleDistributor is IMerkleDistributor {
address public immutable override rKittenAddress;
bytes32 public immutable override merkleRoot;

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

constructor(address token_, bytes32 merkleRoot_) {
rKittenAddress = token_;
merkleRoot = merkleRoot_;
}

function isClaimed(uint256 index) public view override returns (bool) {
uint256 claimedWordIndex = index / 256;
uint256 claimedBitIndex = index % 256;
uint256 claimedWord = claimedBitMap[claimedWordIndex];
uint256 mask = (1 << claimedBitIndex);
return claimedWord & mask == mask;
}

function _setClaimed(uint256 index) private {
uint256 claimedWordIndex = index / 256;
uint256 claimedBitIndex = index % 256;
claimedBitMap[claimedWordIndex] =
claimedBitMap[claimedWordIndex] |
(1 << claimedBitIndex);
}

function claim(
uint256 index,
uint256 amount,
bytes32[] calldata merkleProof
) external override {
require(!isClaimed(index), "MerkleDistributor: Drop already claimed.");

// Verify the merkle proof.
bytes32 node = keccak256(abi.encodePacked(index, msg.sender, amount));
require(
MerkleProof.verify(merkleProof, merkleRoot, node),
"MerkleDistributor: Invalid proof."
);

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

uint256 currentTokenId = KittensHD(rKittenAddress).getRKittenClaimCounter();

// claim NFTs
KittensHD(rKittenAddress).daoRKITTENClaim(amount);

// transfer rkitten amount back to sender from address(this)
for (uint256 i = 0; i < amount; i++) {
KittensHD(rKittenAddress).safeTransferFrom(
address(this),
msg.sender,
currentTokenId + i
);
}

emit Claimed(index, msg.sender, amount);
}
}
40 changes: 40 additions & 0 deletions contracts/SpecialKittensDistributor.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import "@openzeppelin/contracts/access/Ownable.sol";
import "./HonoraryKittens.sol";
import "./KittensHD.sol";

contract SpecialKittensDistributor is Ownable {
HonoraryKittens honoraryKittens =
HonoraryKittens(0xE65469083B4f50d1EcD089584c671Bb1d23F9AC7);

KittensHD kittensHD = KittensHD(0xad956DF38D04A9A555E079Cf5f3fA59CB0a25DC9);

mapping(address => bool) public claims;

constructor() {}

function claim() public {
require(honoraryKittens.balanceOf(msg.sender) > 0);
require(!claims[msg.sender], "You've already claimed");

// check if the sender already minted a kitten hd due to the KittensHD contract bug
uint256 index = 0;
while (true) {
uint256 tokenId_ = kittensHD.tokenOfOwnerByIndex(msg.sender, index);
require(
// check if the honorary holder has already minted within the reserved range
tokenId_ < 667 && tokenId_ > 687,
"You've already claimed your free HD"
);
index++;
}

claims[msg.sender] = true;

// call daoClaim from KittensHD and transfer to msg.sender
uint256 tokenId = kittensHD.getGeneralMintCounter();
kittensHD.daoAnyClaim(1);
kittensHD.safeTransferFrom(address(this), msg.sender, tokenId);
}
}
36 changes: 36 additions & 0 deletions scripts/deploy_merkle_distributor_kittens_hd.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
const hre = require("hardhat");

async function main() {
const [deployer] = await hre.ethers.getSigners();
console.log("Deploying contracts with the account:", deployer.address);
console.log("Account balance:", (await deployer.getBalance()).toString());

const Contract = await hre.ethers.getContractFactory(
"KittenHDRKittenMerkleDistributor"
);
const contract = await Contract.deploy(
"0xad956DF38D04A9A555E079Cf5f3fA59CB0a25DC9",
"0xed5efa371ee21eb0bd68bfbd60b61e901f64efc17687ccb6c4e72537ab6fd84d"
);
await contract.deployed();

console.log("Deployed to:", contract.address);

if (process.env.CHAIN_SCAN_TOKEN) {
console.log("Verifying ze contract");
try {
await hre.run("verify:verify", {
address: contract.address,
});
} catch (e) {
console.log("Verification failed:", e);
}
}
}

main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
29 changes: 29 additions & 0 deletions scripts/deploy_special_kitten_distributor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
const hre = require("hardhat");

async function main() {
const [deployer] = await hre.ethers.getSigners();
console.log("Deploying contracts with the account:", deployer.address);
console.log("Account balance:", (await deployer.getBalance()).toString());

const Contract = await hre.ethers.getContractFactory(
"SpecialKittensDistributor"
);
const contract = await Contract.deploy();
await contract.deployed();

console.log("Deployed to:", contract.address);

if (process.env.CHAIN_SCAN_TOKEN) {
console.log("Verifying ze contract");
await hre.run("verify:verify", {
address: contract.address,
});
}
}

main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
60 changes: 60 additions & 0 deletions test/merkle_distributor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@

const chai = require("chai")
const expect = chai.expect
const { solidity } = require("ethereum-waffle");

chai.use(solidity)

describe("FantomKittens contract", function () {
let owner;
let depositAddress;

this.beforeAll(async () => {
[owner, depositAddress, someFucker] = await ethers.getSigners();

})

it("should mint a token properly and transfer amount to deposit address", async function () {
const depositAddressInitialBalance = await depositAddress.getBalance()
const Contract = await ethers.getContractFactory("FantomKittens");

const contract = await Contract.deploy();

await contract.setDepositAddress(await depositAddress.getAddress())

const receipt = await contract.claim({
value: ethers.utils.parseEther("4.2"),
}).catch(e => e.message)

expect(receipt).to.not.equal(`VM Exception while processing transaction: reverted with reason string 'Invalid amount'`)
expect(await depositAddress.getBalance()).to.equal(ethers.utils.parseEther("4.2").add(depositAddressInitialBalance))
expect(await contract.balanceOf(await owner.getAddress())).to.equal(1)
});


it("should not mint if user didn't send the right amount", async function () {
const Contract = await ethers.getContractFactory("FantomKittens");

const contract = await Contract.deploy();

await contract.setDepositAddress(await depositAddress.getAddress())

const receipt = await contract.claim({
value: ethers.utils.parseEther("2.0"),
}).catch(e => e.message)

expect(receipt).to.equal(`VM Exception while processing transaction: reverted with reason string 'Invalid amount'`)
});

it("only contract owner should change the deposit address", async function () {
const Contract = await ethers.getContractFactory("FantomKittens");

const contract = await Contract.deploy();

const contractFuckerSigner = contract.connect(someFucker)

const receipt = await contractFuckerSigner.setDepositAddress(await someFucker.getAddress()).catch(e => e.message)

expect(receipt).to.equal(`VM Exception while processing transaction: reverted with reason string 'Ownable: caller is not the owner'`)
});
});