// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.4;
import "../interfaces/IBundleBuilder.sol";
import "../loans/direct/loanTypes/LoanData.sol";
import "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol";
/**
* @title NFTfiSigningUtils
* @author NFTfi
* @notice Helper contract for NFTfi. This contract manages verifying signatures from off-chain NFTfi orders.
* Based on the version of this same contract used on NFTfi V1
*/
library NFTfiSigningUtils {
/* ********* */
/* FUNCTIONS */
/* ********* */
/**
* @dev This function gets the current chain ID.
*/
function getChainID() public view returns (uint256) {
uint256 id;
// solhint-disable-next-line no-inline-assembly
assembly {
id := chainid()
}
return id;
}
/**
* @notice This function is when the lender accepts a borrower's binding listing terms, to validate the lender's
* signature that the borrower provided off-chain to verify that it did indeed made such listing.
*
* @param _listingTerms - The listing terms struct containing:
* - loanERC20Denomination: The address of the ERC20 contract of the currency being used as principal/interest
* for this loan.
* - minLoanPrincipalAmount: The minumum sum of money transferred from lender to borrower at the beginning of
* the loan, measured in loanERC20Denomination's smallest units.
* - maxLoanPrincipalAmount: The sum of money transferred from lender to borrower at the beginning of
* the loan, measured in loanERC20Denomination's smallest units.
* - maximumRepaymentAmount: The maximum amount of money that the borrower would be required to retrieve their
* collateral, measured in the smallest units of the ERC20 currency used for the loan. The borrower will always have
* to pay this amount to retrieve their collateral, regardless of whether they repay early.
* - nftCollateralContract: The address of the ERC721 contract of the NFT collateral.
* - nftCollateralId: The ID within the NFTCollateralContract for the NFT being used as collateral for this
* loan. The NFT is stored within this contract during the duration of the loan.
* - revenueSharePartner: The address of the partner that will receive the revenue share.
* - minLoanDuration: The minumum amount of time (measured in seconds) that can elapse before the lender can
* liquidate the loan and seize the underlying collateral NFT.
* - maxLoanDuration: The maximum amount of time (measured in seconds) that can elapse before the lender can
* liquidate the loan and seize the underlying collateral NFT.
* - maxInterestRateForDurationInBasisPoints: This is maximum the interest rate (measured in basis points, e.g.
* hundreths of a percent) for the loan, that must be repaid pro-rata by the borrower at the conclusion of the loan
* or risk seizure of their nft collateral. Note if the type of the loan is fixed then this value is not used and
* is irrelevant so it should be set to 0.
* - referralFeeInBasisPoints: The percent (measured in basis points) of the loan principal amount that will be
* taken as a fee to pay to the referrer, 0 if the lender is not paying referral fee.
* @param _signature - The offer struct containing:
* - signer: The address of the signer. The borrower for `acceptOffer` the lender for `acceptListing`.
* - nonce: The nonce referred here is not the same as an Ethereum account's nonce.
* We are referring instead to a nonce that is used by the lender or the borrower when they are first signing
* off-chain NFTfi orders. These nonce can be any uint256 value that the user has not previously used to sign an
* off-chain order. Each nonce can be used at most once per user within NFTfi, regardless of whether they are the
* lender or the borrower in that situation. This serves two purposes:
* - First, it prevents replay attacks where an attacker would submit a user's off-chain order more than once.
* - Second, it allows a user to cancel an off-chain order by calling
* NFTfi.cancelLoanCommitmentBeforeLoanHasBegun(), which marks the nonce as used and prevents any future loan from
* using the user's off-chain order that contains that nonce.
* - expiry: Date when the signature expires
* - signature: The ECDSA signature of the borrower, obtained off-chain ahead of time, signing the following
* combination of parameters:
* - listingTerms.loanERC20Denomination,
* - listingTerms.minLoanPrincipalAmount,
* - listingTerms.maxLoanPrincipalAmount,
* - listingTerms.nftCollateralContract,
* - listingTerms.nftCollateralId,
* - listingTerms.revenueSharePartner,
* - listingTerms.minLoanDuration,
* - listingTerms.maxLoanDuration,
* - listingTerms.maxInterestRateForDurationInBasisPoints,
* - listingTerms.referralFeeInBasisPoints,
* - signature.signer,
* - signature.nonce,
* - signature.expiry,
* - address of this contract
* - chainId
*/
function isValidBorrowerSignature(LoanData.ListingTerms memory _listingTerms, LoanData.Signature memory _signature)
external
view
returns (bool)
{
return isValidBorrowerSignature(_listingTerms, _signature, address(this));
}
/**
* @dev This function overload the previous function to allow the caller to specify the address of the contract
*
*/
function isValidBorrowerSignature(
LoanData.ListingTerms memory _listingTerms,
LoanData.Signature memory _signature,
address _loanContract
) public view returns (bool) {
require(block.timestamp <= _signature.expiry, "Borrower Signature has expired");
require(_loanContract != address(0), "Loan is zero address");
if (_signature.signer == address(0)) {
return false;
} else {
bytes32 message = keccak256(
abi.encodePacked(
getEncodedListing(_listingTerms),
getEncodedSignature(_signature),
_loanContract,
getChainID()
)
);
return
SignatureChecker.isValidSignatureNow(
_signature.signer,
ECDSA.toEthSignedMessageHash(message),
_signature.signature
);
}
}
/**
* @notice This function is when the lender accepts a borrower's binding listing terms, to validate the lender's
* signature that the borrower provided off-chain to verify that it did indeed made such listing.
*
* @param _listingTerms - The listing terms struct containing:
* - loanERC20Denomination: The address of the ERC20 contract of the currency being used as principal/interest
* for this loan.
* - minLoanPrincipalAmount: The minumum sum of money transferred from lender to borrower at the beginning of
* the loan, measured in loanERC20Denomination's smallest units.
* - maxLoanPrincipalAmount: The sum of money transferred from lender to borrower at the beginning of
* the loan, measured in loanERC20Denomination's smallest units.
* - maximumRepaymentAmount: The maximum amount of money that the borrower would be required to retrieve their
* collateral, measured in the smallest units of the ERC20 currency used for the loan. The borrower will always have
* to pay this amount to retrieve their collateral, regardless of whether they repay early.
* - nftCollateralContract: The address of the ERC721 contract of the NFT collateral.
* - nftCollateralId: The ID within the NFTCollateralContract for the NFT being used as collateral for this
* loan. The NFT is stored within this contract during the duration of the loan.
* - revenueSharePartner: The address of the partner that will receive the revenue share.
* - minLoanDuration: The minumum amount of time (measured in seconds) that can elapse before the lender can
* liquidate the loan and seize the underlying collateral NFT.
* - maxLoanDuration: The maximum amount of time (measured in seconds) that can elapse before the lender can
* liquidate the loan and seize the underlying collateral NFT.
* - maxInterestRateForDurationInBasisPoints: This is maximum the interest rate (measured in basis points, e.g.
* hundreths of a percent) for the loan, that must be repaid pro-rata by the borrower at the conclusion of the loan
* or risk seizure of their nft collateral. Note if the type of the loan is fixed then this value is not used and
* is irrelevant so it should be set to 0.
* - referralFeeInBasisPoints: The percent (measured in basis points) of the loan principal amount that will be
* taken as a fee to pay to the referrer, 0 if the lender is not paying referral fee.
* @param _bundleElements - the lists of erc721-20-1155 tokens that are to be bundled
* @param _signature - The offer struct containing:
* - signer: The address of the signer. The borrower for `acceptOffer` the lender for `acceptListing`.
* - nonce: The nonce referred here is not the same as an Ethereum account's nonce.
* We are referring instead to a nonce that is used by the lender or the borrower when they are first signing
* off-chain NFTfi orders. These nonce can be any uint256 value that the user has not previously used to sign an
* off-chain order. Each nonce can be used at most once per user within NFTfi, regardless of whether they are the
* lender or the borrower in that situation. This serves two purposes:
* - First, it prevents replay attacks where an attacker would submit a user's off-chain order more than once.
* - Second, it allows a user to cancel an off-chain order by calling
* NFTfi.cancelLoanCommitmentBeforeLoanHasBegun(), which marks the nonce as used and prevents any future loan from
* using the user's off-chain order that contains that nonce.
* - expiry: Date when the signature expires
* - signature: The ECDSA signature of the borrower, obtained off-chain ahead of time, signing the following
* combination of parameters:
* - listingTerms.loanERC20Denomination,
* - listingTerms.minLoanPrincipalAmount,
* - listingTerms.maxLoanPrincipalAmount,
* - listingTerms.nftCollateralContract,
* - listingTerms.nftCollateralId,
* - listingTerms.revenueSharePartner,
* - listingTerms.minLoanDuration,
* - listingTerms.maxLoanDuration,
* - listingTerms.maxInterestRateForDurationInBasisPoints,
* - listingTerms.referralFeeInBasisPoints,
* - bundleElements
* - signature.signer,
* - signature.nonce,
* - signature.expiry,
* - address of this contract
* - chainId
*/
function isValidBorrowerSignatureBundle(
LoanData.ListingTerms memory _listingTerms,
IBundleBuilder.BundleElements memory _bundleElements,
LoanData.Signature memory _signature
) external view returns (bool) {
return isValidBorrowerSignatureBundle(_listingTerms, _bundleElements, _signature, address(this));
}
/**
* @dev This function overload the previous function to allow the caller to specify the address of the contract
*
*/
function isValidBorrowerSignatureBundle(
LoanData.ListingTerms memory _listingTerms,
IBundleBuilder.BundleElements memory _bundleElements,
LoanData.Signature memory _signature,
address _loanContract
) public view returns (bool) {
require(block.timestamp <= _signature.expiry, "Borrower Signature has expired");
require(_loanContract != address(0), "Loan is zero address");
if (_signature.signer == address(0)) {
return false;
} else {
bytes32 message = keccak256(
abi.encodePacked(
getEncodedListing(_listingTerms),
abi.encode(_bundleElements),
getEncodedSignature(_signature),
_loanContract,
getChainID()
)
);
return
SignatureChecker.isValidSignatureNow(
_signature.signer,
ECDSA.toEthSignedMessageHash(message),
_signature.signature
);
}
}
/**
* @notice This function is when the borrower accepts a lender's offer, to validate the lender's signature that the
* lender provided off-chain to verify that it did indeed made such offer.
*
* @param _offer - The offer struct containing:
* - loanERC20Denomination: The address of the ERC20 contract of the currency being used as principal/interest
* for this loan.
* - loanPrincipalAmount: The original sum of money transferred from lender to borrower at the beginning of
* the loan, measured in loanERC20Denomination's smallest units.
* - maximumRepaymentAmount: The maximum amount of money that the borrower would be required to retrieve their
* collateral, measured in the smallest units of the ERC20 currency used for the loan. The borrower will always have
* to pay this amount to retrieve their collateral, regardless of whether they repay early.
* - nftCollateralContract: The address of the ERC721 contract of the NFT collateral.
* - nftCollateralId: The ID within the NFTCollateralContract for the NFT being used as collateral for this
* loan. The NFT is stored within this contract during the duration of the loan.
* - referrer: The address of the referrer who found the lender matching the listing, Zero address to signal
* this there is no referrer.
* - loanDuration: The amount of time (measured in seconds) that can elapse before the lender can liquidate the
* loan and seize the underlying collateral NFT.
* - loanInterestRateForDurationInBasisPoints: This is the interest rate (measured in basis points, e.g.
* hundreths of a percent) for the loan, that must be repaid pro-rata by the borrower at the conclusion of the loan
* or risk seizure of their nft collateral. Note if the type of the loan is fixed then this value is not used and
* is irrelevant so it should be set to 0.
* - loanAdminFeeInBasisPoints: The percent (measured in basis points) of the interest earned that will be
* taken as a fee by the contract admins when the loan is repaid. The fee is stored in the loan struct to prevent an
* attack where the contract admins could adjust the fee right before a loan is repaid, and take all of the interest
* earned.
* @param _signature - The signature structure containing:
* - signer: The address of the signer. The borrower for `acceptOffer` the lender for `acceptListing`.
* - nonce: The nonce referred here is not the same as an Ethereum account's nonce.
* We are referring instead to a nonce that is used by the lender or the borrower when they are first signing
* off-chain NFTfi orders. These nonce can be any uint256 value that the user has not previously used to sign an
* off-chain order. Each nonce can be used at most once per user within NFTfi, regardless of whether they are the
* lender or the borrower in that situation. This serves two purposes:
* - First, it prevents replay attacks where an attacker would submit a user's off-chain order more than once.
* - Second, it allows a user to cancel an off-chain order by calling
* NFTfi.cancelLoanCommitmentBeforeLoanHasBegun(), which marks the nonce as used and prevents any future loan from
* using the user's off-chain order that contains that nonce.
* - expiry: Date when the signature expires
* - signature: The ECDSA signature of the lender, obtained off-chain ahead of time, signing the following
* combination of parameters:
* - offer.loanERC20Denomination
* - offer.loanPrincipalAmount
* - offer.maximumRepaymentAmount
* - offer.nftCollateralContract
* - offer.nftCollateralId
* - offer.referrer
* - offer.loanDuration
* - offer.loanAdminFeeInBasisPoints
* - signature.signer,
* - signature.nonce,
* - signature.expiry,
* - address of this contract
* - chainId
*/
function isValidLenderSignature(LoanData.Offer memory _offer, LoanData.Signature memory _signature)
external
view
returns (bool)
{
return isValidLenderSignature(_offer, _signature, address(this));
}
/**
* @dev This function overload the previous function to allow the caller to specify the address of the contract
*
*/
function isValidLenderSignature(
LoanData.Offer memory _offer,
LoanData.Signature memory _signature,
address _loanContract
) public view returns (bool) {
require(block.timestamp <= _signature.expiry, "Lender Signature has expired");
require(_loanContract != address(0), "Loan is zero address");
if (_signature.signer == address(0)) {
return false;
} else {
bytes32 message = keccak256(
abi.encodePacked(getEncodedOffer(_offer), getEncodedSignature(_signature), _loanContract, getChainID())
);
return
SignatureChecker.isValidSignatureNow(
_signature.signer,
ECDSA.toEthSignedMessageHash(message),
_signature.signature
);
}
}
/**
* @notice This function is when the borrower accepts a lender's offer, to validate the lender's signature that the
* lender provided off-chain to verify that it did indeed made such offer.
*
* @param _offer - The offer struct containing:
* - loanERC20Denomination: The address of the ERC20 contract of the currency being used as principal/interest
* for this loan.
* - loanPrincipalAmount: The original sum of money transferred from lender to borrower at the beginning of
* the loan, measured in loanERC20Denomination's smallest units.
* - maximumRepaymentAmount: The maximum amount of money that the borrower would be required to retrieve their
* collateral, measured in the smallest units of the ERC20 currency used for the loan. The borrower will always have
* to pay this amount to retrieve their collateral, regardless of whether they repay early.
* - nftCollateralContract: The address of the ERC721 contract of the NFT collateral.
* - nftCollateralId: The ID within the NFTCollateralContract for the NFT being used as collateral for this
* loan. The NFT is stored within this contract during the duration of the loan.
* - referrer: The address of the referrer who found the lender matching the listing, Zero address to signal
* this there is no referrer.
* - loanDuration: The amount of time (measured in seconds) that can elapse before the lender can liquidate the
* loan and seize the underlying collateral NFT.
* - loanInterestRateForDurationInBasisPoints: This is the interest rate (measured in basis points, e.g.
* hundreths of a percent) for the loan, that must be repaid pro-rata by the borrower at the conclusion of the loan
* or risk seizure of their nft collateral. Note if the type of the loan is fixed then this value is not used and
* is irrelevant so it should be set to 0.
* - loanAdminFeeInBasisPoints: The percent (measured in basis points) of the interest earned that will be
* taken as a fee by the contract admins when the loan is repaid. The fee is stored in the loan struct to prevent an
* attack where the contract admins could adjust the fee right before a loan is repaid, and take all of the interest
* earned.
* @param _bundleElements - the lists of erc721-20-1155 tokens that are to be bundled
* @param _signature - The signature structure containing:
* - signer: The address of the signer. The borrower for `acceptOffer` the lender for `acceptListing`.
* - nonce: The nonce referred here is not the same as an Ethereum account's nonce.
* We are referring instead to a nonce that is used by the lender or the borrower when they are first signing
* off-chain NFTfi orders. These nonce can be any uint256 value that the user has not previously used to sign an
* off-chain order. Each nonce can be used at most once per user within NFTfi, regardless of whether they are the
* lender or the borrower in that situation. This serves two purposes:
* - First, it prevents replay attacks where an attacker would submit a user's off-chain order more than once.
* - Second, it allows a user to cancel an off-chain order by calling
* NFTfi.cancelLoanCommitmentBeforeLoanHasBegun(), which marks the nonce as used and prevents any future loan from
* using the user's off-chain order that contains that nonce.
* - expiry: Date when the signature expires
* - signature: The ECDSA signature of the lender, obtained off-chain ahead of time, signing the following
* combination of parameters:
* - offer.loanERC20Denomination
* - offer.loanPrincipalAmount
* - offer.maximumRepaymentAmount
* - offer.nftCollateralContract
* - offer.nftCollateralId
* - offer.referrer
* - offer.loanDuration
* - offer.loanAdminFeeInBasisPoints
* - bundleElements
* - signature.signer,
* - signature.nonce,
* - signature.expiry,
* - address of this contract
* - chainId
*/
function isValidLenderSignatureBundle(
LoanData.Offer memory _offer,
IBundleBuilder.BundleElements memory _bundleElements,
LoanData.Signature memory _signature
) external view returns (bool) {
return isValidLenderSignatureBundle(_offer, _bundleElements, _signature, address(this));
}
/**
* @dev This function overload the previous function to allow the caller to specify the address of the contract
*
*/
function isValidLenderSignatureBundle(
LoanData.Offer memory _offer,
IBundleBuilder.BundleElements memory _bundleElements,
LoanData.Signature memory _signature,
address _loanContract
) public view returns (bool) {
require(block.timestamp <= _signature.expiry, "Lender Signature has expired");
require(_loanContract != address(0), "Loan is zero address");
if (_signature.signer == address(0)) {
return false;
} else {
bytes32 message = keccak256(
abi.encodePacked(
getEncodedOffer(_offer),
abi.encode(_bundleElements),
getEncodedSignature(_signature),
_loanContract,
getChainID()
)
);
return
SignatureChecker.isValidSignatureNow(
_signature.signer,
ECDSA.toEthSignedMessageHash(message),
_signature.signature
);
}
}
/**
* @notice This function is called in renegotiateLoan() to validate the lender's signature that the lender provided
* off-chain to verify that they did indeed want to agree to this loan renegotiation according to these terms.
*
* @param _loanId - The unique identifier for the loan to be renegotiated
* @param _newLoanDuration - The new amount of time (measured in seconds) that can elapse before the lender can
* liquidate the loan and seize the underlying collateral NFT.
* @param _newMaximumRepaymentAmount - The new maximum amount of money that the borrower would be required to
* retrieve their collateral, measured in the smallest units of the ERC20 currency used for the loan. The
* borrower will always have to pay this amount to retrieve their collateral, regardless of whether they repay
* early.
* @param _renegotiationFee Agreed upon fee in ether that borrower pays for the lender for the renegitiation
* @param _signature - The signature structure containing:
* - signer: The address of the signer. The borrower for `acceptOffer` the lender for `acceptListing`.
* - nonce: The nonce referred here is not the same as an Ethereum account's nonce.
* We are referring instead to a nonce that is used by the lender or the borrower when they are first signing
* off-chain NFTfi orders. These nonce can be any uint256 value that the user has not previously used to sign an
* off-chain order. Each nonce can be used at most once per user within NFTfi, regardless of whether they are the
* lender or the borrower in that situation. This serves two purposes:
* - First, it prevents replay attacks where an attacker would submit a user's off-chain order more than once.
* - Second, it allows a user to cancel an off-chain order by calling NFTfi.cancelLoanCommitmentBeforeLoanHasBegun()
* , which marks the nonce as used and prevents any future loan from using the user's off-chain order that contains
* that nonce.
* - expiry - The date when the renegotiation offer expires
* - lenderSignature - The ECDSA signature of the lender, obtained off-chain ahead of time, signing the
* following combination of parameters:
* - _loanId
* - _newLoanDuration
* - _newMaximumRepaymentAmount
* - _lender
* - _lenderNonce
* - _expiry
* - address of this contract
* - chainId
*/
function isValidLenderRenegotiationSignature(
uint256 _loanId,
uint32 _newLoanDuration,
uint256 _newMaximumRepaymentAmount,
uint256 _renegotiationFee,
LoanData.Signature memory _signature
) external view returns (bool) {
return
isValidLenderRenegotiationSignature(
_loanId,
_newLoanDuration,
_newMaximumRepaymentAmount,
_renegotiationFee,
_signature,
address(this)
);
}
/**
* @dev This function overload the previous function to allow the caller to specify the address of the contract
*
*/
function isValidLenderRenegotiationSignature(
uint256 _loanId,
uint32 _newLoanDuration,
uint256 _newMaximumRepaymentAmount,
uint256 _renegotiationFee,
LoanData.Signature memory _signature,
address _loanContract
) public view returns (bool) {
require(block.timestamp <= _signature.expiry, "Renegotiation Signature has expired");
require(_loanContract != address(0), "Loan is zero address");
if (_signature.signer == address(0)) {
return false;
} else {
bytes32 message = keccak256(
abi.encodePacked(
_loanId,
_newLoanDuration,
_newMaximumRepaymentAmount,
_renegotiationFee,
getEncodedSignature(_signature),
_loanContract,
getChainID()
)
);
return
SignatureChecker.isValidSignatureNow(
_signature.signer,
ECDSA.toEthSignedMessageHash(message),
_signature.signature
);
}
}
/**
* @dev We need this to avoid stack too deep errors.
*/
function getEncodedListing(LoanData.ListingTerms memory _listingTerms) internal pure returns (bytes memory) {
return
abi.encodePacked(
_listingTerms.loanERC20Denomination,
_listingTerms.minLoanPrincipalAmount,
_listingTerms.maxLoanPrincipalAmount,
_listingTerms.nftCollateralContract,
_listingTerms.nftCollateralId,
_listingTerms.revenueSharePartner,
_listingTerms.minLoanDuration,
_listingTerms.maxLoanDuration,
_listingTerms.maxInterestRateForDurationInBasisPoints,
_listingTerms.referralFeeInBasisPoints
);
}
/**
* @dev We need this to avoid stack too deep errors.
*/
function getEncodedOffer(LoanData.Offer memory _offer) internal pure returns (bytes memory) {
return
abi.encodePacked(
_offer.loanERC20Denomination,
_offer.loanPrincipalAmount,
_offer.maximumRepaymentAmount,
_offer.nftCollateralContract,
_offer.nftCollateralId,
_offer.referrer,
_offer.loanDuration,
_offer.loanAdminFeeInBasisPoints
);
}
/**
* @dev We need this to avoid stack too deep errors.
*/
function getEncodedSignature(LoanData.Signature memory _signature) internal pure returns (bytes memory) {
return abi.encodePacked(_signature.signer, _signature.nonce, _signature.expiry);
}
}