-
Notifications
You must be signed in to change notification settings - Fork 63
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* draft factory allowlist * add tests, fix bugs * add mint test * add buildship ascii
- Loading branch information
Showing
5 changed files
with
394 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
49
contracts/extensions/allowlist-factory/AllowlistFactory.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
|
||
} | ||
} |
36 changes: 36 additions & 0 deletions
36
contracts/extensions/allowlist-factory/base/NFTExtensionUpgradeable.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
37 changes: 37 additions & 0 deletions
37
contracts/extensions/allowlist-factory/base/SaleControlUpgradeable.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
|
||
}); | ||
|
||
|
||
}); |