Skip to content

Instantly share code, notes, and snippets.

@shakogegia
Created November 14, 2022 11:44
Show Gist options
  • Save shakogegia/8a47012721d956595ab752c9283413c1 to your computer and use it in GitHub Desktop.
Save shakogegia/8a47012721d956595ab752c9283413c1 to your computer and use it in GitHub Desktop.
Stack Spaceships Smart Contract
/*
/ _____// |______ ____ | | __
\_____ \\ __\__ \ _/ ___\| |/ /
/ \| | / __ \\ \___| <
/_______ /|__| (____ /\___ >__|_ \
\/ \/ \/ \/
_________ .__ .__
/ _____/__________ ____ ____ _____| |__ |__|_____ ______
\_____ \\____ \__ \ _/ ___\/ __ \ / ___/ | \| \____ \/ ___/
/ \ |_> > __ \\ \__\ ___/ \___ \| 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