// File: contracts/Dynamic.sol



// SPDX-License-Identifier: MIT
pragma solidity 0.8.11;

import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
import "./interfaces/ITreasury.sol";
import "./interfaces/IDynamic.sol";
import "./interfaces/INft.sol";
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";

contract DynamicNft is AccessControl, IDynamic {
    bytes32 public constant EXECUTOR_ROLE = keccak256("EXECUTOR_ROLE");
    bytes32 public constant NFT_ROLE = keccak256("NFT_ROLE");
    bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
    bytes32 public merkleRoot = 0xa0fd9888b738d87f115429a3520fdbd602fbe89c095c2a6f2f1f5af661dfc43d;
    bytes32 public constant ZEROSTATE = 0x0000000000000000000000000000000000000000000000000000000000000000;

    address public immutable treasuryAddress;
    address private cap3Wallet;
    address public genesisContractAddress;
    address public subsContractAddress;

    uint256 public genesisPrice = 2 ether;
    uint256 private projectId = 1;
    uint256 public genesisSupply = 2000;
    uint256 public subscriptionSupply = 7000;
    uint256 private genesisVotingPower = 2;
    uint256 private subscriptionVotingPower = 1;
    uint256 private treasuryLimit = 1e6;
    uint256 public projectFund = 1e5;

    bool public genesisStatus;
    bool public subscriptionStatus;
    SubscriptionStage public subsStage;
    SubscriptionTierDetails public subscriptionDetails;
    bool public distributeFlag;
    bool public refundFlag;

    struct Genesis {
        uint256 tokenId;
        address owner;
        bool claimed;
    }

    struct Subscription {
        uint256 tokenId;
        uint256 renewalExpire;
        uint256 price;
        address owner;
        bool expired;
        bool renewed;
    }

    struct Project {
        string id;
        string description;
        address author;
        bool funded;
    }

    struct SubscriptionTierDetails {
        uint256 tierOnePrice;
        uint256 tierTwoPrice;
        uint256 tierThreePrice;
        uint256 tierFourPrice;
        uint256 tierOneQuantities;
        uint256 tierTwoQuantities;
        uint256 tierThreeQuantities;
        uint256 tierFourQuantities;
    }

    enum TOKEN {
        GENESIS,
        SUBSCRIPTION
    }

    enum SubscriptionStage {
        TIER_ONE,
        TIER_TWO,
        TIER_THREE,
        TIER_FOUR
    }

    /*------ Events -------*/

    event SubscriptionMintStateUpdated(bool state);
    event GenesisMintStateUpdated(bool state);
    event GenesisMinted(address to, uint256 id, uint16 quantity);
    event SubscriptionMinted(address to, uint256 id, uint16 quantity);
    event SubscriptionRenewed(address holder, uint256 tokenId);
    event ExpiredSubscription(address holder, uint256 renewalExpire, uint256 tokenId);
    event MerkleRootSet(bytes32 _merkleRoot);
    event TreasuryLimitSet(uint256 newLimit, uint256 oldLimit);
    event SubscriptionBalanceUpdated(uint256 TokenSupply, uint256 tokenId);
    event ProposalApproved(string id, string _title, address author, uint256 amount);
    event ProposalFunded(string id, uint256 amount);
    event UpdatedBackendAddress(address backendAddress, string Role);
    event DistributionActive();
    event NftTransfered(address to, uint256 tokenId, bool isGenesis);
    event SubscriptionStageUpdated(uint256 newStage);
    event SubscriptionPriceUpdated(uint256 stage, uint256 newPrice);
    event SubscriptionQuantitesUpdated(uint256 stage, uint256 newQuantity);
    event Refunded(address sender, uint256[] tokenIds, uint256 amount);

    mapping(string => Project) private proposals;
    mapping(uint256 => Genesis) public genesisHolder;
    mapping(uint256 => Subscription) public subsHolder;
    mapping(address => uint256) public referralCodes;
    mapping(uint256 => address) public codeToAddress;
    mapping(address => uint8) public toRefund;

    AggregatorV3Interface internal priceFeed;

    constructor(
        address _genesis,
        address _subscription,
        address _treasury,
        address _cap3Wallet,
        address _priceFeedAggregator,
        address[] memory _admins
    ) {
        require(_genesis != address(0), "ADDRESS ZERO");
        require(_subscription != address(0), "ADDRESS ZERO");
        require(_treasury != address(0), "ADDRESS ZERO");
        require(_cap3Wallet != address(0), "ADDRESS ZERO");
        require(_priceFeedAggregator != address(0), "ADDRESS ZER0");

        subsContractAddress = _subscription;
        genesisContractAddress = _genesis;
        treasuryAddress = _treasury;
        cap3Wallet = _cap3Wallet;

        priceFeed = AggregatorV3Interface(_priceFeedAggregator);

        _setRoleAdmin(ADMIN_ROLE, ADMIN_ROLE);
        _setRoleAdmin(EXECUTOR_ROLE, ADMIN_ROLE);
        _setupRole(ADMIN_ROLE, _msgSender());
        _setupRole(ADMIN_ROLE, _admins[0]);
        _setupRole(ADMIN_ROLE, _admins[1]);
        _setupRole(ADMIN_ROLE, address(this));
        _setupRole(NFT_ROLE, _genesis);
        _setupRole(NFT_ROLE, _subscription);

        subscriptionDetails.tierOnePrice = 0.2 ether;
        subscriptionDetails.tierTwoPrice = 0.22 ether;
        subscriptionDetails.tierThreePrice = 0.24 ether;
        subscriptionDetails.tierFourPrice = 0.26 ether;
        subscriptionDetails.tierOneQuantities = 1750;
        subscriptionDetails.tierTwoQuantities = 1750;
        subscriptionDetails.tierThreeQuantities = 1750;
        subscriptionDetails.tierFourQuantities = 1750;
    }

    /*------- State Changing Functions ------*/

    function mintGenesis(
        address _to,
        bytes32[] calldata _merkleProof,
        uint256 _quantity
    ) public payable {
        INft GenesisNft = INft(genesisContractAddress);
        require((GenesisNft.totalSupply() + _quantity) <= (genesisSupply / 4), "MAX QUANTITY REACHED");
        require(msg.value >= (genesisPrice * _quantity), "INSUFFICIENT MINTING VALUE");
        require(genesisStatus, "GENESIS MINT CURRENTLY INACTIVE");
        require(GenesisNft.totalSupply() < 500, "INSUFICIENT GENESIS STOCK");

        if (merkleRoot != ZEROSTATE) {
            require(verifyMerkleProof(_to, _merkleProof), "INVALID MERKLE PROOF");
        }

        uint256 _id = GenesisNft.currentIndex();

        for (uint256 x = _id; x < (_quantity + _id); x++) {
            genesisHolder[x] = Genesis({tokenId: _id, owner: _to, claimed: false});
        }

        GenesisNft.mint(_to, _quantity);

        (bool success, ) = cap3Wallet.call{value: msg.value}("");
        require(success, "MINT:ETH TRANSFER FAILED");

        if (GenesisNft.totalSupply() == (genesisSupply / 4)) {
            genesisStatus = false;
        }
        emit GenesisMinted(_to, _id, uint16(_quantity));
    }

    function mintGiftGenesis(address _to) public onlyRole(ADMIN_ROLE) {
        INft GenesisNft = INft(genesisContractAddress);
        require(genesisStatus, "GENESIS MINT CURRENTLY INACTIVE");
        require(GenesisNft.totalSupply() < 500, "INSUFICIENT GENESIS STOCK");

        uint256 _id = GenesisNft.currentIndex();
        genesisHolder[_id] = Genesis({tokenId: _id, owner: _to, claimed: false});

        GenesisNft.mint(_to, 1);

        if (GenesisNft.totalSupply() == 500) {
            genesisStatus = false;
        }

        emit GenesisMinted(_to, _id, 1);
    }

    function mintSubscription(
        address _to,
        uint256 _quantity,
        uint256 _referralCode
    ) public payable {
        INft SubscriptionNft = INft(subsContractAddress);
        uint256 amountPaidPerNFT;

        require(subscriptionStatus, "SUBS MINT CURRENTLY INACTIVE");
        require(SubscriptionNft.totalSupply() + _quantity <= subscriptionSupply, "INSUFICIENT SUBSCRIPTION STOCK");

        if (subsStage == SubscriptionStage.TIER_ONE) {
            if (_quantity + SubscriptionNft.totalSupply() <= subscriptionDetails.tierOneQuantities) {
                require(msg.value >= (subscriptionDetails.tierOnePrice * _quantity), "INSUFFICIENT MINTING VALUE");
                amountPaidPerNFT = subscriptionDetails.tierOnePrice;
            } else {
                uint256 amountLeftOfTierOne = subscriptionDetails.tierOneQuantities - SubscriptionNft.totalSupply();
                uint256 amountToPayTierOne = amountLeftOfTierOne * subscriptionDetails.tierOnePrice;
                uint256 amountOfTierTwoMinted = _quantity - amountLeftOfTierOne;
                uint256 amountToPayTierTwo = amountOfTierTwoMinted * subscriptionDetails.tierTwoPrice;
                require(msg.value == (amountToPayTierOne + amountToPayTierTwo), "INSUFFICIENT MINTING VALUE");
                amountPaidPerNFT = subscriptionDetails.tierOnePrice;
            }
        } else if (subsStage == SubscriptionStage.TIER_TWO) {
            if (
                _quantity + SubscriptionNft.totalSupply() <=
                subscriptionDetails.tierTwoQuantities + subscriptionDetails.tierOneQuantities
            ) {
                require(msg.value == (subscriptionDetails.tierTwoPrice * _quantity), "INSUFFICIENT MINTING VALUE");
                amountPaidPerNFT = subscriptionDetails.tierTwoPrice;
            } else {
                uint256 amountLeftOfTierTwo = (subscriptionDetails.tierTwoQuantities +
                    subscriptionDetails.tierOneQuantities) - SubscriptionNft.totalSupply();
                uint256 amountToPayTierTwo = amountLeftOfTierTwo * subscriptionDetails.tierTwoPrice;
                uint256 amountOfTierThreeMinted = _quantity - amountLeftOfTierTwo;
                uint256 amountToPayTierThree = amountOfTierThreeMinted * subscriptionDetails.tierThreePrice;
                require(msg.value == (amountToPayTierTwo + amountToPayTierThree), "INSUFFICIENT MINTING VALUE");
                amountPaidPerNFT = subscriptionDetails.tierTwoPrice;
            }
        } else if (subsStage == SubscriptionStage.TIER_THREE) {
            if (
                _quantity + SubscriptionNft.totalSupply() <=
                subscriptionDetails.tierThreeQuantities +
                    subscriptionDetails.tierTwoQuantities +
                    subscriptionDetails.tierOneQuantities
            ) {
                require(msg.value == (subscriptionDetails.tierThreePrice * _quantity), "INSUFFICIENT MINTING VALUE");
                amountPaidPerNFT = subscriptionDetails.tierThreePrice;
            } else {
                uint256 amountLeftOfTierThree = (subscriptionDetails.tierTwoQuantities +
                    subscriptionDetails.tierOneQuantities +
                    subscriptionDetails.tierThreeQuantities) - SubscriptionNft.totalSupply();
                uint256 amountToPayTierThree = amountLeftOfTierThree * subscriptionDetails.tierThreePrice;
                uint256 amountOfTierFourMinted = _quantity - amountLeftOfTierThree;
                uint256 amountToPayTierFour = amountOfTierFourMinted * subscriptionDetails.tierFourPrice;
                require(msg.value == (amountToPayTierThree + amountToPayTierFour), "INSUFFICIENT MINTING VALUE");
                amountPaidPerNFT = subscriptionDetails.tierThreePrice;
            }
        } else if (subsStage == SubscriptionStage.TIER_FOUR) {
            require(msg.value == (subscriptionDetails.tierFourPrice * _quantity), "INSUFFICIENT MINTING VALUE");
            amountPaidPerNFT = subscriptionDetails.tierFourPrice;
        }

        uint256 _id = SubscriptionNft.currentIndex();

        do {
            subsHolder[_id] = Subscription({
                tokenId: _id,
                owner: _to,
                price: amountPaidPerNFT,
                expired: false,
                renewed: false,
                renewalExpire: 0
            });

            unchecked {
                ++_id;
            }
        } while (_id < (_quantity + SubscriptionNft.currentIndex()));

        SubscriptionNft.mint(_to, _quantity);

        if (
            SubscriptionNft.totalSupply() >=
            (subscriptionDetails.tierThreeQuantities +
                subscriptionDetails.tierTwoQuantities +
                subscriptionDetails.tierOneQuantities)
        ) {
            subsStage = SubscriptionStage.TIER_FOUR;
        } else if (
            SubscriptionNft.totalSupply() >=
            (subscriptionDetails.tierTwoQuantities + subscriptionDetails.tierOneQuantities)
        ) {
            subsStage = SubscriptionStage.TIER_THREE;
        } else if (SubscriptionNft.totalSupply() >= subscriptionDetails.tierOneQuantities) {
            subsStage = SubscriptionStage.TIER_TWO;
        }

        if (referralCodes[msg.sender] == 0) {
            uint256 referralCode = _generateReferralCode(msg.sender, _quantity);
            referralCodes[msg.sender] = referralCode;
            codeToAddress[referralCode] = msg.sender;
        }

        if (_referralCode != 0) {
            require(isCodeValid(_referralCode), "INVALID REFERRAL CODE");
            address referee = codeToAddress[_referralCode];
            ITreasury treasury = ITreasury(payable(treasuryAddress));
            treasury.payReward(referee);
        }

        cap3TreasuryFundShare(msg.value);

        emit SubscriptionMinted(_to, _id, uint16(_quantity));

    }

    function refund(bool _state) public onlyRole(ADMIN_ROLE) {
        require(genesisStatus == false, "GENESIS MINT STILL OPEN");
        string memory boolString = _state == true ? "true" : "false";
        require(refundFlag != _state, string(abi.encodePacked("Refund Flag already ", boolString)));
        refundFlag = _state;
    }

    function claimRefund(uint256[] calldata tokenIds) public {
        INft GenesisNft = INft(genesisContractAddress);
        ITreasury treasury = ITreasury(treasuryAddress);
        require(genesisStatus == false, "GENESIS MINT STILL OPEN");
        require(refundFlag == true, "REFUND NOT OPEN");
        uint256 arrayLength = tokenIds.length;
        uint256[] memory refundedTokens = new uint256[](arrayLength);

        for (uint256 i = 0; i < tokenIds.length; i++) {
            uint256 tokenId = tokenIds[i];
            if (GenesisNft.ownerOf(tokenId) == msg.sender) {
                uint256 refundAmount = genesisPrice;
                GenesisNft.burn(tokenId);
                delete (genesisHolder[tokenId]);
                treasury.payRefund(msg.sender, refundAmount);
                refundedTokens[i] = tokenId;
            }
        }

        emit Refunded(msg.sender, refundedTokens, genesisPrice);
    }

    function isCodeValid(uint256 _code) public view returns (bool) {
        address referee = codeToAddress[_code];
        if (referee == address(0)) {
            return false;
        } else {
            return true;
        }
    }

    function setDistibuteGenesisTokensActive() public onlyRole(ADMIN_ROLE) {
        require(genesisStatus == false, "GENESIS MINT STILL OPEN");
        require(distributeFlag != true, "PHASE ALREADY ACTIVE");
        distributeFlag = true;
        emit DistributionActive();
    }

    function claimDistributedTokens(uint256 _tokenID) public {
        INft GenesisNft = INft(genesisContractAddress);

        require(distributeFlag == true, "DISTRIBUTION NOT OPEN");
        require(!genesisHolder[_tokenID].claimed, "ALREADY CLAIMED");
        require(msg.sender == genesisHolder[_tokenID].owner, "NOT NFT OWNER");

        uint256 _id = GenesisNft.currentIndex();

        do {
            genesisHolder[_id] = Genesis({tokenId: _id, owner: msg.sender, claimed: true});
            unchecked {
                ++_id;
            }
        } while (_id < (3 + GenesisNft.currentIndex()));

        genesisHolder[_tokenID].claimed = true;
        GenesisNft.mint(msg.sender, 3);

        emit GenesisMinted(msg.sender, _id, 3);

    }

    function burnToken(uint256 _tokenId) public {
        INft GenesisNft = INft(genesisContractAddress);

        require(GenesisNft.totalSupply() < 500, "TOKENS SOLDOUT");
        require(GenesisNft.ownerOf(_tokenId) == msg.sender, "NOT TOKEN HOLDER");
        GenesisNft.burn(_tokenId);
        delete (genesisHolder[_tokenId]);

        unchecked {
            toRefund[msg.sender] += 1;
        }
    }

    function transferNft(address _to, uint256 _tokenId) public onlyRole(NFT_ROLE) {
        INft GenesisNft = INft(genesisContractAddress);
        INft SubscriptionNft = INft(subsContractAddress);

        if (msg.sender == genesisContractAddress) {
            require(_tokenId <= GenesisNft.totalSupply(), "INVALID ID");
            Genesis storage token = genesisHolder[_tokenId];
            token.owner = _to;
            emit NftTransfered(_to, _tokenId, true);
        } else if (msg.sender == subsContractAddress) {
            require(_tokenId <= SubscriptionNft.totalSupply(), "INVALID ID");
            Subscription storage token = subsHolder[_tokenId];
            token.owner = _to;
            emit NftTransfered(_to, _tokenId, false);
        }
    }

    function subscriptionExpiry(uint256 _tokenId) external onlyRole(EXECUTOR_ROLE) {
        INft SubscriptionNft = INft(subsContractAddress);
        require(_tokenId <= SubscriptionNft.currentIndex(), "INVALID ID");
        Subscription storage token = subsHolder[_tokenId];
        require(token.expired == false, "TOKEN ALREADY EXPIRED");
        token.expired = true;
        if (token.renewed == true) {
            _updateSubscriptionMintBalance(_tokenId);
            token.renewalExpire = 0;
        } else token.renewalExpire = block.timestamp + 7 days;

        emit ExpiredSubscription(token.owner, token.renewalExpire, token.tokenId);
    }

    function renewSubscription(uint256 _tokenId) public payable {
        Subscription storage token = subsHolder[_tokenId];

        require(msg.value == (token.price), "INSUFFICIENT AMOUNT SENT");
        require(token.renewalExpire > 0, "SUBSCRIPTION NOT EXPIRED");
        require(block.timestamp <= token.renewalExpire, "RENEWAL DATE HAS EXPIRED");
        require(token.renewed == false, "ALREADY RENEWED");

        cap3TreasuryFundShare(msg.value);
        token.renewed = true;
        token.expired = false;
        emit SubscriptionRenewed(msg.sender, token.tokenId);
    }

    function updateSubscriptionMintBalance(uint256 _tokenId) public onlyRole(EXECUTOR_ROLE) {
        _updateSubscriptionMintBalance(_tokenId);
    }

    function _updateSubscriptionMintBalance(uint256 _tokenId) internal {
        Subscription storage token = subsHolder[_tokenId];
        require(token.expired == true, "NON EXPIRED TOKEN");
        require(block.timestamp >= token.renewalExpire, "RENEWAL DATELINE NOT PASSED");
        unchecked {
            subscriptionSupply++;
        }
        emit SubscriptionBalanceUpdated(subscriptionSupply, _tokenId);
    }

    function addApprovedProposal(
        string memory _id,
        string memory _title,
        address _author
    ) public onlyRole(ADMIN_ROLE) {
        proposals[_id] = Project({id: _id, description: _title, author: _author, funded: false});
        ITreasury treasury = ITreasury(payable(treasuryAddress));

        uint256 dollarValueOfEth = getLatestPrice();
        uint256 _funds = (projectFund * 10**18) / dollarValueOfEth;

        treasury.setProjectBalance(_author, _funds);
        emit ProposalApproved(_id, _title, _author, _funds);
    }

    function fundProposal(string memory _id, uint256 _amount) public onlyRole(ADMIN_ROLE) {
        Project memory proposal = proposals[_id];
        require(proposal.funded == false, "PROJECT HAS BEEN FUNDED");
        ITreasury treasury = ITreasury(treasuryAddress);

        treasury.withdrawToProjectWallet(proposal.author, _amount);

        if (treasury.getProjectBalance(proposal.author) == 0) {
            proposal.funded = true;
        }
        emit ProposalFunded(_id, _amount);
    }

    function cap3TreasuryFundShare(uint256 _amount) internal {
        uint256 dollarValueOfEth = getLatestPrice();
        uint256 limitInEth = (treasuryLimit * 10**18) / dollarValueOfEth;

        if (address(treasuryAddress).balance > limitInEth) {
            uint256 extraBalance = address(treasuryAddress).balance - limitInEth;

            ITreasury treasury = ITreasury(payable(treasuryAddress));
            treasury.payRefund(cap3Wallet, extraBalance);

            (bool success, ) = cap3Wallet.call{value: _amount}("");
            require(success, "MINT:ETH TRANSFER FAILED");
        } else if (address(treasuryAddress).balance == limitInEth) {
            (bool success, ) = cap3Wallet.call{value: _amount}("");
            require(success, "MINT:ETH TRANSFER FAILED");
        } else if ((address(treasuryAddress).balance + _amount) > limitInEth) {
            uint256 treasuryAmount = limitInEth - address(treasuryAddress).balance;
            uint256 cap3amount = _amount - treasuryAmount;

            (bool success, ) = treasuryAddress.call{value: treasuryAmount}("");
            require(success, "MINT:ETH TRANSFER FAILED");

            (success, ) = cap3Wallet.call{value: cap3amount}("");
            require(success, "MINT:ETH TRANSFER FAILED");
        } else if ((address(treasuryAddress).balance + _amount) <= limitInEth) {
            (bool success, ) = treasuryAddress.call{value: _amount}("");
            require(success, "MINT:ETH TRANSFER FAILED");
        }
    }

    function setTreasuryLimit(uint256 _newLimit) public onlyRole(ADMIN_ROLE) {
        _setTreasuryLimit(_newLimit);
    }

    function switchGenesisMint(bool _state) public onlyRole(ADMIN_ROLE) {
        string memory boolString = _state == true ? "true" : "false";
        require(genesisStatus != _state, string(abi.encodePacked("Genesis Flag already ", boolString)));
        genesisStatus = _state;
        emit GenesisMintStateUpdated(_state);
    }

    function switchSubscriptionMint(bool _state) public onlyRole(ADMIN_ROLE) {
        string memory boolString = _state == true ? "true" : "false";
        require(subscriptionStatus != _state, string(abi.encodePacked("Subscription Flag already ", boolString)));
        subscriptionStatus = _state;
        emit SubscriptionMintStateUpdated(_state);
    }

    function switchSubscriptionStage(uint256 _stage) public onlyRole(ADMIN_ROLE) {
        require(_stage <= 4, "Invalid stage");
        require(_stage > 0, "Invalid stage");

        if (_stage == 1) {
            subsStage = SubscriptionStage.TIER_ONE;
        } else if (_stage == 2) {
            subsStage = SubscriptionStage.TIER_TWO;
        } else if (_stage == 3) {
            subsStage = SubscriptionStage.TIER_THREE;
        } else {
            subsStage = SubscriptionStage.TIER_FOUR;
        }

        emit SubscriptionStageUpdated(_stage);
    }

    function setMerkleRoot(bytes32 _merkleRoot) public onlyRole(ADMIN_ROLE) {
        merkleRoot = _merkleRoot;
        emit MerkleRootSet(_merkleRoot);
    }

    function setBackendAdress(address _backendAddress) public onlyRole(ADMIN_ROLE) {
        require(_backendAddress != address(0), "ADDRESS ZERO");
        _setupRole(EXECUTOR_ROLE, _backendAddress);
        emit UpdatedBackendAddress(_backendAddress, "EXECUTOR_ROLE");
    }

    function setGenesisVotingPower(uint256 newVotingPower) public onlyRole(ADMIN_ROLE) {
        genesisVotingPower = newVotingPower;
    }

    function setAdminRole(address _adminAddress) public onlyRole(ADMIN_ROLE) {
        _setupRole(ADMIN_ROLE, _adminAddress);
    }

    function setGenesisPrice(uint256 price) public onlyRole(ADMIN_ROLE) {
        genesisPrice = price;
    }

    function setSubscriptionPrice(uint256 price, uint256 tier) public onlyRole(ADMIN_ROLE) {
        if (tier == 1) {
            subscriptionDetails.tierOnePrice = price;
        } else if (tier == 2) {
            subscriptionDetails.tierTwoPrice = price;
        } else if (tier == 3) {
            subscriptionDetails.tierThreePrice = price;
        } else if (tier == 4) {
            subscriptionDetails.tierFourPrice = price;
        }

        emit SubscriptionPriceUpdated(tier, price);
    }

    function setSubscriptionQuantities(uint256 quantity, uint256 tier) public onlyRole(ADMIN_ROLE) {
        if (tier == 1) {
            subscriptionDetails.tierOneQuantities = quantity;
        } else if (tier == 2) {
            subscriptionDetails.tierTwoQuantities = quantity;
        } else if (tier == 3) {
            subscriptionDetails.tierThreeQuantities = quantity;
        } else if (tier == 4) {
            subscriptionDetails.tierFourQuantities = quantity;
        }

        emit SubscriptionQuantitesUpdated(tier, quantity);
    }

    function setProjectFundDollars(uint256 price) public onlyRole(ADMIN_ROLE) {
        projectFund = price;
    }

    function setSubscriptionVotingPower(uint256 newVotingPower) public onlyRole(ADMIN_ROLE) {
        subscriptionVotingPower = newVotingPower;
    }

    function setCap3Wallet(address _wallet) public onlyRole(ADMIN_ROLE) {
        cap3Wallet = _wallet;
    }

    function setGenesisSupply(uint256 _newGenesisSupply) public onlyRole(ADMIN_ROLE) {
        INft GenesisNft = INft(genesisContractAddress);
        require(_newGenesisSupply >= GenesisNft.totalSupply());
        genesisSupply = _newGenesisSupply;
    }

    function setGenesisMintPublic() public onlyRole(ADMIN_ROLE) {
        merkleRoot = ZEROSTATE;
        emit MerkleRootSet(merkleRoot);
    }

    function setSubscriptionSupply(uint256 _newSubscriptionSupply) public onlyRole(ADMIN_ROLE) {
        INft SubscriptionNft = INft(subsContractAddress);
        require(_newSubscriptionSupply >= SubscriptionNft.totalSupply());
        subscriptionSupply = _newSubscriptionSupply;
    }

    /*------ View Functions -------*/

    function verifyMerkleProof(address _user, bytes32[] calldata _merkleProof) public view returns (bool) {
        return MerkleProof.verify(_merkleProof, merkleRoot, keccak256(abi.encodePacked(_user)));
    }

    function getGenesisSupply() public view returns (uint256) {
        return genesisSupply;
    }

    function getSubscriptionSupply() public view returns (uint256) {
        return subscriptionSupply;
    }

    function getProposal(string memory _tokenId) public view returns (Project memory) {
        return proposals[_tokenId];
    }

    function getGenesisHolder(uint256 _tokenId) public view returns (Genesis memory) {
        return genesisHolder[_tokenId];
    }

    function getSubscriptionHolder(uint256 _tokenId) public view returns (Subscription memory) {
        return subsHolder[_tokenId];
    }

    function getTreasuryLimit() public view returns (uint256) {
        return treasuryLimit;
    }

    function getCap3WalletAddress() public view returns (address) {
        return cap3Wallet;
    }

    function subscriptionHasExpired(uint256 _tokenId) public view returns (bool) {
        Subscription storage token = subsHolder[_tokenId];
        return token.expired;
    }

    function userVotingPower(address _holder) public view returns (uint256) {
        INft GenesisNft = INft(genesisContractAddress);
        uint256 votingPower = 0;
        votingPower += getValidSubscriptions(_holder) * subscriptionVotingPower;
        votingPower += GenesisNft.balanceOf(_holder) * genesisVotingPower;
        return votingPower;
    }

    function getValidSubscriptions(address _holder) public view returns (uint256) {
        INft SubscriptionNft = INft(subsContractAddress);
        uint256 subscriptionsValid = 0;
        uint256 subscriptionsIndex = 1;
        uint256 subscriptionsChecked = 0;

        while (subscriptionsChecked < SubscriptionNft.balanceOf(_holder)) {
            if (SubscriptionNft.ownerOf(subscriptionsIndex) == _holder) {
                if (!subscriptionHasExpired(subscriptionsIndex)) {
                    subscriptionsValid++;
                }
                subscriptionsChecked++;
            }
            subscriptionsIndex++;
        }

        return subscriptionsValid;
    }

    function getLatestPrice() public view returns (uint256) {
        (
            ,
            /*uint80 roundID*/
            int256 price, /*uint startedAt*/ /*uint timeStamp*/ /*uint80 answeredInRound*/
            ,
            ,

        ) = priceFeed.latestRoundData();

        return uint256(price / 10**8);
    }

    function getReferralCode(address _referee) public view returns (uint256) {
        return referralCodes[_referee];
    }

    /*------ Internal Functions -------*/

    function _setTreasuryLimit(uint256 _newLimit) internal {
        uint256 oldLimit = treasuryLimit;
        treasuryLimit = _newLimit;
        emit TreasuryLimitSet(_newLimit, oldLimit);
    }

    function _generateReferralCode(address _sender, uint256 _numberOfTokensMinted) internal view returns (uint256) {
        uint256 randomHash = uint256(keccak256(abi.encode(_sender, _numberOfTokensMinted, block.timestamp)));
        uint256 code = randomHash % 10000000;
        return code;
    }
}

