Artist Financing - 1 of 1 NFTs

In the traditional art world via entities like Sotheby's or Christie's offer financing to buyers as a way to increase sale prices and sell more art. Now artists can offer financing on a 1/1 or limited piece drop on their own collection without any additional capital required up front, they simply accept payments over time plus interest. Artists can sell more art, more quickly, for more money. As a result buyers have the ability to spend more on art with the same initial capital. This might result in higher sale prices for artists and larger collections for buyers. It may also provide a more consistent stream of income for artists rather than the lump sum payments they commonly have in todays digital art markets.

Implementing artist financing via the NiftyApes SDK is simple and easy. This guide assumes artists on your platform have already minted their NFTs, perhaps from a monolithic smart contract (like with SuperRare or ArtBlocks) or other 3rd party contract, but have not yet listed them for sale. If your platform enables artists to list their art for sale directly from mint please see your mint financing guide. This guide also assume that artists will be listing 1 of 1 art one NFT at a time. If artists on your platform will be listing more than piece at a time you should look to implement the useERC721SetApprovalForAll hook in place of the useERC721Approve hook in step 2.

1. Deploy MarketplaceIntegration.sol

First, you will want to deploy a Marketplace Integration contract so that you can set your marketplace fee and marketplace fee recipient address if desired. This enables you as the marketplace to make any transaction fee you already charge up front, in full.

The address of this deployed contract will be added as a prop to the NiftyApes provider during set up of the NiftyApes SDK

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

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/Pausable.sol";
import "@openzeppelin/contracts/utils/Address.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol";
import "../interfaces/sellerFinancing/ISellerFinancing.sol";

/// @title MarketplaceIntegration
/// @custom:version 1.0
/// @author zishansami102 (zishansami.eth)
/// @custom:contributor captnseagraves (captnseagraves.eth)

