// File: src/NinfaEnglishAuction.sol



/*----------------------------------------------------------*|
|*          ███    ██ ██ ███    ██ ███████  █████           *|
|*          ████   ██ ██ ████   ██ ██      ██   ██          *|
|*          ██ ██  ██ ██ ██ ██  ██ █████   ███████          *|
|*          ██  ██ ██ ██ ██  ██ ██ ██      ██   ██          *|
|*          ██   ████ ██ ██   ████ ██      ██   ██          *|
|*----------------------------------------------------------*/

// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;

import "./utils/Counters.sol";
import "./utils/RoyaltyEngineV1.sol";
import "./access/Ownable.sol";

/**
 *
 * @title NinfaEnglishAuction                                *
 *                                                           *
 * @notice On-chain english auction                          *
 *                                                           *
 * @custom:security-contact tech@ninfa.io                    *
 *
 */

contract NinfaEnglishAuction is Ownable, RoyaltyEngineV1 {
    using Counters for Counters.Counter;

    Counters.Counter private _auctionId;
    /// @notice Ninfa's external registry, mapping Ninfa's ERC721 sovereign
    /// collection's tokenIds to a boolean value
    /// indicating if the token has been sold on any of Ninfa's contracts, such
    /// as this marketplace or an auction
    /// contract.
    address private immutable _PRIMARY_MARKET_REGISTRY;
    /**
     * @notice auctions ids mapped to NFT auction data.
     * @dev This is deleted when an auction is finalized or canceled.
     * @dev Visibility needs to be public so that it can be called by a frontend
     * as the auction creation event only
     * emits auction id.
     */
    mapping(uint256 => _Auction) private _auctions;
    /// @notice whitelist of address codehashes of Ninfa's sovreign ERC721
    /// collections, in order to determine if it is a
    /// primary market sale
    bytes32 private _ERC721SovreignV1CodeHash;
    /**
     * @notice _feeRecipient multisig for receiving trading fees
     */
    address payable private _feeRecipient;
    /// @notice factory contract for deploying self-sovereign collections.
    address private _factory;
    address private _whitelist;
    /**
     * @notice How long an auction lasts for once the first bid has been
     * received.
     */
    uint256 private constant _DURATION = 1 days;
    /**
     * @notice The window for auction extensions, any bid placed in the final 15
     * minutes of an auction will reset the
     * time remaining to 15 minutes.
     */
    uint256 private constant _EXTENSION_DURATION = 15 minutes;
    /**
     * @notice the last highest bid is divided by this number in order to obtain
     * the minimum bid increment. E.g.
     * _MIN_BID_RAISE = 10 is 10% increment, 20 is 5%, 2 is 50%. I.e. 100 /
     * _MIN_BID_RAISE. OpenSea uses a fixed 5%
     * increment while SuperRare between 5-10%
     */
    uint256 private constant _MIN_BID_RAISE = 20;
    /// @notice Ninfa Marketplace fee percentage on primary sales, expressed in
    /// basis points
    uint256 private _primaryMarketFee;
    /// @notice Ninfa Marketplace fee percentage on all secondary sales,
    /// expressed in basis points
    uint256 private _secondaryMarketFee;

    /**
     * @notice Stores the auction configuration for a specific NFT.
     * @param operator since the order creator may be a gallery, i.e. the
     * commission receiver itself, they would not be
     * able to cancel or update the order as there would be no way to know if
     * the order creator was the seller or the
     * commissionReceiver,
     * @dev therefore an additional parameter is needed to store the address of
     * `msg.sender`
     * @param end the time at which this auction will not accept any new bids.
     * This is `0` until the first bid is
     * placed.
     * @param bidder highest bidder, needs to be payable in order to receive
     * refund in case of being outbid
     * @param price reserve price, highest bid, and all bids in between
     * @param erc1155Amount 0 for erc721, 1> for erc1155
     */
    struct _Auction {
        address operator;
        address seller;
        address collection;
        address bidder;
        uint256 tokenId;
        uint256 bidPrice;
        uint256 end;
        uint256[] commissionBps;
        address[] commissionReceivers;
    }

    /**
     * @notice Emitted when an NFT is listed for auction.
     * @param auctionId The id of the auction that was created.
     * @dev the only parameter needed is auctionId, the emitted event must
     * trigger the backend to retrieve all auction
     * data from a getter function and store it in DB.
     */
    event AuctionCreated(uint256 auctionId);

    /**
     * @notice Emitted when an auction is cancelled.
     * @dev This is only possible if the auction has not received any bids.
     * @param auctionId The id of the auction that was cancelled.
     */
    event AuctionCanceled(uint256 auctionId);

    /**
     * @notice Emitted when the auction's reserve price is updated.
     * @dev This is only possible if the auction has not received any bids.
     * @param auctionId The id of the auction that was updated.
     */
    event AuctionUpdated(uint256 auctionId);

    /**
     * @notice Emitted when an auction that has already ended is finalized,
     * indicating that the NFT has been transferred and revenue from the sale
     * distributed.
     */
    event AuctionFinalized(uint256 auctionId);

    /**
     * @notice Emitted when a bid is placed.
     * @param auctionId The id of the auction this bid was for.
     */
    event Bid(uint256 auctionId);

    /**
     * @notice ERC721 and ERC1155 collections must be whitelisted.
     * @dev if the collection has not been whitelisted, check if it is one of
     * Ninfa's factory clones (Ninfa's
     * self-sovereign collections)
     * @dev by checking the whitelist first with nested `if`s avoids having to
     * make an external call unnecessarily
     */
    modifier isWhitelisted(address _collection) {
        bytes memory authorized;
            (, authorized) = _factory.staticcall(
                abi.encodeWithSelector(0xf6a3d24e, _collection)
            );
        if (abi.decode(authorized, (bool)) == false) {
            (, authorized) = _whitelist.staticcall(
                abi.encodeWithSelector(0x3af32abf, _collection)
            );
            if (abi.decode(authorized, (bool)) == false) revert Unauthorized();
        }
        _;
    }

    /**
     * @notice Creates an auction for the given NFT. The NFT is held in escrow
     * until the auction is finalized or
     * canceled.
     * @param _id The id of the NFT.
     * @param _reservePrice The initial reserve price for the auction.
     * @dev reserve price may also be 0, clearly a mistake but not strictly
     * required, only done in order to save gas by
     * removing the need for a condition such as `if (_price == 0) revert
     * InvalidAmount(_price)`
     * @param _commissionReceivers address of sale commissions receiver
     * @dev if `msg.sender` is also the `_commissionReceiver`, e.g. if
     * `msg.sender` is a gallery, they must put their
     * own address as the `_commissionReceiver`, and set the `_seller` parameter
     * with the artist's/collector's address.
     * @dev if there is no commission receiver, it must be set to address(0)
     * @dev it is not required for `_commissionReceiver` and `_seller` addresses
     * to be different (in order to save gas),
     * although it would likely be a mistake, it cannot be exploited as the
     * total amount paid out will never exceed the
     * price set for the order. I.e. in the worst case the same address will
     * receive both principal sale profit and
     * commissions.
     */
    function _createAuction(
        address _operator, //  either the previous owner or operator, i.e.
            // whichever address called safeTransferFrom on
            // the ERC1155 contract
        address _from, // previous owner, i.e. seller
        uint256 _id,
        uint256 _reservePrice,
        uint256[] memory _commissionBps,
        address[] memory _commissionReceivers
    )
        private
    {
        _auctionId.increment(); // start counter at 1

        uint256 auctionId_ = _auctionId.current();

        _auctions[auctionId_] = _Auction(
            _operator,
            _from, // auction beneficiary, needs to be payable in order to
                // receive funds from the auction sale
            msg.sender,
            address(0), // bidder is only known once a bid has been placed. //
                // highest bidder, needs to be payable in
                // order to receive refund in case of being outbid
            _id,
            _reservePrice,
            0,
            _commissionBps,
            _commissionReceivers
        );

        emit AuctionCreated(auctionId_);
    }

    /**
     * @dev Whenever an {IERC721} `tokenId` token is transferred to this
     * contract via {IERC721-safeTransferFrom}
     * by `operator` from `from`, this function is called.
     *
     * It must return its Solidity selector to confirm the token transfer.
     * If any other value is returned or the interface is not implemented by the
     * recipient, the transfer will be
     * reverted.
     *
     * The selector can be obtained in Solidity with
     * `IERC721.onERC721Received.selector`.
     * @param _operator The address which called `safeTransferFrom` function
     * @param _from The address which previously owned the token
     * @param _tokenId The NFT identifier which is being transferred
     * @param _data Additional data with no specified format
     */
    function onERC721Received(
        address _operator,
        address _from,
        uint256 _tokenId,
        bytes calldata _data
    )
        external
        returns (bytes4)
    {
        (uint256 reservePrice, uint256[] memory commissionBps, address[] memory commissionReceivers) =
            abi.decode(_data, (uint256, uint256[], address[]));

        _createAuction(_operator, _from, _tokenId, reservePrice, commissionBps, commissionReceivers);

        return 0x150b7a02;
    }

    function _transferNFT(address _collection, address _from, address _to, uint256 _tokenId) private {
        (bool success,) = _collection.call(
            abi.encodeWithSelector(
                0x42842e0e, // bytes4(keccak256('safeTransferFrom(address,address,uint256)'))
                    // == 0x42842e0e
                _from,
                _to,
                _tokenId
            )
        );

        require(success);
    }

    function firstBid(uint256 auctionId_) external payable {
        _Auction storage _auction = _auctions[auctionId_];

        // hardcoded 0x0 address in order to avoid reading from storage.
        // there is no need to check whether the auction exists already, because
        // even if someone managed to set end,
        // price and bidder for a (yet) non-existing auction, they would be
        // reser when an auction with the same id gets
        // created
        // the important thing is that no one can reset these variables for
        // auctions that have already started, and
        // can't happen because _auction.bidder would be set after the first bid
        // is placed by calling this function.
        if (
            _auction.bidder != 0x0000000000000000000000000000000000000000 // if
                // auction has started
                || msg.value < _auction.bidPrice
        ) revert Unauthorized();

        // if the auction exists and this is the firsat bid, start the auction
        // timer.
        // On the first bid, set the end to now + duration. `_DURATION` is a
        // constant set to 24hrs therefore the below
        // addition can't overflow.
        unchecked {
            _auction.end = block.timestamp + _DURATION;
            _auction.bidPrice = msg.value; // new highest bid
            _auction.bidder = msg.sender; // new highest bidder
        }

        emit Bid(auctionId_);
    }

    /**
     * @notice Place a bid in an auction.
     * A bidder may place a bid which is at least the amount defined by
     * `getMinBidAmount`.
     * If this is the first bid on the auction, the countdown will begin.
     * If there is already an outstanding bid, the previous bidder will be
     * refunded at this time
     * and if the bid is placed in the final moments of the auction, the
     * countdown may be extended.
     * @dev bids MUST be at least 5% higher than previous bid.
     * @param auctionId_ The id of the auction to bid on.
     * @dev auctionId_ MUST exist, auction MUST have begun and MUST not have
     * ended.
     */
    function bid(uint256 auctionId_) external payable {
        _Auction storage _auction = _auctions[auctionId_];
        // if auction hasn't started or doesn't exist, i.e. no one has called
        // firstBid() yet, _auction.end will still be
        // 0,
        // therefore the following require statement implicitly checks that
        // auction has started and explicitly that it
        // has not ended

        if (
            block.timestamp > _auction.end || _auction.end == 0 // required
                // otherwise calling this function would start
                // a 15 minutes auction rather than 24h
                || msg.value - _auction.bidPrice < _auction.bidPrice / _MIN_BID_RAISE
        ) revert Unauthorized();

        // if there is less than 15 minutes left, increment end time by 15 more.
        // _EXTENSION_DURATION is always set to 15
        // minutes so the below can't overflow.
        // already checking in previous if statement that if `block.timestamp >
        // _auction.end` the tx reverts, meaning
        // that `block.timestamp` must be less than `_auction.end`, i.e. auction
        // hasn't expired,
        // if you combine that with `block.timestamp + _EXTENSION_DURATION >
        // _auction.end` that means that
        // `block.timestamp` must be between `_auction.end` and `_auction.end -
        // 15 minutes`, i.e. it's the last 15
        // minutes of the auction.
        if (block.timestamp + _EXTENSION_DURATION > _auction.end) {
            unchecked {
                _auction.end += _EXTENSION_DURATION;
            }
        }

        // refund the previous bidder
        _sendValue(_auction.bidder, _auction.bidPrice);

        // does not follow check-effects-interactions pattern so that storing
        // previous bidder and amount in memory is
        // not required, however there is no reentrancy exploit in this case;
        // calling back into `bid()` requires that `msg.value` is 5% higher than
        // previous bid, meaning that the extra 5%
        // would not be refunded because storage has not been updated yet
        // besides the bid() function, there is no other function that can be
        // called back into which represents a
        // security risk, namely `createAuction()` and `firstBid()`, i.e.
        // _auction.bidPrice and _auction.bidder are not
        // read by any other function that may be reentered
        _auction.bidPrice = msg.value; // new highest bid
        _auction.bidder = msg.sender; // new highest bidder

        emit Bid(auctionId_);
    }

    /**
     * @notice If an auction has been created but has not yet received bids, the
     * `reservePrice` may be edited by the
     * seller.
     * @param auctionId_ The id of the auction to change.
     * @param _newReservePrice The new reserve price for this auction, may be
     * higher or lower than the previoius price.
     * @dev `_newReservePrice` may be equal to old price
     * (`_auctions[auctionId_].price`); although this doesn't make much
     * sense it isn't a security requirement, hence `require(_auction.bidPrice
     * != _price)` it has been omitted in order
     * to save the user some gas
     * @dev `_newReservePrice` may also be 0, clearly a mistake but not a
     * security requirement,  hence `require(_price >
     * 0)` has been omitted in order to save the user some gas
     */
    function updateReservePrice(uint256 auctionId_, uint256 _newReservePrice) external {
        _Auction storage _auction = _auctions[auctionId_];
        // code duplication because modifiers can't pass variables to functions,
        // meanining that storage pointer cannot
        // be instantiated in modifier
        require(_auction.operator == msg.sender && _auction.end == 0);

        // Update the current reserve price.
        _auction.bidPrice = _newReservePrice;

        emit AuctionUpdated(auctionId_);
    }

    function setCommissions(
        uint256 auctionId_, 
        uint256[] memory _commissionBps, 
        address[] memory _commissionReceivers) external {
        require(msg.sender == _auctions[auctionId_].operator);
        _auctions[auctionId_].commissionBps = _commissionBps;
        _auctions[auctionId_].commissionReceivers = _commissionReceivers;
        emit AuctionUpdated(auctionId_);
    }

    /**
     * @notice If an auction has been created but has not yet received bids, it
     * may be canceled by the seller.
     * @dev The NFT is transferred back to the owner unless there is still a buy
     * price set.
     * @param auctionId_ The id of the auction to cancel.
     */
    function cancelAuction(uint256 auctionId_) external {
        _Auction memory _auction = _auctions[auctionId_];

        require(_auction.operator == msg.sender && _auction.end == 0);

        // Delete the _auction.
        delete _auctions[auctionId_];

        _transferNFT(_auction.collection, address(this), msg.sender, _auction.tokenId);

        emit AuctionCanceled(auctionId_);
    }

    function finalize(uint256 auctionId_) external {
        _Auction memory auction = _auctions[auctionId_];
        address payable[] memory royaltyRecipients; // declare
            // `royaltyRecipients`, its value will be calculated based
            // on whether it is a primary or secondary sale
        uint256[] memory royaltyAmounts; // declare `royaltyAmounts`, its value
            // will be calculated based on whether it
            // is a primary or secondary sale
        // sellerAmount is a security check as well as a variable assignment,
        // because it would revert if there was an
        // underflow
        // sellerAmount may be 0 if royalties are set too high for an external
        // collection. If `royaltyAmount ==
        // (auction.bidPrice - marketplaceAmount)` then `sellerAmount == 0`. if
        // royalties amount exceeds price - fees
        // amount the transaction will revert.
        uint256 sellerAmount = auction.bidPrice;
        uint256 marketplaceAmount;

        // there must be at least one bid higher than the reserve price in order
        // to execute the trade, no bids mean no
        // end time
        if (block.timestamp < auction.end || auction.end == 0) {
            revert Unauthorized();
        }

        // Remove the auction.
        delete _auctions[auctionId_];

        bool checkSecondaryMarket = true;

        if (auction.collection.codehash == _ERC721SovreignV1CodeHash) {
            // it's a v1 721 token, check market registry
            (, bytes memory secondaryMarket) = _PRIMARY_MARKET_REGISTRY.call(
                abi.encodeWithSelector(
                    0x7abab711,
                    auction.collection,
                    auction.tokenId
                ) // bytes4(keccak256("secondaryMarketInfo(address,uint256)")) == 0x7abab711
            );

            checkSecondaryMarket = abi.decode(secondaryMarket, (bool));
        }

        /*----------------------------------------------------------*|
        |*  # PAY ROYALTIES                                         *|
        |*----------------------------------------------------------*/
        // > "Marketplaces that support this standard MUST pay royalties no
        // matter where the sale occurred or in what
        // currency" - https://eips.ethereum.org/EIPS/eip-2981.

        /*----------------------------------------------------------*|
        |*  # IF ROYALTIES SUPPORTED                                *|
        |*----------------------------------------------------------*/
        // The collection implements some royalty standard, otherwise the length
        // of the arrays returned would be 0.
        if (checkSecondaryMarket) {
            (royaltyRecipients, royaltyAmounts) = getRoyalty(auction.collection, auction.tokenId, sellerAmount);
        }
        
        uint256 royaltyRecipientsLength = royaltyRecipients.length; // assign to

        if (royaltyRecipientsLength > 0) {
            if (_secondaryMarketFee > 0) {
                /*----------------------------------------------------------*|
                |*  # PAY MARKETPLACE FEE                                   *|
                |*----------------------------------------------------------*/
                marketplaceAmount = (auction.bidPrice * _secondaryMarketFee) / 10_000;
                // subtracting primary or secondary fee amount from seller
                // amount, this is a security check (will revert
                // on underflow) as well as a variable assignment.
                sellerAmount -= marketplaceAmount; // subtract before external
                    // call
                _sendValue(_feeRecipient, marketplaceAmount);
            }

            do {
                royaltyRecipientsLength--;
                // subtracting royalty amount from seller amount, this is a
                // security check (will revert on
                // underflow) as well as a variable assignment.
                if(royaltyAmounts[royaltyRecipientsLength] > 0){
                    sellerAmount -= royaltyAmounts[royaltyRecipientsLength]; // subtract
                        // before external call
                    _sendValue(royaltyRecipients[royaltyRecipientsLength], royaltyAmounts[royaltyRecipientsLength]);
                }
            } while (royaltyRecipientsLength > 0);
        } else {
            //case primary
            marketplaceAmount = (auction.bidPrice * _primaryMarketFee) / 10_000;
            sellerAmount -= marketplaceAmount; // subtract before external
            // call
            _sendValue(_feeRecipient, marketplaceAmount);
        }

        /**
         *
         * Pay seller commissions *
         *
         */

        uint256 commissionReceiversLength = auction.commissionReceivers.length;

        if (commissionReceiversLength > 0) {
            do {
                commissionReceiversLength--;
                if(auction.commissionBps[commissionReceiversLength] > 0){
                    uint256 commissionAmount = (auction.commissionBps[commissionReceiversLength] * auction.bidPrice) / 10_000; // calculate
    
                    sellerAmount -= commissionAmount; // subtract before external

                    _sendValue(auction.commissionReceivers[commissionReceiversLength], commissionAmount);
                }
            } while (commissionReceiversLength > 0);
        }

        /**
         *
         * Pay seller *
         *
         */

        _sendValue(auction.seller, sellerAmount);

        // transfer nft to auction winner
        _transferNFT(auction.collection, address(this), auction.bidder, auction.tokenId);

        emit AuctionFinalized(auctionId_);
    }

    /**
     * @dev Replacement for Solidity's `transfer`: sends `_amount` wei to
     * `_receiver`, forwarding all available gas and reverting on errors.
     *
     * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] raises the gas cost
     * of certain opcodes, possibly making contracts go over the 2300 gas limit
     * imposed by `transfer`, making them unable to receive funds via
     * `transfer`. {_sendValue} removes this limitation.
     *
     * https://diligence.consensys.net/posts/2019/09/stop-using-soliditys-transfer-now/[Learn
     * more].
     *
     * IMPORTANT: because control is transferred to `_receiver`, care must be
     * taken to not create reentrancy vulnerabilities. Consider using
     * {ReentrancyGuard} or the
     * https://solidity.readthedocs.io/en/v0.5.11/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions
     * pattern].
     */
    function _sendValue(address _receiver, uint256 _amount) private {
        // solhint-disable-next-line avoid-low-level-calls
        (bool success,) = payable(_receiver).call{ value: _amount }("");
        require(success);
    }

    /**
     * @dev setter function only callable by contract admin used to change the
     * address to which fees are paid
     * @param _newFeeAccount is the address owned by NINFA that will collect
     * sales fees
     */
    function setFeeRecipient(address payable _newFeeAccount) external onlyOwner {
        _feeRecipient = _newFeeAccount;
    }

    function setWhitelist(address whitelist_) external onlyOwner {
        _whitelist = whitelist_;
    }

    /**
     * @notice sets primary sale fees for NINFA_ERC721_V2 communal collection.
     */
    function setMarketFees(uint256 primaryMarketFee_, uint256 secondaryMarketFee_) external onlyOwner {
        _primaryMarketFee = primaryMarketFee_;
        _secondaryMarketFee = secondaryMarketFee_;
    }

    function auctions(uint256 auctionId_) external view returns (_Auction memory) {
        return _auctions[auctionId_];
    }

    /**
     * @dev See {IERC165-supportsInterface}.
     */
    function supportsInterface(bytes4 interfaceId) external pure returns (bool) {
        return
        // Interface ID for IERC165
        interfaceId == 0x01ffc9a7
        // Interface ID for IERC721Receiver. A wallet/broker/auction application
        // MUST implement the wallet interface if
        // it will accept safe transfers.
        || interfaceId == 0x150b7a02;
    }

    /*----------------------------------------------------------*|
    |*  # CONSTRUCTOR                                           *|
    |*----------------------------------------------------------*/

    /**
     * @dev Grants `DEFAULT_ADMIN_ROLE`
     * @dev after deployment admin needs to manually whitelist collections.
     * @param _royaltyRegistry see https://royaltyregistry.xyz/lookup for public
     * addresses
     */
    constructor(
        address _royaltyRegistry,
        address _primarySalesRegistry,
        address factory_,
        address whitelist_,
        bytes32 ERC721SovreignV1CodeHash
    )
        RoyaltyEngineV1(_royaltyRegistry)
    {
        _PRIMARY_MARKET_REGISTRY = _primarySalesRegistry;
        _factory = factory_;
        _whitelist = whitelist_;
        _ERC721SovreignV1CodeHash = ERC721SovreignV1CodeHash;
    }
}