// File: @openzeppelin/contracts/access/AccessControl.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.6.0) (access/AccessControl.sol)

pragma solidity ^0.8.0;

import "./IAccessControl.sol";
import "../utils/Context.sol";
import "../utils/Strings.sol";
import "../utils/introspection/ERC165.sol";

/**
 * @dev Contract module that allows children to implement role-based access
 * control mechanisms. This is a lightweight version that doesn't allow enumerating role
 * members except through off-chain means by accessing the contract event logs. Some
 * applications may benefit from on-chain enumerability, for those cases see
 * {AccessControlEnumerable}.
 *
 * Roles are referred to by their `bytes32` identifier. These should be exposed
 * in the external API and be unique. The best way to achieve this is by
 * using `public constant` hash digests:
 *
 * ```
 * bytes32 public constant MY_ROLE = keccak256("MY_ROLE");
 * ```
 *
 * Roles can be used to represent a set of permissions. To restrict access to a
 * function call, use {hasRole}:
 *
 * ```
 * function foo() public {
 *     require(hasRole(MY_ROLE, msg.sender));
 *     ...
 * }
 * ```
 *
 * Roles can be granted and revoked dynamically via the {grantRole} and
 * {revokeRole} functions. Each role has an associated admin role, and only
 * accounts that have a role's admin role can call {grantRole} and {revokeRole}.
 *
 * By default, the admin role for all roles is `DEFAULT_ADMIN_ROLE`, which means
 * that only accounts with this role will be able to grant or revoke other
 * roles. More complex role relationships can be created by using
 * {_setRoleAdmin}.
 *
 * WARNING: The `DEFAULT_ADMIN_ROLE` is also its own admin: it has permission to
 * grant and revoke this role. Extra precautions should be taken to secure
 * accounts that have been granted it.
 */
