Skip to content

Commit

Permalink
Allowlist factory (#64)
Browse files Browse the repository at this point in the history
* draft factory allowlist

* add tests, fix bugs

* add mint test

* add buildship ascii
  • Loading branch information
caffeinum authored Aug 1, 2022
1 parent 1bfa961 commit d3a0f0c
Show file tree
Hide file tree
Showing 5 changed files with 394 additions and 0 deletions.
105 changes: 105 additions & 0 deletions contracts/extensions/allowlist-factory/Allowlist.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

// Want to launch your own collection?
// Check out https://buildship.xyz
//
// ,:loxO0KXXc
// ,cdOKKKOxol:lKWl
// ;oOXKko:, ;KNc
// 'ox0X0d: cNK,
// ',' ;xXX0x: dWk
// ,cdO0KKKKKXKo, ,0Nl
// ;oOXKko:,;kWMNl dWO'
// ,o0XKd:' oNMMK: cXX:
// 'ckNNk: ;KMN0c cXXl
// 'OWMMWKOdl;' cl; oXXc
// ;cclldxOKXKkl, ;kNO;
// ;cdk0kl' ;clxXXo
// ':oxo' c0WMMMMK;
// :l: lNMWXxOWWo
// '; :xdc' :XWd
// , cXK;
// ':, xXl
// ;: ' o0c
// ;c;,,,,' lx;
// ''' cc
// ,'

import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/utils/cryptography/MerkleProofUpgradeable.sol";

import "./base/SaleControlUpgradeable.sol";

import "./base/NFTExtensionUpgradeable.sol";

contract Allowlist is NFTExtensionUpgradeable, SaleControlUpgradeable {
uint256 public price;
uint256 public maxPerAddress;

bytes32 public whitelistRoot;

mapping(address => uint256) public claimedByAddress;

/// @custom:oz-upgrades-unsafe-allow constructor
constructor() initializer {}

function initialize(
address _nft,
bytes32 _whitelistRoot,
uint256 _price,
uint256 _maxPerAddress
) initializer public {
NFTExtensionUpgradeable.initialize(_nft);
SaleControlUpgradeable.initialize();

price = _price;
maxPerAddress = _maxPerAddress;
whitelistRoot = _whitelistRoot;
}

function updatePrice(uint256 _price) public onlyOwner {
price = _price;
}

function updateMaxPerAddress(uint256 _maxPerAddress) public onlyOwner {
maxPerAddress = _maxPerAddress;
}

function updateWhitelistRoot(bytes32 _whitelistRoot) public onlyOwner {
whitelistRoot = _whitelistRoot;
}

function mint(uint256 nTokens, bytes32[] memory proof)
external
payable
whenSaleStarted
{
require(
isWhitelisted(whitelistRoot, msg.sender, proof),
"Not whitelisted"
);

require(
claimedByAddress[msg.sender] + nTokens <= maxPerAddress,
"Cannot claim more per address"
);

require(msg.value >= nTokens * price, "Not enough ETH to mint");

claimedByAddress[msg.sender] += nTokens;

nft.mintExternal{value: msg.value}(nTokens, msg.sender, bytes32(0x0));
}

function isWhitelisted(
bytes32 root,
address receiver,
bytes32[] memory proof
) public pure returns (bool) {
bytes32 leaf = keccak256(abi.encodePacked(receiver));

return MerkleProofUpgradeable.verify(proof, root, leaf);
}
}
49 changes: 49 additions & 0 deletions contracts/extensions/allowlist-factory/AllowlistFactory.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import "@openzeppelin/contracts/proxy/Clones.sol";

import "./Allowlist.sol";

contract AllowlistFactory {

event ContractDeployed(
address indexed deployedAddress,
address indexed nft,
address indexed owner,
string title
);

address public immutable implementation;

constructor() {
implementation = address(new Allowlist());
}

function createAllowlist(
string memory title,
address nft,
bytes32 root,
uint256 price,
uint256 maxPerAddress,
bool startSale
) external returns (address) {

address payable clone = payable(Clones.clone(implementation));

Allowlist list = Allowlist(clone);

list.initialize(nft, root, price, maxPerAddress);

if (startSale) {
list.startSale();
}

list.transferOwnership(msg.sender);

emit ContractDeployed(clone, nft, msg.sender, title);

return clone;

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

import "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol";

import "../../../interfaces/INFTExtension.sol";
import "../../../interfaces/IMetaverseNFT.sol";

contract NFTExtensionUpgradeable is INFTExtension, ERC165Upgradeable {
IMetaverseNFT public nft;

function initialize(address _nft) internal onlyInitializing {
__ERC165_init();

nft = IMetaverseNFT(_nft);
}

function beforeMint() internal view {
require(
nft.isExtensionAdded(address(this)),
"NFTExtension: this contract is not allowed to be used as an extension"
);
}

function supportsInterface(bytes4 interfaceId)
public
view
virtual
override(IERC165, ERC165Upgradeable)
returns (bool)
{
return
interfaceId == type(INFTExtension).interfaceId ||
super.supportsInterface(interfaceId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";

abstract contract SaleControlUpgradeable is OwnableUpgradeable {
uint256 public constant __SALE_NEVER_STARTS = 2**256 - 1;

uint256 public startTimestamp;

function initialize() internal onlyInitializing {
__Ownable_init();

startTimestamp = __SALE_NEVER_STARTS;
}

modifier whenSaleStarted() {
require(saleStarted(), "Sale not started yet");
_;
}

function updateStartTimestamp(uint256 _startTimestamp) public onlyOwner {
startTimestamp = _startTimestamp;
}

function startSale() public onlyOwner {
startTimestamp = block.timestamp;
}

function stopSale() public onlyOwner {
startTimestamp = __SALE_NEVER_STARTS;
}

function saleStarted() public view returns (bool) {
return block.timestamp >= startTimestamp;
}
}
167 changes: 167 additions & 0 deletions test/allowlist.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import { expect } from "chai";
import { ethers } from "hardhat";
import { AllowlistFactory } from "../typechain-types";

const { parseEther } = ethers.utils;

describe("Allowlist Factory", () => {
let factory: AllowlistFactory;

beforeEach(async () => {
const f = await ethers.getContractFactory("AllowlistFactory")

factory = await f.deploy();
});

it("should deploy factory", async () => {
expect(factory.address).to.be.a("string");
})

it("should deploy contract", async function () {
const [owner, user1, user2] = await ethers.getSigners();

const nftAddress = user1.address;

const tx = await factory.connect(user2).createAllowlist(
"Test List",
nftAddress,
"0xbd204967d5ef69fe133d1e2e9509f68bf3ee681006804e37b0bd51a64aea0116",
parseEther("0.1"),
1,
true,
);

const res = await tx.wait();

const event = res.events?.find(e => e.event === "ContractDeployed")

expect(event).to.exist;

expect(event?.args?.nft).to.equal(nftAddress);

const contract = await ethers.getContractAt(
"Allowlist",
event?.args?.deployedAddress,
);

expect(event?.args?.title).to.equal("Test List");
expect(contract.address).to.equal(event?.args?.deployedAddress);

expect(await contract.owner()).to.equal(user2.address);
expect(await contract.nft()).to.equal(nftAddress);

expect(await contract.saleStarted()).to.equal(true);

await contract.connect(user2).startSale();

expect(await contract.saleStarted()).to.equal(true);
});

// it should mint successfully
it("should check proof validity", async function () {
const address = "0xffe06cb4807917bd79382981f23d16a70c102c3b"
const root = "0x01a190633cf36eb0a207f16c11b88f873b92aa9c248482ed87bae56fe75c6871"
const proof = [
"0x8b37442083367ac89eaf201381abfda29d6f9761782890715d67e7945771a467"
]

const [owner, user1, user2] = await ethers.getSigners();

const nftAddress = user1.address;

const tx = await factory.createAllowlist(
"Test List",
nftAddress,
root,
parseEther("0"),
1,
false,
);

const res = await tx.wait();

const event = res.events?.find(e => e.event === "ContractDeployed")

const contract = await ethers.getContractAt(
"Allowlist",
event?.args?.deployedAddress,
);

expect(await contract.whitelistRoot()).to.equal(root);
expect(await contract.isWhitelisted(root, address, proof)).to.equal(true);

expect(await contract.isWhitelisted(root, user2.address, proof)).to.equal(false);

});

// // it should mint successfully
it("should mint successfully", async function () {
const NFT = await ethers.getContractFactory("MetaverseBaseNFT");

const [ minter1, minter2, minter3 ] = await ethers.getSigners();

const root = "0xfbc2f54de92972c0f2c6bbd5003031662aa9b8240f4375dc03d3157d8651ec45"

const proof1 = [
"0x343750465941b29921f50a28e0e43050e5e1c2611a3ea8d7fe1001090d5e1436"
]
const proof2 = [
"0x8a3552d60a98e0ade765adddad0a2e420ca9b1eef5f326ba7ab860bb4ea72c94",
"0xe9707d0e6171f728f7473c24cc0432a9b07eaaf1efed6a137a4a8c12c79552d9"
]

const proof3 = [
"0x00314e565e0574cb412563df634608d76f5c59d9f817e85966100ec1d48005c0",
"0xe9707d0e6171f728f7473c24cc0432a9b07eaaf1efed6a137a4a8c12c79552d9"
]

const nft1 = await NFT.deploy(
parseEther("0.01"),
100,
1,
1,
100, // 1%
"Test NFT",
"TEST",
"https://example.com",
false,
);

const tx = await factory.createAllowlist(
"Test List",
nft1.address,
root,
parseEther("0"),
1, // max per address
true, // start sale
);

const res = await tx.wait();
const event = res.events?.find(e => e.event === "ContractDeployed")

const list = await ethers.getContractAt(
"Allowlist",
event?.args?.deployedAddress,
);

expect(await list.whitelistRoot()).to.equal(root);
expect(await list.isWhitelisted(root, minter1.address, proof1)).to.equal(true);

await nft1.addExtension(list.address);

await list.connect(minter1).mint(1, proof1);

expect(await nft1.balanceOf(minter1.address)).to.equal(1);

await list.connect(minter2).mint(1, proof2);

expect(await nft1.balanceOf(minter2.address)).to.equal(1);

await list.connect(minter3).mint(1, proof3);

expect(await nft1.balanceOf(minter3.address)).to.equal(1);

});


});

0 comments on commit d3a0f0c

Please sign in to comment.