// File: src/utils/Counters.sol



/*----------------------------------------------------------*|
|*          ███    ██ ██ ███    ██ ███████  █████           *|
|*          ████   ██ ██ ████   ██ ██      ██   ██          *|
|*          ██ ██  ██ ██ ██ ██  ██ █████   ███████          *|
|*          ██  ██ ██ ██ ██  ██ ██ ██      ██   ██          *|
|*          ██   ████ ██ ██   ████ ██      ██   ██          *|
|*----------------------------------------------------------*/

// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;

/**
 *
 * @title Counters                                           *
 *                                                           *
 * @dev Stripped down version of OpenZeppelin Contracts       *
 * v4.4.1 (utils/Counters.sol), identical to                 *
 * CountersUpgradeable.sol being a library. Provides         *
 * counters that can only be incremented.                    *
 * Used to track the total supply of ERC721 ids.             *
 * @dev Include with `using Counters for Counters.Counter;`  *
 *                                                           *
 * @custom:security-contact tech@ninfa.io                    *
 *
 */
/**
 * @title Counters
 * @dev Stripped down version of OpenZeppelin Contracts v4.4.1
 * (utils/Counters.sol), identical to
 * CountersUpgradeable.sol being a library. Provides counters that can only be
 * incremented. Used to track the total
 * supply of ERC721 ids.
 * @dev Include with `using Counters for Counters.Counter;`
 */