abstract contract AccessControl is Context, IAccessControl, ERC165 {
    struct RoleData {
        mapping(address => bool) members;
        bytes32 adminRole;
    }

    mapping(bytes32 => RoleData) private _roles;

    bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00;

    /**
     * @dev Modifier that checks that an account has a specific role. Reverts
     * with a standardized message including the required role.
     *
     * The format of the revert reason is given by the following regular expression:
     *
     *  /^AccessControl: account (0x[0-9a-f]{40}) is missing role (0x[0-9a-f]{64})$/
     *
     * _Available since v4.1._
     */
    modifier onlyRole(bytes32 role) {
        _checkRole(role);
        _;
    }

    /**
     * @dev See {IERC165-supportsInterface}.
     */
    function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
        return interfaceId == type(IAccessControl).interfaceId || super.supportsInterface(interfaceId);
    }

    /**
     * @dev Returns `true` if `account` has been granted `role`.
     */
    function hasRole(bytes32 role, address account) public view virtual override returns (bool) {
        return _roles[role].members[account];
    }

    /**
     * @dev Revert with a standard message if `_msgSender()` is missing `role`.
     * Overriding this function changes the behavior of the {onlyRole} modifier.
     *
     * Format of the revert message is described in {_checkRole}.
     *
     * _Available since v4.6._
     */
    function _checkRole(bytes32 role) internal view virtual {
        _checkRole(role, _msgSender());
    }

    /**
     * @dev Revert with a standard message if `account` is missing `role`.
     *
     * The format of the revert reason is given by the following regular expression:
     *
     *  /^AccessControl: account (0x[0-9a-f]{40}) is missing role (0x[0-9a-f]{64})$/
     */
    function _checkRole(bytes32 role, address account) internal view virtual {
        if (!hasRole(role, account)) {
            revert(
                string(
                    abi.encodePacked(
                        "AccessControl: account ",
                        Strings.toHexString(uint160(account), 20),
                        " is missing role ",
                        Strings.toHexString(uint256(role), 32)
                    )
                )
            );
        }
    }

    /**
     * @dev Returns the admin role that controls `role`. See {grantRole} and
     * {revokeRole}.
     *
     * To change a role's admin, use {_setRoleAdmin}.
     */
    function getRoleAdmin(bytes32 role) public view virtual override returns (bytes32) {
        return _roles[role].adminRole;
    }

    /**
     * @dev Grants `role` to `account`.
     *
     * If `account` had not been already granted `role`, emits a {RoleGranted}
     * event.
     *
     * Requirements:
     *
     * - the caller must have ``role``'s admin role.
     */
    function grantRole(bytes32 role, address account) public virtual override onlyRole(getRoleAdmin(role)) {
        _grantRole(role, account);
    }

    /**
     * @dev Revokes `role` from `account`.
     *
     * If `account` had been granted `role`, emits a {RoleRevoked} event.
     *
     * Requirements:
     *
     * - the caller must have ``role``'s admin role.
     */
    function revokeRole(bytes32 role, address account) public virtual override onlyRole(getRoleAdmin(role)) {
        _revokeRole(role, account);
    }

    /**
     * @dev Revokes `role` from the calling account.
     *
     * Roles are often managed via {grantRole} and {revokeRole}: this function's
     * purpose is to provide a mechanism for accounts to lose their privileges
     * if they are compromised (such as when a trusted device is misplaced).
     *
     * If the calling account had been revoked `role`, emits a {RoleRevoked}
     * event.
     *
     * Requirements:
     *
     * - the caller must be `account`.
     */
    function renounceRole(bytes32 role, address account) public virtual override {
        require(account == _msgSender(), "AccessControl: can only renounce roles for self");

        _revokeRole(role, account);
    }

    /**
     * @dev Grants `role` to `account`.
     *
     * If `account` had not been already granted `role`, emits a {RoleGranted}
     * event. Note that unlike {grantRole}, this function doesn't perform any
     * checks on the calling account.
     *
     * [WARNING]
     * ====
     * This function should only be called from the constructor when setting
     * up the initial roles for the system.
     *
     * Using this function in any other way is effectively circumventing the admin
     * system imposed by {AccessControl}.
     * ====
     *
     * NOTE: This function is deprecated in favor of {_grantRole}.
     */
    function _setupRole(bytes32 role, address account) internal virtual {
        _grantRole(role, account);
    }

    /**
     * @dev Sets `adminRole` as ``role``'s admin role.
     *
     * Emits a {RoleAdminChanged} event.
     */
    function _setRoleAdmin(bytes32 role, bytes32 adminRole) internal virtual {
        bytes32 previousAdminRole = getRoleAdmin(role);
        _roles[role].adminRole = adminRole;
        emit RoleAdminChanged(role, previousAdminRole, adminRole);
    }

    /**
     * @dev Grants `role` to `account`.
     *
     * Internal function without access restriction.
     */
    function _grantRole(bytes32 role, address account) internal virtual {
        if (!hasRole(role, account)) {
            _roles[role].members[account] = true;
            emit RoleGranted(role, account, _msgSender());
        }
    }

    /**
     * @dev Revokes `role` from `account`.
     *
     * Internal function without access restriction.
     */
    function _revokeRole(bytes32 role, address account) internal virtual {
        if (hasRole(role, account)) {
            _roles[role].members[account] = false;
            emit RoleRevoked(role, account, _msgSender());
        }
    }
}

// File: @openzeppelin/contracts/utils/cryptography/MerkleProof.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.6.0) (utils/cryptography/MerkleProof.sol)

pragma solidity ^0.8.0;

/**
 * @dev These functions deal with verification of Merkle Trees proofs.
 *
 * The proofs can be generated using the JavaScript library
 * https://github.com/miguelmota/merkletreejs[merkletreejs].
 * Note: the hashing algorithm should be keccak256 and pair sorting should be enabled.
 *
 * See `test/utils/cryptography/MerkleProof.test.js` for some examples.
 *
 * WARNING: You should avoid using leaf values that are 64 bytes long prior to
 * hashing, or use a hash function other than keccak256 for hashing leaves.
 * This is because the concatenation of a sorted pair of internal nodes in
 * the merkle tree could be reinterpreted as a leaf value.
 */
library MerkleProof {
    /**
     * @dev Returns true if a `leaf` can be proved to be a part of a Merkle tree
     * defined by `root`. For this, a `proof` must be provided, containing
     * sibling hashes on the branch from the leaf to the root of the tree. Each
     * pair of leaves and each pair of pre-images are assumed to be sorted.
     */
    function verify(
        bytes32[] memory proof,
        bytes32 root,
        bytes32 leaf
    ) internal pure returns (bool) {
        return processProof(proof, leaf) == root;
    }

    /**
     * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up
     * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt
     * hash matches the root of the tree. When processing the proof, the pairs
     * of leafs & pre-images are assumed to be sorted.
     *
     * _Available since v4.4._
     */
    function processProof(bytes32[] memory proof, bytes32 leaf) internal pure returns (bytes32) {
        bytes32 computedHash = leaf;
        for (uint256 i = 0; i < proof.length; i++) {
            bytes32 proofElement = proof[i];
            if (computedHash <= proofElement) {
                // Hash(current computed hash + current element of the proof)
                computedHash = _efficientHash(computedHash, proofElement);
            } else {
                // Hash(current element of the proof + current computed hash)
                computedHash = _efficientHash(proofElement, computedHash);
            }
        }
        return computedHash;
    }

    function _efficientHash(bytes32 a, bytes32 b) private pure returns (bytes32 value) {
        assembly {
            mstore(0x00, a)
            mstore(0x20, b)
            value := keccak256(0x00, 0x40)
        }
    }
}

