pragma solidity ^ 0.8 .4;








contract BoredStackers is ERC721A, Pausable, Ownable {
    using Strings for uint256;

    enum SalePhase {
        Phase01,
        Phase02,
        Phase03
    }

    enum CouponType {
        Vip,
        Normal,
        Raffle,
        Public
    }

    struct Coupon {
        bytes32 r;
        bytes32 s;
        uint8 v;
    }

    uint64 public maxSupply = 5000;
    uint64 private constant mintPrice_TypeVip =  0.10 ether;
    uint64 private constant mintPrice_TypeNormal =  0.125 ether;
    uint64 private constant mintPrice_TypeRaffle =  0.15 ether;
    uint64 private mintPrice_Phase03_TypeAll =  0.15 ether;
    uint64 private constant maxMints_GiveAway = 100;
    uint64 private maxMints_PerAddress = 20;
    uint8 private constant maxMint_TypeVip_PerAddress = 5;
    uint8 private constant maxMint_TypeNormal_PerAddress = 3;
    uint8 private constant maxMint_TypeRaffle_PerAddress = 1;

    bool private isGiveAway;

    string private baseURI = "https://nft.boredstackers.com/metadata/";

    address private constant _adminSigner = 0x948C509D377b94e500d1ed97a4E28d4DF3289d14;
    address private constant treasuryAddress = 0xAc05046Eb1271489B061C8E95204800ac0bcaB72;

    mapping(address => uint256) public MintsCount;

    event NewURI(string newURI, address updatedBy);
    event WithdrawnPayment(uint256 tresuryBalance, address tresuryAddress, uint256 ownerBalance, address owner);
    event updatePhase(SalePhase phase, address updatedBy);
    event updateAdminSigner(address adminSigner, address updatedBy);
    event updateGiveAwayAddress(address giveAwayAddress, address updatedBy);
    event updateMaxSupply(uint256 newMaxSupply, address updatedBy);
    event updateMaxMintsPerAddress(uint256 newMaxMintsPerAddress, address updatedBy);
    event updateMintPricePhase03TypeAll(uint256 MintPricePhase03TypeAll, address updatedBy);

    SalePhase public phase = SalePhase.Phase01;

    constructor() ERC721A("Bored Stackers", "B$") {
        pause();
    }


    modifier onlyPhase03() {
        require(phase == SalePhase.Phase03, "Invalid phase");
        _;
    }

    /**
     * @dev setMaxSupply updates maxSupply
     *
     * Emits a {updateMaxSupply} event.
     *
     * Requirements:
     *
     * - Only the owner can call this function
     */
    function setMaxSupply(uint64 newMaxSupply)
    external
    onlyOwner {
        require(newMaxSupply > totalSupply(), "Invalid max supply");
        maxSupply = newMaxSupply;
        emit updateMaxSupply(newMaxSupply, msg.sender);
    }

    /**
     * @dev setMaxMintsPerAddress updates maxMintsPerAddress
     *
     * Emits a {updateMaxMintsPerAddress} event.
     *
     * Requirements:
     *
     * - Only the owner can call this function
     */
    function setMaxMintsPerAddress(uint8 max)
    external
    onlyOwner
    onlyPhase03 {
        maxMints_PerAddress = max;
        emit updateMaxMintsPerAddress(max, msg.sender);
    }

    /**
     * @dev setMintPricePhase03TypeAll updates mintPrice_Phase03_TypeAll
     *
     * Emits a {updateMintPricePhase03TypeAll} event.
     * Requirements:
     *
     * - Only the owner can call this function
     */
    function setMintPricePhase03TypeAll(uint8 max)
    external
    onlyOwner
    onlyPhase03 {
        mintPrice_Phase03_TypeAll = max;
        emit updateMintPricePhase03TypeAll(max, msg.sender);
    }

    /**
     * @dev setPhase updates the price and the phase to (Locked, Private, Presale or Public).
     $
     * Emits a {Unpaused} event.
     *
     * Requirements:
     *
     * - Only the owner can call this function
     */

    function setPhase(SalePhase phase_)
    external
    onlyOwner {
        phase = phase_;
        emit updatePhase(phase_, msg.sender);
    }


    /**
     * @dev setBaseUri updates the new token URI in contract.
     *
     * Emits a {NewURI} event.
     *
     * Requirements:
     *
     * - Only owner of contract can call this function
     **/

    function setBaseUri(string memory uri)
    external
    onlyOwner {
        baseURI = uri;
        emit NewURI(uri, msg.sender);
    }

    /**
     * @dev Mint to mint nft
     *
     * Emits [Transfer] event.
     *
     * Requirements:
     *
     * - should have a valid coupon if we are ()
     **/

    function mint(uint64 amount, Coupon memory coupon, CouponType couponType)
    external
    payable
    whenNotPaused {

        // verify coupon
        bytes32 digest = keccak256(
            abi.encode(couponType, msg.sender)
        );
        require(_isVerifiedCoupon(digest, coupon), "Invalid coupon");

        // check if user can mint
        if(phase == SalePhase.Phase01) {
            require(couponType == CouponType.Vip || couponType == CouponType.Normal,
                "Invalid coupon");
            if(couponType == CouponType.Vip) {
                _checkMint(amount, maxMint_TypeVip_PerAddress, mintPrice_TypeVip);
            }
            else if(couponType == CouponType.Normal) {
                _checkMint(amount, maxMint_TypeNormal_PerAddress, mintPrice_TypeNormal);
            }
        } else if(phase == SalePhase.Phase02) {
             require(couponType == CouponType.Vip || couponType == CouponType.Normal || couponType == CouponType.Raffle,
                "Invalid coupon");
            if(couponType == CouponType.Vip) {
                _checkMint(amount, maxMint_TypeVip_PerAddress, mintPrice_TypeVip);
            } else if(couponType == CouponType.Normal) {
                _checkMint(amount, maxMint_TypeNormal_PerAddress, mintPrice_TypeNormal);
            } else {
                _checkMint(amount, maxMint_TypeRaffle_PerAddress, mintPrice_TypeRaffle);
            }
        } else {
            require(couponType == CouponType.Vip || couponType == CouponType.Normal
                        || couponType == CouponType.Raffle || couponType == CouponType.Public,
                "Invalid coupon");
            _checkMint(amount, maxMints_PerAddress, mintPrice_Phase03_TypeAll);
        }

        // increment mint count
        unchecked {
            MintsCount[msg.sender] = MintsCount[msg.sender] + amount;
        }

        // mint
        _mint(msg.sender, amount);
    }

    function _checkMint(uint64 amount, uint64 maxMint, uint64 price) internal {
        require(amount < maxMint + 1, "Invalid amount");
        require(msg.value > amount * price - 1, "Insufficient funds in the wallet");
        require(MintsCount[msg.sender] + amount < maxMint + 1,
            "Maximum amount per wallet already minted for this phase");
        require(totalSupply() + amount < maxSupply + 1,
            "Max supply reached");
    }

    /**
     * @dev giveAway mints 100 NFT once.
     *
     * Emits a {Transfer} event.
     *
     * Requirements:
     *
     * - Only the giveAwayAddress call this function
     */

    function giveAway() external {
        require(msg.sender == treasuryAddress, "Invalid user");
        require(!isGiveAway, "Already minted");
        uint256 _tokenId = _nextTokenId();
        require(_tokenId + maxMints_GiveAway < maxSupply + 1, "Max supply limit reached");
        _mint(msg.sender, maxMints_GiveAway);
        isGiveAway = true;
    }

    /**
     * @dev _isVerifiedCoupon verify the coupon
     *
     */

    function _isVerifiedCoupon(bytes32 digest, Coupon memory coupon)
    internal
    pure
    returns(bool) {
        address signer = ecrecover(digest, coupon.v, coupon.r, coupon.s);
        require(signer != address(0), "ECDSA: invalid signature"); // Added check for zero address
        return signer == _adminSigner;
    }

    /**
     * @dev getAdminSigner returns the adminSigner address
     *
     */

    function getAdminSigner() public pure returns(address) {
        return _adminSigner;
    }

    /**
     * @dev getTreasuryAddress returns the treasury address
     *
     */

    function getTreasuryAddress() public pure returns(address) {
        return treasuryAddress;
    }

    /**
         * @dev getMintsCount returns count mints by address
     *
     */
    function getMintsCount(address _address) public view returns(uint256) {
        return MintsCount[_address];
    }

    /**
     * @dev getbaseURI returns the base uri
     *
     */

    function getbaseURI() public view returns(string memory) {
        return baseURI;
    }

    /**
     * @dev tokenURI returns the uri to meta data
     *
     */

    function tokenURI(uint256 tokenId)
    public
    view
    override
    returns(string memory) {
        require(_exists(tokenId), "ERC721A: Query for non-existent token");
            return bytes(baseURI).length > 0 ?
                string(abi.encodePacked(baseURI, tokenId.toString())) :
                "";

    }

    /// @dev Returns the starting token ID.
    function _startTokenId() internal view virtual override returns (uint256) {
        return 1;
    }


    /**
     * @dev pause() is used to pause contract.
     *
     * Emits a {Paused} event.
     *
     * Requirements:
     *
     * - Only the owner can call this function
     **/

    function pause() public onlyOwner whenNotPaused {
        _pause();
    }

    /**
     * @dev unpause() is used to unpause contract.
     *
     * Emits a {Unpaused} event.
     *
     * Requirements:
     *
     * - Only the owner can call this function
     **/

    function unpause() public onlyOwner whenPaused {
        _unpause();
    }

    /**
     * @dev withdraw is used to withdraw payment from contract.
     *
     * Emits a {WithdrawnPayment} event.
     *
     * Requirements:
     *
     * - Only the owner can call this function
     **/

    function withdraw() public {
        require(msg.sender == treasuryAddress || msg.sender == owner(), "Invalid user");
        uint256 balanceOwner = address(this).balance * 400 / 10000;
        address owner = owner();
        (bool success, ) = payable(owner).call{ value: balanceOwner }("");
        require(success, "Failed Transfer funds to owner");
        uint256 balanceTreasury = address(this).balance;
        (success, ) = payable(treasuryAddress).call{ value: balanceTreasury }("");
        require(success, "Failed Transfer funds to treasury");
        emit WithdrawnPayment(balanceTreasury, treasuryAddress, balanceOwner, owner);
    }

}
[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"ApprovalCallerNotOwnerNorApproved","type":"error"},{"inputs":[],"name":"ApprovalQueryForNonexistentToken","type":"error"},{"inputs":[],"name":"ApprovalToCurrentOwner","type":"error"},{"inputs":[],"name":"ApproveToCaller","type":"error"},{"inputs":[],"name":"BalanceQueryForZeroAddress","type":"error"},{"inputs":[],"name":"MintToZeroAddress","type":"error"},{"inputs":[],"name":"MintZeroQuantity","type":"error"},{"inputs":[],"name":"OwnerQueryForNonexistentToken","type":"error"},{"inputs":[],"name":"TransferCallerNotOwnerNorApproved","type":"error"},{"inputs":[],"name":"TransferFromIncorrectOwner","type":"error"},{"inputs":[],"name":"TransferToNonERC721ReceiverImplementer","type":"error"},{"inputs":[],"name":"TransferToZeroAddress","type":"error"},{"inputs":[],"name":"URIQueryForNonexistentToken","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"approved","type":"address"},{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"operator","type":"address"},{"indexed":false,"internalType":"bool","name":"approved","type":"bool"}],"name":"ApprovalForAll","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"string","name":"newURI","type":"string"},{"indexed":false,"internalType":"address","name":"updatedBy","type":"address"}],"name":"NewURI","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"previousOwner","type":"address"},{"indexed":true,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"account","type":"address"}],"name":"Paused","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"account","type":"address"}],"name":"Unpaused","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"tresuryBalance","type":"uint256"},{"indexed":false,"internalType":"address","name":"tresuryAddress","type":"address"},{"indexed":false,"internalType":"uint256","name":"ownerBalance","type":"uint256"},{"indexed":false,"internalType":"address","name":"owner","type":"address"}],"name":"WithdrawnPayment","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"adminSigner","type":"address"},{"indexed":false,"internalType":"address","name":"updatedBy","type":"address"}],"name":"updateAdminSigner","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"giveAwayAddress","type":"address"},{"indexed":false,"internalType":"address","name":"updatedBy","type":"address"}],"name":"updateGiveAwayAddress","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"newMaxMintsPerAddress","type":"uint256"},{"indexed":false,"internalType":"address","name":"updatedBy","type":"address"}],"name":"updateMaxMintsPerAddress","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"newMaxSupply","type":"uint256"},{"indexed":false,"internalType":"address","name":"updatedBy","type":"address"}],"name":"updateMaxSupply","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"MintPricePhase03TypeAll","type":"uint256"},{"indexed":false,"internalType":"address","name":"updatedBy","type":"address"}],"name":"updateMintPricePhase03TypeAll","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"enum BoredStackers.SalePhase","name":"phase","type":"uint8"},{"indexed":false,"internalType":"address","name":"updatedBy","type":"address"}],"name":"updatePhase","type":"event"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"MintsCount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"approve","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getAdminSigner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"getApproved","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_address","type":"address"}],"name":"getMintsCount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getTreasuryAddress","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"getbaseURI","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"giveAway","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"operator","type":"address"}],"name":"isApprovedForAll","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"maxSupply","outputs":[{"internalType":"uint64","name":"","type":"uint64"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint64","name":"amount","type":"uint64"},{"components":[{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"},{"internalType":"uint8","name":"v","type":"uint8"}],"internalType":"struct BoredStackers.Coupon","name":"coupon","type":"tuple"},{"internalType":"enum BoredStackers.CouponType","name":"couponType","type":"uint8"}],"name":"mint","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"ownerOf","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"pause","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"paused","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"phase","outputs":[{"internalType":"enum BoredStackers.SalePhase","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"renounceOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"safeTransferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"bytes","name":"_data","type":"bytes"}],"name":"safeTransferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"operator","type":"address"},{"internalType":"bool","name":"approved","type":"bool"}],"name":"setApprovalForAll","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"uri","type":"string"}],"name":"setBaseUri","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint8","name":"max","type":"uint8"}],"name":"setMaxMintsPerAddress","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint64","name":"newMaxSupply","type":"uint64"}],"name":"setMaxSupply","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint8","name":"max","type":"uint8"}],"name":"setMintPricePhase03TypeAll","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"enum BoredStackers.SalePhase","name":"phase_","type":"uint8"}],"name":"setPhase","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes4","name":"interfaceId","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"tokenURI","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"transferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"unpause","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"withdraw","outputs":[],"stateMutability":"nonpayable","type":"function"}]