library Counters {
    struct Counter {
        uint256 _value; // default: 0
    }

    function current(Counter storage counter) internal view returns (uint256) {
        return counter._value;
    }

    /// @dev if implementing ERC721A there could be an overflow risk by removing
    /// overflow protection with `unchecked`,
    /// unless we limit the amount of tokens that can be minted, or require that
    /// totalsupply be less than 2^256 - 1
    function increment(Counter storage counter) internal {
        unchecked {
            counter._value += 1;
        }
    }
}

// File: src/utils/RoyaltyEngineV1.sol



/*----------------------------------------------------------*|
|*          ███    ██ ██ ███    ██ ███████  █████           *|
|*          ████   ██ ██ ████   ██ ██      ██   ██          *|
|*          ██ ██  ██ ██ ██ ██  ██ █████   ███████          *|
|*          ██  ██ ██ ██ ██  ██ ██ ██      ██   ██          *|
|*          ██   ████ ██ ██   ████ ██      ██   ██          *|
|*----------------------------------------------------------*/

// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;

import "./specs/IRoyaltyRegistry.sol";
import "./specs/IRoyaltySplitter.sol";
import "./specs/IManifold.sol";
import "./specs/IRarible.sol";
import "./specs/IFoundation.sol";
import "./specs/ISuperRare.sol";
import "./specs/IEIP2981.sol";
import "./specs/INinfaRoyalty.sol";
import "./specs/IZoraOverride.sol";
import "./specs/IArtBlocksOverride.sol";
import "./specs/IKODAV2Override.sol";

/**
 *
 * @title RoyaltyEngineV1                                    *
 *                                                           *
 * @notice Custom implementation of Manifold RoyaltyEngineV1 *
 *                                                           *
 * @dev > "Marketplaces may choose to directly inherit the   *
 * Royalty Engine to save a bit of gas".                     *
 *                                                           *
 * @dev ERC165 was removed because interface selector will   *
 * be different from Manifold's (0xcb23f816) and this engine *
 * implementation is not meant for used by other contracts   *
 *                                                           *
 * @author Fork of Manifold's RoyaltyRegistryV1              *
 *                                                           *
 * @custom:security-contact tech@ninfa.io                    *
 *
 */

contract RoyaltyEngineV1 {
    /**
     * @dev ROYALTY_REGISTRY could be a hardcoded constant, however using an
     * immutable variable
     * is useful for deploying the engine onto other (test) networks where its
     * address differs from Mainnet
     */
    address internal immutable ROYALTY_REGISTRY;
    address internal constant SUPERRARE_REGISTRY = 0x17B0C8564E53f22364A6C8de6F7ca5CE9BEa4e5D;
    address internal constant SUPERRARE_V1 = 0x41A322b28D0fF354040e2CbC676F0320d8c8850d;
    address internal constant SUPERRARE_V2 = 0xb932a70A57673d89f4acfFBE830E8ed7f75Fb9e0;

    error Unauthorized();
    error InvalidAmount(uint256 amount);
    error LengthMismatch(uint256 recipients, uint256 bps); // only used in
        // RoyaltyEngineV1

    /**
     * Get the royalties for a given token and sale amount.
     *
     * @param tokenAddress - address of token
     * @param tokenId - id of token
     * @param value - sale value of token
     * Returns two arrays, first is the list of royalty recipients, second is
     * the amounts for each recipient.
     */
    function getRoyalty(
        address tokenAddress,
        uint256 tokenId,
        uint256 value
    )
        internal
        returns (address payable[] memory recipients, uint256[] memory amounts)
    {
        /**
         * @dev Control-flow hijack and gas griefing vulnerability within
         * Manifold's RoyaltyEngine, mitigated in
         * https://github.com/manifoldxyz/royalty-registry-solidity/commit/c5ba6db3e04e0b364f7afd7aae853a25542a7439.
         *      "To mitigate the griefing vector and other potential
         * vulnerabilities, limit the gas by default that
         * _getRoyalty is given to at most 50,000 gas, but certainly no more
         * than 100,000 gas."
         *      -
         * https://githubrecord.com/issue/manifoldxyz/royalty-registry-solidity/17/1067105243
         *      However, Ninfa's ERC-2981 implementation (ERC2981N) needs to write to
         * storage upon primary sales, this consumes
         * 800,000 at most gas,
         *      while it only reads from storage upon secondary sales, see
         * {ERC2981N-rotaltyInfo}
         */
        try this._getRoyalty{ gas: 1_000_000 }(tokenAddress, tokenId, value) returns (
            address payable[] memory _recipients, uint256[] memory _amounts
        ) {
            return (_recipients, _amounts);
        } catch {
            revert("Royalty lookup failed");
        }
    }

    /**
     * @dev Get the royalty for a given token
     * @dev the original RoyaltyEngineV1 has been modified by removing the
     * _specCache and the associated code,
     * using try catch statements is very cheap, no need to store `_specCache`
     * mapping, see
     * {RoyaltyEngineV1-_specCache}.
     * - https://www.reddit.com/r/ethdev/comments/szot8r/comment/hy5vsxb/
     * @dev EIP-2981 standard lookup is performed first unlike Manifold's
     * implementation, as it is the most prevalent
     * royalty standard as well as the one being used by Ninfa's collections
     * @return recipients array and amounts array, if no royalty standard has
     * been found, the returned arrays will be
     * empty
     */
    function _getRoyalty(
        address tokenAddress,
        uint256 tokenId,
        uint256 value
    )
        external
        returns (address payable[] memory recipients, uint256[] memory amounts)
    {
        address royaltyAddress = IRoyaltyRegistry(ROYALTY_REGISTRY).getRoyaltyLookupAddress(tokenAddress);

        try INinfaRoyalty(royaltyAddress).ninfaRoyaltyInfo(tokenId, value) returns (
            address payable[] memory recipients_, uint256[] memory bps_
        ) {
            require(recipients_.length == bps_.length);
            return (recipients_, _computeAmounts(value, bps_));
        } catch { }

        try IEIP2981(royaltyAddress).royaltyInfo(tokenId, value) returns (address recipient, uint256 amount) {
            if (amount > value) revert InvalidAmount(amount);
            uint32 recipientSize;
            assembly {
                recipientSize := extcodesize(recipient)
            }
            if (recipientSize > 0) {
                try IRoyaltySplitter(recipient).getRecipients() returns (Recipient[] memory splitRecipients) {
                    recipients = new address payable[](splitRecipients.length);
                    amounts = new uint256[](splitRecipients.length);
                    uint256 sum = 0;
                    uint256 splitRecipientsLength = splitRecipients.length;
                    for (uint256 i = 0; i < splitRecipientsLength;) {
                        Recipient memory splitRecipient = splitRecipients[i];
                        recipients[i] = payable(splitRecipient.recipient);
                        uint256 splitAmount = splitRecipient.bps * amount / 10_000;
                        amounts[i] = splitAmount;
                        sum += splitAmount;
                        unchecked {
                            ++i;
                        }
                    }
                    // sum can be less than amount, otherwise small-value listings can break
                    require(sum <= amount, "Invalid split");

                    return (recipients, amounts);
                } catch {}
            }

            try
                IRoyaltySplitter(royaltyAddress).getRecipients(tokenId)
            returns (Recipient[] memory royaltyInfo) {
                uint256 splitRecipientsLength = royaltyInfo.length;
                recipients = new address payable[](splitRecipientsLength);
                amounts = new uint256[](splitRecipientsLength);
                uint256 sum;

                for (uint256 i; i < splitRecipientsLength; ) {
                    Recipient memory splitRecipient = royaltyInfo[i];
                    recipients[i] = payable(splitRecipient.recipient);
                    uint256 splitAmount = (splitRecipient.bps * amount) / 10000;
                    amounts[i] = splitAmount;
                    sum += splitAmount;
                    unchecked {
                        ++i;
                    }
                }
                // sum can be less than amount, otherwise small-value listings can break
                require(sum <= amount, "Invalid split");

                return (recipients, amounts);
            } catch {
                // Supports EIP2981. Return amounts
                recipients = new address payable[](1);
                amounts = new uint256[](1);
                recipients[0] = payable(recipient);
                amounts[0] = amount;

                return (recipients, amounts);
            }
        } catch { }

        try IManifold(royaltyAddress).getRoyalties(tokenId) returns (
            address payable[] memory recipients_, uint256[] memory bps
        ) {
            // Supports manifold interface.  Compute amounts
            require(recipients_.length == bps.length);
            return (recipients_, _computeAmounts(value, bps));
        } catch { }

        // SuperRare handling
        if (tokenAddress == SUPERRARE_V1 || tokenAddress == SUPERRARE_V2) {
            try ISuperRareRegistry(SUPERRARE_REGISTRY).tokenCreator(tokenAddress, tokenId) returns (
                address payable creator
            ) {
                try ISuperRareRegistry(SUPERRARE_REGISTRY).calculateRoyaltyFee(tokenAddress, tokenId, value) returns (
                    uint256 amount
                ) {
                    recipients = new address payable[](1);
                    amounts = new uint256[](1);
                    recipients[0] = creator;
                    amounts[0] = amount;
                    return (recipients, amounts);
                } catch { }
            } catch { }
        }

        try IRaribleV2(royaltyAddress).getRaribleV2Royalties(tokenId) returns (IRaribleV2.Part[] memory royalties) {
            // Supports rarible v2 interface. Compute amounts
            recipients = new address payable[](royalties.length);
            amounts = new uint256[](royalties.length);
            uint256 totalAmount;
            for (uint256 i = 0; i < royalties.length; i++) {
                recipients[i] = royalties[i].account;
                amounts[i] = (value * royalties[i].value) / 10_000;
                totalAmount += amounts[i];
            }
            if (totalAmount > value) revert InvalidAmount(totalAmount);
            return (recipients, amounts);
        } catch { }
        try IRaribleV1(royaltyAddress).getFeeRecipients(tokenId) returns (address payable[] memory recipients_) {
            // Supports rarible v1 interface. Compute amounts
            recipients_ = IRaribleV1(royaltyAddress).getFeeRecipients(tokenId);
            try IRaribleV1(royaltyAddress).getFeeBps(tokenId) returns (uint256[] memory bps) {
                if (recipients_.length != bps.length) {
                    revert LengthMismatch(recipients_.length, bps.length);
                }
                return (recipients_, _computeAmounts(value, bps));
            } catch { }
        } catch { }
        try IFoundation(royaltyAddress).getFees(tokenId) returns (
            address payable[] memory recipients_, uint256[] memory bps
        ) {
            // Supports foundation interface.  Compute amounts
            if (recipients_.length != bps.length) {
                revert LengthMismatch(recipients_.length, bps.length);
            }
            return (recipients_, _computeAmounts(value, bps));
        } catch { }
        try IZoraOverride(royaltyAddress).convertBidShares(tokenAddress, tokenId) returns (
            address payable[] memory recipients_, uint256[] memory bps
        ) {
            // Support Zora override
            if (recipients_.length != bps.length) {
                revert LengthMismatch(recipients_.length, bps.length);
            }
            return (recipients_, _computeAmounts(value, bps));
        } catch { }
        try IArtBlocksOverride(royaltyAddress).getRoyalties(tokenAddress, tokenId) returns (
            address payable[] memory recipients_, uint256[] memory bps
        ) {
            // Support Art Blocks override
            if (recipients_.length != bps.length) {
                revert LengthMismatch(recipients_.length, bps.length);
            }
            return (recipients_, _computeAmounts(value, bps));
        } catch { }
        try IKODAV2Override(royaltyAddress).getKODAV2RoyaltyInfo(tokenAddress, tokenId, value) returns (
            address payable[] memory _recipients, uint256[] memory _amounts
        ) {
            // Support KODA V2 override
            if (_recipients.length != _amounts.length) {
                revert LengthMismatch(_recipients.length, _amounts.length);
            }
            return (_recipients, _amounts);
        } catch { }

        // No supported royalties configured
        return (recipients, amounts);
    }

    /**
     * Compute royalty amounts
     */
    function _computeAmounts(uint256 value, uint256[] memory bps) private pure returns (uint256[] memory amounts) {
        amounts = new uint256[](bps.length);
        uint256 totalAmount;
        for (uint256 i = 0; i < bps.length; i++) {
            amounts[i] = (value * bps[i]) / 10_000;
            totalAmount += amounts[i];
        }
        if (totalAmount > value) revert InvalidAmount(totalAmount);
        return amounts;
    }

    constructor(address _royaltyRegistry) {
        ROYALTY_REGISTRY = _royaltyRegistry;
    }
}