// File: contracts/interfaces/ITreasury.sol



// SPDX-License-Identifier: MIT
pragma solidity 0.8.11;

interface ITreasury {
    function withdrawToProjectWallet(address projectWallet, uint256 amount) external;

    function shutdown(bool _isShutdown) external;

    function viewFundsInTreasury() external view returns (uint256);

    function payRefund(address _to, uint256 _amount) external;

    function setProjectBalance(address _projectWallet, uint256 _balance) external;

    function getProjectBalance(address _projectWallet) external view returns (uint256);

    function moveFundsOutOfTreasury() external;

    function setAdminRole(address _adminAddress) external;

    function payReward(address _to) external;
}

// File: contracts/interfaces/IDynamic.sol



// SPDX-License-Identifier: MIT

pragma solidity 0.8.11;

interface IDynamic {
    function renewSubscription(uint256 tokenid) external payable;

    function mintGenesis(
        address to,
        bytes32[] calldata _merkleProof,
        uint256 amount
    ) external payable;

    function mintGiftGenesis(address _to) external;

    function mintSubscription(
        address to,
        uint256 amount,
        uint256 code
    ) external payable;

    function setDistibuteGenesisTokensActive() external;

    function claimDistributedTokens(uint256 tokenId) external;

    function subscriptionExpiry(uint256 tokenId) external;

