KingdomlyNFTStandardV3

Description:

Multi-signature wallet contract requiring multiple confirmations for transaction execution.

Blockchain: Ethereum

Source Code: View Code On The Blockchain

Solidity Source Code:

{{
  "language": "Solidity",
  "sources": {
    "src/KingdomlyNFTStandardV3.sol": {
      "content": "// SPDX-License-Identifier: MIT

pragma solidity 0.8.24;

import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {ERC2981} from "@openzeppelin/contracts/token/common/ERC2981.sol";
import {Context as OZContext} from "@openzeppelin/contracts/utils/Context.sol";
import {ERC721AC} from "@creator-token-standards/src/erc721c/ERC721AC.sol";
import {IDelegateRegistry} from "src/utils/IDelegateRegistry.sol";
import {IKingdomlyFeeContract} from "src/utils/IKingdomlyFeeContract.sol";
import {IDelegateRegistry} from "src/utils/IDelegateRegistry.sol";
import {IKingdomlyFeeContract} from "src/utils/IKingdomlyFeeContract.sol";

contract KingdomlyNFTStandardV3 is ERC721AC, ERC2981, Ownable, ReentrancyGuard {
    struct BaseVariables {
        string name;
        string symbol;
        address ownerPayoutAddress;
        string initialBaseURI;
        uint256 maxSupply;
    }

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:´:°•.°*/
    /*                              ERRORS                              */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•.•°:°.*/
    error KingdomlyNFTStandardV3__MintInactive();
    error KingdomlyNFTStandardV3__MintOnBehalfNotActive();
    error KingdomlyNFTStandardV3__AddressZero();
    error KingdomlyNFTStandardV3__Unauthorized(address caller);
    error KingdomlyNFTStandardV3__InvalidOperation(string reason);
    error KingdomlyNFTStandardV3__ExceedsMaxSupply(uint256 requested, uint256 available);
    error KingdomlyNFTStandardV3__InsufficientEther(uint256 required, uint256 provided);
    error KingdomlyNFTStandardV3__ExceedsMaxPerWallet(uint256 requested, uint256 allowed);
    error KingdomlyNFTStandardV3__ExceedsMintQuota(uint256 requested, uint256 allowed);
    error KingdomlyNFTStandardV3__ExceedsMaxMintGroupSupply(uint256 requested, uint256 available);
    error KingdomlyNFTStandardV3__MintGroupInactive(uint256 mintGroup);
    error KingdomlyNFTStandardV3__NotInPresale(address caller, uint256 mintGroup);
    error KingdomlyNFTStandardV3__MintGroupDoesNotExist(uint256 mintGroup);
    error KingdomlyNFTStandardV3__ArrayLengthMismatch();
    error KingdomlyNFTStandardV3__InvalidKingdomlyFeeContract();
    error KingdomlyNFTStandardV3__InvalidKingdomlyAdminAddress();

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:´:°•.°*/
    /*                              EVENTS                              */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•.•°:°.*/
    event BatchMetadataUpdate(uint256 indexed fromTokenId, uint256 indexed toTokenId);
    event TokensMinted(address indexed recipient, uint256 amount, uint256 mintGroup);
    event TokensDelegateMinted(address indexed vault, address indexed hotWallet, uint256 amount, uint256 mintGroup);
    event SalePriceChanged(uint256 indexed mintGroup, uint256 newPrice);
    event PublicMaxMintPerWalletChanged(uint256 newMaxMintPerWallet);
    event PreSaleMintStatusChanged(bool status, uint256 mintGroupId);
    event PreSaleMintScheduledStartTimestampChanged(uint256 timestamp, uint256 mintGroupId);
    event KingdomlyFeeContractChanged(address feeContractAddress);
    event KingdomlyAdminChanged(address kingdomlyAdmin);
    event MintOnBehalfStatusChanged(bool status);
    event CreatorFeeUpdated(uint256 newCreatorFee);
    event MintFeeUpdated(uint256 newMintFee);
    event MintQuotasAdded(uint256 mintGroup, uint256 numberOfAddresses);
    event MintOnBehalfUpdated(bool newStatus);
    event MintStatusUpdated(bool newStatus);
    event MaxPerMintGroupUpdated(uint256 mintGroup, uint256 newMax);
    event MintLiveTimestampUpdated(uint256 timestamp);
    event OwnerPayoutAddressUpdated(address newOwnerPayoutAddress);
    event DefaultRoyaltyUpdated(address receiver, uint96 feeNumerator);
    event TokenRoyaltyUpdated(uint256 tokenId, address receiver, uint96 feeNumerator);
    event NftsAirdropped(uint256 recipients, uint256 amount);
    event MintFundsWithdrew();
    event FeeFundsWithdrew();

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:´:°•.°*/
    /*                        STORAGE VARIABLES                         */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•.•°:°.*/
    uint256 private constant BASIS_POINTS = 10000;
    address private constant DELEGATE_REGISTRY = 0x00000000000000447e69651d841bD8D104Bed493;
    uint256 private immutable i_maxSupply;
    address private immutable i_feeAddress;
    uint256 private s_threeDollarsInCents;
    uint256 private s_scheduledMintLiveTimestamp;
    uint256 private s_creatorFeeBasisPoints = 300;
    uint256 private s_publicMaxMintPerWallet;
    uint256[] private s_activeMintGroups;
    bool private s_contractMintLive;
    bool private s_mintOnBehalfActive;
    string private s_baseURI;
    address private s_ownerPayoutAddress;
    address private s_kingdomlyAdmin;
    address private s_kingdomlyPriceOracleContract;

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:´:°•.°*/
    /*                             MAPPINGS                             */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•.•°:°.*/
    mapping(address => uint256) private s_pendingBalances;
    mapping(uint256 => uint256) private s_mintPrice;
    mapping(uint256 => uint256) private s_maxSupplyPerMintGroup;
    mapping(uint256 => uint256) private s_mintGroupMints;
    mapping(uint256 => uint256) private s_presaleScheduledStartTimestamp;
    mapping(address => mapping(uint256 => uint256)) private s_addressMints;
    mapping(uint256 => mapping(address => uint256)) private s_mintQuotas;
    mapping(uint256 => bool) private s_contractPresaleActive;

    constructor(
        BaseVariables memory _baseVariables,
        uint256[] memory _maxSupplyPerMintGroup,
        uint256[] memory _mintPrice,
        uint256 _publicMaxMintPerWallet,
        uint96 _royaltyPercentage,
        address _owner,
        address _royaltyPayoutAddress
    ) ERC721AC(_baseVariables.name, _baseVariables.symbol) Ownable(_owner) {
        if (_mintPrice.length != _maxSupplyPerMintGroup.length) {
            revert KingdomlyNFTStandardV3__ArrayLengthMismatch();
        }
        uint256 totalMaxSupplyPerMintGroup = 0;
        for (uint256 i = 0; i < _maxSupplyPerMintGroup.length; i++) {
            totalMaxSupplyPerMintGroup += _maxSupplyPerMintGroup[i];
            s_maxSupplyPerMintGroup[i] = _maxSupplyPerMintGroup[i];
            s_mintPrice[i] = _mintPrice[i];
            s_mintGroupMints[i] = 0;
            s_activeMintGroups.push(i);
        }
        if (totalMaxSupplyPerMintGroup > _baseVariables.maxSupply) {
            revert KingdomlyNFTStandardV3__InvalidOperation({
                reason: "Max supply per mint group exceeds total max supply"
            });
        }
        i_maxSupply = _baseVariables.maxSupply;
        s_contractMintLive = false;
        s_scheduledMintLiveTimestamp = 0;
        s_baseURI = _baseVariables.initialBaseURI;
        s_ownerPayoutAddress = _baseVariables.ownerPayoutAddress;
        i_feeAddress = 0x10317Fa93da2a2e6d7B8e29D2BC2e6B95f2ECc84;
        s_kingdomlyAdmin = 0x89695e9D4C489128471798766f1EE4b2a8AB5B5B;
        s_kingdomlyPriceOracleContract = 0x65Db9966492C0A5aC0EF15c018C19eE383F7a8Cf;
        s_publicMaxMintPerWallet = _publicMaxMintPerWallet;

        /**
         * Set default royalty for ERC2981
         */
        _setDefaultRoyalty(_royaltyPayoutAddress, _royaltyPercentage);
        s_threeDollarsInCents = 300;
    }

    /**
     * @notice Mints NFTs to the caller for a specific mint group
     * @param amount Amount of NFTs to be minted
     * @param mintGroup Id of mint group
     * @return totalCostWithFee Total cost of the mint for batch mint
     */
    function batchMint(uint256 amount, uint256 mintGroup) external payable returns (uint256 totalCostWithFee) {
        totalCostWithFee = _batchMint(msg.sender, amount, mintGroup, msg.sender);
        emit TokensMinted(msg.sender, amount, mintGroup);
        _refundExcessEther(totalCostWithFee);
    }

    /**
     * @notice Mints NFTs on behalf of a vault using delegation
     * @param amount Number of tokens to mint.
     * @param mintGroup Identifier for the minting group or phase.
     * @param vault The vault whose delegation is being exercised.
     */
    function delegatedMint(uint256 amount, uint256 mintGroup, address vault)
        external
        payable
        returns (uint256 totalCostWithFee)
    {
        // Verify delegation
        if (!IDelegateRegistry(DELEGATE_REGISTRY).checkDelegateForContract(msg.sender, vault, address(this), "")) {
            revert KingdomlyNFTStandardV3__Unauthorized(vault);
        }

        totalCostWithFee = _batchMint(vault, amount, mintGroup, msg.sender);
        emit TokensDelegateMinted(vault, msg.sender, amount, mintGroup);
        _refundExcessEther(totalCostWithFee);
    }

    /**
     * @notice Mints NFTs to a specified receiver address
     * @param amount The number of tokens to mint
     * @param mintGroup The mint group ID
     * @param receiver The address to mint the tokens to
     */
    function mintOnBehalf(uint256 amount, uint256 mintGroup, address receiver)
        external
        payable
        returns (uint256 totalCostWithFee)
    {
        if (!s_mintOnBehalfActive) {
            revert KingdomlyNFTStandardV3__MintOnBehalfNotActive();
        }
        totalCostWithFee = _batchMint(receiver, amount, mintGroup, receiver);
        emit TokensMinted(receiver, amount, mintGroup);
        _refundExcessEther(totalCostWithFee);
    }

    /**
     * @notice Allows the contract owner to withdraw the funds that have been paid into the contract
     */
    function withdrawMintFunds() external nonReentrant {
        _withdrawFor(s_ownerPayoutAddress);
        _withdrawFor(i_feeAddress);
        emit MintFundsWithdrew();
    }

    /**
     * @notice Allows the fee address to withdraw their portion of the funds
     */
    function withdrawFeeFunds() external nonReentrant {
        _withdrawFor(i_feeAddress);
        emit FeeFundsWithdrew();
    }

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:´:°•.°*/
    /*                    KINGDOMLY ADMIN FUNCTIONS                     */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•.•°:°.*/
    /**
     * @notice Sets the creator fee basis points for Kingdomly
     * @param _creatorFeeBasisPoints The new creator fee for kingdomly
     */
    function setCreatorFee(uint256 _creatorFeeBasisPoints) external isKingdomlyAdmin {
        if (_creatorFeeBasisPoints > BASIS_POINTS) {
            revert KingdomlyNFTStandardV3__InvalidOperation({reason: "Creator fee cannot exceed 100%"});
        }
        s_creatorFeeBasisPoints = _creatorFeeBasisPoints;
        emit CreatorFeeUpdated(_creatorFeeBasisPoints);
    }

    /**
     * @notice Updates the Kingdomly price oracle contract address
     * @param _kingdomlyPriceOracleContract address of new kingdomly price oracle contract
     */
    function setNewKingdomlyPriceOracleContract(address _kingdomlyPriceOracleContract) external isKingdomlyAdmin {
        if (_kingdomlyPriceOracleContract == address(0)) {
            revert KingdomlyNFTStandardV3__InvalidKingdomlyFeeContract();
        }
        s_kingdomlyPriceOracleContract = _kingdomlyPriceOracleContract;
        emit KingdomlyFeeContractChanged(address(_kingdomlyPriceOracleContract));
    }

    /**
     * @notice Updates the Kingdomly admin address
     * @param _kingdomlyAdmin Address to be the new Kingdomly Admin
     */
    function setNewKingdomlyAdmin(address _kingdomlyAdmin) external isKingdomlyAdmin {
        if (_kingdomlyAdmin == address(0)) {
            revert KingdomlyNFTStandardV3__InvalidKingdomlyAdminAddress();
        }
        s_kingdomlyAdmin = _kingdomlyAdmin;
        emit KingdomlyAdminChanged(address(_kingdomlyAdmin));
    }

    /**
     * @notice Sets the fee amount in cents for minting operations
     * @param _newCents New fee per mint
     */
    function setThreeDollarsInCents(uint256 _newCents) external isKingdomlyAdmin {
        s_threeDollarsInCents = _newCents;
        emit MintFeeUpdated(_newCents);
    }

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:´:°•.°*/
    /*                         OWNER FUNCTIONS                          */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•.•°:°.*/
    /**
     *
     * @param addressToAdd Addresses to change mint quota
     * @param mintGroup mint ID group of addresses to change mint quota
     * @param _mintQuotas mint quotas address in array
     * @notice Changed add to presale to set mint quota for individual addresses.
     */
    function setMintQuota(address[] memory addressToAdd, uint256 mintGroup, uint256[] memory _mintQuotas)
        external
        onlyOwner
    {
        if (addressToAdd.length != _mintQuotas.length) {
            revert KingdomlyNFTStandardV3__ArrayLengthMismatch();
        }

        if (!isMintGroupActive(mintGroup)) {
            initializeNewMintGroup(mintGroup);
        }

        uint256 addressesToAddLength = addressToAdd.length;

        for (uint256 i = 0; i < addressesToAddLength; i++) {
            s_mintQuotas[mintGroup][addressToAdd[i]] = _mintQuotas[i];
        }
        emit MintQuotasAdded(mintGroup, addressesToAddLength);
    }

    /**
     * @notice Sets the maximum number of tokens that can be minted in a batch of the public mint group
     */
    function setPublicMaxMintPerWallet(uint256 _newPublicMaxMintPerWallet) external onlyOwner {
        s_publicMaxMintPerWallet = _newPublicMaxMintPerWallet;
        emit PublicMaxMintPerWalletChanged(_newPublicMaxMintPerWallet);
    }

    /**
     * @notice Sets the mint on behalf status
     * @param status new mint on behalf status
     */
    function setMintOnBehalfStatus(bool status) external onlyOwner {
        s_mintOnBehalfActive = status;
        emit MintOnBehalfUpdated(status);
    }

    /**
     * @notice Changes the price to mint a token of a specific mint group id
     * @param newMintPrice New mint Price
     * @param mintGroup Mint ID to change price
     */
    function changeSalePrice(uint256 newMintPrice, uint256 mintGroup) external onlyOwner {
        //Checks if mintGroup already exists inside activeMintGroups. This allows the contract to adjust the mappings for new mint groups
        if (!isMintGroupActive(mintGroup)) {
            initializeNewMintGroup(mintGroup);
        }
        s_mintPrice[mintGroup] = newMintPrice;
        emit SalePriceChanged(mintGroup, newMintPrice);
    }

    /**
     * @notice Changes the minting status. Only the contract owner can call this function.
     * @param status new status of mint
     */
    function changeMintStatus(bool status) external onlyOwner {
        if (s_contractMintLive == status) {
            revert KingdomlyNFTStandardV3__InvalidOperation({reason: "Mint status is already the one you entered"});
        }
        if (!status) {
            s_scheduledMintLiveTimestamp = 0;
        }
        s_contractMintLive = status;
        emit MintStatusUpdated(status);
    }

    /**
     * @notice Changes the max mint per mint group
     * @param mintGroup mint ID of the group to change max mint
     * @param newMax the new max mint per mint group
     */
    function setNewMaxPerMintGroup(uint256 mintGroup, uint256 newMax) external onlyOwner {
        //Checks if mintGroup already exists inside activeMintGroups. This allows the contract to adjust the mappings for new mint groups
        if (!isMintGroupActive(mintGroup)) {
            initializeNewMintGroup(mintGroup);
        }

        // Checker if new max exceeds total supply
        uint256 totalMaxMintPerMG = 0;
        uint256 activeMintGroupsLength = s_activeMintGroups.length;
        for (uint256 i = 0; i < activeMintGroupsLength; i++) {
            if (s_activeMintGroups[i] == mintGroup) {
                totalMaxMintPerMG += newMax; // Use the new max for the specified mintGroup
            } else {
                totalMaxMintPerMG += s_maxSupplyPerMintGroup[s_activeMintGroups[i]];
            }
        }

        if (totalMaxMintPerMG > i_maxSupply) {
            revert KingdomlyNFTStandardV3__InvalidOperation({reason: "New supply per mint group exceeds total supply."});
        }

        s_maxSupplyPerMintGroup[mintGroup] = newMax;
        emit MaxPerMintGroupUpdated(mintGroup, newMax);
    }

    /**
     * @notice Starts or stops presale minting for a specific mint group
     * @param presaleStatus status for presale
     * @param mintGroup mintGroup to change presale status
     */
    function stopOrStartpresaleMint(bool presaleStatus, uint256 mintGroup) external onlyOwner {
        // Checks if mintGroup already exists inside activeMintGroups.
        if (!isMintGroupActive(mintGroup)) {
            revert KingdomlyNFTStandardV3__MintGroupDoesNotExist({mintGroup: mintGroup});
        }
        s_contractPresaleActive[mintGroup] = presaleStatus;
        if (!presaleStatus) {
            s_presaleScheduledStartTimestamp[mintGroup] = 0;
        }
        emit PreSaleMintStatusChanged(presaleStatus, mintGroup);
    }

    /**
     * @notice Schedules when presale minting will start for a specific mint group
     * @param startTimestamp timestamp of the mintGroup to start
     * @param mintGroup mint ID to change start presale timestamp
     */
    function schedulePresaleMintStart(uint256 startTimestamp, uint256 mintGroup) external onlyOwner {
        if (!isMintGroupActive(mintGroup)) {
            revert KingdomlyNFTStandardV3__MintGroupDoesNotExist({mintGroup: mintGroup});
        }
        s_presaleScheduledStartTimestamp[mintGroup] = startTimestamp;
        emit PreSaleMintScheduledStartTimestampChanged(startTimestamp, mintGroup);
    }

    /**
     * @notice Sets the base URI for the token metadata. Only the contract owner can call this function.
     * @param newBaseURI the newBASEURI for the token metadata
     */
    function setBaseURI(string memory newBaseURI) external onlyOwner {
        s_baseURI = newBaseURI;
        emit BatchMetadataUpdate(1, type(uint256).max);
    }

    /**
     * @notice Sets the timestamp when minting should begin
     * @param timestamp The Unix timestamp when minting should begin
     */
    function setMintLiveTimestamp(uint256 timestamp) external onlyOwner {
        s_scheduledMintLiveTimestamp = timestamp;
        emit MintLiveTimestampUpdated(timestamp);
    }

    /**
     * @notice Updates the owner's payout address and transfers pending balance to new address
     * @dev Transfers existing balance from old to new address, then updates ownerPayoutAddress
     * @param newOwnerPayoutAddress The new address for owner payouts
     */
    function setOwnerPayoutAddress(address newOwnerPayoutAddress) external onlyOwner {
        if (newOwnerPayoutAddress == address(0)) {
            revert KingdomlyNFTStandardV3__AddressZero();
        }
        uint256 ownerBalance = s_pendingBalances[s_ownerPayoutAddress];
        s_pendingBalances[newOwnerPayoutAddress] += ownerBalance;
        s_pendingBalances[s_ownerPayoutAddress] = 0;
        s_ownerPayoutAddress = newOwnerPayoutAddress;
        emit OwnerPayoutAddressUpdated(newOwnerPayoutAddress);
    }

    /**
     * @notice Sets the default royalty recipient and fee percentage for all tokens
     * @dev Calls internal _setDefaultRoyalty function. Only callable by owner
     * @param receiver Address that will receive royalty payments
     * @param feeNumerator Royalty fee as basis points (e.g., 250 = 2.5%)
     */
    function setDefaultRoyalty(address receiver, uint96 feeNumerator) external onlyOwner {
        _setDefaultRoyalty(receiver, feeNumerator);
        emit DefaultRoyaltyUpdated(receiver, feeNumerator);
    }

    /**
     * @notice Sets royalty recipient and fee for a specific token, overriding default royalty
     * @dev Calls internal _setTokenRoyalty function. Only callable by owner
     * @param tokenId The token ID to set specific royalty for
     * @param receiver Address that will receive royalty payments for this token
     * @param feeNumerator Royalty fee as basis points (e.g., 250 = 2.5%)
     */
    function setTokenRoyalty(uint256 tokenId, address receiver, uint96 feeNumerator) external onlyOwner {
        _setTokenRoyalty(tokenId, receiver, feeNumerator);
        emit TokenRoyaltyUpdated(tokenId, receiver, feeNumerator);
    }

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:´:°•.°*/
    /*                        AIRDROP FUNCTIONS                         */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•.•°:°.*/
    /**
     * @notice Airdrops NFTs to multiple recipients with a fixed fee charged to the owner
     * @param recipients Recipent to receive NFTs
     * @param amounts the number of NFTs to airdrop per recipient
     * @dev Modified airdrop function to charge the owner threeDollarsEth per mint
     */
    function airdropNFTs(address[] memory recipients, uint256[] memory amounts)
        external
        payable
        onlyOwner
        returns (uint256 totalCharge)
    {
        uint256 amountLengths = amounts.length;
        if (recipients.length != amountLengths) {
            revert KingdomlyNFTStandardV3__InvalidOperation({reason: "Mismatch between recipients and amounts"});
        }
        uint256 totalNFTToMint = 0;
        for (uint256 i = 0; i < amountLengths; i++) {
            totalNFTToMint += amounts[i];
        }
        if (totalSupply() + totalNFTToMint > i_maxSupply) {
            revert KingdomlyNFTStandardV3__InvalidOperation({reason: "Airdrop exceeds max supply"});
        }
        totalCharge = quoteAirdropFees(totalNFTToMint);
        if (msg.value < totalCharge) {
            revert KingdomlyNFTStandardV3__InvalidOperation({reason: "Not enough Ether sent for the airdrop charge"});
        }

        s_pendingBalances[i_feeAddress] += totalCharge; // Fee goes to the fee address
        uint256 recipientsLength = recipients.length;
        for (uint256 j = 0; j < recipientsLength; j++) {
            uint256 nftAmount = amounts[j];
            _safeMint(recipients[j], nftAmount); // Mint NFTs to recipients
        }
        emit NftsAirdropped(recipientsLength, totalNFTToMint);

        _refundExcessEther(totalCharge);
    }

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:´:°•.°*/
    /*                            MODIFIERS                             */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•.•°:°.*/
    /**
     * @dev Ensures the caller is the Kingdomly Admin.
     */
    modifier isKingdomlyAdmin() {
        if (msg.sender != s_kingdomlyAdmin) {
            revert KingdomlyNFTStandardV3__Unauthorized(msg.sender);
        }
        _;
    }

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:´:°•.°*/
    /*                          VIEW FUNCTIONS                          */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•.•°:°.*/

    function maxSupply() external view returns (uint256) {
        return i_maxSupply;
    }

    function threeDollarsInCents() external view returns (uint256) {
        return s_threeDollarsInCents;
    }

    function scheduledMintLiveTimestamp() external view returns (uint256) {
        return s_scheduledMintLiveTimestamp;
    }

    function creatorFeeBasisPoints() external view returns (uint256) {
        return s_creatorFeeBasisPoints;
    }

    function publicMaxMintPerWallet() external view returns (uint256) {
        return s_publicMaxMintPerWallet;
    }

    function activeMintGroups() external view returns (uint256[] memory) {
        return s_activeMintGroups;
    }

    function contractMintLive() external view returns (bool) {
        return s_contractMintLive;
    }

    function mintOnBehalfActive() external view returns (bool) {
        return s_mintOnBehalfActive;
    }

    function baseURI() external view returns (string memory) {
        return s_baseURI;
    }

    function feeAddress() external view returns (address) {
        return i_feeAddress;
    }

    function ownerPayoutAddress() external view returns (address) {
        return s_ownerPayoutAddress;
    }

    function kingdomlyAdmin() external view returns (address) {
        return s_kingdomlyAdmin;
    }

    function kingdomlyPriceOracleContract() external view returns (address) {
        return s_kingdomlyPriceOracleContract;
    }

    function delegateRegistry() external pure returns (address) {
        return DELEGATE_REGISTRY;
    }

    function mintPrice(uint256 mintGroup) external view returns (uint256) {
        return s_mintPrice[mintGroup];
    }

    function maxSupplyPerMintGroup(uint256 mintGroup) external view returns (uint256) {
        return s_maxSupplyPerMintGroup[mintGroup];
    }

    function mintGroupMints(uint256 mintGroup) external view returns (uint256) {
        return s_mintGroupMints[mintGroup];
    }

    function presaleScheduledStartTimestamp(uint256 mintGroup) external view returns (uint256) {
        return s_presaleScheduledStartTimestamp[mintGroup];
    }

    function mintQuotas(uint256 mintGroup, address minter) external view returns (uint256) {
        return s_mintQuotas[mintGroup][minter];
    }

    function contractPresaleActive(uint256 mintGroup) external view returns (bool) {
        return s_contractPresaleActive[mintGroup];
    }

    /**
     * @notice Checks if a mint operation is valid for the given parameters
     * @param amount Amount of ERC
     * @param mintGroup mintGroup to check if eligible to mint
     * @param minterAddress Address to mint
     */
    function canMintCheck(uint256 amount, uint256 mintGroup, address minterAddress)
        external
        view
        returns (bool, string memory)
    {
        if (amount == 0) {
            return (false, "InvalidOperation");
        }

        // Pre-conditions checks
        if (!mintLive()) {
            return (false, "MintInactive");
        }

        if (!s_contractPresaleActive[mintGroup]) {
            if (
                s_presaleScheduledStartTimestamp[mintGroup] == 0
                    || block.timestamp <= s_presaleScheduledStartTimestamp[mintGroup]
            ) {
                return (false, "MintGroupInactive");
            }
        }

        if (mintGroup != 0) {
            if (s_mintQuotas[mintGroup][minterAddress] == 0) {
                return (false, "NotInPresale");
            }

            if (amount > s_mintQuotas[mintGroup][minterAddress]) {
                return (false, "ExceedsMintQuota");
            }
        } else {
            if (amount + s_addressMints[minterAddress][mintGroup] > s_publicMaxMintPerWallet) {
                return (false, "ExceedsPublicMaxMintPerWallet");
            }
        }

        if (s_mintGroupMints[mintGroup] + amount > s_maxSupplyPerMintGroup[mintGroup]) {
            return (false, "ExceedsMaxMintGroupSupply");
        }

        if (totalSupply() + amount > i_maxSupply) {
            return (false, "ExceedsMaxSupply");
        }

        return (true, "");
    }

    /**
     * @notice Checks if a delegated mint operation is valid for the given parameters
     * @param amount amount of NFTs to be minted
     * @param mintGroup mint group id to check mint eligibility
     * @param vault Vault address with mint quota
     * @param minterAddress Minter address that vault delgated to
     */
    function canDelegateMintCheck(uint256 amount, uint256 mintGroup, address vault, address minterAddress)
        external
        view
        returns (bool, string memory)
    {
        if (!IDelegateRegistry(DELEGATE_REGISTRY).checkDelegateForContract(minterAddress, vault, address(this), "")) {
            return (false, "Unauthorized");
        }

        if (amount == 0) {
            return (false, "InvalidOperation");
        }

        // Pre-conditions checks
        if (!mintLive()) {
            return (false, "MintInactive");
        }

        if (!s_contractPresaleActive[mintGroup]) {
            if (
                s_presaleScheduledStartTimestamp[mintGroup] == 0
                    || block.timestamp <= s_presaleScheduledStartTimestamp[mintGroup]
            ) {
                return (false, "MintGroupInactive");
            }
        }

        if (mintGroup != 0) {
            if (s_mintQuotas[mintGroup][vault] == 0) {
                return (false, "NotInPresale");
            }

            if (amount > s_mintQuotas[mintGroup][vault]) {
                return (false, "ExceedsMintQuota");
            }
        } else {
            if (amount + s_addressMints[vault][mintGroup] > s_publicMaxMintPerWallet) {
                return (false, "ExceedsPublicMaxPerWallet");
            }
        }
        if (s_mintGroupMints[mintGroup] + amount > s_maxSupplyPerMintGroup[mintGroup]) {
            return (false, "ExceedsMaxMintGroupSupply");
        }

        if (totalSupply() + amount > i_maxSupply) {
            return (false, "ExceedsMaxSupply");
        }

        return (true, "");
    }

    /**
     * @notice Quote the total cost of minting a batch of tokens
     * @dev This is the same price for both the owner and the delegate
     * @param mintGroup The mint group ID
     * @param amount The number of tokens to mint
     * @return totalCostWithFee The total cost of minting the batch, including the minter fee
     * @return mintingCost The base cost of minting without any fees
     * @return minterFee The fixed $3 fee per token minted
     * @return creatorFee The 3% fee calculated from the base minting cost
     */
    function quoteBatchMint(uint256 mintGroup, uint256 amount)
        public
        view
        returns (uint256 totalCostWithFee, uint256 mintingCost, uint256 minterFee, uint256 creatorFee)
    {
        mintingCost = s_mintPrice[mintGroup] * amount;
        minterFee = threeDollarsEth() * amount;
        creatorFee = (mintingCost * s_creatorFeeBasisPoints) / BASIS_POINTS;
        totalCostWithFee = mintingCost + minterFee + creatorFee;
    }

    /**
     * @notice Checks the balance pending withdrawal for the sender.
     */
    function checkPendingBalance() external view returns (uint256) {
        return s_pendingBalances[msg.sender];
    }

    /**
     * @notice Checks the balance pending withdrawal for the user.
     */
    function checkPendingBalanceFor(address user) external view returns (uint256) {
        return s_pendingBalances[user];
    }

    /**
     * @notice Returns whether this contract supports the given interface
     */
    function supportsInterface(bytes4 interfaceId) public view override(ERC721AC, ERC2981) returns (bool) {
        return ERC721AC.supportsInterface(interfaceId) || ERC2981.supportsInterface(interfaceId);
    }

    /**
     * @notice Checks if a mint group ID is active
     */
    function isMintGroupActive(uint256 mintGroup) private view returns (bool) {
        uint256 activeMintGroupsLength = s_activeMintGroups.length;
        for (uint256 i = 0; i < activeMintGroupsLength; i++) {
            if (s_activeMintGroups[i] == mintGroup) {
                return true;
            }
        }
        return false;
    }

    /**
     * @notice Returns whether minting is currently live
     */
    function mintLive() public view returns (bool) {
        if (!s_contractMintLive) {
            if (s_scheduledMintLiveTimestamp == 0 || block.timestamp <= s_scheduledMintLiveTimestamp) {
                return false;
            }
        }
        return true;
    }

    /**
     * @notice Returns the current cost of $3 in ETH using the price oracle
     */
    function threeDollarsEth() public view returns (uint256) {
        uint256 oneDollarInWei = IKingdomlyFeeContract(s_kingdomlyPriceOracleContract).getOneDollarInWei();
        return (oneDollarInWei * s_threeDollarsInCents) / 100;
    }

    function tokenURI(uint256 tokenId) public view override returns (string memory) {
        if (!_exists(tokenId)) revert URIQueryForNonexistentToken();
        if (bytes(s_baseURI).length == 0) {
            return "";
        }
        if (bytes(s_baseURI)[bytes(s_baseURI).length - 1] != bytes("/")[0]) {
            return s_baseURI;
        }
        return string(abi.encodePacked(s_baseURI, _toString(tokenId)));
    }

    /**
     * @notice Returns the creator fee percentage
     */
    function getCreatorFeePercentage() external view returns (uint256) {
        return (s_creatorFeeBasisPoints * 100) / BASIS_POINTS;
    }

    /**
     * @notice Calculates the total cost for airdropping a given amount of tokens
     * @param amount The number of tokens to mint
     * @return totalAirdropCostWithFee The total cost of minting the batch ($0.33 per nft)
     */
    function quoteAirdropFees(uint256 amount) public view returns (uint256 totalAirdropCostWithFee) {
        totalAirdropCostWithFee = (threeDollarsEth() * amount * 11) / 100;
    }

    /**
     * @notice Checker whether presale is already active both on timestmap and the mapping
     */
    function presaleActive(uint256 mintGroup) external view returns (bool) {
        if (!s_contractPresaleActive[mintGroup]) {
            if (
                s_presaleScheduledStartTimestamp[mintGroup] == 0
                    || block.timestamp <= s_presaleScheduledStartTimestamp[mintGroup]
            ) {
                return false;
            }
        }

        return true;
    }

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:´:°•.°*/
    /*                        INTERNAL FUNCTIONS                        */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•.•°:°.*/
    /**
     * @notice Initializes a new mint group with default values
     */
    function initializeNewMintGroup(uint256 mintGroup) internal {
        s_mintPrice[mintGroup] = 0;
        s_maxSupplyPerMintGroup[mintGroup] = 0;
        s_mintGroupMints[mintGroup] = 0;
        s_activeMintGroups.push(mintGroup);
    }

    /**
     * @notice Internal function to handle batch minting logic
     */
    function _batchMint(address quotaHolder, uint256 amount, uint256 mintGroup, address receiver)
        internal
        returns (uint256)
    {
        if (amount == 0) {
            revert KingdomlyNFTStandardV3__InvalidOperation({reason: "Amount must be greater than 0"});
        }

        // Pre-conditions checks
        if (!mintLive()) {
            revert KingdomlyNFTStandardV3__MintInactive();
        }

        if (!s_contractPresaleActive[mintGroup]) {
            if (
                s_presaleScheduledStartTimestamp[mintGroup] == 0
                    || block.timestamp <= s_presaleScheduledStartTimestamp[mintGroup]
            ) {
                revert KingdomlyNFTStandardV3__MintGroupInactive({mintGroup: mintGroup});
            }
        }

        // Checks required for non public mint
        if (mintGroup != 0) {
            if (s_mintQuotas[mintGroup][quotaHolder] == 0) {
                revert KingdomlyNFTStandardV3__NotInPresale({caller: quotaHolder, mintGroup: mintGroup});
            }

            if (amount > s_mintQuotas[mintGroup][quotaHolder]) {
                revert KingdomlyNFTStandardV3__ExceedsMintQuota({
                    requested: amount,
                    allowed: s_mintQuotas[mintGroup][quotaHolder]
                });
            }
            s_mintQuotas[mintGroup][quotaHolder] -= amount;
        } else {
            if (amount + s_addressMints[quotaHolder][mintGroup] > s_publicMaxMintPerWallet) {
                revert KingdomlyNFTStandardV3__ExceedsMaxPerWallet({
                    requested: amount,
                    allowed: s_publicMaxMintPerWallet - s_addressMints[quotaHolder][mintGroup]
                });
            }
        }

        // Check mint group supply limits
        if (s_mintGroupMints[mintGroup] + amount > s_maxSupplyPerMintGroup[mintGroup]) {
            revert KingdomlyNFTStandardV3__ExceedsMaxMintGroupSupply({
                requested: amount,
                available: s_maxSupplyPerMintGroup[mintGroup] - s_mintGroupMints[mintGroup]
            });
        }

        // Check total supply limit
        if (totalSupply() + amount > i_maxSupply) {
            revert KingdomlyNFTStandardV3__ExceedsMaxSupply({requested: amount, available: i_maxSupply - totalSupply()});
        }

        // Calculate fees, check if we have enough msg.value
        (uint256 totalCostWithFee, uint256 mintingCost, uint256 minterFee, uint256 creatorFee) =
            quoteBatchMint(mintGroup, amount);

        if (msg.value < totalCostWithFee) {
            revert KingdomlyNFTStandardV3__InsufficientEther({required: totalCostWithFee, provided: msg.value});
        }

        uint256 totalFee = minterFee + creatorFee;

        s_addressMints[quotaHolder][mintGroup] += amount;

        // Update balances
        s_pendingBalances[i_feeAddress] += totalFee;
        s_pendingBalances[s_ownerPayoutAddress] += mintingCost - creatorFee;

        // Finalize minting
        s_mintGroupMints[mintGroup] += amount;
        _safeMint(receiver, amount);

        return totalCostWithFee;
    }

    /**
     * @notice Internal function to withdraw funds for a specific user
     */
    function _withdrawFor(address user) internal returns (uint256 payout) {
        payout = s_pendingBalances[user];
        s_pendingBalances[user] = 0;
        (bool success,) = payable(user).call{value: payout}("");
        if (!success) {
            revert KingdomlyNFTStandardV3__InvalidOperation({reason: "Withdraw Transfer Failed"});
        }
    }

    /**
     * @notice Internal function to refund excess Ether sent in a transaction
     */
    function _refundExcessEther(uint256 totalCharge) internal {
        uint256 excess = msg.value - totalCharge;
        if (excess > 0) {
            (bool success,) = payable(msg.sender).call{value: excess}("");
            if (!success) {
                s_pendingBalances[msg.sender] += excess;
            }
        }
    }

    /**
     * @notice Overrides the start token ID function from the ERC721A contract
     */
    function _startTokenId() internal view virtual override returns (uint256) {
        return 1;
    }

    /**
     * @notice Returns the base URI for the token metadata
     */
    function _baseURI() internal view override returns (string memory) {
        return s_baseURI;
    }

    /**
     * @notice Internal function to check if caller is contract owner
     */
    function _requireCallerIsContractOwner() internal view virtual override {
        _checkOwner();
    }

    /**
     * @notice Returns the message data for the current call
     */
    function _msgData() internal view override(OZContext) returns (bytes calldata) {
        return super._msgData();
    }

    /**
     * @notice Returns the sender of the current call
     */
    function _msgSender() internal view override(OZContext) returns (address) {
        return super._msgSender();
    }
}
"
    },
    "lib/openzeppelin-contracts/contracts/utils/ReentrancyGuard.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (utils/ReentrancyGuard.sol)

pragma solidity ^0.8.20;

/**
 * @dev Contract module that helps prevent reentrant calls to a function.
 *
 * Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier
 * available, which can be applied to functions to make sure there are no nested
 * (reentrant) calls to them.
 *
 * Note that because there is a single `nonReentrant` guard, functions marked as
 * `nonReentrant` may not call one another. This can be worked around by making
 * those functions `private`, and then adding `external` `nonReentrant` entry
 * points to them.
 *
 * TIP: If EIP-1153 (transient storage) is available on the chain you're deploying at,
 * consider using {ReentrancyGuardTransient} instead.
 *
 * TIP: If you would like to learn more about reentrancy and alternative ways
 * to protect against it, check out our blog post
 * https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul].
 */
abstract contract ReentrancyGuard {
    // Booleans are more expensive than uint256 or any type that takes up a full
    // word because each write operation emits an extra SLOAD to first read the
    // slot's contents, replace the bits taken up by the boolean, and then write
    // back. This is the compiler's defense against contract upgrades and
    // pointer aliasing, and it cannot be disabled.

    // The values being non-zero value makes deployment a bit more expensive,
    // but in exchange the refund on every call to nonReentrant will be lower in
    // amount. Since refunds are capped to a percentage of the total
    // transaction's gas, it is best to keep them low in cases like this one, to
    // increase the likelihood of the full refund coming into effect.
    uint256 private constant NOT_ENTERED = 1;
    uint256 private constant ENTERED = 2;

    uint256 private _status;

    /**
     * @dev Unauthorized reentrant call.
     */
    error ReentrancyGuardReentrantCall();

    constructor() {
        _status = NOT_ENTERED;
    }

    /**
     * @dev Prevents a contract from calling itself, directly or indirectly.
     * Calling a `nonReentrant` function from another `nonReentrant`
     * function is not supported. It is possible to prevent this from happening
     * by making the `nonReentrant` function external, and making it call a
     * `private` function that does the actual work.
     */
    modifier nonReentrant() {
        _nonReentrantBefore();
        _;
        _nonReentrantAfter();
    }

    function _nonReentrantBefore() private {
        // On the first call to nonReentrant, _status will be NOT_ENTERED
        if (_status == ENTERED) {
            revert ReentrancyGuardReentrantCall();
        }

        // Any calls to nonReentrant after this point will fail
        _status = ENTERED;
    }

    function _nonReentrantAfter() private {
        // By storing the original value once again, a refund is triggered (see
        // https://eips.ethereum.org/EIPS/eip-2200)
        _status = NOT_ENTERED;
    }

    /**
     * @dev Returns true if the reentrancy guard is currently set to "entered", which indicates there is a
     * `nonReentrant` function in the call stack.
     */
    function _reentrancyGuardEntered() internal view returns (bool) {
        return _status == ENTERED;
    }
}
"
    },
    "lib/openzeppelin-contracts/contracts/access/Ownable.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (access/Ownable.sol)

pragma solidity ^0.8.20;

import {Context} from "../utils/Context.sol";

/**
 * @dev Contract module which provides a basic access control mechanism, where
 * there is an account (an owner) that can be granted exclusive access to
 * specific functions.
 *
 * The initial owner is set to the address provided by the deployer. This can
 * later be changed with {transferOwnership}.
 *
 * This module is used through inheritance. It will make available the modifier
 * `onlyOwner`, which can be applied to your functions to restrict their use to
 * the owner.
 */
abstract contract Ownable is Context {
    address private _owner;

    /**
     * @dev The caller account is not authorized to perform an operation.
     */
    error OwnableUnauthorizedAccount(address account);

    /**
     * @dev The owner is not a valid owner account. (eg. `address(0)`)
     */
    error OwnableInvalidOwner(address owner);

    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    /**
     * @dev Initializes the contract setting the address provided by the deployer as the initial owner.
     */
    constructor(address initialOwner) {
        if (initialOwner == address(0)) {
            revert OwnableInvalidOwner(address(0));
        }
        _transferOwnership(initialOwner);
    }

    /**
     * @dev Throws if called by any account other than the owner.
     */
    modifier onlyOwner() {
        _checkOwner();
        _;
    }

    /**
     * @dev Returns the address of the current owner.
     */
    function owner() public view virtual returns (address) {
        return _owner;
    }

    /**
     * @dev Throws if the sender is not the owner.
     */
    function _checkOwner() internal view virtual {
        if (owner() != _msgSender()) {
            revert OwnableUnauthorizedAccount(_msgSender());
        }
    }

    /**
     * @dev Leaves the contract without owner. It will not be possible to call
     * `onlyOwner` functions. Can only be called by the current owner.
     *
     * NOTE: Renouncing ownership will leave the contract without an owner,
     * thereby disabling any functionality that is only available to the owner.
     */
    function renounceOwnership() public virtual onlyOwner {
        _transferOwnership(address(0));
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Can only be called by the current owner.
     */
    function transferOwnership(address newOwner) public virtual onlyOwner {
        if (newOwner == address(0)) {
            revert OwnableInvalidOwner(address(0));
        }
        _transferOwnership(newOwner);
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Internal function without access restriction.
     */
    function _transferOwnership(address newOwner) internal virtual {
        address oldOwner = _owner;
        _owner = newOwner;
        emit OwnershipTransferred(oldOwner, newOwner);
    }
}
"
    },
    "lib/openzeppelin-contracts/contracts/token/common/ERC2981.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.4.0) (token/common/ERC2981.sol)

pragma solidity ^0.8.20;

import {IERC2981} from "../../interfaces/IERC2981.sol";
import {IERC165, ERC165} from "../../utils/introspection/ERC165.sol";

/**
 * @dev Implementation of the NFT Royalty Standard, a standardized way to retrieve royalty payment information.
 *
 * Royalty information can be specified globally for all token ids via {_setDefaultRoyalty}, and/or individually for
 * specific token ids via {_setTokenRoyalty}. The latter takes precedence over the first.
 *
 * Royalty is specified as a fraction of sale price. {_feeDenominator} is overridable but defaults to 10000, meaning the
 * fee is specified in basis points by default.
 *
 * IMPORTANT: ERC-2981 only specifies a way to signal royalty information and does not enforce its payment. See
 * https://eips.ethereum.org/EIPS/eip-2981#optional-royalty-payments[Rationale] in the ERC. Marketplaces are expected to
 * voluntarily pay royalties together with sales, but note that this standard is not yet widely supported.
 */
abstract contract ERC2981 is IERC2981, ERC165 {
    struct RoyaltyInfo {
        address receiver;
        uint96 royaltyFraction;
    }

    RoyaltyInfo private _defaultRoyaltyInfo;
    mapping(uint256 tokenId => RoyaltyInfo) private _tokenRoyaltyInfo;

    /**
     * @dev The default royalty set is invalid (eg. (numerator / denominator) >= 1).
     */
    error ERC2981InvalidDefaultRoyalty(uint256 numerator, uint256 denominator);

    /**
     * @dev The default royalty receiver is invalid.
     */
    error ERC2981InvalidDefaultRoyaltyReceiver(address receiver);

    /**
     * @dev The royalty set for a specific `tokenId` is invalid (eg. (numerator / denominator) >= 1).
     */
    error ERC2981InvalidTokenRoyalty(uint256 tokenId, uint256 numerator, uint256 denominator);

    /**
     * @dev The royalty receiver for `tokenId` is invalid.
     */
    error ERC2981InvalidTokenRoyaltyReceiver(uint256 tokenId, address receiver);

    /// @inheritdoc IERC165
    function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC165) returns (bool) {
        return interfaceId == type(IERC2981).interfaceId || super.supportsInterface(interfaceId);
    }

    /// @inheritdoc IERC2981
    function royaltyInfo(
        uint256 tokenId,
        uint256 salePrice
    ) public view virtual returns (address receiver, uint256 amount) {
        RoyaltyInfo storage _royaltyInfo = _tokenRoyaltyInfo[tokenId];
        address royaltyReceiver = _royaltyInfo.receiver;
        uint96 royaltyFraction = _royaltyInfo.royaltyFraction;

        if (royaltyReceiver == address(0)) {
            royaltyReceiver = _defaultRoyaltyInfo.receiver;
            royaltyFraction = _defaultRoyaltyInfo.royaltyFraction;
        }

        uint256 royaltyAmount = (salePrice * royaltyFraction) / _feeDenominator();

        return (royaltyReceiver, royaltyAmount);
    }

    /**
     * @dev The denominator with which to interpret the fee set in {_setTokenRoyalty} and {_setDefaultRoyalty} as a
     * fraction of the sale price. Defaults to 10000 so fees are expressed in basis points, but may be customized by an
     * override.
     */
    function _feeDenominator() internal pure virtual returns (uint96) {
        return 10000;
    }

    /**
     * @dev Sets the royalty information that all ids in this contract will default to.
     *
     * Requirements:
     *
     * - `receiver` cannot be the zero address.
     * - `feeNumerator` cannot be greater than the fee denominator.
     */
    function _setDefaultRoyalty(address receiver, uint96 feeNumerator) internal virtual {
        uint256 denominator = _feeDenominator();
        if (feeNumerator > denominator) {
            // Royalty fee will exceed the sale price
            revert ERC2981InvalidDefaultRoyalty(feeNumerator, denominator);
        }
        if (receiver == address(0)) {
            revert ERC2981InvalidDefaultRoyaltyReceiver(address(0));
        }

        _defaultRoyaltyInfo = RoyaltyInfo(receiver, feeNumerator);
    }

    /**
     * @dev Removes default royalty information.
     */
    function _deleteDefaultRoyalty() internal virtual {
        delete _defaultRoyaltyInfo;
    }

    /**
     * @dev Sets the royalty information for a specific token id, overriding the global default.
     *
     * Requirements:
     *
     * - `receiver` cannot be the zero address.
     * - `feeNumerator` cannot be greater than the fee denominator.
     */
    function _setTokenRoyalty(uint256 tokenId, address receiver, uint96 feeNumerator) internal virtual {
        uint256 denominator = _feeDenominator();
        if (feeNumerator > denominator) {
            // Royalty fee will exceed the sale price
            revert ERC2981InvalidTokenRoyalty(tokenId, feeNumerator, denominator);
        }
        if (receiver == address(0)) {
            revert ERC2981InvalidTokenRoyaltyReceiver(tokenId, address(0));
        }

        _tokenRoyaltyInfo[tokenId] = RoyaltyInfo(receiver, feeNumerator);
    }

    /**
     * @dev Resets royalty information for the token id back to the global default.
     */
    function _resetTokenRoyalty(uint256 tokenId) internal virtual {
        delete _tokenRoyaltyInfo[tokenId];
    }
}
"
    },
    "lib/openzeppelin-contracts/contracts/utils/Context.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.1) (utils/Context.sol)

pragma solidity ^0.8.20;

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

    function _contextSuffixLength() internal view virtual returns (uint256) {
        return 0;
    }
}
"
    },
    "lib/creator-token-standards/src/erc721c/ERC721AC.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import "../utils/AutomaticValidatorTransferApproval.sol";