// File: src/access/Ownable.sol



// SPDX-License-Identifier: MIT

pragma solidity 0.8.20;

/**
 * @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.
 *
 * By default, the owner account will be the one that deploys the contract. 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 {
    address private _owner;

    event OwnershipTransferred(address previousOwner, address newOwner);

    /**
     * @dev Initializes the contract setting the deployer as the initial owner.
     */
    constructor() {
        _transferOwnership(msg.sender);
    }

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

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

    /**
     * @dev Leaves the contract without owner. It will not be possible to call
     * `onlyOwner` functions anymore. Can only be called by the current owner.
     *
     * NOTE: Renouncing ownership will leave the contract without an owner,
     * thereby removing any functionality that is only available to the owner.
     */
    function renounceOwnership() external 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) external onlyOwner {
        require(newOwner != address(0));
        _transferOwnership(newOwner);
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Internal function without access restriction.
     */
    function _transferOwnership(address newOwner) private {
        address oldOwner = _owner;
        _owner = newOwner;
        emit OwnershipTransferred(oldOwner, newOwner);
    }
}

// File: src/utils/specs/IRoyaltyRegistry.sol



// SPDX-License-Identifier: MIT

pragma solidity 0.8.20;

/// @author: manifold.xyz

import "../introspection/IERC165.sol";

