Buy Now, Pay Later

Sellers can offer financing as a way to achieve a higher sale price they may be targeting, as well as selling more NFTs, more quickly, for more money.

Buyers can purchase an NFT in installments over time while still accessing the on-chain and IRL benefits of the NFT such as discord access, air drops, voting in governance, and attending IRL events via Delegate.xyz

Buyer can also sell their purchased NFTs at any time by accepting an open valid Seaport bid.

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 sellers

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.

5. Enable buyers to select financing at point of sale

Fifth, we will make the financing offer available at the point of sale.

  1. We will implement a UI like the screens below to guide the user through the financing process
  2. And we will use the useBuyWithFinancing hook to execute the loan.

Select financing on individual asset page

Show details of financing and execute purchase

6. Enable sweeps

Finally, we implement sweeps on sell financing offers so that users can purchase multiple NFTs with seller financing at once.

  1. We will integrate seller financing into the sweep UI similar to the screen below.
  2. We will use the useBuyWithFinancingBatch hook to execute multiple loans in one transaction.

Sweeps with Seller Financing