Created
November 14, 2022 11:44
-
-
Save shakogegia/8a47012721d956595ab752c9283413c1 to your computer and use it in GitHub Desktop.
Stack Spaceships Smart Contract
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
/* | |
/ _____// |______ ____ | | __ | |
\_____ \\ __\__ \ _/ ___\| |/ / | |
/ \| | / __ \\ \___| < | |
/_______ /|__| (____ /\___ >__|_ \ | |
\/ \/ \/ \/ | |
_________ .__ .__ | |
/ _____/__________ ____ ____ _____| |__ |__|_____ ______ | |
\_____ \\____ \__ \ _/ ___\/ __ \ / ___/ | \| \____ \/ ___/ | |
/ \ |_> > __ \\ \__\ ___/ \___ \| Y \ | |_> >___ \ | |
/_______ / __(____ /\___ >___ >____ >___| /__| __/____ > | |
\/|__| \/ \/ \/ \/ \/ |__| \/ | |
*/ | |
// SPDX-License-Identifier: MIT | |
pragma solidity ^0.8.14; | |
import "@openzeppelin/contracts/utils/Counters.sol"; | |
import "@openzeppelin/contracts/access/Ownable.sol"; | |
import "@openzeppelin/contracts/utils/Base64.sol"; | |
import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; | |
import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; | |
contract StackSpaceships is ERC721, Ownable { | |
using Strings for uint; | |
using Counters for Counters.Counter; | |
enum SaleStatus { PAUSED, LIVE } | |
enum SaleStage { WHITELIST, PUBLIC, PRIVATE, FREE_MINT } | |
Counters.Counter private _tokenIds; | |
uint256 public startingIndex; | |
uint256 public startingIndexBlock; | |
string public contractURI; | |
uint public collectionSize = 4242; | |
uint public reservedAmount = 100; | |
uint public tokensPerPersonPrivateLimit = 5; | |
uint public tokensPerPersonWhitelistLimit = 5; | |
uint public tokensPerPersonPublicLimit = 5; | |
uint public tokensPerPersonFreeMintLimit = 1; | |
uint public whitelistMintPrice = 0.0777 ether; | |
uint public privateMintPrice = 0.0777 ether; | |
uint public publicMintPrice = 0.0999 ether; | |
address public crossmintAddress = 0xdAb1a1854214684acE522439684a145E62505233; | |
string public preRevealURL = "https://stackbrowser.com/api/nft/placeholder.json"; | |
string public baseURL; | |
bytes32 public whitelistMerkleRoot; | |
bytes32 public privateMerkleRoot; | |
bytes32 public freeMintMerkleRoot; | |
SaleStatus public privateSaleStatus = SaleStatus.PAUSED; | |
SaleStatus public whitelistSaleStatus = SaleStatus.PAUSED; | |
SaleStatus public publicSaleStatus = SaleStatus.PAUSED; | |
SaleStatus public freeMintSaleStatus = SaleStatus.PAUSED; | |
event Minted(address to, uint count, SaleStage stage); | |
mapping(address => uint) private _privateMintedCount; | |
mapping(address => uint) private _whitelistMintedCount; | |
mapping(address => uint) private _publicMintedCount; | |
mapping(address => uint) private _freeMintedCount; | |
constructor() ERC721("StackSpaceships", "STK"){} | |
/// @notice Update Contract-level metadata | |
function setContractURI(string memory _contractURI) external onlyOwner { | |
contractURI = string(abi.encodePacked( | |
"data:application/json;base64,", | |
Base64.encode( | |
bytes( | |
_contractURI | |
) | |
) | |
)); | |
} | |
/// @notice Update the whitelist merkle tree root | |
function setWhitelistMerkleRoot(bytes32 root) external onlyOwner { | |
whitelistMerkleRoot = root; | |
} | |
/// @notice Update the private merkle tree root | |
function setPrivateMerkleRoot(bytes32 root) external onlyOwner { | |
privateMerkleRoot = root; | |
} | |
/// @notice Update the free mint merkle tree root | |
function setFreeMintMerkleRoot(bytes32 root) external onlyOwner { | |
freeMintMerkleRoot = root; | |
} | |
/// @notice Update base url | |
function setBaseUrl(string calldata url) external onlyOwner { | |
require(startingIndex == 0, "Collection has been already revealed"); | |
baseURL = url; | |
} | |
/// @notice Set Pre Reveal URL | |
function setPreRevealUrl(string calldata url) external onlyOwner { | |
preRevealURL = url; | |
} | |
function totalSupply() external view returns (uint) { | |
return _tokenIds.current(); | |
} | |
/// @notice Update private sale stage | |
function setPrivateSaleStatus(SaleStatus status) external onlyOwner { | |
privateSaleStatus = status; | |
} | |
/// @notice Update whitelist sale stage | |
function setWhitelistSaleStatus(SaleStatus status) external onlyOwner { | |
whitelistSaleStatus = status; | |
} | |
/// @notice Update public sale stage | |
function setPublicSaleStatus(SaleStatus status) external onlyOwner { | |
publicSaleStatus = status; | |
} | |
/// @notice Update free mint sale stage | |
function setFreeMintSaleStatus(SaleStatus status) external onlyOwner { | |
freeMintSaleStatus = status; | |
} | |
/// @notice Update collection size | |
function setCollectionSize(uint size) external onlyOwner { | |
require(startingIndex == 0, "Collection has been already revealed"); | |
collectionSize = size; | |
} | |
/// @notice Update token limit per address | |
function setTokensPerPersonPublicLimit(uint limit) external onlyOwner { | |
tokensPerPersonPublicLimit = limit; | |
} | |
/// @notice Update token limit per whitelisted address | |
function setTokensPerPersonWhitelistLimit(uint limit) external onlyOwner { | |
tokensPerPersonWhitelistLimit = limit; | |
} | |
/// @notice Update token limit per private address | |
function setTokensPerPersonPrivateLimit(uint limit) external onlyOwner { | |
tokensPerPersonPrivateLimit = limit; | |
} | |
/// @notice Update token limit per free mint address | |
function setTokensPerPersonFreeMintLimit(uint limit) external onlyOwner { | |
tokensPerPersonFreeMintLimit = limit; | |
} | |
/// @notice Update reserved amount | |
function setReservedAmount(uint amount) external onlyOwner { | |
reservedAmount = amount; | |
} | |
/// @notice Update whitelist mint price | |
function setWhitelistMintPrice(uint price) external onlyOwner { | |
whitelistMintPrice = price; | |
} | |
/// @notice Update private mint price | |
function setPrivateMintPrice(uint price) external onlyOwner { | |
privateMintPrice = price; | |
} | |
/// @notice Update public mint price | |
function setPublicMintPrice(uint price) external onlyOwner { | |
publicMintPrice = price; | |
} | |
/// @notice Update crossmint address | |
function setCrossmintAddress(address addr) external onlyOwner { | |
crossmintAddress = addr; | |
} | |
/// @notice Reveal metadata for all the tokens | |
function reveal() external onlyOwner { | |
require(startingIndex == 0, "Collection has been already revealed"); | |
require(startingIndexBlock != 0, "Starting index block must be set"); | |
startingIndex = uint(blockhash(startingIndexBlock)) % collectionSize; | |
// Just a sanity case in the worst case if this function is called late (EVM only stores last 256 block hashes) | |
if (block.number - startingIndexBlock > 255) { | |
startingIndex = uint(blockhash(block.number - 1)) % collectionSize; | |
} | |
// Prevent default sequence | |
if (startingIndex == 0) { | |
startingIndex = startingIndex + 1; | |
} | |
} | |
function emergencySetStartingIndexBlock() public onlyOwner { | |
require(startingIndex == 0, "Collection has been already revealed"); | |
startingIndexBlock = block.number; | |
} | |
/// @notice Withdraw contract balance | |
function withdraw() external onlyOwner { | |
uint balance = address(this).balance; | |
require(balance > 0, "No balance"); | |
payable(owner()).transfer(balance); | |
} | |
/// @notice Allows owner to mint tokens to a specified address | |
function airdrop(address to, uint count) external onlyOwner { | |
require(_tokenIds.current() + count <= collectionSize, "Request exceeds collection size"); | |
_mintTokens(to, count); | |
} | |
/// @notice Get token URI. In case of delayed reveal we give user the json of the placeholer metadata. | |
/// @param tokenId token ID | |
function tokenURI(uint tokenId) public view override returns (string memory) { | |
require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token"); | |
string memory sequenceId; | |
if (startingIndex > 0) { | |
sequenceId = ( 1 + (tokenId + startingIndex) % collectionSize ).toString(); | |
return string(abi.encodePacked(baseURL, sequenceId, '.json')); | |
} else { | |
return preRevealURL; | |
} | |
} | |
function publicMint(uint count) external payable { | |
_publicMint(count, msg.sender); | |
} | |
function crossmint(address _to, uint count) external payable { | |
require(msg.sender == crossmintAddress, "This function is for Crossmint only."); | |
_publicMint(count, _to); | |
} | |
function _publicMint(uint count, address _to) internal { | |
require(startingIndex == 0, "Collection has been already revealed"); | |
require(publicSaleStatus == SaleStatus.LIVE, "Public Sales are off"); | |
require(_tokenIds.current() + count <= collectionSize - reservedAmount, "Number of requested tokens will exceed collection size"); | |
require(msg.value >= count * publicMintPrice, "Ether value sent is not sufficient"); | |
require(_publicMintedCount[_to] + count <= tokensPerPersonPublicLimit, "Number of requested tokens exceeds allowance"); | |
_publicMintedCount[_to] += count; | |
_mintTokens(_to, count); | |
emit Minted(_to, count, SaleStage.PUBLIC); | |
} | |
function whitelistMint(bytes32[] calldata merkleProof, uint count) external payable { | |
require(startingIndex == 0, "Collection has been already revealed"); | |
require(whitelistSaleStatus == SaleStatus.LIVE, "Whitelist sales are closed"); | |
require(_tokenIds.current() + count <= collectionSize - reservedAmount, "Number of requested tokens will exceed collection size"); | |
require(msg.value >= count * whitelistMintPrice, "Ether value sent is not sufficient"); | |
require(_whitelistMintedCount[msg.sender] + count <= tokensPerPersonWhitelistLimit, "Number of requested tokens exceeds allowance"); | |
bytes32 leaf = keccak256(abi.encodePacked(msg.sender)); | |
require(MerkleProof.verify(merkleProof, whitelistMerkleRoot, leaf), "You are not whitelisted"); | |
_whitelistMintedCount[msg.sender] += count; | |
_mintTokens(msg.sender, count); | |
emit Minted(msg.sender, count, SaleStage.WHITELIST); | |
} | |
function privateMint(bytes32[] calldata merkleProof, uint count) external payable { | |
require(startingIndex == 0, "Collection has been already revealed"); | |
require(privateSaleStatus == SaleStatus.LIVE, "Private sales are closed"); | |
require(_tokenIds.current() + count <= collectionSize, "Number of requested tokens will exceed collection size"); | |
require(msg.value >= count * privateMintPrice, "Ether value sent is not sufficient"); | |
require(_privateMintedCount[msg.sender] + count <= tokensPerPersonPrivateLimit, "Number of requested tokens exceeds allowance"); | |
bytes32 leaf = keccak256(abi.encodePacked(msg.sender)); | |
require(MerkleProof.verify(merkleProof, privateMerkleRoot, leaf), "You are not whitelisted"); | |
_privateMintedCount[msg.sender] += count; | |
_mintTokens(msg.sender, count); | |
emit Minted(msg.sender, count, SaleStage.PRIVATE); | |
} | |
function freeMint(bytes32[] calldata merkleProof, uint count) public { | |
require(startingIndex == 0, "Collection has been already revealed"); | |
require(freeMintSaleStatus == SaleStatus.LIVE, "Free mint sales are closed"); | |
require(_tokenIds.current() + count <= collectionSize - reservedAmount, "Number of requested tokens will exceed collection size"); | |
require(_freeMintedCount[msg.sender] + count <= tokensPerPersonFreeMintLimit, "Number of requested tokens exceeds allowance"); | |
bytes32 leaf = keccak256(abi.encodePacked(msg.sender)); | |
require(MerkleProof.verify(merkleProof, freeMintMerkleRoot, leaf), "You are not allowed to mint for free"); | |
_freeMintedCount[msg.sender] += count; | |
_mintTokens(msg.sender, count); | |
emit Minted(msg.sender, count, SaleStage.FREE_MINT); | |
} | |
/// @dev Perform actual minting of the tokens | |
function _mintTokens(address to, uint count) internal { | |
for(uint index = 0; index < count; index++) { | |
_tokenIds.increment(); | |
uint newItemId = _tokenIds.current(); | |
_safeMint(to, newItemId); | |
} | |
// if Minting is done, set startingIndexBlock | |
if (startingIndexBlock == 0 && (_tokenIds.current() == collectionSize)) { | |
startingIndexBlock = block.number; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment