/*----------------------------------------------------------*|
|* ███ ██ ██ ███ ██ ███████ █████ *|
|* ████ ██ ██ ████ ██ ██ ██ ██ *|
|* ██ ██ ██ ██ ██ ██ ██ █████ ███████ *|
|* ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ *|
|* ██ ████ ██ ██ ████ ██ ██ ██ *|
|*----------------------------------------------------------*/
// 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;
}
}
}
// 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);
}
// 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);
}
// 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);
}
// 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);
}
// 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;
}
// 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);
}
// 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);
}
// 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);
}
// 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);
}
// 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);
}
// 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);
}
// 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);
}
/*----------------------------------------------------------*|
|* ███ ██ ██ ███ ██ ███████ █████ *|
|* ████ ██ ██ ████ ██ ██ ██ ██ *|
|* ██ ██ ██ ██ ██ ██ ██ █████ ███████ *|
|* ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ *|
|* ██ ████ ██ ██ ████ ██ ██ ██ *|
|*----------------------------------------------------------*/
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
import "./utils/Counters.sol";
import "./utils/RoyaltyEngineV1.sol";
import "./access/Ownable.sol";
/**
*
* @title NinfaMarketplace *
* *
* @notice On-chain NFT marketplace *
* *
* @custom:security-contact tech@ninfa.io *
*
*/
contract NinfaMarketplace is Ownable, RoyaltyEngineV1 {
/*----------------------------------------------------------*|
|* # VARIABLES *|
|*----------------------------------------------------------*/
using Counters for Counters.Counter;
/// @notice Orders counter
Counters.Counter private _orderCount;
/// @notice Offers counter
Counters.Counter private _offerCount;
/// @notice address of admin multisig contract for receiving fees generated
/// by the marketplace
address private _feeRecipient;
/// @notice factory contract for deploying self-sovereign collections.
address private _factory;
/// @notice Ninfa's whitelist contract, used for checking if a collection is whitelisted
address private _whitelist;
/// @notice Ninfa's external registry, mapping Ninfa's ERC721 sovreign
/// 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 constant 10,000 BPS = 100% shares sale price
uint256 private constant _BPS_DENOMINATOR = 10_000;
/// @notice Ninfa Marketplace fee percentage on primary sales from orders,
/// expressed in basis points
uint256 private _primaryOrdersFee;
/// @notice Ninfa Marketplace fee percentage on primary sales from offers,
/// expressed in basis points
uint256 private _primaryOffersFee;
/// @notice Ninfa Marketplace fee percentage on all secondary sales,
/// expressed in basis points
uint256 private _secondaryMarketFee;
/// @notice codehash of Ninfa's ERC721 sovreign collection
bytes32 private _ERC721SovreignV1CodeHash;
/// @notice `_orderCount` counter to `_Order` struct mapping
mapping(uint256 => _Order) private _orders;
/// @notice `_offerCount` counter to `_Offer` struct mapping
mapping(uint256 => _Offer) public offers;
/*----------------------------------------------------------*|
|* # STRUCTS *|
|*----------------------------------------------------------*/
/**
* @dev the `Order` struct is used both for storing order information, as
* well as trade information when passed as a
* function parameter to the private `_trade` function
* @param tokenId the NFT id, for now we only allow trading NINFA NFT's so
* no erc721 address is needed
* @param unitPrice ERC-1155 unit price in ETH, or total price if ERC-721
* since there is only 1 unit of each token.
* @dev when the `Order` struct is passed as a function parameter to
* `_trade`, `unitPrice` always refers to the
* total price of ERC-1155 tokens, i.e. token value * unit price
* @param collection address of the ERC721 or ERC1155 contract. Doesn't
* require any access control besides the
* collection being whitelisted, i.e. msg.sender may be any address.
* @param erc1155Value the NFT amount, _amount == 0 for ERC721 and _amount >
* 0 for ERC1155
* @param commissionBps commission amounts, expressed in basis points 0 -
* 10000
* @param commissionReceivers receivers of commission on sales (primary AND
* secondary)
* @param collection address of the collection being sold
* @param from always refers to the seller, who either creater an order or
* is accepting an offer.
* @param operator address of an authorized operator, such as a gallery
* managing an artist; the operator is allowed
* to change or cancel an order they created on the Marketplace.
* @dev operator address usually also corresponds to the commission receiver
* in order to receive sale commissions,
* unless `commissionReceiver` is set to yet another address, e.g. a payment
* splitter.
*/
struct _Order {
uint256 tokenId;
uint256 unitPrice;
uint256 erc1155Value;
address collection;
address from;
address operator;
uint256[] commissionBps;
address[] commissionReceivers;
}
struct _Offer {
uint256 tokenId;
uint256 unitPrice;
uint256 erc1155Value;
address collection;
address from; // buyer
}
/*----------------------------------------------------------*|
|* # EVENTS *|
|*----------------------------------------------------------*/
event OrderCreated(uint256 orderId);
event OrderUpdated(uint256 orderId);
event OrderDeleted(uint256 orderId);
event OfferCreated(uint256 offerId);
event OfferUpdated(uint256 offerId);
event OfferDeleted(uint256 offerId);
// we have order/offer id and all the related data stored in db.
event Trade( // seller
address indexed collection,
uint256 indexed tokenId,
address indexed from,
uint256 id,
uint256 price,
uint256 erc1155Value
);
/*----------------------------------------------------------*|
|* # MODIFIERS *|
|*----------------------------------------------------------*/
/**
* @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();
}
_;
}
/*----------------------------------------------------------*|
|* # ORDERS *|
|*----------------------------------------------------------*/
/**
* @notice create a new order on the marketplace by transfering an NFT to
* it.
* @dev Will create a new order by sending tokens with data bytes containing
* function parameters
*
* Require:
*
* - can only be called by an NFT smart contract transfering an NFT to the
* marketplace
* - collection must be whitelisted
*
*/
function _createOrder(
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 _value,
uint256 _unitPrice,
uint256[] memory _commissionBps,
address[] memory _commissionReceivers
)
private
isWhitelisted(msg.sender)
{
// `_orderCount` starts at 1
_orderCount.increment();
uint256 _orderId = _orderCount.current();
// create order with new `_orderId` in orders mapping
_orders[_orderId] = _Order(
_id,
_unitPrice,
_value,
msg.sender, // collection
_from,
_operator,
_commissionBps,
_commissionReceivers
);
emit OrderCreated(_orderId);
}
/// @dev only for 1155 for updating the price and LOWER the amount
function updateOrder(uint256 _erc1155RedeemAmount, uint256 _unitPrice, uint256 _orderId) external {
lowerOrderErc1155Amount(_orderId, _erc1155RedeemAmount);
_orders[_orderId].unitPrice = _unitPrice;
}
/**
* @notice cancels order and transfers NFT back to owner
* @param _orderId the Id of the order
* @dev delete `_orders[_orderId]` from storage BEFORE making external calls
* for transfering the NFT back to the
* seller (check-effects pattern)
*
*
* SHOULD:
*
* This function does not check whether the order exists or not
*
*/
function deleteOrder(uint256 _orderId) external {
_Order memory order = _orders[_orderId];
require(msg.sender == order.operator);
delete _orders[_orderId];
_transferNFT(order.collection, address(this), msg.sender, order.tokenId, order.erc1155Value);
emit OrderDeleted(_orderId);
}
/**
* @param _unitPrice will override the old price, note that it may be 0
* although this could only mean that a mistake
* was made
* @dev this function doesn't enforce a positive value for `_unitPrice` in
* order to save a little gas for the users.
* @dev if `_unitPrice` is set to 0, the order will be deleted from the
* database (not from the smart contract), see
* {NinfaMarketplace-OrderUpdated} modifier
*/
function updateOrderPrice(uint256 _orderId, uint256 _unitPrice) external {
_Order storage order = _orders[_orderId];
require(msg.sender == order.operator);
order.unitPrice = _unitPrice;
emit OrderUpdated(_orderId);
}
/**
* @notice function to lower an order's amount of the ERC-1155 tokenId on
* sale, doesn't apply to ERC-721 because it
* is non-fungible
* @param _erc1155RedeemAmount is the (negative) difference of tokens to be
* withdrawn by the seller. Should be
* different from 0, although not strictly required.
*/
function lowerOrderErc1155Amount(uint256 _orderId, uint256 _erc1155RedeemAmount) public {
_Order storage order = _orders[_orderId];
require(msg.sender == order.operator);
/// @dev warning, make changes to storage BEFORE making external calls
/// for transfering the NFT back to the
/// seller (check-effects-interactions pattern)
order.erc1155Value -= _erc1155RedeemAmount;
(bool success,) = order.collection.call(
abi.encodeWithSelector(
0xf242432a, // bytes4(keccak256('safeTransferFrom(address,address,uint256,uint256,bytes)'))
address(this),
msg.sender,
order.tokenId,
_erc1155RedeemAmount,
""
)
);
require(success);
if (order.erc1155Value == 0) {
delete _orders[_orderId]; // it is not possible to delete using
// storage pointers
// https://docs.soliditylang.org/en/develop/types.html#data-location
emit OrderDeleted(_orderId);
} else {
emit OrderUpdated(_orderId);
}
}
function setOrderCommission(
uint256 _orderId,
uint256[] memory _commissionBps,
address[] memory _commissionReceivers) external {
// should we check if the total commission is less than 10000?
// should revert in trade function anyway
require(msg.sender == _orders[_orderId].operator);
_orders[_orderId].commissionBps = _commissionBps;
_orders[_orderId].commissionReceivers = _commissionReceivers;
emit OrderUpdated(_orderId);
}
/*----------------------------------------------------------*|
|* # OFFERS *|
|*----------------------------------------------------------*/
/**
* @dev offers can be made independently of whether the token is on sale or
* not, the msg.value is used to determine
* the offer amount, so no function parameter is needed for that
* @dev there is no require to check that an offer or offer doesn't already
* exist and if so, that the offer amount
* is not greater than the order itself, this was omitted in order to save
* gas; the frontend should check this in
* order to prevent mistakes from the user
* @param _collection address of the erc721 implementation contract or proxy
* contract
* @param _tokenId the token Id to make an offer to
* @param _amount the NFT amount, _amount == 0 for ERC721 and _amount > 0
* for ERC1155
* @param _from needed in order to integrate Wert payment solution, because
* in every txn Wert is the `msg.sender`.
* using _msgSender does not represent a security risk, on the other
* hand, it is possible for the buyer to use
* this parameter simply in order to transfer the NFT to an address other
* than their own, this can be useful for
* external contract buying NFTs.
*/
function createOffer(
address _collection,
uint256 _tokenId,
uint256 _amount,
address _from,
uint256 _unitPrice
)
external
payable
isWhitelisted(_collection)
{
_offerCount.increment(); // start count at 1
if (_amount == 0) require(msg.value == _unitPrice);
else require(msg.value == _unitPrice * _amount);
offers[_offerCount.current()] = _Offer(
_tokenId, // uint256 tokenId;
_unitPrice, // uint256 unitPrice;
_amount, // uint256 amount;
_collection, // address collection;
_from // address from;
);
emit OfferCreated(_offerCount.current());
}
/**
* @dev cancels offer and refunds ETH back to bidder. When an order gets
* filled, the offer isn't marked as
* cancelled, in order to allow users to claim back their money.
* @param _offerId the Id of the offer.
*/
function deleteOffer(uint256 _offerId) external {
// in memory copy needed so that it is possible to delete the struct
// inside the storage offers mapping, while
// keeping check effects interact pattern intact
_Offer memory offer = offers[_offerId];
uint256 refund;
if (offer.erc1155Value == 0) {
refund = offer.unitPrice;
} else {
refund = offer.unitPrice * offer.erc1155Value;
}
require(msg.sender == offer.from);
// mark offer as cancelled forever, updating offer price before external
// call, Checks Effects Interactions
// pattern
delete offers[_offerId];
// transfer the offer amount back to bidder
_sendValue(offer.from, refund);
emit OfferDeleted(_offerId);
}
/**
* @dev this is one of two functions called by a buyer in order to modify
* their offer, there are two functions,
* `raiseOffer()` and `lowerOffer()`, because they expect different
* parameters depending on whether the offer is
* being raised or lowerd.
* A `msg.value` is required, this function will add the amount sent to
* the old offer amount. The frontend
* needs to calculate the difference between the old and new offer.
* E.g. A buyer calls createOffer() and pays 0.1 ETH. The same buyer
* later wants to raise the offer to 0.3 ETH,
* therefore they now need to send 0.2 ETH, because 0.1 was was sent before.
* @dev anyone can call this function, i.e. requiring that caller is offer
* creator is not needed
* @param _offerId the id of the offer
* call this function only if the new total price is greater than the old
* total price
*/
function raiseOfferPrice(uint256 _offerId, uint256 _erc1155Value, uint256 _unitPrice) external payable {
_Offer storage _offer = offers[_offerId];
require(msg.sender == _offer.from);
if (_offer.erc1155Value == 0) {
require(msg.value == _unitPrice - _offer.unitPrice);
} else {
require(msg.value == (_unitPrice * _erc1155Value) - (_offer.unitPrice * _offer.erc1155Value));
}
_offer.unitPrice = _unitPrice; // transfer extra amount needed on top of
// older offer
_offer.erc1155Value = _erc1155Value;
emit OfferUpdated(_offerId);
}
/**
* @dev this is one of two functions called by a buyer in order to modify
* their offer, there are two functions,
* `raiseOffer()` and `lowerOffer()`, because they expect different
* parameters depending on whether the offer is
* being raised or lowerd.
* In contrast with `raiseOffer()`, instead of `msg.value` this
* function expects a uint parameter representing
* the new (lower) offer; the buyer will get refunded the difference.
* E.g. A buyer calls createOffer() and pays 0.3 ETH. The same buyer
* later wants to lower the offer to 0.1 ETH,
* therefore they will get refunded 0.2 ETH. I.e. The amount expected by the
* `_newAmount` paramer is 0.1 ETH (1^17).
* @param _offerId the id of the offer
*/
function lowerOfferPrice(uint256 _offerId, uint256 _erc1155Amount, uint256 _unitPrice) external {
_Offer storage _offer = offers[_offerId];
require(msg.sender == _offer.from, "hwy");
uint256 refund;
if (_erc1155Amount == 0) {
refund = _offer.unitPrice - _unitPrice;
} else {
refund = (_offer.unitPrice * _offer.erc1155Value) - (_unitPrice * _erc1155Amount);
_offer.erc1155Value = _erc1155Amount;
}
_offer.unitPrice = _unitPrice; // needed to store result before offer
// price is updated
// transfer the difference between old and new lower offer to the user
_sendValue(msg.sender, refund);
emit OfferUpdated(_offerId);
}
function acceptListedTokenOffer(
uint256 _orderId,
uint256 _offerId,
uint256[] memory _commissionBps,
address[] memory _commissionReceivers
)
external
{
_Order memory order = _orders[_orderId];
_Offer memory offer = offers[_offerId];
require(order.operator == msg.sender && order.tokenId == offer.tokenId && order.collection == offer.collection);
delete _orders[_orderId];
delete offers[_offerId];
_trade(
_Order(
order.tokenId, // uint256 tokenId
offer.unitPrice, // offer price, not order price
0,
order.collection, // address collection
msg.sender, // seller
offer.from, // buyer / nft recipient
_commissionBps, // uint256 commissionBps
_commissionReceivers
),
_orderId,
_primaryOffersFee
);
}
/*----------------------------------------------------------*|
|* # TRADING *|
|*----------------------------------------------------------*/
/**
* @notice the collector calls this function to buy an NFT at the ask price,
* only if an order exists
* @notice if someone has an open offer but calls fillOrder, the offer will
* remain open, meaning they will need to
* call cancelOffer() to get a refund. This is unlikely, as users will
* likely be aware of this and use the refund in
* order to pay for part of the order.
* @param _id avoids having to store a mapping to order id like the
* deprecated `mapping(address => mapping(uint256
* => uint256)) private _tokenToOrderId` which would have not worked for
* erc1155 as each token has a supply.
* _orderId does not constitute a vulnerability as it is user provided,
* since A) a regular user will go through the
* frontend which gets orderId from events
* @param _buyer needed in order to integrate Wert payment solution, because
* in every txn Wert is the msg.sender,
* although using msg.sender would cost less gas.
* using _msgSender does not represent a security risk, on the other hand,
* it is possible for the buyer to use this
* parameter simply in order to transfer the NFT to an address other than
* their own, this can be useful for external
* contract buying NFTs.
* + @param _erc1155Value market order amount (total or partial fill).
* `_erc1155Value == 0` corresponds to one erc721 tokenId, `_erc1155Value >
* 0` for erc1155 tokenIds
*
* MUST:
*
* - `msg.value` must be equal to `_orders[_orderId]der.unitPrice *
* buyAmount`
* - `_orders[_orderId].sellAmount >= buyAmount`
*
*/
function fillOrder(uint256 _id, address _buyer, uint256 _erc1155Value) external payable {
_Order memory order = _orders[_id];
require(msg.value == order.unitPrice * (_erc1155Value == 0 ? 1 : _erc1155Value));
// subtracting user-suplied `_erc1155Value` from order amount,
// transaction will revert if underflow, implicitly
// requiring `_orders[_id]._erc1155Value >= _erc1155Value`
if (_orders[_id].erc1155Value - _erc1155Value == 0) delete _orders[_id];
else _orders[_id].erc1155Value -= _erc1155Value;
_trade(
_Order(
order.tokenId, // uint256 tokenId
msg.value, // price
_erc1155Value,
order.collection, // address collection
order.from, // seller or from
_buyer, // address buyer used for nft transfer
order.commissionBps, // uint256 commissionBps
order.commissionReceivers // address commissionReceiver
),
_id,
_primaryOrdersFee // uint256 primaryFee
);
}
/**
*
* @dev Handles the receipt of a single ERC1155 token type. This function is
* called at the end of a `safeTransferFrom` after the balance has been
* updated.
*
* NOTE: To accept the transfer, this must return
* `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))`
* (i.e. 0xf23a6e61, or its own function selector).
*
* @param _operator The address which initiated the transfer (i.e.
* msg.sender)
* @param _from The address which previously owned the token
* @param _tokenId The ID of the token being transferred
* @param _value The amount of tokens being transferred
* @param _data Additional data with no specified format
* @param _data `uint256 id` corresponding to an order to be updated or
* an offer to be accepted, if the `id`
* parameter is 0 create a new order.
* @param _data `uint256 unitPrice` is only required for updating an
* order, i.e. if `unitPrice == 0` `id` an
* offer id, if `unitPrice > 0` `id` is an order id
* @return
* `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))`
* if transfer is allowed
*/
function onERC1155Received(
address _operator, // either the previous owner or operator, whoever
// called safeTransferFrom on the ERC1155
// contract
address _from, // previous owner
uint256 _tokenId,
uint256 _value,
bytes memory _data
)
external
returns (bytes4)
{
(uint256 id, uint256 unitPrice, uint256[] memory commissionBps, address[] memory commissionReceivers) =
abi.decode(_data, (uint256, uint256, uint256[], address[]));
if (unitPrice == 0) {
/*----------------------------------------------------------*|
|* # ACCEPT OFFER *|
|*----------------------------------------------------------*/
// if `_value` is more than the amount the following if check will
// revert due to underflow,
// intended to stop someone from sending more erc1155 tokens then
// there are available in the order
_Offer memory offer = offers[id];
require(offer.collection == msg.sender && offer.tokenId == _tokenId);
// subtracting `_value` from offer amount, transaction will revert
// if underflow, implicitly requiring
// `offers[id]._erc1155Value >= _value`
if (offers[id].erc1155Value - _value == 0) delete offers[id];
else offers[id].erc1155Value -= _value;
_trade(
_Order(
_tokenId, // uint256 tokenId
offer.unitPrice * _value, // uint256 price (unitPrice *
// value)
_value,
msg.sender, // address collection
_from, // seller
offer.from, // buyer or operator
commissionBps, // uint256 commissionBps
commissionReceivers // address commissionReceivers
),
id,
_primaryOffersFee
);
} else if (id == 0) {
/*----------------------------------------------------------*|
|* # CREATE ORDER *|
|*----------------------------------------------------------*/
// if the order/offer id parameter is 0, create a new order
_createOrder(_operator, _from, _tokenId, _value, unitPrice, commissionBps, commissionReceivers);
} else {
/*----------------------------------------------------------*|
|* # UPDATE ORDER *|
|*----------------------------------------------------------*/
// if the user supplied a non-zero value for `unitPrice` then the
// `id` parameter must correspond to an order
// that needs to be updated
// the operator, collection and tokenId of the NFT received by the
// marketplace must match the ones stored at
// the id provided by the operator
// in order to avoid operators increasing allowance for orders with
// different (more valuable) NFTs
_Order storage order = _orders[id];
require(order.operator == _operator && order.collection == msg.sender && order.tokenId == _tokenId);
order.erc1155Value += _value;
order.unitPrice = unitPrice;
emit OrderUpdated(id);
}
return 0xf23a6e61;
}
/**
* @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 id, uint256 unitPrice, uint256[] memory commissionBps, address[] memory commissionReceivers) =
abi.decode(_data, (uint256, uint256, uint256[], address[]));
if (id == 0) {
/*----------------------------------------------------------*|
|* # CREATE ORDER *|
|*----------------------------------------------------------*/
// if the order/offer id parameter is 0, create a new order
_createOrder(_operator, _from, _tokenId, 0, unitPrice, commissionBps, commissionReceivers);
} else {
/*----------------------------------------------------------*|
|* # ACCEPT OFFER *|
|*----------------------------------------------------------*/
// if `_value` is more than the amount the following if check will
// revert due to underflow,
// intended to stop someone from sending more erc1155 tokens then
// there are available in the order
_Offer memory offer = offers[id];
require(offer.collection == msg.sender && offer.tokenId == _tokenId);
delete offers[id]; // ERC-721 doesn't have any supply therefore the
// offer may be deleted after accepting the
// offer
_trade(
_Order(
_tokenId, // uint256 tokenId
offer.unitPrice, // uint256 price
0,
msg.sender, // address collection
_from, // address buyer or from
offer.from, // seller
commissionBps, // uint256 commissionBps
commissionReceivers
),
id,
_primaryOffersFee //uint256 primaryFee
);
}
return 0x150b7a02;
}
/**
* @dev ERC-721 tokens are transferred to the buyer via `transferFrom`
* rather than `safeTransferFrom`
* i.e. the caller is responsible to confirm that the recipient is
* capable of receiving ERC721
*/
function _transferNFT(
address _collection,
address _from,
address _to,
uint256 _tokenId,
uint256 _erc1155Value
)
private
{
bool success;
if (_erc1155Value == 0) {
// bytes4(keccak256('safeTransferFrom(address,address,uint256)')) == 0x42842e0e
(success,) = _collection.call(abi.encodeWithSelector(0x42842e0e, _from, _to, _tokenId));
} else {
// bytes4(keccak256('safeTransferFrom(address,address,uint256,uint256,bytes)')) == 0xf242432a
(success,) = _collection.call(abi.encodeWithSelector(0xf242432a, _from, _to, _tokenId, _erc1155Value, ""));
}
require(success);
}
function _trade(_Order memory _order, uint256 _id, uint256 _primaryMarketFee) private {
uint256 marketplaceAmount; // declare `marketplaceAmount`, its value
// will be calculated based on whether it is a
// primary or secondary sale
uint256 sellerAmount = _order.unitPrice; // sellerAmount is set equal to
// price and reduced at each step by
// subtracting fees, royalties and commissions, if any.
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
bool checkSecondaryMarket = true;
if (_order.collection.codehash == _ERC721SovreignV1CodeHash) {
// it's a v1 721 token, check market registry
(, bytes memory secondaryMarket) = _PRIMARY_MARKET_REGISTRY.call(
abi.encodeWithSelector(
0x7abab711,
_order.collection,
_order.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(_order.collection, _order.tokenId, _order.unitPrice);
}
uint256 royaltyRecipientsLength = royaltyRecipients.length; // assign to
// memory variable to save gas
if (royaltyRecipientsLength > 0) {
if (_secondaryMarketFee > 0) {
/*----------------------------------------------------------*|
|* # PAY MARKETPLACE FEE *|
|*----------------------------------------------------------*/
marketplaceAmount = (_order.unitPrice * _secondaryMarketFee) / _BPS_DENOMINATOR;
// 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 = (_order.unitPrice * _primaryMarketFee) / _BPS_DENOMINATOR;
sellerAmount -= marketplaceAmount; // subtract before external
// call
_sendValue(_feeRecipient, marketplaceAmount);
}
/*----------------------------------------------------------*|
|* # PAY ORDER COMMISSIONS (if any) *|
|*----------------------------------------------------------*/
uint256 commissionReceiversLength = _order.commissionReceivers.length; // assign
if (commissionReceiversLength > 0) {
do {
commissionReceiversLength--;
if(_order.commissionBps[commissionReceiversLength] > 0){
uint256 commissionAmount = (_order.commissionBps[commissionReceiversLength] * _order.unitPrice) / _BPS_DENOMINATOR; // calculate
sellerAmount -= commissionAmount; // subtract before external
_sendValue(_order.commissionReceivers[commissionReceiversLength], commissionAmount);
}
} while (commissionReceiversLength > 0);
}
/*----------------------------------------------------------*|
|* # PAY SELLER *|
|*----------------------------------------------------------*/
_sendValue(_order.from, sellerAmount);
/*----------------------------------------------------------*|
|* # TRANSFER NFT *|
|*----------------------------------------------------------*/
_transferNFT(
_order.collection,
address(this),
_order.operator, // buyer
_order.tokenId,
_order.erc1155Value
);
emit Trade(
_order.collection,
_order.tokenId,
_order.from, // seller
_id,
_order.unitPrice,
_order.erc1155Value
);
}
/**
* @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);
}
/*----------------------------------------------------------*|
|* # ADMIN FUNCTIONS *|
|*----------------------------------------------------------*/
/**
* @param feeRecipient_ address (multisig) controlled by Ninfa that will
* receive any market fees
*/
function setFeeRecipient(address feeRecipient_) external onlyOwner {
_feeRecipient = feeRecipient_;
}
function setWhitelist(address whitelist_) external onlyOwner {
_whitelist = whitelist_;
}
/**
* @notice sets market sale fees for NINFA_ERC721_V2 communal collection.
* @param primaryOrdersFee_ fee BPS for primary market orders, set to 500
* BPS (5% shares) at deployment.
* @param primaryOffersFee_ fee BPS for primary market offers, set to 500
* BPS (5% shares) at deployment.
*/
function setMarketFees(
uint256 primaryOrdersFee_,
uint256 primaryOffersFee_,
uint256 secondaryMarketFee_
)
external
onlyOwner
{
_primaryOrdersFee = primaryOrdersFee_;
_primaryOffersFee = primaryOffersFee_;
_secondaryMarketFee = secondaryMarketFee_;
}
function orders(uint256 _orderId) external view returns (_Order memory) {
return _orders[_orderId];
}
/*----------------------------------------------------------*|
|* # VIEW FUNCTIONS *|
|*----------------------------------------------------------*/
/**
* @dev See {IERC165-supportsInterface}.
* Interface ID for IERC165 == 0x01ffc9a7
* Return value from `onERC1155Received` call if a contract accepts receipt
* (i.e
* `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))`).
* In all other cases the ERC1155TokenReceiver rules MUST be followed as
* appropriate for the implementation (i.e.
* safe, custom and/or hybrid).
* Interface ID for IERC721Receiver. A wallet/broker/auction application
* MUST implement the wallet interface if it
* will accept safe transfers.
*/
function supportsInterface(bytes4 interfaceId) external pure returns (bool) {
return interfaceId == 0x01ffc9a7 || interfaceId == 0xf23a6e61 || 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 _primaryMarketRegistry,
address factory_,
address whitelist_,
bytes32 ERC721SovreignV1CodeHash
)
RoyaltyEngineV1(_royaltyRegistry)
{
_PRIMARY_MARKET_REGISTRY = _primaryMarketRegistry;
_factory = factory_;
_whitelist = whitelist_;
_ERC721SovreignV1CodeHash = ERC721SovreignV1CodeHash;
}
}
// 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);
}
}
/*----------------------------------------------------------*|
|* ███ ██ ██ ███ ██ ███████ █████ *|
|* ████ ██ ██ ████ ██ ██ ██ ██ *|
|* ██ ██ ██ ██ ██ ██ ██ █████ ███████ *|
|* ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ *|
|* ██ ████ ██ ██ ████ ██ ██ ██ *|
|*----------------------------------------------------------*/
// 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;
}
}
{
"compilationTarget": {
"src/NinfaMarketplace.sol": "NinfaMarketplace"
},
"evmVersion": "paris",
"libraries": {},
"metadata": {
"appendCBOR": false,
"bytecodeHash": "none"
},
"optimizer": {
"details": {
"constantOptimizer": true,
"cse": true,
"deduplicate": true,
"inliner": true,
"jumpdestRemover": true,
"orderLiterals": true,
"peephole": true,
"yul": true,
"yulDetails": {
"optimizerSteps": "dhfoDgvulfnTUtnIf:fDnTOc",
"stackAllocation": true
}
},
"runs": 4194304
},
"remappings": [
":ds-test/=lib/forge-std/lib/ds-test/src/",
":forge-std/=lib/forge-std/src/",
":openzeppelin-contracts/=lib/openzeppelin-contracts/",
":openzeppelin/=lib/openzeppelin-contracts/contracts/",
":solmate/=lib/solmate/src/",
":src/=src/",
":test/=test/"
],
"viaIR": true
}
[{"inputs":[{"internalType":"address","name":"_royaltyRegistry","type":"address"},{"internalType":"address","name":"_primaryMarketRegistry","type":"address"},{"internalType":"address","name":"factory_","type":"address"},{"internalType":"address","name":"whitelist_","type":"address"},{"internalType":"bytes32","name":"ERC721SovreignV1CodeHash","type":"bytes32"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"InvalidAmount","type":"error"},{"inputs":[{"internalType":"uint256","name":"recipients","type":"uint256"},{"internalType":"uint256","name":"bps","type":"uint256"}],"name":"LengthMismatch","type":"error"},{"inputs":[],"name":"Unauthorized","type":"error"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"offerId","type":"uint256"}],"name":"OfferCreated","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"offerId","type":"uint256"}],"name":"OfferDeleted","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"offerId","type":"uint256"}],"name":"OfferUpdated","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"orderId","type":"uint256"}],"name":"OrderCreated","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"orderId","type":"uint256"}],"name":"OrderDeleted","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"orderId","type":"uint256"}],"name":"OrderUpdated","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"previousOwner","type":"address"},{"indexed":false,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"collection","type":"address"},{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"},{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":false,"internalType":"uint256","name":"id","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"price","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"erc1155Value","type":"uint256"}],"name":"Trade","type":"event"},{"inputs":[{"internalType":"address","name":"tokenAddress","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"_getRoyalty","outputs":[{"internalType":"address payable[]","name":"recipients","type":"address[]"},{"internalType":"uint256[]","name":"amounts","type":"uint256[]"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_orderId","type":"uint256"},{"internalType":"uint256","name":"_offerId","type":"uint256"},{"internalType":"uint256[]","name":"_commissionBps","type":"uint256[]"},{"internalType":"address[]","name":"_commissionReceivers","type":"address[]"}],"name":"acceptListedTokenOffer","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_collection","type":"address"},{"internalType":"uint256","name":"_tokenId","type":"uint256"},{"internalType":"uint256","name":"_amount","type":"uint256"},{"internalType":"address","name":"_from","type":"address"},{"internalType":"uint256","name":"_unitPrice","type":"uint256"}],"name":"createOffer","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_offerId","type":"uint256"}],"name":"deleteOffer","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_orderId","type":"uint256"}],"name":"deleteOrder","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_id","type":"uint256"},{"internalType":"address","name":"_buyer","type":"address"},{"internalType":"uint256","name":"_erc1155Value","type":"uint256"}],"name":"fillOrder","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_offerId","type":"uint256"},{"internalType":"uint256","name":"_erc1155Amount","type":"uint256"},{"internalType":"uint256","name":"_unitPrice","type":"uint256"}],"name":"lowerOfferPrice","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_orderId","type":"uint256"},{"internalType":"uint256","name":"_erc1155RedeemAmount","type":"uint256"}],"name":"lowerOrderErc1155Amount","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"offers","outputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"uint256","name":"unitPrice","type":"uint256"},{"internalType":"uint256","name":"erc1155Value","type":"uint256"},{"internalType":"address","name":"collection","type":"address"},{"internalType":"address","name":"from","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_operator","type":"address"},{"internalType":"address","name":"_from","type":"address"},{"internalType":"uint256","name":"_tokenId","type":"uint256"},{"internalType":"uint256","name":"_value","type":"uint256"},{"internalType":"bytes","name":"_data","type":"bytes"}],"name":"onERC1155Received","outputs":[{"internalType":"bytes4","name":"","type":"bytes4"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_operator","type":"address"},{"internalType":"address","name":"_from","type":"address"},{"internalType":"uint256","name":"_tokenId","type":"uint256"},{"internalType":"bytes","name":"_data","type":"bytes"}],"name":"onERC721Received","outputs":[{"internalType":"bytes4","name":"","type":"bytes4"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_orderId","type":"uint256"}],"name":"orders","outputs":[{"components":[{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"uint256","name":"unitPrice","type":"uint256"},{"internalType":"uint256","name":"erc1155Value","type":"uint256"},{"internalType":"address","name":"collection","type":"address"},{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"operator","type":"address"},{"internalType":"uint256[]","name":"commissionBps","type":"uint256[]"},{"internalType":"address[]","name":"commissionReceivers","type":"address[]"}],"internalType":"struct NinfaMarketplace._Order","name":"","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_offerId","type":"uint256"},{"internalType":"uint256","name":"_erc1155Value","type":"uint256"},{"internalType":"uint256","name":"_unitPrice","type":"uint256"}],"name":"raiseOfferPrice","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"renounceOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"feeRecipient_","type":"address"}],"name":"setFeeRecipient","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"primaryOrdersFee_","type":"uint256"},{"internalType":"uint256","name":"primaryOffersFee_","type":"uint256"},{"internalType":"uint256","name":"secondaryMarketFee_","type":"uint256"}],"name":"setMarketFees","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_orderId","type":"uint256"},{"internalType":"uint256[]","name":"_commissionBps","type":"uint256[]"},{"internalType":"address[]","name":"_commissionReceivers","type":"address[]"}],"name":"setOrderCommission","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"whitelist_","type":"address"}],"name":"setWhitelist","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes4","name":"interfaceId","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_erc1155RedeemAmount","type":"uint256"},{"internalType":"uint256","name":"_unitPrice","type":"uint256"},{"internalType":"uint256","name":"_orderId","type":"uint256"}],"name":"updateOrder","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_orderId","type":"uint256"},{"internalType":"uint256","name":"_unitPrice","type":"uint256"}],"name":"updateOrderPrice","outputs":[],"stateMutability":"nonpayable","type":"function"}]