/**
 * @dev Royalty registry interface
 */
interface IRoyaltyRegistry is IERC165 {
    event RoyaltyOverride(address owner, address tokenAddress, address royaltyAddress);

    /**
     * Override the location of where to look up royalty information for a given
     * token contract.
     * Allows for backwards compatibility and implementation of royalty logic
     * for contracts that did not previously
     * support them.
     *
     * @param tokenAddress    - The token address you wish to override
     * @param royaltyAddress  - The royalty override address
     */
    function setRoyaltyLookupAddress(address tokenAddress, address royaltyAddress) external returns (bool);

    /**
     * Returns royalty address location.  Returns the tokenAddress by default,
     * or the override if it exists
     *
     * @param tokenAddress    - The token address you are looking up the royalty
     * for
     */
    function getRoyaltyLookupAddress(address tokenAddress) external view returns (address);

    /**
     * Returns the token address that an overrideAddress is set for.
     * Note: will not be accurate if the override was created before this
     * function was added.
     *
     * @param overrideAddress - The override address you are looking up the
     * token for
     */
    function getOverrideLookupTokenAddress(address overrideAddress) external view returns (address);

    /**
     * Whether or not the message sender can override the royalty address for
     * the given token address
     *
     * @param tokenAddress    - The token address you are looking up the royalty
     * for
     */
    function overrideAllowed(address tokenAddress) external view returns (bool);
}