import "../utils/CreatorTokenBase.sol";
import "erc721a/contracts/ERC721A.sol";
import {TOKEN_TYPE_ERC721} from "@limitbreak/permit-c/Constants.sol";

/**
 * @title ERC721AC
 * @author Limit Break, Inc.
 * @notice Extends Azuki's ERC721-A implementation with Creator Token functionality, which
 *         allows the contract owner to update the transfer validation logic by managing a security policy in
 *         an external transfer validation security policy registry.  See {CreatorTokenTransferValidator}.
 */
abstract contract ERC721AC is ERC721A, CreatorTokenBase, AutomaticValidatorTransferApproval {

    constructor(string memory name_, string memory symbol_) CreatorTokenBase() ERC721A(name_, symbol_) {}

    /**
     * @notice Overrides behavior of isApprovedFor all such that if an operator is not explicitly approved
     *         for all, the contract owner can optionally auto-approve the 721-C transfer validator for transfers.
     */
    function isApprovedForAll(address owner, address operator) public view virtual override returns (bool isApproved) {
        isApproved = super.isApprovedForAll(owner, operator);

        if (!isApproved) {
            if (autoApproveTransfersFromValidator) {
                isApproved = operator == address(getTransferValidator());
            }
        }
    }

    /**
     * @notice Indicates whether the contract implements the specified interface.
     * @dev Overrides supportsInterface in ERC165.
     * @param interfaceId The interface id
     * @return true if the contract implements the specified interface, false otherwise
     */
    function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
        return 
        interfaceId == type(ICreatorToken).interfaceId || 
        interfaceId == type(ICreatorTokenLegacy).interfaceId || 
        super.supportsInterface(interfaceId);
    }

    /**
     * @notice Returns the function selector for the transfer validator's validation function to be called 
     * @notice for transaction simulation. 
     */
    function getTransferValidationFunction() external pure returns (bytes4 functionSignature, bool isViewFunction) {
        functionSignature = bytes4(keccak256("validateTransfer(address,address,address,uint256)"));
        isViewFunction = true;
    }

    /// @dev Ties the erc721a _beforeTokenTransfers hook to more granular transfer validation logic
    function _beforeTokenTransfers(
        address from,
        address to,
        uint256 startTokenId,
        uint256 quantity
    ) internal virtual override {
        for (uint256 i = 0; i < quantity;) {
            _validateBeforeTransfer(from, to, startTokenId + i);
            unchecked {
                ++i;
            }
        }
    }

    /// @dev Ties the erc721a _afterTokenTransfer hook to more granular transfer validation logic
    function _afterTokenTransfers(
        address from,
        address to,
        uint256 startTokenId,
        uint256 quantity
    ) internal virtual override {
        for (uint256 i = 0; i < quantity;) {
            _validateAfterTransfer(from, to, startTokenId + i);
            unchecked {
                ++i;
            }
        }
    }

    function _msgSenderERC721A() internal view virtual override returns (address) {
        return _msgSender();
    }

    function _tokenType() internal pure override returns(uint16) {
        return uint16(TOKEN_TYPE_ERC721);
    }
}
"
    },
    "src/utils/IDelegateRegistry.sol": {
      "content": "// SPDX-License-Identifier: CC0-1.0
pragma solidity >=0.8.13;

/**
 * @title IDelegateRegistry
 * @custom:version 2.0
 * @custom:author foobar (0xfoobar)
 * @notice A standalone immutable registry storing delegated permissions from one address to another
 */
interface IDelegateRegistry {
    /// @notice Delegation type, NONE is used when a delegation does not exist or is revoked
    enum DelegationType {
        NONE,
        ALL,
        CONTRACT,
        ERC721,
        ERC20,
        ERC1155
    }

    /// @notice Struct for returning delegations
    struct Delegation {
        DelegationType type_;
        address to;
        address from;
        bytes32 rights;
        address contract_;
        uint256 tokenId;
        uint256 amount;
    }

    /// @notice Emitted when an address delegates or revokes rights for their entire wallet
    event DelegateAll(address indexed from, address indexed to, bytes32 rights, bool enable);

    /// @notice Emitted when an address delegates or revokes rights for a contract address
    event DelegateContract(
        address indexed from, address indexed to, address indexed contract_, bytes32 rights, bool enable
    );

    /// @notice Emitted when an address delegates or revokes rights for an ERC721 tokenId
    event DelegateERC721(
        address indexed from,
        address indexed to,
        address indexed contract_,
        uint256 tokenId,
        bytes32 rights,
        bool enable
    );

    /// @notice Emitted when an address delegates or revokes rights for an amount of ERC20 tokens
    event DelegateERC20(
        address indexed from, address indexed to, address indexed contract_, bytes32 rights, uint256 amount
    );

    /// @notice Emitted when an address delegates or revokes rights for an amount of an ERC1155 tokenId
    event DelegateERC1155(
        address indexed from,
        address indexed to,
        address indexed contract_,
        uint256 tokenId,
        bytes32 rights,
        uint256 amount
    );

    /// @notice Thrown if multicall calldata is malformed
    error MulticallFailed();

    /**
     * -----------  WRITE -----------
     */

    /**
     * @notice Call multiple functions in the current contract and return the data from all of them if they all succeed
     * @param data The encoded function data for each of the calls to make to this contract
     * @return results The results from each of the calls passed in via data
     */
    function multicall(bytes[] calldata data) external payable returns (bytes[] memory results);

    /**
     * @notice Allow the delegate to act on behalf of `msg.sender` for all contracts
     * @param to The address to act as delegate
     * @param rights Specific subdelegation rights granted to the delegate, pass an empty bytestring to encompass all rights
     * @param enable Whether to enable or disable this delegation, true delegates and false revokes
     * @return delegationHash The unique identifier of the delegation
     */
    function delegateAll(address to, bytes32 rights, bool enable) external payable returns (bytes32 delegationHash);

    /**
     * @notice Allow the delegate to act on behalf of `msg.sender` for a specific contract
     * @param to The address to act as delegate
     * @param contract_ The contract whose rights are being delegated
     * @param rights Specific subdelegation rights granted to the delegate, pass an empty bytestring to encompass all rights
     * @param enable Whether to enable or disable this delegation, true delegates and false revokes
     * @return delegationHash The unique identifier of the delegation
     */
    function delegateContract(address to, address contract_, bytes32 rights, bool enable)
        external
        payable
        returns (bytes32 delegationHash);

    /**
     * @notice Allow the delegate to act on behalf of `msg.sender` for a specific ERC721 token
     * @param to The address to act as delegate
     * @param contract_ The contract whose rights are being delegated
     * @param tokenId The token id to delegate
     * @param rights Specific subdelegation rights granted to the delegate, pass an empty bytestring to encompass all rights
     * @param enable Whether to enable or disable this delegation, true delegates and false revokes
     * @return delegationHash The unique identifier of the delegation
     */
    function delegateERC721(address to, address contract_, uint256 tokenId, bytes32 rights, bool enable)
        external
        payable
        returns (bytes32 delegationHash);

    /**
     * @notice Allow the delegate to act on behalf of `msg.sender` for a specific amount of ERC20 tokens
     * @dev The actual amount is not encoded in the hash, just the existence of a amount (since it is an upper bound)
     * @param to The address to act as delegate
     * @param contract_ The address for the fungible token contract
     * @param rights Specific subdelegation rights granted to the delegate, pass an empty bytestring to encompass all rights
     * @param amount The amount to delegate, > 0 delegates and 0 revokes
     * @return delegationHash The unique identifier of the delegation
     */
    function delegateERC20(address to, address contract_, bytes32 rights, uint256 amount)
        external
        payable
        returns (bytes32 delegationHash);

    /**
     * @notice Allow the delegate to act on behalf of `msg.sender` for a specific amount of ERC1155 tokens
     * @dev The actual amount is not encoded in the hash, just the existence of a amount (since it is an upper bound)
     * @param to The address to act as delegate
     * @param contract_ The address of the contract that holds the token
     * @param tokenId The token id to delegate
     * @param rights Specific subdelegation rights granted to the delegate, pass an empty bytestring to encompass all rights
     * @param amount The amount of that token id to delegate, > 0 delegates and 0 revokes
     * @return delegationHash The unique identifier of the delegation
     */
    function delegateERC1155(address to, address contract_, uint256 tokenId, bytes32 rights, uint256 amount)
        external
        payable
        returns (bytes32 delegationHash);

    /**
     * ----------- CHECKS -----------
     */

    /**
     * @notice Check if `to` is a delegate of `from` for the entire wallet
     * @param to The potential delegate address
     * @param from The potential address who delegated rights
     * @param rights Specific rights to check for, pass the zero value to ignore subdelegations and check full delegations only
     * @return valid Whether delegate is granted to act on the from's behalf
     */
    function checkDelegateForAll(address to, address from, bytes32 rights) external view returns (bool);

Tags:
ERC721, ERC165, Multisig, Non-Fungible, Upgradeable, Multi-Signature, Factory, Oracle|addr:0x6316f5c1a3093ab706d7956067356b3a26549ceb|verified:true|block:23418809|tx:0xc850537fdc82a374ff200d082effe6eb2cbbc5d1be82f4b379a51aabe5aa7f89|first_check:1758547193

Submitted on: 2025-09-22 15:19:54

Comments

Log in to comment.

No comments yet.