    function updateSubscriptionMintBalance(uint256 tokenId) external;

    function addApprovedProposal(
        string memory id,
        string memory title,
        address author
    ) external;

    function transferNft(address _to, uint256 _tokenId) external;

    function fundProposal(string memory id, uint256 amount) external;

    function getReferralCode(address referree) external view returns (uint256);
}

// File: contracts/interfaces/INft.sol



// SPDX-License-Identifier: MIT
pragma solidity 0.8.11;
import "erc721a/contracts/IERC721A.sol";

interface INft is IERC721A {
    function mint(address to, uint256 amount) external;

    function burn(uint256 tokenId) external;

    function currentIndex() external returns (uint256);
}

// File: @chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface AggregatorV3Interface {
  function decimals() external view returns (uint8);

  function description() external view returns (string memory);

  function version() external view returns (uint256);

  // getRoundData and latestRoundData should both raise "No data present"
  // if they do not have data to report, instead of returning unset values
  // which could be misinterpreted as actual reported values.
  function getRoundData(uint80 _roundId)
    external
    view
    returns (
      uint80 roundId,
      int256 answer,
      uint256 startedAt,
      uint256 updatedAt,
      uint80 answeredInRound
    );

  function latestRoundData()
    external
    view
    returns (
      uint80 roundId,
      int256 answer,
      uint256 startedAt,
      uint256 updatedAt,
      uint80 answeredInRound
    );
}