contract MarketplaceIntegration is Ownable, Pausable, ERC721Holder {
    using Address for address payable;

    /// @notice The base value for fees in the protocol.
    uint256 private constant BASE_BPS = 10_000;

    /// @dev The status of sanctions checks
    bool internal _sanctionsPause;

    uint256 public marketplaceFeeBps;

    address payable public marketplaceFeeRecipient;

    address public sellerFinancingContractAddress;

    error ZeroAddress();

    error InsufficientMsgValue(uint256 given, uint256 expected);

    error SanctionedAddress(address account);

    error InvalidInputLength();

    error InstantSellCallRevertedAt(uint256 index);

    error BuyerTicketTransferRevertedAt(uint256 index, address from, address to);

    constructor(
        address _sellerFinancingContractAddress,
        address _marketplaceFeeRecipient,
        uint256 _marketplaceFeeBps
    ) {
        _requireNonZeroAddress(_sellerFinancingContractAddress);
        _requireNonZeroAddress(_marketplaceFeeRecipient);

        sellerFinancingContractAddress = _sellerFinancingContractAddress;
        marketplaceFeeRecipient = payable(_marketplaceFeeRecipient);
        marketplaceFeeBps = _marketplaceFeeBps;
    }

    /// @param newSellerFinancingContractAddress New address for SellerFinancing contract
    function updateSellerFinancingContractAddress(
        address newSellerFinancingContractAddress
    ) external onlyOwner {
        _requireNonZeroAddress(newSellerFinancingContractAddress);
        sellerFinancingContractAddress = newSellerFinancingContractAddress;
    }

    /// @param newMarketplaceFeeRecipient New address for MarketplaceFeeRecipient
    function updateMarketplaceFeeRecipient(address newMarketplaceFeeRecipient) external onlyOwner {
        _requireNonZeroAddress(newMarketplaceFeeRecipient);
        marketplaceFeeRecipient = payable(newMarketplaceFeeRecipient);
    }

    /// @param newMarketplaceFeeBps New value for marketplaceFeeBps
    function updateMarketplaceFeeBps(uint256 newMarketplaceFeeBps) external onlyOwner {
        marketplaceFeeBps = newMarketplaceFeeBps;
    }

    function pause() external onlyOwner {
        _pause();
    }

    function unpause() external onlyOwner {
        _unpause();
    }

    function pauseSanctions() external onlyOwner {
        _sanctionsPause = true;
    }

    function unpauseSanctions() external onlyOwner {
        _sanctionsPause = false;
    }

    function buyWithFinancing(
        ISellerFinancing.Offer memory offer,
        bytes calldata signature,
        address buyer,
        uint256 nftId
    ) external payable whenNotPaused {
        uint256 marketplaceFeeAmount = (offer.price * marketplaceFeeBps) / BASE_BPS;
        if (msg.value < offer.downPaymentAmount + marketplaceFeeAmount) {
            revert InsufficientMsgValue(msg.value, offer.downPaymentAmount + marketplaceFeeAmount);
        }
        marketplaceFeeRecipient.sendValue(marketplaceFeeAmount);

        ISellerFinancing(sellerFinancingContractAddress).buyWithFinancing{
            value: msg.value - marketplaceFeeAmount
        }(offer, signature, buyer, nftId);
    }
  
    /// @notice Execute loan offers in batch for buyer
    /// @param offers The list of the offers to execute
    /// @param signatures The list of corresponding signatures from the offer creators
    /// @param buyer The address of the buyer
    /// @param nftIds The nftIds of the nfts the buyer intends to buy
    /// @param partialExecution If set to true, will continue to attempt transaction executions regardless
    ///        if previous transactions have failed or had insufficient value available
    function buyWithFinancingBatch(
        ISellerFinancing.Offer[] memory offers,
        bytes[] calldata signatures,
        address buyer,
        uint256[] calldata nftIds,
        bool partialExecution
    ) external payable whenNotPaused {
        uint256 offersLength = offers.length;

        // requireLengthOfAllInputArraysAreEqual
        if (offersLength != signatures.length || offersLength != nftIds.length) {
            revert InvalidInputLength();
        }

        uint256 marketplaceFeeAccumulated;
        uint256 valueConsumed;

        // loop through list of offers to execute
        for (uint256 i; i < offersLength; ++i) {
            // instantiate ith offer
            ISellerFinancing.Offer memory offer = offers[i];

            // calculate marketplace fee for ith offer
            uint256 marketplaceFeeAmount = (offer.price * marketplaceFeeBps) / BASE_BPS;

            // if remaining value is not sufficient to execute ith offer
            if (msg.value - valueConsumed < offer.downPaymentAmount + marketplaceFeeAmount) {
                // if partial execution is allowed then move to next offer
                if (partialExecution) {
                    continue;
                }
                // else revert
                else {
                    revert InsufficientMsgValue(
                        msg.value,
                        valueConsumed + offer.downPaymentAmount + marketplaceFeeAmount
                    );
                }
            }
            // try executing current offer,
            try
                ISellerFinancing(sellerFinancingContractAddress).buyWithFinancing{
                    value: offer.downPaymentAmount
                }(offer, signatures[i], buyer, nftIds[i])
            {
                // if successful
                // increment marketplaceFeeAccumulated
                marketplaceFeeAccumulated += marketplaceFeeAmount;
                // increment valueConsumed
                valueConsumed += offer.downPaymentAmount + marketplaceFeeAmount;
            } catch {
                // if failed
                // if partial execution is not allowed, revert
                if (!partialExecution) {
                    revert BuyWithFinancingCallRevertedAt(i);
                }
            }
        }

        // send accumulated marketplace fee to marketplace fee recipient
        marketplaceFeeRecipient.sendValue(marketplaceFeeAccumulated);

        // send any unused value back to msg.sender
        if (msg.value - valueConsumed > 0) {
            payable(msg.sender).sendValue(msg.value - valueConsumed);
        }
    }

    /// @notice Execute instantSell on all the NFTs in the provided input
    /// @param nftContractAddresses The list of all the nft contract addresses
    /// @param nftIds The list of all the nft IDs
    /// @param minProfitAmounts List of minProfitAmount for each `instantSell` call
    /// @param data The list of data to be passed to each `instantSell` call
    /// @param partialExecution If set to true, will continue to attempt next request in the loop
    ///        when one `instantSell` or transfer ticket call fails
    function instantSellBatch(
        address[] memory nftContractAddresses,
        uint256[] memory nftIds,
        uint256[] memory minProfitAmounts,
        bytes[] calldata data,
        bool partialExecution
    ) external whenNotPaused {
        uint256 executionCount = nftContractAddresses.length;
        // requireLengthOfAllInputArraysAreEqual
        if(nftIds.length != executionCount || minProfitAmounts.length != executionCount || data.length != executionCount) {
            revert InvalidInputLength();
        }
        
        uint256 contractBalanceBefore = address(this).balance;
        for (uint256 i; i < executionCount; ++i) {
            // intantiate NFT details
            address nftContractAddress = nftContractAddresses[i];
            uint256 nftId = nftIds[i];
            // fetech active loan details
            ISellerFinancing.Loan memory loan = ISellerFinancing(sellerFinancingContractAddress).getLoan(nftContractAddress, nftId);
            // transfer buyerNft from caller to this contract.
            // this call also ensures that loan exists and caller is the current buyer
            try IERC721(sellerFinancingContractAddress).safeTransferFrom(msg.sender, address(this), loan.buyerNftId) {
                // call instantSell to close the loan
                try ISellerFinancing(sellerFinancingContractAddress).instantSell(nftContractAddress, nftId, minProfitAmounts[i], data[i]) {} 
                catch {
                    if (!partialExecution) {
                        revert InstantSellCallRevertedAt(i);
                    } else {
                        IERC721(sellerFinancingContractAddress).safeTransferFrom(address(this), msg.sender, loan.buyerNftId);
                    }
                }
            } catch {
                if (!partialExecution) {
                    revert BuyerTicketTransferRevertedAt(i, msg.sender, address(this));
                }
            }
            
        }
        // accumulate value received
        uint256 valueReceived = address(this).balance - contractBalanceBefore;
        // send all the amount received to the caller
        if( valueReceived > 0) {
            payable(msg.sender).sendValue(valueReceived);
        }
    }

    function _requireNonZeroAddress(address given) internal pure {
        if (given == address(0)) {
            revert ZeroAddress();
        }
    }

    /// @notice This contract needs to accept ETH from `instantSell` calls
    receive() external payable {}
}

2. Provide an option to offer financing for artists

Second, we'll need to enable artists to offer financing on their art. This means providing them with a button or selection mechanism like the one shown below. This could be implemented on an NFT Card as below or on an individual NFT screen.

3. Enable financing offer creation

Third, once the option to offer financing has been selected we'll need to enable the artist to set the terms of the financing in the UI, call approve on the 721 contract, and post a signed offer to the NiftyApes API.

  1. We will implement the useERC721Approve hook to approve the asset for financing in the ERC721MintFinancing contract
  2. And POST a signed financing offer to the NiftyApes API using the /offers route for the NFT.
  3. Inside of a financing terms UI similar to the screens below:

Example Standard Terms View

Expanded Custom Terms View

4. Enable buyers to discover financing offers

Fourth, we'll need to enable buyers to discover the financing offers created by a seller. You may chose to implement this on your home screen, a collection section, and/or an individual asset screen.

  1. We will implement the useOffers hooks anywhere you would like to fetch offers, perhaps your home screen, a collection section, and/or an individual asset screen.

need image

5. Enable buyers to select financing at point of sale

Select financing on individual asset page

Show details of financing

Purchase confirmation and additional loan information