// File: src/utils/specs/IRoyaltySplitter.sol



// SPDX-License-Identifier: MIT

pragma solidity 0.8.20;

/// @author: manifold.xyz
struct Recipient {
    address payable recipient;
    uint16 bps;
}

interface IRoyaltySplitter {
    /**
     * @dev Get the splitter recipients;
     */
    function getRecipients() external view returns (Recipient[] memory);

    function getRecipients(
        uint256 tokenId
    ) external view returns (Recipient[] memory);
}

// File: src/utils/specs/IManifold.sol



// SPDX-License-Identifier: MIT

pragma solidity 0.8.20;

/// @author: manifold.xyz

/**
 * @dev Royalty interface for creator core classes
 */
interface IManifold {
    /**
     * @dev Get royalites of a token.  Returns list of receivers and basisPoints
     *
     *  bytes4(keccak256('getRoyalties(uint256)')) == 0xbb3bafd6
     *
     *  => 0xbb3bafd6 = 0xbb3bafd6
     */
    function getRoyalties(uint256 tokenId) external view returns (address payable[] memory, uint256[] memory);
}

// File: src/utils/specs/IRarible.sol



// SPDX-License-Identifier: MIT

pragma solidity 0.8.20;

interface IRaribleV1 {
    /*
     * bytes4(keccak256('getFeeBps(uint256)')) == 0x0ebd4c7f
     * bytes4(keccak256('getFeeRecipients(uint256)')) == 0xb9c4d9fb
     *
     * => 0x0ebd4c7f ^ 0xb9c4d9fb == 0xb7799584
     */
    function getFeeBps(uint256 id) external view returns (uint256[] memory);

    function getFeeRecipients(uint256 id) external view returns (address payable[] memory);
}

interface IRaribleV2 {
    /*
     *  bytes4(keccak256('getRaribleV2Royalties(uint256)')) == 0xcad96cca
     */
    struct Part {
        address payable account;
        uint96 value;
    }

    function getRaribleV2Royalties(uint256 id) external view returns (Part[] memory);
}

// File: src/utils/specs/IFoundation.sol



// SPDX-License-Identifier: MIT

pragma solidity 0.8.20;

interface IFoundation {
    /*
     *  bytes4(keccak256('getFees(uint256)')) == 0xd5a06d4c
     *
     *  => 0xd5a06d4c = 0xd5a06d4c
     */
    function getFees(uint256 tokenId) external view returns (address payable[] memory, uint256[] memory);
}

interface IFoundationTreasuryNode {
    function getFoundationTreasury() external view returns (address payable);
}

