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