//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; }