// File: @openzeppelin/contracts/access/IAccessControl.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (access/IAccessControl.sol)

pragma solidity ^0.8.0;

/**
 * @dev External interface of AccessControl declared to support ERC165 detection.
 */
interface IAccessControl {
    /**
     * @dev Emitted when `newAdminRole` is set as ``role``'s admin role, replacing `previousAdminRole`
     *
     * `DEFAULT_ADMIN_ROLE` is the starting admin for all roles, despite
     * {RoleAdminChanged} not being emitted signaling this.
     *
     * _Available since v3.1._
     */
    event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole);

    /**
     * @dev Emitted when `account` is granted `role`.
     *
     * `sender` is the account that originated the contract call, an admin role
     * bearer except when using {AccessControl-_setupRole}.
     */
    event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender);

    /**
     * @dev Emitted when `account` is revoked `role`.
     *
     * `sender` is the account that originated the contract call:
     *   - if using `revokeRole`, it is the admin role bearer
     *   - if using `renounceRole`, it is the role bearer (i.e. `account`)
     */
    event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender);

    /**
     * @dev Returns `true` if `account` has been granted `role`.
     */
    function hasRole(bytes32 role, address account) external view returns (bool);

    /**
     * @dev Returns the admin role that controls `role`. See {grantRole} and
     * {revokeRole}.
     *
     * To change a role's admin, use {AccessControl-_setRoleAdmin}.
     */
    function getRoleAdmin(bytes32 role) external view returns (bytes32);

    /**
     * @dev Grants `role` to `account`.
     *
     * If `account` had not been already granted `role`, emits a {RoleGranted}
     * event.
     *
     * Requirements:
     *
     * - the caller must have ``role``'s admin role.
     */
    function grantRole(bytes32 role, address account) external;

    /**
     * @dev Revokes `role` from `account`.
     *
     * If `account` had been granted `role`, emits a {RoleRevoked} event.
     *
     * Requirements:
     *
     * - the caller must have ``role``'s admin role.
     */
    function revokeRole(bytes32 role, address account) external;

    /**
     * @dev Revokes `role` from the calling account.
     *
     * Roles are often managed via {grantRole} and {revokeRole}: this function's
     * purpose is to provide a mechanism for accounts to lose their privileges
     * if they are compromised (such as when a trusted device is misplaced).
     *
     * If the calling account had been granted `role`, emits a {RoleRevoked}
     * event.
     *
     * Requirements:
     *
     * - the caller must be `account`.
     */
    function renounceRole(bytes32 role, address account) external;
}

// File: @openzeppelin/contracts/utils/Context.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (utils/Context.sol)

pragma solidity ^0.8.0;

/**
 * @dev Provides information about the current execution context, including the
 * sender of the transaction and its data. While these are generally available
 * via msg.sender and msg.data, they should not be accessed in such a direct
 * manner, since when dealing with meta-transactions the account sending and
 * paying for execution may not be the actual sender (as far as an application
 * is concerned).
 *
 * This contract is only required for intermediate, library-like contracts.
 */
abstract contract Context {
    function _msgSender() internal view virtual returns (address) {
        return msg.sender;
    }

    function _msgData() internal view virtual returns (bytes calldata) {
        return msg.data;
    }
}

// File: @openzeppelin/contracts/utils/Strings.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (utils/Strings.sol)

pragma solidity ^0.8.0;

/**
 * @dev String operations.
 */
library Strings {
    bytes16 private constant _HEX_SYMBOLS = "0123456789abcdef";

    /**
     * @dev Converts a `uint256` to its ASCII `string` decimal representation.
     */
    function toString(uint256 value) internal pure returns (string memory) {
        // Inspired by OraclizeAPI's implementation - MIT licence
        // https://github.com/oraclize/ethereum-api/blob/b42146b063c7d6ee1358846c198246239e9360e8/oraclizeAPI_0.4.25.sol

        if (value == 0) {
            return "0";
        }
        uint256 temp = value;
        uint256 digits;
        while (temp != 0) {
            digits++;
            temp /= 10;
        }
        bytes memory buffer = new bytes(digits);
        while (value != 0) {
            digits -= 1;
            buffer[digits] = bytes1(uint8(48 + uint256(value % 10)));
            value /= 10;
        }
        return string(buffer);
    }

    /**
     * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation.
     */
    function toHexString(uint256 value) internal pure returns (string memory) {
        if (value == 0) {
            return "0x00";
        }
        uint256 temp = value;
        uint256 length = 0;
        while (temp != 0) {
            length++;
            temp >>= 8;
        }
        return toHexString(value, length);
    }

    /**
     * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length.
     */
    function toHexString(uint256 value, uint256 length) internal pure returns (string memory) {
        bytes memory buffer = new bytes(2 * length + 2);
        buffer[0] = "0";
        buffer[1] = "x";
        for (uint256 i = 2 * length + 1; i > 1; --i) {
            buffer[i] = _HEX_SYMBOLS[value & 0xf];
            value >>= 4;
        }
        require(value == 0, "Strings: hex length insufficient");
        return string(buffer);
    }
}

// File: @openzeppelin/contracts/utils/introspection/ERC165.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (utils/introspection/ERC165.sol)

pragma solidity ^0.8.0;

import "./IERC165.sol";

/**
 * @dev Implementation of the {IERC165} interface.
 *
 * Contracts that want to implement ERC165 should inherit from this contract and override {supportsInterface} to check
 * for the additional interface id that will be supported. For example:
 *
 * ```solidity
 * function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
 *     return interfaceId == type(MyInterface).interfaceId || super.supportsInterface(interfaceId);
 * }
 * ```
 *
 * Alternatively, {ERC165Storage} provides an easier to use but more expensive implementation.
 */
abstract contract ERC165 is IERC165 {
    /**
     * @dev See {IERC165-supportsInterface}.
     */
    function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
        return interfaceId == type(IERC165).interfaceId;
    }
}

// File: @openzeppelin/contracts/utils/introspection/IERC165.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (utils/introspection/IERC165.sol)

pragma solidity ^0.8.0;

/**
 * @dev Interface of the ERC165 standard, as defined in the
 * https://eips.ethereum.org/EIPS/eip-165[EIP].
 *
 * Implementers can declare support of contract interfaces, which can then be
 * queried by others ({ERC165Checker}).
 *
 * For an implementation, see {ERC165}.
 */
interface IERC165 {
    /**
     * @dev Returns true if this contract implements the interface defined by
     * `interfaceId`. See the corresponding
     * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section]
     * to learn more about how these ids are created.
     *
     * This function call must use less than 30 000 gas.
     */
    function supportsInterface(bytes4 interfaceId) external view returns (bool);
}

// File: erc721a/contracts/IERC721A.sol



// SPDX-License-Identifier: MIT
// ERC721A Contracts v4.2.2
// Creator: Chiru Labs

pragma solidity ^0.8.4;

/**
 * @dev Interface of ERC721A.
 */
