---
# System prepended metadata

title: Untitled

---

```
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import "@openzeppelin/contracts/utils/Strings.sol";
import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/utils/CountersUpgradeable.sol";
import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
import { IERC2981 } from "@openzeppelin/contracts/interfaces/IERC2981.sol";

/** 
* @title An NFT smart contract
* @author Lunar Defi - Cypher
* @notice This contract should mint NFTs to users. It controls two types of NFTs - regular and special NFTs
* @dev The minting functions can only be called by the owner. The contract is upgradeable
*/
contract LunarNFT is Initializable, ERC721Upgradeable, PausableUpgradeable, AccessControlUpgradeable, UUPSUpgradeable {
  using CountersUpgradeable for CountersUpgradeable.Counter;
  using MerkleProof for bytes32[];
  /// total amount of tokens that can be minted
  uint256 public maxTokenMintable;  
  uint256 public mintPrice;
  /// 
  bytes32 public merkleRoot;
  address public royaltyAddress;
  /// total amount of special tokens that can be minted 
  uint256 private maxSpecialTokenMintable;
  mapping(address => uint256) public userBalance;
  string public uri;

  /**
  @dev Event for notifiying when minting changes from NOT_MINITING to WHITELIST_MINT and vice versa
  */
  event MintStateUpdated(MintState indexed _state, uint256 updatedAt);
   /**
  @dev Event for notifiying when a user Transforms token from 
  */
  event TokenTransformed(address indexed tokenOwner, uint256 tokenId);
  event Redeemed(uint256 tokenId, address owner);
  event UserDeposit( address indexed user, uint256 amount);
  event UserMinted (address indexed user, uint256 tokenId );

  bytes32 public constant TRANSACTION_ROLE = keccak256("TRANSACTION_ROLE");
  bytes32 public constant LIQUIDITY_ROLE = keccak256("LIQUIDITY_ROLE");

  struct TokenCollection {
    uint256[] tokenIds;
    uint256[] tokenTypes;
  }

  mapping(uint256 => bool) public redeemed;

  enum MintState { NOT_MINTING, WHITELIST_MINT, PUBLIC_MINT }
  /// tracking number of minting done by an account during whitelisting stage
  mapping(address => uint256) public userAlreadyMinted; /// tracking number of mintings done by an account durng public miniting stage
  
  //// tracking the tokensupply
  CountersUpgradeable.Counter private tokenSupply; 
  //// tracking the special supply
  CountersUpgradeable.Counter private specialSupply; 
  //// Tracks mint state from 0 to 2, 0 being not_minting
  MintState public mintState;

  /// @custom:oz-upgrades-unsafe-allow constructor
  constructor() initializer {}

  /** @notice replaces constructor for an uppgradeable contract
    * @dev oz-contracts-upgrades are initialized here along with some state variables
    * @param _name - name of NFT
    * @param _symbol - symbol identifier for NFT contract
    * @param _maxTokenMintable - total amount of reqular tokens this contract should mint
    * @param _maxSpecialTokenMintable - total amount of special tokens this contract should mint
  */
  function initialize(string memory _name, string memory _symbol, string memory _nonRevealUri, uint256 _maxTokenMintable, uint256 _maxSpecialTokenMintable, uint256 _mintPrice, address _royaltyAddress) public initializer {
    __ERC721_init(_name, _symbol);
    __Pausable_init();
    __AccessControl_init();
    __UUPSUpgradeable_init();
    
    _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
    _grantRole(TRANSACTION_ROLE, msg.sender);
    _grantRole(LIQUIDITY_ROLE, msg.sender);

    maxTokenMintable = _maxTokenMintable;
    maxSpecialTokenMintable = _maxSpecialTokenMintable;
    mintPrice = _mintPrice;
    royaltyAddress = _royaltyAddress;
    uri = _nonRevealUri;
  }

  function deposit() external payable{
    require(msg.value > 0, "invalid amount");
    userBalance[msg.sender] += msg.value;
    emit UserDeposit(msg.sender, msg.value);
  }

  function withdrawDeposit() external {
    uint256 _amount = userBalance[msg.sender];
    userBalance[msg.sender] = 0;
    (bool sent,) = payable(msg.sender).call{ value: _amount }("");
    require(sent, "Failed To Withdraw");
  }

  /**
  * @dev Base URI for computing {tokenURI}. If set, the resulting URI for each
  * token will be the concatenation of the `baseURI` and the `tokenId`. Empty
  * by default, can be overridden in child contracts.
  */
  function _baseURI() internal view virtual override returns (string memory) {
      return uri;
  }

  function setURI(string memory newURI) external onlyRole(TRANSACTION_ROLE) {
    uri = newURI;
  }

  /**
   * @notice gets the total number of regular tokens that has been minted.
   * @return total sullply of NFTs minted
  */
  function totalSupply() external view returns(uint256) {
    return tokenSupply.current();
  }
  /** @notice stops contract from transferring or minting NFT tokens */
  function pause() public onlyRole(DEFAULT_ADMIN_ROLE) {
    _pause();
  }

  /** @notice Lets contract resume transfer or minting of NFT tokens */
  function unpause() public onlyRole(DEFAULT_ADMIN_ROLE) {
    _unpause();
  }

  /**
  * @notice Mints NFT for whitelisted addresses
  * @dev Enables minting of NFTs restricted to only the whitelisted accounts
  * @param _root - merkle root provided must match what has been stored in the contract for verification
  * @param proofs - merkle proof for the current address requesting minting
  * @param quantity - total number of NFTs to be minted when this function is called
   * @param to- the receiver of the token(s) to be minted
  */
  function whitelistMint(bytes32 _root, bytes32[] memory proofs, uint256 quantity, address to)
    external
    onlyRole(TRANSACTION_ROLE)
  {
    require(mintState == MintState.WHITELIST_MINT, "Not Open");
    require(_root == merkleRoot, "Incorrect Root/Proof");
    require(userAlreadyMinted[to] <= 3, "Already Minted Maximum");
    require(userBalance[to] > 0, "Deposit too low");
    require(proofs.verify(_root, keccak256(abi.encodePacked(to))), "Not Whitelisted!");
    (uint256 cost, uint256 adjustedQty) = _isMinting(quantity, to, true);
    require((userAlreadyMinted[to] + adjustedQty) <= 3, "Cannot Exceed Maximum");
    userBalance[to] = userBalance[to] - cost;
    _minter(to, adjustedQty);
    // _processRefund(refund, to);
  }

 /** 
  * @notice Mints NFT for any address
  * @dev Enables minting of NFTs restricted to only the whitelisted accounts
  * @param to - the receiver of the token(s) to be minted
  * @param quantity - total number of NFTs to be minted when this function is called
  */
  function closedMint(address to, uint256 quantity) 
    external 
    onlyRole(TRANSACTION_ROLE)  
  {
    require(mintState == MintState.PUBLIC_MINT, "Not Open");
    require(userBalance[to] > 0, "Deposit too low");
    require(userAlreadyMinted[to] <= 5, "Already Minted Maximum");
    (uint256 cost, uint256 adjustedQty) = _isMinting(quantity, to, false);
    require((userAlreadyMinted[to] + adjustedQty) <= 5, "Cannot Exceed Maximum");
    userBalance[to] = userBalance[to] - cost;
   _minter(to, adjustedQty);
  }

   /** 
  * @notice update the address which royalties will be paid to on secondary exchange
  * @param _royaltyAddress - the new address to which royalties will be paid to
  */
  function setRoyaltyAddress(address _royaltyAddress) public onlyRole(DEFAULT_ADMIN_ROLE) {
    royaltyAddress = _royaltyAddress;
  }

  /**
  * @notice  handles minting in whitelist and public stages
  * @param to - account where the NFT will be minted to
  * @param quantity - number of NFT to be minted for this request
  */
  function _minter(address to, uint256 quantity) private {
    for(uint256 i = 0; i < quantity; i++) {
      uint256 tokenId = tokenSupply.current() +1;
      userAlreadyMinted[to] += 1;
      tokenSupply.increment();
      _safeMint(to, tokenId);
      emit UserMinted (to,tokenId);
    }
  }

  /**
  * @dev - checks if 
    *    - contract is in minting stage
    *    - quantity is less than 0
    * @param quantity - number of NFT to be minted for this request
    * @param to - the beneficiary account
    * @return cost - total cost of minting
    * @return adjustedQty - adjusted quantity if necessary in edge cases
  */
  function _isMinting(uint256 quantity, address to, bool isWhitelist) internal view returns(uint256 cost, uint256 adjustedQty) {
    require(mintState != MintState.NOT_MINTING, "Minting Disabled");
    require(quantity >= 1, "Invalid Amount");
    uint256 _adjustedQty = quantity;
    uint256 _cost = mintPrice * quantity;
    require(_adjustedQty > 0, "Invalid Amount To Mint");
    require(userBalance[to] >= _cost, "Deposit Too Low");
    /// If minting will be more than total possible
    if((tokenSupply.current() + _adjustedQty) > maxTokenMintable) { 
      _adjustedQty = maxTokenMintable - tokenSupply.current(); /// compute total possible mint
    }
    uint256 _totalMintForUser;
    if(isWhitelist) {
      _totalMintForUser = userAlreadyMinted[to];
      if((userAlreadyMinted[to] + _adjustedQty) > 3) {
        _adjustedQty = 3 - userAlreadyMinted[to];
      }
    } else {
      _totalMintForUser = userAlreadyMinted[to] + _adjustedQty;
      if(_totalMintForUser > 5) {
        _adjustedQty = 5 - (userAlreadyMinted[to]);
      }
    }
    _cost = mintPrice * _adjustedQty;  /// update cost of mintingfor edge cases
    // _refundAmount = userBalance[to] - _cost; /// calculate tge refund for this request
    require(userBalance[to] >= _cost, "Deposit Too low");
    return (_cost, _adjustedQty);
  }

  /** @notice returns the token uri string for a given tokenId
    * @dev returns a concatenated string of the metadata baseURI, tokenId and the extension, pressumed to be .json
    * @param tokenId - the tokenId that is being viewd
    * @return metadata string for tokenId 
  */
  function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
    require(_exists(tokenId), "URI query for nonexistent token");
    string memory baseURI = _baseURI();
    return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, Strings.toString(tokenId), ".json")) : "";
  }
  
  /**
  * @notice withdraws any fund in the wbnb contract 
  * @dev withdraws BNB from contract. This should be redundant since funcd is immediately withdrawn once minting is concluded
   TODO: needs to have the destination address hardcoded into the contract
  */
  function withdrawFunds(address account) external onlyRole(LIQUIDITY_ROLE)  {
    payable(account).transfer(address(this).balance);
  }

  /**
  * @notice This will update the whitelist merkle root definition responsible for verifying whitelists
  * @dev Updates the merkleRoot controlling the whitelisted addresses and updates minting state. For closedMint, merkleRoot will not be read
  * @param _merkleRoot - merkleRoot of whitelist computed outside of the smart contract
  */
  function updateWhitelist(bytes32 _merkleRoot) external onlyRole(TRANSACTION_ROLE) {
    merkleRoot = _merkleRoot;
  }


  /**
    * @dev Mints a special token if the owner has proven the ownership of all 8 types regular tokens 
    * @param _tokenCollection - contains an array of the tokenIds of the redeemed and an array of the quantity of each type
    * @param _tokenOwner - owner of tokens making the request
  */
  function mintShadowToken(TokenCollection calldata _tokenCollection, address _tokenOwner) external  onlyRole(TRANSACTION_ROLE) {
    uint256 mintAmount = _canMintShadowToken(_tokenCollection, _tokenOwner);
    /// increase total amount of shadow tokens 
    for(uint256 i = 0; i< mintAmount; i++) {
      uint256 nextId = specialSupply.current() + 1;
      tokenSupply.increment();
      require(nextId <= maxSpecialTokenMintable, "More Than Available Allowed Mint"); 
      specialSupply.increment();
      tokenSupply.increment();
      _safeMint(_tokenOwner, nextId); 
      emit Redeemed(nextId, _tokenOwner);
    }
  }


  /**
    * @dev Checks if the supplied info about the tokenIDs and the corresponding type is adequate
    * @param _tokenCollection - contains an array of the tokenIds of the redeemed and an array of the quantity of each type
    * @param _tokenOwner - owner of tokens making the request
  */
  function _canMintShadowToken(TokenCollection calldata _tokenCollection, address _tokenOwner) private returns(uint256 mintAmount) {
    uint256[] memory _toRedeem = _tokenCollection.tokenIds;
    uint256[] memory  _toTypes = _tokenCollection.tokenTypes;
    require((_toRedeem.length % 8) == 0, "Not Multiple Of 8");
    require(_toTypes.length == 8, "Must Be 8 Types");
    uint256 amount; // amount of shadow tokens to be minted
    uint256 ofEach = _toRedeem.length / 8; /// how many of each type is expected
    /// verify ownership of each token
    for (uint256 id = 0; id < _toRedeem.length; id++) {
      require(ownerOf(_toRedeem[id]) == _tokenOwner, "Not Owner For At Least 1 Token"); 
      /// check if the token has not been redeemed
      require(!redeemed[id], "1 Token Previously Redeemed");
      redeemed[_toRedeem[id]] = true; /// redeem the token
      amount+=1;
    }
    /// confirm the types are accurate quantity
    uint256 _toTypeQuantity = _toTypes[0];
    for (uint256 k = 0; k < _toTypes.length; k++) {
      require(_toTypes[k] == ofEach, "Invalid quantity");
      require(k == _toTypeQuantity, "Collection Must Have Equal Qty");
    }
    return amount;
  }

  /**
  * @notice This will update the whitelist merkle root definition and/or the minting state of the contract
  * @dev Updates the merkleRoot controlling the whitelisted addresses and updates minting state. For closedMint, merkleRoot will not be read
  * @param _mintState - expects uint ranging from 0 - 2 with 0 being not_minting.
  */
  function updateMintState(MintState _mintState) external onlyRole(TRANSACTION_ROLE) {
    mintState = _mintState;
    emit MintStateUpdated(_mintState, block.timestamp);
  }


  // IERC2981
  function royaltyInfo(uint256 _tokenId, uint256 _salePrice) external view returns (address, uint256 royaltyAmount) {
    _tokenId; // silence solc warning
    royaltyAmount = (_salePrice / 100) * 6; /// 6 % sales tax 
    return (royaltyAddress, royaltyAmount);
  }

  /** @notice hook for checking if NFT is paused so transfers cannot happen in case of emergencies
    * @param from - the origin account for the token
    * @param to - the receiver of the token
    * @param tokenId - the tokenId of the NFT being transferred
  */
  function _beforeTokenTransfer(address from, address to, uint256 tokenId)
    internal
    whenNotPaused
    override
  {
    super._beforeTokenTransfer(from, to, tokenId);
  }
  
  /** @notice this upgrades the contract with a new implementation
    * @dev this function is called during upgrade by using openzepplin upgrade resources during deployment of new implementation 
    * @param newImplementation - the address of the new contract
  */
  function _authorizeUpgrade(address newImplementation)
    internal
    onlyRole(DEFAULT_ADMIN_ROLE)
    override
  {}


  
  /** @notice shows the version of the contract being used
    * @dev the value represents the curreent version of the contract should be updated and overriden with new implementations
    * @return version -the current version of the contract
  */
  function version() external virtual pure returns(string memory) {
    return "1.0.0";
  }

  /** @notice Displays to other contracts standards in which our contracts implements 
    * @dev ERC165 - implementation
    * @param interfaceId- interface to query for
  */
   function supportsInterface(bytes4 interfaceId)
        public
        view
        override(ERC721Upgradeable, AccessControlUpgradeable)
        returns (bool)
    {
        return super.supportsInterface(interfaceId);
    }
}
```