Mint Financing

Creators can offer financing on a lazy mint of their collection or after an NFT has been minted during a drop. Creators can sell more NFTs, more quickly, for more money. Buyers have the ability to buy more NFTs with the same amount of money. Buyers can sell the NFTs they have minted at any time if the proceeds of the sale cover the remaining principal and interest of the loan.

1. Deploy ERC721MintFinancing.sol or update mint contract

First, you will need to enable your users to offer financing on the mint of their NFTs at the contract level.

NiftyApes has created an extension for ERC721, ERC721MintFinancing.sol, that you can easily deploy on behalf of your users. It is an extension of a full ERC721 Implementation with metadata. It does not support enumerability.

Alternatively, you can update your own mint contract with the relevant functions and state variables found in ERC721MintFinancing.sol.

Your contract may look differently, but to integrate NiftyApes into your mint contract you will need to add some version of the following solidity:

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

import "@openzeppelin/contracts/utils/math/Math.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "../interfaces/sellerFinancing/ISellerFinancing.sol";

/// @title ERC721MintFinancing
/// @custom:version 1.0
/// @author captnseagraves (captnseagraves.eth)

contract ERC721MintFinancing is ERC721, Ownable, ReentrancyGuard {
    using Counters for Counters.Counter;

    /// @dev Token ID Tracker
    Counters.Counter private _tokenIdTracker;

    /// @dev The stored address for the seller financing contract
    address public sellerFinancingContractAddress;

    error ZeroAddress();

    error CannotMint0();

    error CollectionOfferLimitReached();

    error InsufficientMsgValue(uint256 given, uint256 expected);

    error InvalidNftContractAddress(address given, address expected);

    error InvalidSigner(address signer, address expected);

    error ReturnValueFailed();

    constructor(
        string memory _name,
        string memory _symbol,
        address _sellerFinancingContractAddress
    ) ERC721(_name, _symbol) {
        _requireNonZeroAddress(_sellerFinancingContractAddress);
        sellerFinancingContractAddress = _sellerFinancingContractAddress;
    }

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

    /// @notice Mints an NFT with financing
    /// @dev The Mint Financing Offer must come from the owner of this contract
    /// @param offer The seller financing offer made by this contract owner
    /// @param signature The signed seller financing offer made by this contract owner,
    /// @param count The number of NFTs requested to mint
    /// @dev   The count must be greater than 0.
    ///        If the count increments the sellerfinancing.collectionOfferLimit counter up to the collectionOffer limit
    ///        all NFTs will be minted up to the limit and excess funds will be returned.
    ///        If the first NFT of a collection is minted with finance the collection tokenIds will begin at index 1
    function mintWithFinancing(
        ISellerFinancing.Offer memory offer,
        bytes calldata signature,
        uint256 count
    ) external payable nonReentrant returns (uint256[] memory tokenIds) {
        address signer = ISellerFinancing(sellerFinancingContractAddress).getOfferSigner(
            offer,
            signature
        );

        uint64 collectionOfferLimitCount = ISellerFinancing(sellerFinancingContractAddress)
            .getCollectionOfferCount(signature);

        tokenIds = new uint256[](count);

        // requireSignerIsOwner
        if (signer != owner()) {
            revert InvalidSigner(signer, owner());
        }
        // requireValidNftContractAddress
        if (offer.nftContractAddress != address(this)) {
            revert InvalidNftContractAddress(offer.nftContractAddress, address(this));
        }
        // requireMsgValueGreaterThanOrEqualToOfferDownPaymentAmountTimesCount
        if (msg.value < (offer.downPaymentAmount * count)) {
            revert InsufficientMsgValue(msg.value, (offer.downPaymentAmount * count));
        }
        // requireCountIsNot0
        if (count == 0) {
            revert CannotMint0();
        }
        // requireCollectionOfferLimitNotReached
        if (collectionOfferLimitCount >= offer.collectionOfferLimit) {
            revert CollectionOfferLimitReached();
        }

        // calculate number of nfts to mint
        uint256 nftsToMint = Math.min(
            count,
            (offer.collectionOfferLimit - collectionOfferLimitCount)
        );

        // loop through and mint nfts
        for (uint i; i < nftsToMint; ++i) {
            // increment nftid tracker
            _tokenIdTracker.increment();
            // mint nft
            _safeMint(owner(), _tokenIdTracker.current());
            // append new nftId to returned tokensIds
            tokenIds[i] = _tokenIdTracker.current();

            // Execute loan
            ISellerFinancing(sellerFinancingContractAddress).buyWithFinancing{
                value: offer.downPaymentAmount
            }(offer, signature, msg.sender, _tokenIdTracker.current());
        }

        // if there is a greater number of NFTs requested than available return value
        if (nftsToMint < count) {
            (bool success, ) = address(msg.sender).call{
                value: msg.value - (offer.downPaymentAmount * nftsToMint)
            }("");
            // require ETH is successfully sent to msg.sender
            // we do not want ETH hanging in contract.
            if (!success) {
                revert ReturnValueFailed();
            }
        }
    }

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

What we added to the ERC721 contract:

  1. A state variable referencing the core NiftyApes Seller Financing contract.
  2. A function to update the Seller Financing Contract Address in the event of a new protocol version release.
  3. A function the allows a user to mint a specified number of NFTs with a valid financing offer from the owner of the ERC721 contract.

2. Provide an option to offer financing for creators

Second, you'll need to enable creators to offer financing on their mint. This means providing them with a button or selection mechanism during collection creation like the one shown below. This can happen after a collection has been created as well.

Collection Creation Form with Mint Financing Option

3. Enable financing offer creation

Third, once the option to offer financing has been selected you'll need to enable the creator to set the terms of the financing in the UI, call setApprovalForAll on the 721 contract, and post a signed offer to the NiftyApes API. The user will fill out the Collection form and financing terms prior to any contract deployment, but the setApprovalForAll transaction call and POST call to the NiftyApes API will occur following a successful deployment of the ERC721MintFinancing contract.

  1. You will need to implement the useERC721SetApprovalForAll hook to approve any newly minted asset for financing in the ERC721MintFinancing contract
  2. And POST a signed offer to the NiftyApes API using the /offers route for the new collection. The creator has the option to specify how many NFTs they will offer financing on via the collectionOfferLimitvariable in their offer.
  3. Inside of a financing terms UI similar to the screens below:

Selected Mint Financing With Standard Terms

Selected Mint Financing With Expanded Form

4. Enable buyers to discover financing offers

Fourth, you'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. You will need to implement the useOffers hooks anywhere you would like to fetch offers, perhaps your home screen, a collection section, and/or an individual asset screen.

Mint Page with Financing Option

5. Enable buyers to select financing at mint

Finally, you will need to make the financing option available at the time of mint.

  1. You will need to implement the mintWithFinancing hook to execute a new mint and financing loan in the same transaction.

Mint Financing Option on Trending Screen

Show details of financing

Purchase confirmation and additional loan information