interface IFoundationTreasury {
    function isAdmin(address account) external view returns (bool);
}

// File: src/utils/specs/ISuperRare.sol



// SPDX-License-Identifier: MIT

pragma solidity 0.8.20;

interface ISuperRareRegistry {
    /**
     * @dev Get the royalty fee percentage for a specific ERC721 contract.
     * @param _contractAddress address ERC721Contract address.
     * @param _tokenId uint256 token ID.
     * @return uint8 wei royalty fee.
     */
    function getERC721TokenRoyaltyPercentage(
        address _contractAddress,
        uint256 _tokenId
    )
        external
        view
        returns (uint8);

    /**
     * @dev Utililty function to calculate the royalty fee for a token.
     * @param _contractAddress address ERC721Contract address.
     * @param _tokenId uint256 token ID.
     * @param _amount uint256 wei amount.
     * @return uint256 wei fee.
     */
    function calculateRoyaltyFee(
        address _contractAddress,
        uint256 _tokenId,
        uint256 _amount
    )
        external
        view
        returns (uint256);

    /**
     * @dev Get the token creator which will receive royalties of the given
     * token
     * @param _contractAddress address ERC721Contract address.
     * @param _tokenId uint256 token ID.
     */
    function tokenCreator(address _contractAddress, uint256 _tokenId) external view returns (address payable);
}

// File: src/utils/specs/IEIP2981.sol



// SPDX-License-Identifier: MIT

pragma solidity 0.8.20;

/**
 * EIP-2981
 */
interface IEIP2981 {
    /**
     * bytes4(keccak256("royaltyInfo(uint256,uint256)")) == 0x2a55205a
     *
     * => 0x2a55205a = 0x2a55205a
     */
    function royaltyInfo(uint256 tokenId, uint256 value) external view returns (address, uint256);
}

// File: src/utils/specs/INinfaRoyalty.sol



// SPDX-License-Identifier: MIT

pragma solidity 0.8.20;

/**
 * EIP-2981
 */
interface INinfaRoyalty {
    /**
     * bytes4(keccak256("royaltyInfo(uint256,uint256)")) == 0x2a55205a
     *
     * => 0x2a55205a = 0x2a55205a
     */
    function ninfaRoyaltyInfo(
        uint256 tokenId,
        uint256 value
    )
        external
        returns (address payable[] memory, uint256[] memory);
}

// File: src/utils/specs/IZoraOverride.sol



// SPDX-License-Identifier: MIT

pragma solidity 0.8.20;

/**
 * Paired down version of the Zora Market interface
 */
interface IZoraMarket {
    struct ZoraDecimal {
        uint256 value;
    }

    struct ZoraBidShares {
        // % of sale value that goes to the _previous_ owner of the nft
        ZoraDecimal prevOwner;
        // % of sale value that goes to the original creator of the nft
        ZoraDecimal creator;
        // % of sale value that goes to the seller (current owner) of the nft
        ZoraDecimal owner;
    }

    function bidSharesForToken(uint256 tokenId) external view returns (ZoraBidShares memory);
}

/**
 * Paired down version of the Zora Media interface
 */
interface IZoraMedia {
    /**
     * Auto-generated accessors of public variables
     */
    function marketContract() external view returns (address);

    function previousTokenOwners(uint256 tokenId) external view returns (address);

    function tokenCreators(uint256 tokenId) external view returns (address);

    /**
     * ERC721 function
     */
    function ownerOf(uint256 tokenId) external view returns (address owner);
}

/**
 * Interface for a Zora media override
 */
interface IZoraOverride {
    /**
     * @dev Convert bid share configuration of a Zora Media token into an array
     * of receivers and bps values
     *      Does not support prevOwner and sell-on amounts as that is specific
     * to Zora marketplace implementation
     *      and requires updates on the Zora Media and Marketplace to update the
     * sell-on amounts/previous owner values.
     *      An off-Zora marketplace sale will break the sell-on functionality.
     */
    function convertBidShares(
        address media,
        uint256 tokenId
    )
        external
        view
        returns (address payable[] memory, uint256[] memory);
}

// File: src/utils/specs/IArtBlocksOverride.sol



// SPDX-License-Identifier: MIT

pragma solidity 0.8.20;

/**
 *  Interface for an Art Blocks override
 */
interface IArtBlocksOverride {
    /**
     * @dev Get royalites of a token at a given tokenAddress.
     *      Returns array of receivers and basisPoints.
     *
     *  bytes4(keccak256('getRoyalties(address,uint256)')) == 0x9ca7dc7a
     *
     *  => 0x9ca7dc7a = 0x9ca7dc7a
     */
    function getRoyalties(
        address tokenAddress,
        uint256 tokenId
    )
        external
        view
        returns (address payable[] memory, uint256[] memory);
}

// File: src/utils/specs/IKODAV2Override.sol



// SPDX-License-Identifier: MIT

/// @author: knownorigin.io

pragma solidity 0.8.20;

interface IKODAV2 {
    function editionOfTokenId(uint256 _tokenId) external view returns (uint256 _editionNumber);

    function artistCommission(uint256 _editionNumber)
        external
        view
        returns (address _artistAccount, uint256 _artistCommission);

    function editionOptionalCommission(uint256 _editionNumber)
        external
        view
        returns (uint256 _rate, address _recipient);
}

interface IKODAV2Override {
    /// @notice Emitted when the royalties fee changes
    event CreatorRoyaltiesFeeUpdated(uint256 _oldCreatorRoyaltiesFee, uint256 _newCreatorRoyaltiesFee);

    /// @notice For the given KO NFT and token ID, return the addresses and the
    /// amounts to pay
    function getKODAV2RoyaltyInfo(
        address _tokenAddress,
        uint256 _id,
        uint256 _amount
    )
        external
        view
        returns (address payable[] memory, uint256[] memory);

    /// @notice Allows the owner() to update the creator royalties
    function updateCreatorRoyalties(uint256 _creatorRoyaltiesFee) external;
}

// File: src/utils/introspection/IERC165.sol



// SPDX-License-Identifier: MIT

pragma solidity 0.8.20;

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