// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.4;
import "./DirectLoanBaseMinimal.sol";
import "../../../utils/ContractKeys.sol";
/**
* @title DirectLoanFixedOffer
* @author NFTfi
* @notice Main contract for NFTfi Direct Loans Fixed Type. This contract manages the ability to create NFT-backed
* peer-to-peer loans of type Fixed (agreed to be a fixed-repayment loan) where the borrower pays the
* maximumRepaymentAmount regardless of whether they repay early or not.
*
* There are two ways to commence an NFT-backed loan:
*
* a. The borrower accepts a lender's offer by calling `acceptOffer`.
* 1. the borrower calls nftContract.approveAll(NFTfi), approving the NFTfi contract to move their NFT's on their
* be1alf.
* 2. the lender calls erc20Contract.approve(NFTfi), allowing NFTfi to move the lender's ERC20 tokens on their
* behalf.
* 3. the lender signs an off-chain message, proposing its offer terms.
* 4. the borrower calls `acceptOffer` to accept these terms and enter into the loan. The NFT is stored in
* the contract, the borrower receives the loan principal in the specified ERC20 currency, the lender receives an
* NFTfi promissory note (in ERC721 form) that represents the rights to either the principal-plus-interest, or the
* underlying NFT collateral if the borrower does not pay back in time, and the borrower receives obligation receipt
* (in ERC721 form) that gives them the right to pay back the loan and get the collateral back.
*
* b. The lender accepts a borrowe's binding terms by calling `acceptListing`.
* 1. the borrower calls nftContract.approveAll(NFTfi), approving the NFTfi contract to move their NFT's on their
* be1alf.
* 2. the lender calls erc20Contract.approve(NFTfi), allowing NFTfi to move the lender's ERC20 tokens on their
* behalf.
* 3. the borrower signs an off-chain message, proposing its binding terms.
* 4. the lender calls `acceptListing` with an offer matching the binding terms and enter into the loan. The NFT is
* stored in the contract, the borrower receives the loan principal in the specified ERC20 currency, the lender
* receives an NFTfi promissory note (in ERC721 form) that represents the rights to either the principal-plus-interest,
* or the underlying NFT collateral if the borrower does not pay back in time, and the borrower receives obligation
* receipt (in ERC721 form) that gives them the right to pay back the loan and get the collateral back.
*
* The lender can freely transfer and trade this ERC721 promissory note as they wish, with the knowledge that
* transferring the ERC721 promissory note tranfsers the rights to principal-plus-interest and/or collateral, and that
* they will no longer have a claim on the loan. The ERC721 promissory note itself represents that claim.
*
* The borrower can freely transfer and trade this ERC721 obligaiton receipt as they wish, with the knowledge that
* transferring the ERC721 obligaiton receipt tranfsers the rights right to pay back the loan and get the collateral
* back.
*
*
* A loan may end in one of two ways:
* - First, a borrower may call NFTfi.payBackLoan() and pay back the loan plus interest at any time, in which case they
* receive their NFT back in the same transaction.
* - Second, if the loan's duration has passed and the loan has not been paid back yet, a lender can call
* NFTfi.liquidateOverdueLoan(), in which case they receive the underlying NFT collateral and forfeit the rights to the
* principal-plus-interest, which the borrower now keeps.
*/
contract DirectLoanFixedOffer is DirectLoanBaseMinimal {
/* *********** */
/* CONSTRUCTOR */
/* *********** */
/**
* @dev Sets `hub` and permitted erc20-s
*
* @param _admin - Initial admin of this contract.
* @param _nftfiHub - NFTfiHub address
* @param _permittedErc20s - list of permitted ERC20 token contract addresses
*/
constructor(
address _admin,
address _nftfiHub,
address[] memory _permittedErc20s
)
DirectLoanBaseMinimal(
_admin,
_nftfiHub,
ContractKeys.getIdFromStringKey("DIRECT_LOAN_COORDINATOR"),
_permittedErc20s
)
{
// solhint-disable-previous-line no-empty-blocks
}
/* ********* */
/* FUNCTIONS */
/* ********* */
/**
* @notice This function is called by the borrower when accepting a lender's offer to begin a loan.
*
* @param _offer - The offer made by the lender.
* @param _signature - The components of the lender's signature.
* @param _borrowerSettings - Some extra parameters that the borrower needs to set when accepting an offer.
*/
function acceptOffer(
Offer memory _offer,
Signature memory _signature,
BorrowerSettings memory _borrowerSettings
) external whenNotPaused nonReentrant {
address nftWrapper = _getWrapper(_offer.nftCollateralContract);
_loanSanityChecks(_offer, nftWrapper);
_loanSanityChecksOffer(_offer);
_acceptOffer(
_setupLoanTerms(_offer, nftWrapper),
_setupLoanExtras(_borrowerSettings.revenueSharePartner, _borrowerSettings.referralFeeInBasisPoints),
_offer,
_signature
);
}
/* ******************* */
/* READ-ONLY FUNCTIONS */
/* ******************* */
/**
* @notice This function returns a bytes32 value identifying the loan type for the coordinator
*/
// all caps, because used to be a constant storage and the interface should be the same
// solhint-disable-next-line func-name-mixedcase
function LOAN_TYPE() public pure virtual returns (bytes32) {
return bytes32("DIRECT_LOAN_FIXED_OFFER");
}
/**
* @notice This function can be used to view the current quantity of the ERC20 currency used in the specified loan
* required by the borrower to repay their loan, measured in the smallest unit of the ERC20 currency.
*
* @param _loanId A unique identifier for this particular loan, sourced from the Loan Coordinator.
*
* @return The amount of the specified ERC20 currency required to pay back this loan, measured in the smallest unit
* of the specified ERC20 currency.
*/
function getPayoffAmount(uint32 _loanId) external view override returns (uint256) {
LoanTerms storage loan = loanIdToLoan[_loanId];
return loan.maximumRepaymentAmount;
}
/* ****************** */
/* INTERNAL FUNCTIONS */
/* ****************** */
/**
* @notice This function is called by the borrower when accepting a lender's offer to begin a loan.
*
* @param _loanTerms - The main Loan Terms struct. This data is saved upon loan creation on loanIdToLoan.
* @param _loanExtras - The main Loan Terms struct. This data is saved upon loan creation on loanIdToLoanExtras.
* @param _offer - The offer made by the lender.
* @param _signature - The components of the lender's signature.
*/
function _acceptOffer(
LoanTerms memory _loanTerms,
LoanExtras memory _loanExtras,
Offer memory _offer,
Signature memory _signature
) internal virtual {
// Check loan nonces. These are different from Ethereum account nonces.
// Here, these are uint256 numbers that should uniquely identify
// each signature for each user (i.e. each user should only create one
// off-chain signature for each nonce, with a nonce being any arbitrary
// uint256 value that they have not used yet for an off-chain NFTfi
// signature).
require(!_nonceHasBeenUsedForUser[_signature.signer][_signature.nonce], "Lender nonce invalid");
_nonceHasBeenUsedForUser[_signature.signer][_signature.nonce] = true;
require(NFTfiSigningUtils.isValidLenderSignature(_offer, _signature), "Lender signature is invalid");
address bundle = hub.getContract(ContractKeys.NFTFI_BUNDLER);
require(_loanTerms.nftCollateralContract != bundle, "Collateral cannot be bundle");
uint32 loanId = _createLoan(
LOAN_TYPE(),
_loanTerms,
_loanExtras,
msg.sender,
_signature.signer,
_offer.referrer
);
// Emit an event with all relevant details from this transaction.
emit LoanStarted(loanId, msg.sender, _signature.signer, _loanTerms, _loanExtras);
}
/**
* @dev Creates a `LoanTerms` struct using data sent as the lender's `_offer` on `acceptOffer`.
* This is needed in order to avoid stack too deep issues.
* Since this is a Fixed loan type loanInterestRateForDurationInBasisPoints is ignored.
*/
function _setupLoanTerms(Offer memory _offer, address _nftWrapper) internal view returns (LoanTerms memory) {
return
LoanTerms({
loanERC20Denomination: _offer.loanERC20Denomination,
loanPrincipalAmount: _offer.loanPrincipalAmount,
maximumRepaymentAmount: _offer.maximumRepaymentAmount,
nftCollateralContract: _offer.nftCollateralContract,
nftCollateralWrapper: _nftWrapper,
nftCollateralId: _offer.nftCollateralId,
loanStartTime: uint64(block.timestamp),
loanDuration: _offer.loanDuration,
loanInterestRateForDurationInBasisPoints: uint16(0),
loanAdminFeeInBasisPoints: _offer.loanAdminFeeInBasisPoints,
borrower: msg.sender
});
}
/**
* @dev Calculates the payoff amount and admin fee
*
* @param _loanTerms - Struct containing all the loan's parameters
*/
function _payoffAndFee(LoanTerms memory _loanTerms)
internal
pure
override
returns (uint256 adminFee, uint256 payoffAmount)
{
// Calculate amounts to send to lender and admins
uint256 interestDue = _loanTerms.maximumRepaymentAmount - _loanTerms.loanPrincipalAmount;
adminFee = LoanChecksAndCalculations.computeAdminFee(
interestDue,
uint256(_loanTerms.loanAdminFeeInBasisPoints)
);
payoffAmount = _loanTerms.maximumRepaymentAmount - adminFee;
}
/**
* @dev Function that performs some validation checks over loan parameters when accepting an offer
*
*/
function _loanSanityChecksOffer(LoanData.Offer memory _offer) internal pure {
require(
_offer.maximumRepaymentAmount >= _offer.loanPrincipalAmount,
"Negative interest rate loans are not allowed."
);
}
}