//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
import "../interfaces/IN.sol";
import "./SeedPhrasePricing.sol";
import "../libraries/Randomize.sol";
import "../libraries/NilProtocolUtils.sol";
import "../libraries/SeedPhraseUtils.sol";
/**
* @title Seed Phrases
* @author maximonee (twitter.com/maximonee_)
* @notice This contract provides minting for the Seed Phrases NFT by Sean Elliott (twitter.com/seanelliottoc)
*/
contract SeedPhrase is SeedPhrasePricing {
using Strings for uint256;
using Strings for uint16;
using Strings for uint8;
using Counters for Counters.Counter;
using Randomize for Randomize.Random;
Counters.Counter private _tokenIds;
Counters.Counter private _doublePanelTokens;
// TODO: Check if this is the correct way to store double tokens
// Idea is to map double panel tokens (let's say they go from 3000 - 3100)
// In the tokenUri we can check if a token is >= 3000
// If it is check that it exists in this mapping which should contain the original two (now burned tokens)
mapping(uint256 => uint256[2]) burnedTokensPairings;
event Burnt(address to, uint256 firstBurntToken, uint256 secondBurntToken, uint256 doublePaneledToken);
DerivativeParameters params = DerivativeParameters(false, false, 0, 2048, 4);
constructor(
address _n,
address masterMint,
address dao
) SeedPhrasePricing("NFT #1337", "LEET", IN(_n), params, 40000000000000000, 80000000000000000, masterMint, dao) {
// Start token IDs at 1
_tokenIds.increment();
preSaleLimits[PreSaleType.N] = 1044;
preSaleLimits[PreSaleType.GenesisSketch] = 40;
preSaleLimits[PreSaleType.GM] = 500;
}
mapping(address => bool) genesisSketchAddresses;
mapping(address => bool) gmAddresses;
mapping(PreSaleType => uint256) preSaleLimits;
bool public preSaleActive = false;
bool public publicSaleActive = false;
bool public isSaleHalted = false;
uint8 public constant MAX_PUBLIC_MINT = 8;
uint8 public constant MAX_BURNS = 100;
uint16 public constant OWNER_LIMIT = 4;
// Time TBD
uint32 preSaleLaunchTime = 1633723200;
// Time TBD
uint32 publicSaleLaunchTime = 1633723200;
// NFT creation vars
uint8 constant gridSize = 16;
uint16 constant viewBox = 1600;
uint16 constant segmentSize = 100;
uint16 constant radius = 50;
uint16 constant r = 40;
uint16 constant padding = 10;
uint16 constant panelWidth = segmentSize * 4;
uint16 constant panelHeight = segmentSize * 10;
uint16 constant singlePanelX = (segmentSize * 6);
uint16 constant doublePanel1X = (segmentSize * 3);
uint16 constant doublePanel2X = doublePanel1X + (segmentSize * 6);
uint16 constant panelY = (segmentSize * 3);
uint8 constant strokeWeight = 7;
// Map token to their unique seed
mapping(uint256 => bytes32) internal tokenSeed;
struct Colors {
string background;
string panel;
string panelStroke;
string selectedCircleStroke;
string selectedCircleFill;
string negativeCircleStroke;
string negativeCircleFill;
string blackOrWhite;
string bOrWStroke;
}
struct Attrs {
bool showStroke;
bool showPanel;
bool backgroundCircles;
bool showGrid;
}
enum PreSaleType {
N,
GenesisSketch,
GM,
None
}
function getTokenSeed(uint256 tokenId) public view returns (bytes32) {
return tokenSeed[tokenId];
}
function tokenSVG(uint256 tokenId) public view virtual returns (string memory, string memory) {
require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");
return _render(tokenId, getTokenSeed(tokenId));
}
function tokenSVG(uint256 tokenId) public view virtual returns (string memory svg, bytes memory) {
require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");
Randomize.Random memory random = Randomize.Random({ seed: uint256(getTokenSeed(tokenId)), offsetBit: 0 });
(SeedPhraseUtils.Attrs memory attributes, bytes memory traits) = tokenAttributes(tokenId, random);
return (_render(tokenId, random, attributes), traits);
}
function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");
SeedPhraseUtils.Attrs memory attributes = tokenAttributes(tokenId);
// We might also want to return the attributes string from tokenSVG also (as all the info is already in there)
(string memory output, bytes memory traits) = tokenSVG(tokenId);
string memory json = NilProtocolUtils.base64encode(
bytes(
string(
abi.encodePacked(
'{"name": "Seed Phrases #',
NilProtocolUtils.stringify(tokenId),
'", "image": "data:image/svg+xml;base64,',
NilProtocolUtils.base64encode(bytes(output)),
'", "attributes": ',
traits,
"}"
)
)
)
);
output = string(abi.encodePacked("data:application/json;base64,", json));
return output;
}
/**
Updates the presale state for n holders
*/
function setPreSaleState(bool _preSaleActiveState) public onlyAdmin {
preSaleActive = _preSaleActiveState;
}
/**
Updates the public sale state for non-n holders
*/
function setPublicSaleState(bool _publicSaleActiveState) public onlyAdmin {
publicSaleActive = _publicSaleActiveState;
}
/**
Give the ability to halt the sale if necessary due to automatic sale enablement based on time
*/
function setSaleHaltedState(bool _saleHaltedState) public onlyAdmin {
isSaleHalted = _saleHaltedState;
}
function setGenesisSketchAllowList(address[] calldata addresses) external onlyAdmin {
for (uint256 i = 0; i < addresses.length; i++) {
genesisSketchAddresses[addresses[i]] = true;
}
}
function setGmAllowList(address[] calldata addresses) external onlyAdmin {
for (uint256 i = 0; i < addresses.length; i++) {
gmAddresses[addresses[i]] = true;
}
}
function isPreSaleActive() public view returns (bool) {
if ((block.timestamp >= preSaleLaunchTime || preSaleActive) && !isSaleHalted) {
return true;
}
return false;
}
function isPublicSaleActive() public view returns (bool) {
if ((block.timestamp >= publicSaleLaunchTime || publicSaleActive) && !isSaleHalted) {
return true;
}
return false;
}
function canMintPresale(address addr, uint256 amount) internal view returns (bool, PreSaleType) {
if (genesisSketchAddresses[addr] && preSaleLimits[PreSaleType.GenesisSketch] - amount > 0) {
return (true, PreSaleType.GenesisSketch);
}
if (gmAddresses[addr] && preSaleLimits[PreSaleType.GM] - amount > 0) {
return (true, PreSaleType.GM);
}
if (n.balanceOf(addr) > 0 && preSaleLimits[PreSaleType.N] - amount > 0) {
return (true, PreSaleType.N);
}
return (false, PreSaleType.None);
}
/**
* @notice Allow a n token holder to bulk mint tokens with id of their n tokens' id
* @param recipient Recipient of the mint
* @param tokenIds Ids to be minted
* @param paid Amount paid for the mint
*/
function mintWithN(
address recipient,
uint256[] calldata tokenIds,
uint256 paid
) public virtual override nonReentrant {
uint256 maxTokensToMint = tokenIds.length;
(bool preSaleEligible, PreSaleType presaleType) = canMintPresale(recipient, maxTokensToMint);
require(isPreSaleActive() && preSaleEligible, "SeedPhrase:SALE_NOT_ACTIVE");
require(
balanceOf(recipient) + maxTokensToMint <= _getMaxMintPerWallet(),
"NilPass:MINT_ABOVE_MAX_MINT_ALLOWANCE"
);
require(totalSupply() + maxTokensToMint <= params.maxTotalSupply, "NilPass:MAX_ALLOCATION_REACHED");
require(paid == getNextPriceForNHoldersInWei(maxTokensToMint), "NilPass:INVALID_PRICE");
for (uint256 i = 0; i < maxTokensToMint; i++) {
uint256 tokenId = _tokenIds.current();
tokenSeed[tokenId] = SeedPhraseUtils.generateSeed(tokenId);
_safeMint(recipient, tokenId);
_tokenIds.increment();
}
if (preSaleEligible) {
preSaleLimits[presaleType] -= maxTokensToMint;
}
}
/**
* @notice Allow anyone to mint a token with the supply id if this pass is unrestricted.
* n token holders can use this function without using the n token holders allowance,
* this is useful when the allowance is fully utilized.
* @param recipient Recipient of the mint
* @param amount Amount of tokens to mint
* @param paid Amount paid for the mint
*/
function mint(
address recipient,
uint8 amount,
uint256 paid
) public virtual override nonReentrant {
(bool preSaleEligible, PreSaleType presaleType) = canMintPresale(recipient, amount);
require(isPublicSaleActive() || (isPreSaleActive() && preSaleEligible), "SeedPhrase:SALE_NOT_ACTIVE");
require(!params.supportsTokenId, "NilPass:NON_TOKENID_MINTING_DISABLED");
require(balanceOf(msg.sender) + amount <= _getMaxMintPerWallet(), "NilPass:MINT_ABOVE_MAX_MINT_ALLOWANCE");
require(totalSupply() + amount <= params.maxTotalSupply, "NilPass:MAX_ALLOCATION_REACHED");
require(paid == getNextPriceForOpenMintInWei(amount), "NilPass:INVALID_PRICE");
for (uint256 i = 0; i < amount; i++) {
uint256 tokenId = _tokenIds.current();
tokenSeed[tokenId] = SeedPhraseUtils.generateSeed(tokenId);
_safeMint(recipient, tokenId);
_tokenIds.increment();
}
if (preSaleEligible) {
preSaleLimits[presaleType] -= amount;
}
}
/**
* @notice Allow anyone to burn two single panels they own in order to mint
* a double paneled token.
* @param firstTokenId Token ID of the first token
* @param secondTokenId Token ID of the second token
*/
function burnForDoublePanel(uint256 firstTokenId, uint256 secondTokenId) public nonReentrant {
require(_doublePanelTokens.current() < MAX_BURNS, "SeedPhrase:MAX_BURNS_REACHED");
require(
ownerOf(firstTokenId) == msg.sender && ownerOf(secondTokenId) == msg.sender,
"SeedPhrase:INCORRECT_OWNER"
);
require(firstTokenId != secondTokenId, "SeedPhrase:EQUAL_TOKENS");
// Ensure two owned tokens are in Burnable token pairings
require(
doubleWordPairings[abi.encode(string(firstTokenId), "-", string(secondTokenId))],
"SeedPhrase:INVALID_TOKEN_PAIRING"
);
_burn(firstTokenId);
_burn(secondTokenId);
// Any Token ID of 3000 or greater indicates it is a double panel e.g. 3000, 3001, 3002...
uint256 doublePaneledTokenId = 3000 + _doublePanelTokens;
_mint(msg.sender, doublePaneledTokenId);
// Add burned tokens to burnedTokensPairings mapping so we can use them to render the double panels later
burnedTokensPairings[doublePaneledTokenId] = [firstTokenId, secondTokenId];
_doublePanelTokens.increment();
emit Burnt(msg.sender, firstTokenId, secondTokenId, doublePaneledTokenId);
}
/**
* @notice Calculate the total available number of mints
* @return total mint available
*/
function totalMintsAvailable() public view override returns (uint256) {
return derivativeParams.maxTotalSupply - totalSupply();
}
function _getMaxMintPerWallet() internal view returns (uint128) {
return isPublicSaleActive() ? MAX_PUBLIC_MINT : params.maxMintAllowance;
}
function canMint(address account) public view virtual override returns (bool) {
uint256 balance = balanceOf(account);
if (isPublicSaleActive() && (totalMintsAvailable() > 0) && balance < MAX_PUBLIC_MINT) {
return true;
}
if (isPreSaleActive()) {
(bool preSaleEligible, ) = canMintPresale(account, 1);
if (preSaleEligible) {
return true;
}
}
return false;
}
function tokenAttributes(uint256 tokenId, Randomize.Random memory random)
public
view
virtual
returns (SeedPhraseUtils.Attrs memory attributes, bytes memory traits)
{
attributes = SeedPhraseUtils.Attrs({
showStroke: random.boolPercentage(70), // Show stroke 70% of the time
showPanel: random.boolPercentage(80), // Show panel 80% of the time
backgroundCircles: random.boolPercentage(2), // Show background circles 2% of the time
doublePanel: (tokenId >= 3000), // To render a double parameter second token must not be 0
bigBackgroundCircle: random.boolPercentage(15),
border: false,
backgroundSquare: false,
showGrid: false,
greyscale: false
});
// Only give option of background square if bigBackgroundCircle is not true
if (!attributes.bigBackgroundCircle) {
attributes.backgroundSquare = random.boolPercentage(15);
// Border should only be on if not using background shapes
if (!attributes.backgroundSquare) {
attributes.border = random.boolPercentage(40);
}
}
if (attributes.showStroke) {
attributes.greyscale = random.boolPercentage(2); // Greyscale should only be seen if stroke is shown
if (!attributes.backgroundCircles) {
attributes.showGrid = random.boolPercentage(3); // Only set use grid if stroke is true and background circles is not true
}
}
// Start Traits JSON
traits = abi.encodePacked('[{"trait_type": "BIP39", "value": "', tokenId.toString(), '"},');
if (!attributes.showStroke) {
traits = abi.encodePacked(traits, '{"value": "No Stroke"},');
} else if (attributes.greyscale) {
traits = abi.encodePacked(traits, '{"value": "Greyscale"},');
}
if (!attributes.showPanel) {
traits = abi.encodePacked(traits, '{"value": "No Panel"},');
}
if (attributes.backgroundCircles) {
traits = abi.encodePacked(traits, '{"value": "Background Circles"},');
} else if (attributes.showGrid) {
traits = abi.encodePacked(traits, '{"value": "Grid"},');
}
if (attributes.bigBackgroundCircle) {
traits = abi.encodePacked(traits, '{"value": "Giant Circle Background"},');
} else if (attributes.backgroundSquare) {
traits = abi.encodePacked(traits, '{"value": "Window Pane"},');
} else if (attributes.border) {
traits = abi.encodePacked(traits, '{"value": "Border"},');
}
traits = abi.encodePacked(traits, "]");
}
/// @param tokenId the tokenId (only double panels will have two)
/// @return the json
function _render(
uint256 tokenId,
Randomize.Random memory random,
SeedPhraseUtils.Attrs memory attributes
) internal view virtual returns (string memory) {
// Get color pallet
SeedPhraseUtils.Colors memory pallet = SeedPhraseUtils._getPalette(random, attributes);
// Start SVG (viewbox & static patterns)
bytes memory svg = abi.encodePacked(
"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 ",
viewBox.toString(),
" ",
viewBox.toString(),
"'><path fill='",
pallet.background,
"' ",
SeedPhraseUtils._getStrokeStyle(attributes.border, pallet.blackOrWhite, "0.3", 50),
" d='M0 0h",
viewBox.toString(),
"v",
viewBox.toString(),
"H0z'/>",
" <pattern id='panelCircles' x='0' y='0' width='.25' height='.1' patternUnits='objectBoundingBox'>",
"<circle cx='",
radius.toString(),
"' cy='",
radius.toString(),
"' r='",
r.toString(),
"' fill='",
pallet.negativeCircleFill,
"' ",
SeedPhraseUtils._getStrokeStyle(attributes.showStroke, pallet.negativeCircleStroke, "1", strokeWeight),
" /></pattern>"
);
// Render optional patterns (grid OR background circles)
if (attributes.backgroundCircles) {
svg = abi.encodePacked(
svg,
"<pattern id='backgroundCircles' x='0' y='0' width='",
segmentSize.toString(),
"' height='",
segmentSize.toString(),
"' patternUnits='userSpaceOnUse'><circle cx='",
radius.toString(),
"' cy='",
radius.toString(),
"' r='",
r.toString(),
"' fill='",
pallet.blackOrWhite,
"' style='fill-opacity: ",
pallet.dynamicOpacity,
";'></circle></pattern><path fill='url(#backgroundCircles)' d='M0 0h",
viewBox.toString(),
"v",
viewBox.toString(),
"H0z'/>"
);
} else if (attributes.showGrid) {
svg = abi.encodePacked(
svg,
"<pattern id='grid' x='0' y='0' width='",
segmentSize.toString(),
"' height='",
segmentSize.toString(),
"' patternUnits='userSpaceOnUse'><rect x='0' y='0' width='",
segmentSize.toString(),
"' height='",
segmentSize.toString(),
"' fill='none' ",
SeedPhraseUtils._getStrokeStyle(true, pallet.blackOrWhite, pallet.dynamicOpacity, strokeWeight),
"'/></pattern><path fill='url(#grid)' d='M0 0h",
viewBox.toString(),
"v",
viewBox.toString(),
"H0z'/>"
);
}
if (attributes.bigBackgroundCircle) {
(uint16 shapeSize, uint16 stroke) = SeedPhraseUtils._backgroundShapeSizing(random, attributes);
uint16 centerCircle = (viewBox / 2);
svg = abi.encodePacked(
svg,
"<circle cx='",
centerCircle.toString(),
"' cy='",
centerCircle.toString(),
"' r='",
(shapeSize / 2).toString(),
"' fill='",
pallet.backgroundCircle,
"' stroke='",
pallet.negativeCircleStroke,
"' style='stroke-width:",
stroke.toString(),
";stroke-opacity:0.3'/>"
);
} else if (attributes.backgroundSquare) {
(uint16 shapeSize, uint16 stroke) = SeedPhraseUtils._backgroundShapeSizing(random, attributes);
uint16 centerSquare = ((viewBox - shapeSize) / 2);
svg = abi.encodePacked(
svg,
"<rect x='",
centerSquare.toString(),
"' y='",
centerSquare.toString(),
"' width='",
shapeSize.toString(),
"' height='",
shapeSize.toString(),
"' fill='",
pallet.backgroundCircle,
"' stroke='",
pallet.negativeCircleStroke,
"' style='stroke-width:",
stroke.toString(),
";stroke-opacity:0.3'/>"
);
}
// Double panel (only if holder has burned two tokens from the defined pairings)
// TODO: Add further checking here to confirm we are safe to render a double panel
if (attributes.doublePanel) {
uint256[2] memory tokens = burnedTokensPairings[tokenId];
(uint8[4] memory firstBipIndexArray, string memory firstBipIndexStr) = SeedPhraseUtils.transformTokenId(
tokens[0]
);
(uint8[4] memory secondBipIndexArray, string memory secondBipIndexStr) = SeedPhraseUtils.transformTokenId(
tokens[1]
);
svg = abi.encodePacked(
svg,
_renderSinglePanel(firstBipIndexArray, attributes, pallet, doublePanel1X, false),
_renderSinglePanel(secondBipIndexArray, attributes, pallet, doublePanel2X, true)
);
// Create text
bytes memory combinedText = abi.encodePacked(firstBipIndexStr, " - #", secondBipIndexStr);
svg = abi.encodePacked(
svg,
SeedPhraseUtils._renderText(string(combinedText), pallet.blackOrWhite),
"</svg>"
);
}
// Single Panel
else {
(uint8[4] memory bipIndexArray, string memory bipIndexStr) = SeedPhraseUtils.transformTokenId(tokenId);
svg = abi.encodePacked(svg, _renderSinglePanel(bipIndexArray, attributes, pallet, singlePanelX, false));
// Add closing text and svg element
svg = abi.encodePacked(svg, SeedPhraseUtils._renderText(bipIndexStr, pallet.blackOrWhite), "</svg>");
}
return string(svg);
}
function _renderSinglePanel(
uint8[4] memory bipIndexArray,
SeedPhraseUtils.Attrs memory attributes,
SeedPhraseUtils.Colors memory pallet,
uint16 panelX,
bool secondPanel
) internal pure returns (bytes memory panelSvg) {
// Draw panels
bool squareEdges = (attributes.doublePanel && attributes.backgroundSquare);
if (attributes.showPanel) {
panelSvg = abi.encodePacked(
"<rect x='",
(panelX - padding).toString(),
"' y='",
(panelY - padding).toString(),
"' width='",
(panelWidth + (padding * 2)).toString(),
"' height='",
(panelHeight + (padding * 2)).toString(),
"' rx='",
(squareEdges ? 0 : radius).toString(),
"' fill='",
(secondPanel ? pallet.panel2 : pallet.panel),
"' ",
SeedPhraseUtils._getStrokeStyle(attributes.showStroke, pallet.panelStroke, "1", strokeWeight),
"/>"
);
}
// Fill panel with negative circles, should resemble M600 300h400v1000H600z
panelSvg = abi.encodePacked(
panelSvg,
"<path fill='url(#panelCircles)' d='M",
panelX.toString(),
" ",
panelY.toString(),
"h",
panelWidth.toString(),
"v",
panelHeight.toString(),
"H",
panelX.toString(),
"z'/>"
);
// Draw selected circles
panelSvg = abi.encodePacked(
panelSvg,
_renderSelectedCircles(bipIndexArray, pallet, attributes.showStroke, panelX, secondPanel)
);
}
function _renderSelectedCircles(
uint8[4] memory bipIndexArray,
SeedPhraseUtils.Colors memory pallet,
bool showStroke,
uint16 panelX,
bool secondPanel
) internal pure returns (bytes memory svg) {
for (uint8 i = 0; i < bipIndexArray.length; i++) {
svg = abi.encodePacked(
svg,
"<circle cx='",
(panelX + (segmentSize * i) + radius).toString(),
"' cy='",
(panelY + (segmentSize * bipIndexArray[i]) + radius).toString(),
"' r='",
(radius - padding).toString(),
".1' fill='", // Increase the size a tiny bit here (0.1) to hide negative circle outline
(secondPanel ? pallet.selectedCircleFill2 : pallet.selectedCircleFill),
"' ",
SeedPhraseUtils._getStrokeStyle(showStroke, pallet.selectedCircleStroke, "1", strokeWeight),
" />"
);
}
}
function validPairingKey(string memory key) public view returns (bool) {
return pairings[key];
}
// TODO: Make this function only accessible by admin (not sure how to do this)
function ammendPairings(string memory key, bool value) private {
pairings[key] = value;
}
// Mapping should be a string containing both tokens e.g. "tokenId1-tokenId2"
mapping(string => bool) doubleWordPairings;
}