interface IERC721A {
    /**
     * The caller must own the token or be an approved operator.
     */
    error ApprovalCallerNotOwnerNorApproved();

    /**
     * The token does not exist.
     */
    error ApprovalQueryForNonexistentToken();

    /**
     * The caller cannot approve to their own address.
     */
    error ApproveToCaller();

    /**
     * Cannot query the balance for the zero address.
     */
    error BalanceQueryForZeroAddress();

    /**
     * Cannot mint to the zero address.
     */
    error MintToZeroAddress();

    /**
     * The quantity of tokens minted must be more than zero.
     */
    error MintZeroQuantity();

    /**
     * The token does not exist.
     */
    error OwnerQueryForNonexistentToken();

    /**
     * The caller must own the token or be an approved operator.
     */
    error TransferCallerNotOwnerNorApproved();

    /**
     * The token must be owned by `from`.
     */
    error TransferFromIncorrectOwner();

    /**
     * Cannot safely transfer to a contract that does not implement the
     * ERC721Receiver interface.
     */
    error TransferToNonERC721ReceiverImplementer();

    /**
     * Cannot transfer to the zero address.
     */
    error TransferToZeroAddress();

    /**
     * The token does not exist.
     */
    error URIQueryForNonexistentToken();

    /**
     * The `quantity` minted with ERC2309 exceeds the safety limit.
     */
    error MintERC2309QuantityExceedsLimit();

    /**
     * The `extraData` cannot be set on an unintialized ownership slot.
     */
    error OwnershipNotInitializedForExtraData();

    // =============================================================
    //                            STRUCTS
    // =============================================================

    struct TokenOwnership {
        // The address of the owner.
        address addr;
        // Stores the start time of ownership with minimal overhead for tokenomics.
        uint64 startTimestamp;
        // Whether the token has been burned.
        bool burned;
        // Arbitrary data similar to `startTimestamp` that can be set via {_extraData}.
        uint24 extraData;
    }

    // =============================================================
    //                         TOKEN COUNTERS
    // =============================================================

    /**
     * @dev Returns the total number of tokens in existence.
     * Burned tokens will reduce the count.
     * To get the total number of tokens minted, please see {_totalMinted}.
     */
    function totalSupply() external view returns (uint256);

    // =============================================================
    //                            IERC165
    // =============================================================

    /**
     * @dev Returns true if this contract implements the interface defined by
     * `interfaceId`. See the corresponding
     * [EIP section](https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified)
     * to learn more about how these ids are created.
     *
     * This function call must use less than 30000 gas.
     */
    function supportsInterface(bytes4 interfaceId) external view returns (bool);

    // =============================================================
    //                            IERC721
    // =============================================================

    /**
     * @dev Emitted when `tokenId` token is transferred from `from` to `to`.
     */
    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);

    /**
     * @dev Emitted when `owner` enables `approved` to manage the `tokenId` token.
     */
    event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);

    /**
     * @dev Emitted when `owner` enables or disables
     * (`approved`) `operator` to manage all of its assets.
     */
    event ApprovalForAll(address indexed owner, address indexed operator, bool approved);

    /**
     * @dev Returns the number of tokens in `owner`'s account.
     */
    function balanceOf(address owner) external view returns (uint256 balance);

    /**
     * @dev Returns the owner of the `tokenId` token.
     *
     * Requirements:
     *
     * - `tokenId` must exist.
     */
    function ownerOf(uint256 tokenId) external view returns (address owner);

    /**
     * @dev Safely transfers `tokenId` token from `from` to `to`,
     * checking first that contract recipients are aware of the ERC721 protocol
     * to prevent tokens from being forever locked.
     *
     * Requirements:
     *
     * - `from` cannot be the zero address.
     * - `to` cannot be the zero address.
     * - `tokenId` token must exist and be owned by `from`.
     * - If the caller is not `from`, it must be have been allowed to move
     * this token by either {approve} or {setApprovalForAll}.
     * - If `to` refers to a smart contract, it must implement
     * {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
     *
     * Emits a {Transfer} event.
     */
    function safeTransferFrom(
        address from,
        address to,
        uint256 tokenId,
        bytes calldata data
    ) external;

    /**
     * @dev Equivalent to `safeTransferFrom(from, to, tokenId, '')`.
     */
    function safeTransferFrom(
        address from,
        address to,
        uint256 tokenId
    ) external;

    /**
     * @dev Transfers `tokenId` from `from` to `to`.
     *
     * WARNING: Usage of this method is discouraged, use {safeTransferFrom}
     * whenever possible.
     *
     * Requirements:
     *
     * - `from` cannot be the zero address.
     * - `to` cannot be the zero address.
     * - `tokenId` token must be owned by `from`.
     * - If the caller is not `from`, it must be approved to move this token
     * by either {approve} or {setApprovalForAll}.
     *
     * Emits a {Transfer} event.
     */
    function transferFrom(
        address from,
        address to,
        uint256 tokenId
    ) external;

    /**
     * @dev Gives permission to `to` to transfer `tokenId` token to another account.
     * The approval is cleared when the token is transferred.
     *
     * Only a single account can be approved at a time, so approving the
     * zero address clears previous approvals.
     *
     * Requirements:
     *
     * - The caller must own the token or be an approved operator.
     * - `tokenId` must exist.
     *
     * Emits an {Approval} event.
     */
    function approve(address to, uint256 tokenId) external;

    /**
     * @dev Approve or remove `operator` as an operator for the caller.
     * Operators can call {transferFrom} or {safeTransferFrom}
     * for any token owned by the caller.
     *
     * Requirements:
     *
     * - The `operator` cannot be the caller.
     *
     * Emits an {ApprovalForAll} event.
     */
    function setApprovalForAll(address operator, bool _approved) external;

    /**
     * @dev Returns the account approved for `tokenId` token.
     *
     * Requirements:
     *
     * - `tokenId` must exist.
     */
    function getApproved(uint256 tokenId) external view returns (address operator);

    /**
     * @dev Returns if the `operator` is allowed to manage all of the assets of `owner`.
     *
     * See {setApprovalForAll}.
     */
    function isApprovedForAll(address owner, address operator) external view returns (bool);

    // =============================================================
    //                        IERC721Metadata
    // =============================================================

    /**
     * @dev Returns the token collection name.
     */
    function name() external view returns (string memory);

    /**
     * @dev Returns the token collection symbol.
     */
    function symbol() external view returns (string memory);

    /**
     * @dev Returns the Uniform Resource Identifier (URI) for `tokenId` token.
     */
    function tokenURI(uint256 tokenId) external view returns (string memory);

    // =============================================================
    //                           IERC2309
    // =============================================================

    /**
     * @dev Emitted when tokens in `fromTokenId` to `toTokenId`
     * (inclusive) is transferred from `from` to `to`, as defined in the
     * [ERC2309](https://eips.ethereum.org/EIPS/eip-2309) standard.
     *
     * See {_mintERC2309} for more details.
     */
    event ConsecutiveTransfer(uint256 indexed fromTokenId, uint256 toTokenId, address indexed from, address indexed to);
}