// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/**
* @dev Collection of functions related to the address type
*/
library Address {
/**
* @dev Returns true if `account` is a contract.
*
* [IMPORTANT]
* ====
* It is unsafe to assume that an address for which this function returns
* false is an externally-owned account (EOA) and not a contract.
*
* Among others, `isContract` will return false for the following
* types of addresses:
*
* - an externally-owned account
* - a contract in construction
* - an address where a contract will be created
* - an address where a contract lived, but was destroyed
* ====
*/
function isContract(address account) internal view returns (bool) {
// This method relies on extcodesize, which returns 0 for contracts in
// construction, since the code is only stored at the end of the
// constructor execution.
uint256 size;
assembly {
size := extcodesize(account)
}
return size > 0;
}
/**
* @dev Replacement for Solidity's `transfer`: sends `amount` wei to
* `recipient`, forwarding all available gas and reverting on errors.
*
* https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost
* of certain opcodes, possibly making contracts go over the 2300 gas limit
* imposed by `transfer`, making them unable to receive funds via
* `transfer`. {sendValue} removes this limitation.
*
* https://diligence.consensys.net/posts/2019/09/stop-using-soliditys-transfer-now/[Learn more].
*
* IMPORTANT: because control is transferred to `recipient`, care must be
* taken to not create reentrancy vulnerabilities. Consider using
* {ReentrancyGuard} or the
* https://solidity.readthedocs.io/en/v0.5.11/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern].
*/
function sendValue(address payable recipient, uint256 amount) internal {
require(address(this).balance >= amount, "Address: insufficient balance");
(bool success, ) = recipient.call{value: amount}("");
require(success, "Address: unable to send value, recipient may have reverted");
}
/**
* @dev Performs a Solidity function call using a low level `call`. A
* plain `call` is an unsafe replacement for a function call: use this
* function instead.
*
* If `target` reverts with a revert reason, it is bubbled up by this
* function (like regular Solidity function calls).
*
* Returns the raw returned data. To convert to the expected return value,
* use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`].
*
* Requirements:
*
* - `target` must be a contract.
* - calling `target` with `data` must not revert.
*
* _Available since v3.1._
*/
function functionCall(address target, bytes memory data) internal returns (bytes memory) {
return functionCall(target, data, "Address: low-level call failed");
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with
* `errorMessage` as a fallback revert reason when `target` reverts.
*
* _Available since v3.1._
*/
function functionCall(
address target,
bytes memory data,
string memory errorMessage
) internal returns (bytes memory) {
return functionCallWithValue(target, data, 0, errorMessage);
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
* but also transferring `value` wei to `target`.
*
* Requirements:
*
* - the calling contract must have an ETH balance of at least `value`.
* - the called Solidity function must be `payable`.
*
* _Available since v3.1._
*/
function functionCallWithValue(
address target,
bytes memory data,
uint256 value
) internal returns (bytes memory) {
return functionCallWithValue(target, data, value, "Address: low-level call with value failed");
}
/**
* @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but
* with `errorMessage` as a fallback revert reason when `target` reverts.
*
* _Available since v3.1._
*/
function functionCallWithValue(
address target,
bytes memory data,
uint256 value,
string memory errorMessage
) internal returns (bytes memory) {
require(address(this).balance >= value, "Address: insufficient balance for call");
require(isContract(target), "Address: call to non-contract");
(bool success, bytes memory returndata) = target.call{value: value}(data);
return verifyCallResult(success, returndata, errorMessage);
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
* but performing a static call.
*
* _Available since v3.3._
*/
function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) {
return functionStaticCall(target, data, "Address: low-level static call failed");
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`],
* but performing a static call.
*
* _Available since v3.3._
*/
function functionStaticCall(
address target,
bytes memory data,
string memory errorMessage
) internal view returns (bytes memory) {
require(isContract(target), "Address: static call to non-contract");
(bool success, bytes memory returndata) = target.staticcall(data);
return verifyCallResult(success, returndata, errorMessage);
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
* but performing a delegate call.
*
* _Available since v3.4._
*/
function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) {
return functionDelegateCall(target, data, "Address: low-level delegate call failed");
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`],
* but performing a delegate call.
*
* _Available since v3.4._
*/
function functionDelegateCall(
address target,
bytes memory data,
string memory errorMessage
) internal returns (bytes memory) {
require(isContract(target), "Address: delegate call to non-contract");
(bool success, bytes memory returndata) = target.delegatecall(data);
return verifyCallResult(success, returndata, errorMessage);
}
/**
* @dev Tool to verifies that a low level call was successful, and revert if it wasn't, either by bubbling the
* revert reason using the provided one.
*
* _Available since v4.3._
*/
function verifyCallResult(
bool success,
bytes memory returndata,
string memory errorMessage
) internal pure returns (bytes memory) {
if (success) {
return returndata;
} else {
// Look for revert reason and bubble it up if present
if (returndata.length > 0) {
// The easiest way to bubble the revert reason is using memory via assembly
assembly {
let returndata_size := mload(returndata)
revert(add(32, returndata), returndata_size)
}
} else {
revert(errorMessage);
}
}
}
}
// SPDX-License-Identifier: MIT
pragma solidity 0.8.18;
/**
* @title FeeLookups
* @author Non-Fungible Technologies, Inc.
*
* Enumerates unique identifiers for fee identifiers
* that the lending protocol uses.
*/
abstract contract FeeLookups {
/// @dev Origination fees: amount in bps, payable in loan token
bytes32 public constant FL_01 = keccak256("BORROWER_ORIGINATION_FEE");
bytes32 public constant FL_02 = keccak256("LENDER_ORIGINATION_FEE");
/// @dev Rollover fees: amount in bps, payable in loan token
bytes32 public constant FL_03 = keccak256("BORROWER_ROLLOVER_FEE");
bytes32 public constant FL_04 = keccak256("LENDER_ROLLOVER_FEE");
/// @dev Loan closure fees: amount in bps, payable in loan token
bytes32 public constant FL_05 = keccak256("LENDER_DEFAULT_FEE");
bytes32 public constant FL_06 = keccak256("LENDER_INTEREST_FEE");
bytes32 public constant FL_07 = keccak256("LENDER_PRINCIPAL_FEE");
bytes32 public constant FL_08 = keccak256("LENDER_REDEEM_FEE");
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/**
* @dev Interface of the ERC165 standard, as defined in the
* https://eips.ethereum.org/EIPS/eip-165[EIP].
*
* Implementers can declare support of contract interfaces, which can then be
* queried by others ({ERC165Checker}).
*
* For an implementation, see {ERC165}.
*/
interface IERC165 {
/**
* @dev Returns true if this contract implements the interface defined by
* `interfaceId`. See the corresponding
* https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section]
* to learn more about how these ids are created.
*
* This function call must use less than 30 000 gas.
*/
function supportsInterface(bytes4 interfaceId) external view returns (bool);
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/**
* @dev Interface of the ERC20 standard as defined in the EIP.
*/
interface IERC20 {
/**
* @dev Returns the amount of tokens in existence.
*/
function totalSupply() external view returns (uint256);
/**
* @dev Returns the amount of tokens owned by `account`.
*/
function balanceOf(address account) external view returns (uint256);
/**
* @dev Moves `amount` tokens from the caller's account to `recipient`.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transfer(address recipient, uint256 amount) external returns (bool);
/**
* @dev Returns the remaining number of tokens that `spender` will be
* allowed to spend on behalf of `owner` through {transferFrom}. This is
* zero by default.
*
* This value changes when {approve} or {transferFrom} are called.
*/
function allowance(address owner, address spender) external view returns (uint256);
/**
* @dev Sets `amount` as the allowance of `spender` over the caller's tokens.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* IMPORTANT: Beware that changing an allowance with this method brings the risk
* that someone may use both the old and the new allowance by unfortunate
* transaction ordering. One possible solution to mitigate this race
* condition is to first reduce the spender's allowance to 0 and set the
* desired value afterwards:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
*
* Emits an {Approval} event.
*/
function approve(address spender, uint256 amount) external returns (bool);
/**
* @dev Moves `amount` tokens from `sender` to `recipient` using the
* allowance mechanism. `amount` is then deducted from the caller's
* allowance.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transferFrom(
address sender,
address recipient,
uint256 amount
) external returns (bool);
/**
* @dev Emitted when `value` tokens are moved from one account (`from`) to
* another (`to`).
*
* Note that `value` may be zero.
*/
event Transfer(address indexed from, address indexed to, uint256 value);
/**
* @dev Emitted when the allowance of a `spender` for an `owner` is set by
* a call to {approve}. `value` is the new allowance.
*/
event Approval(address indexed owner, address indexed spender, uint256 value);
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "../../utils/introspection/IERC165.sol";
/**
* @dev Required interface of an ERC721 compliant contract.
*/
interface IERC721 is IERC165 {
/**
* @dev Emitted when `tokenId` token is transferred from `from` to `to`.
*/
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
/**
* @dev Emitted when `owner` enables `approved` to manage the `tokenId` token.
*/
event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
/**
* @dev Emitted when `owner` enables or disables (`approved`) `operator` to manage all of its assets.
*/
event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
/**
* @dev Returns the number of tokens in ``owner``'s account.
*/
function balanceOf(address owner) external view returns (uint256 balance);
/**
* @dev Returns the owner of the `tokenId` token.
*
* Requirements:
*
* - `tokenId` must exist.
*/
function ownerOf(uint256 tokenId) external view returns (address owner);
/**
* @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients
* are aware of the ERC721 protocol to prevent tokens from being forever locked.
*
* Requirements:
*
* - `from` cannot be the zero address.
* - `to` cannot be the zero address.
* - `tokenId` token must exist and be owned by `from`.
* - If the caller is not `from`, it must be have been allowed to move this token by either {approve} or {setApprovalForAll}.
* - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
*
* Emits a {Transfer} event.
*/
function safeTransferFrom(
address from,
address to,
uint256 tokenId
) external;
/**
* @dev Transfers `tokenId` token from `from` to `to`.
*
* WARNING: Usage of this method is discouraged, use {safeTransferFrom} whenever possible.
*
* Requirements:
*
* - `from` cannot be the zero address.
* - `to` cannot be the zero address.
* - `tokenId` token must be owned by `from`.
* - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}.
*
* Emits a {Transfer} event.
*/
function transferFrom(
address from,
address to,
uint256 tokenId
) external;
/**
* @dev Gives permission to `to` to transfer `tokenId` token to another account.
* The approval is cleared when the token is transferred.
*
* Only a single account can be approved at a time, so approving the zero address clears previous approvals.
*
* Requirements:
*
* - The caller must own the token or be an approved operator.
* - `tokenId` must exist.
*
* Emits an {Approval} event.
*/
function approve(address to, uint256 tokenId) external;
/**
* @dev Returns the account approved for `tokenId` token.
*
* Requirements:
*
* - `tokenId` must exist.
*/
function getApproved(uint256 tokenId) external view returns (address operator);
/**
* @dev Approve or remove `operator` as an operator for the caller.
* Operators can call {transferFrom} or {safeTransferFrom} for any token owned by the caller.
*
* Requirements:
*
* - The `operator` cannot be the caller.
*
* Emits an {ApprovalForAll} event.
*/
function setApprovalForAll(address operator, bool _approved) external;
/**
* @dev Returns if the `operator` is allowed to manage all of the assets of `owner`.
*
* See {setApprovalForAll}
*/
function isApprovedForAll(address owner, address operator) external view returns (bool);
/**
* @dev Safely transfers `tokenId` token from `from` to `to`.
*
* Requirements:
*
* - `from` cannot be the zero address.
* - `to` cannot be the zero address.
* - `tokenId` token must exist and be owned by `from`.
* - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}.
* - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
*
* Emits a {Transfer} event.
*/
function safeTransferFrom(
address from,
address to,
uint256 tokenId,
bytes calldata data
) external;
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "../IERC721.sol";
/**
* @title ERC-721 Non-Fungible Token Standard, optional enumeration extension
* @dev See https://eips.ethereum.org/EIPS/eip-721
*/
interface IERC721Enumerable is IERC721 {
/**
* @dev Returns the total amount of tokens stored by the contract.
*/
function totalSupply() external view returns (uint256);
/**
* @dev Returns a token ID owned by `owner` at a given `index` of its token list.
* Use along with {balanceOf} to enumerate all of ``owner``'s tokens.
*/
function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256 tokenId);
/**
* @dev Returns a token ID at a given `index` of all the tokens stored by the contract.
* Use along with {totalSupply} to enumerate all tokens.
*/
function tokenByIndex(uint256 index) external view returns (uint256);
}
// SPDX-License-Identifier: MIT
pragma solidity 0.8.18;
interface IFeeController {
// ================ Structs ================
struct FeesOrigination {
uint16 borrowerOriginationFee;
uint16 lenderOriginationFee;
uint16 lenderDefaultFee;
uint16 lenderInterestFee;
uint16 lenderPrincipalFee;
}
struct FeesRollover {
uint16 borrowerRolloverFee;
uint16 lenderRolloverFee;
}
// ================ Events =================
event SetLendingFee(bytes32 indexed id, uint16 fee);
event SetVaultMintFee(uint64 fee);
// ================ Getter/Setter =================
function setLendingFee(bytes32 id, uint16 fee) external;
function setVaultMintFee(uint64 fee) external;
function getLendingFee(bytes32 id) external view returns (uint16);
function getVaultMintFee() external view returns (uint64);
function getFeesOrigination() external view returns (FeesOrigination memory);
function getFeesRollover() external view returns (FeesRollover memory);
function getMaxLendingFee(bytes32 id) external view returns (uint16);
function getMaxVaultMintFee() external view returns (uint64);
}
// SPDX-License-Identifier: MIT
pragma solidity 0.8.18;
import "../libraries/LoanLibrary.sol";
import "./IPromissoryNote.sol";
interface ILoanCore {
// ================ Data Types =================
struct AffiliateSplit {
address affiliate;
uint96 splitBps;
}
struct NoteReceipt {
address token;
uint256 amount;
}
// ================ Events =================
event LoanStarted(uint256 loanId, address lender, address borrower);
event LoanRepaid(uint256 loanId);
event ForceRepay(uint256 loanId);
event LoanRolledOver(uint256 oldLoanId, uint256 newLoanId);
event LoanClaimed(uint256 loanId);
event NoteRedeemed(address indexed token, address indexed caller, address indexed to, uint256 tokenId, uint256 amount);
event NonceUsed(address indexed user, uint160 nonce);
event FeesWithdrawn(address indexed token, address indexed caller, address indexed to, uint256 amount);
event AffiliateSet(bytes32 indexed code, address indexed affiliate, uint96 splitBps);
// ============== Lifecycle Operations ==============
function startLoan(
address lender,
address borrower,
LoanLibrary.LoanTerms calldata terms,
uint256 _amountFromLender,
uint256 _amountToBorrower,
LoanLibrary.FeeSnapshot calldata feeSnapshot
) external returns (uint256 loanId);
function repay(
uint256 loanId,
address payer,
uint256 _amountFromPayer,
uint256 _amountToLender
) external;
function forceRepay(
uint256 loanId,
address payer,
uint256 _amountFromPayer,
uint256 _amountToLender
) external;
function claim(
uint256 loanId,
uint256 _amountFromLender
) external;
function redeemNote(
uint256 loanId,
uint256 _amountFromLender,
address to
) external;
function rollover(
uint256 oldLoanId,
address borrower,
address lender,
LoanLibrary.LoanTerms calldata terms,
uint256 _settledAmount,
uint256 _amountToOldLender,
uint256 _amountToLender,
uint256 _amountToBorrower
) external returns (uint256 newLoanId);
// ============== Nonce Management ==============
function consumeNonce(address user, uint160 nonce) external;
function cancelNonce(uint160 nonce) external;
// ============== Fee Management ==============
function withdraw(address token, uint256 amount, address to) external;
function withdrawProtocolFees(address token, address to) external;
// ============== Admin Operations ==============
function setAffiliateSplits(bytes32[] calldata codes, AffiliateSplit[] calldata splits) external;
// ============== View Functions ==============
function getLoan(uint256 loanId) external view returns (LoanLibrary.LoanData calldata loanData);
function getNoteReceipt(uint256 loanId) external view returns (address token, uint256 amount);
function isNonceUsed(address user, uint160 nonce) external view returns (bool);
function borrowerNote() external view returns (IPromissoryNote);
function lenderNote() external view returns (IPromissoryNote);
}
// SPDX-License-Identifier: MIT
pragma solidity 0.8.18;
interface INFTWithDescriptor {
// ============= Events ==============
event SetDescriptor(address indexed caller, address indexed descriptor);
// ================ Resource Metadata ================
function tokenURI(uint256 tokenId) external view returns (string memory);
function setDescriptor(address descriptor) external;
}
// SPDX-License-Identifier: MIT
pragma solidity 0.8.18;
import "@openzeppelin/contracts/token/ERC721/extensions/IERC721Enumerable.sol";
import "./INFTWithDescriptor.sol";
interface IPromissoryNote is INFTWithDescriptor, IERC721Enumerable {
// ============== Token Operations ==============
function mint(address to, uint256 loanId) external returns (uint256);
function burn(uint256 tokenId) external;
// ============== Initializer ==============
function initialize(address loanCore) external;
}
// SPDX-License-Identifier: MIT
pragma solidity 0.8.18;
interface IRepaymentController {
// ============== Lifeycle Operations ==============
function repay(uint256 loanId) external;
function forceRepay(uint256 loanId) external;
function claim(uint256 loanId) external;
function redeemNote(uint256 loanId, address to) external;
}
// SPDX-License-Identifier: MIT
pragma solidity 0.8.18;
/**
* @title InterestCalculator
* @author Non-Fungible Technologies, Inc.
*
* Interface for calculating the interest amount
* given an interest rate and principal amount. Assumes
* that the interestRate is already expressed over the desired
* time period.
*/
abstract contract InterestCalculator {
// ============================================ STATE ==============================================
/// @dev The units of precision equal to the minimum interest of 1 basis point.
uint256 public constant INTEREST_RATE_DENOMINATOR = 1e18;
uint256 public constant BASIS_POINTS_DENOMINATOR = 1e4;
// ======================================== CALCULATIONS ===========================================
/**
* @notice Calculate the interest due over a full term.
*
* @dev Interest and principal must be entered with 18 units of
* precision from the basis point unit (e.g. 1e18 == 0.01%)
*
* @param principal Principal amount in the loan terms.
* @param proratedInterestRate Interest rate in the loan terms, prorated over loan duration.
*
* @return interest The amount of interest due.
*/
function getInterestAmount(uint256 principal, uint256 proratedInterestRate) public pure returns (uint256) {
return principal * proratedInterestRate / (INTEREST_RATE_DENOMINATOR * BASIS_POINTS_DENOMINATOR);
}
}
// SPDX-License-Identifier: MIT
pragma solidity 0.8.18;
import "../libraries/LoanLibrary.sol";
/**
* @title LendingErrors
* @author Non-Fungible Technologies, Inc.
*
* This file contains custom errors for the core lending protocol contracts, with errors
* prefixed by the contract that throws them (e.g., "OC_" for OriginationController).
* Errors located in one place to make it possible to holistically look at all
* protocol failure cases.
*/
// ==================================== ORIGINATION CONTROLLER ======================================
/// @notice All errors prefixed with OC_, to separate from other contracts in the protocol.
/**
* @notice Zero address passed in where not allowed.
*
* @param addressType The name of the parameter for which a zero address was provided.
*/
error OC_ZeroAddress(string addressType);
/**
* @notice Ensure valid loan state for loan lifceycle operations.
*
* @param state Current state of a loan according to LoanState enum.
*/
error OC_InvalidState(LoanLibrary.LoanState state);
/**
* @notice Loan duration must be greater than 1hr and less than 3yrs.
*
* @param durationSecs Total amount of time in seconds.
*/
error OC_LoanDuration(uint256 durationSecs);
/**
* @notice Interest must be greater than 0.01% and less than 10,000%. (interestRate / 1e18 >= 1)
*
* @param interestRate InterestRate with 1e18 multiplier.
*/
error OC_InterestRate(uint256 interestRate);
/**
* @notice One of the predicates for item verification failed.
*
* @param borrower The address of the borrower.
* @param lender The address of the lender.
* @param verifier The address of the verifier contract.
* @param collateralAddress The address of the collateral token.
* @param collateralId The token ID of the collateral.
* @param data The verification data (to be parsed by verifier).
*/
error OC_PredicateFailed(
address borrower,
address lender,
address verifier,
address collateralAddress,
uint256 collateralId,
bytes data
);
/**
* @notice The predicates array is empty.
*/
error OC_PredicatesArrayEmpty();
/**
* @notice A caller attempted to approve themselves.
*
* @param caller The caller of the approve function.
*/
error OC_SelfApprove(address caller);
/**
* @notice A caller attempted to originate a loan with their own signature.
*
* @param caller The caller of the approve function, who was also the signer.
*/
error OC_ApprovedOwnLoan(address caller);
/**
* @notice The signature could not be recovered to the counterparty or approved party.
*
* @param target The target party of the signature, which should either be the signer,
* or someone who has approved the signer.
* @param signer The signer determined from ECDSA.recover.
*/
error OC_InvalidSignature(address target, address signer);
/**
* @notice The verifier contract specified in a predicate has not been whitelisted.
*
* @param verifier The verifier the caller attempted to use.
*/
error OC_InvalidVerifier(address verifier);
/**
* @notice The function caller was neither borrower or lender, and was not approved by either.
*
* @param caller The unapproved function caller.
*/
error OC_CallerNotParticipant(address caller);
/**
* @notice Signer is attempting to take the wrong side of the loan.
*
* @param signer The address of the external signer.
*/
error OC_SideMismatch(address signer);
/**
* @notice Two related parameters for batch operations did not match in length.
*/
error OC_BatchLengthMismatch();
/**
* @notice Principal must be greater than 9999 Wei.
*
* @param principal Principal in ether.
*/
error OC_PrincipalTooLow(uint256 principal);
/**
* @notice Signature must not be expired.
*
* @param deadline Deadline in seconds.
*/
error OC_SignatureIsExpired(uint256 deadline);
/**
* @notice New currency does not match for a loan rollover request.
*
* @param oldCurrency The currency of the active loan.
* @param newCurrency The currency of the new loan.
*/
error OC_RolloverCurrencyMismatch(address oldCurrency, address newCurrency);
/**
* @notice New currency does not match for a loan rollover request.
*
* @param oldCollateralAddress The address of the active loan's collateral.
* @param newCollateralAddress The token ID of the active loan's collateral.
* @param oldCollateralId The address of the new loan's collateral.
* @param newCollateralId The token ID of the new loan's collateral.
*/
error OC_RolloverCollateralMismatch(
address oldCollateralAddress,
uint256 oldCollateralId,
address newCollateralAddress,
uint256 newCollateralId
);
/**
* @notice Provided payable currency address is not approved for lending.
*
* @param payableCurrency ERC20 token address supplied in loan terms.
*/
error OC_InvalidCurrency(address payableCurrency);
/**
* @notice Provided collateral address is not approved for lending.
*
* @param collateralAddress ERC721 or ERC1155 token address supplied in loan terms.
*/
error OC_InvalidCollateral(address collateralAddress);
/**
* @notice Provided token array does not hold any token addresses.
*/
error OC_ZeroArrayElements();
/**
* @notice Provided token array holds more than 50 token addresses.
*/
error OC_ArrayTooManyElements();
// ==================================== ITEMS VERIFIER ======================================
/// @notice All errors prefixed with IV_, to separate from other contracts in the protocol.
/**
* @notice The predicate payload was decoded successfully, but list of predicates is empty.
*/
error IV_NoPredicates();
/**
* @notice Provided SignatureItem is missing an address.
*/
error IV_ItemMissingAddress();
/**
* @notice Provided SignatureItem has an invalid collateral type.
* @dev Should never actually fire, since cType is defined by an enum, so will fail on decode.
*
* @param asset The NFT contract being checked.
* @param cType The collateralTytpe provided.
*/
error IV_InvalidCollateralType(address asset, uint256 cType);
/**
* @notice Provided signature item with no required amount. For single ERC721s, specify 1.
*
* @param asset The NFT contract being checked.
* @param amount The amount provided (should be 0).
*/
error IV_NoAmount(address asset, uint256 amount);
/**
* @notice Provided a wildcard for a non-ERC721.
*
* @param asset The NFT contract being checked.
*/
error IV_InvalidWildcard(address asset);
/**
* @notice The provided token ID is out of bounds for the given collection.
*
* @param tokenId The token ID provided.
*/
error IV_InvalidTokenId(int256 tokenId);
/**
* @notice The provided project ID does not exist on the target contract. Only
* used for ArtBlocks.
*
* @param projectId The project ID provided.
* @param nextProjectId The contract's reported nextProjectId.
*/
error IV_InvalidProjectId(uint256 projectId, uint256 nextProjectId);
/**
* @notice The provided collateralId converts to a vault, but
* the vault's address does not convert back to the provided collateralId
* when casted to a uint256.
*/
error IV_InvalidCollateralId(uint256 collateralId);
// ==================================== REPAYMENT CONTROLLER ======================================
/// @notice All errors prefixed with RC_, to separate from other contracts in the protocol.
/**
* @notice Zero address passed in where not allowed.
*
* @param addressType The name of the parameter for which a zero address was provided.
*/
error RC_ZeroAddress(string addressType);
/**
* @notice Could not dereference loan from loan ID.
*
* @param target The loanId being checked.
*/
error RC_CannotDereference(uint256 target);
/**
* @notice Ensure valid loan state for loan lifceycle operations.
*
* @param state Current state of a loan according to LoanState enum.
*/
error RC_InvalidState(LoanLibrary.LoanState state);
/**
* @notice Caller is not the owner of lender note.
*
* @param lender The owner of the lender note.
* @param caller Msg.sender of the function call.
*/
error RC_OnlyLender(address lender, address caller);
// ==================================== Loan Core ======================================
/// @notice All errors prefixed with LC_, to separate from other contracts in the protocol.
/**
* @notice Zero address passed in where not allowed.
*
* @param addressType The name of the parameter for which a zero address was provided.
*/
error LC_ZeroAddress(string addressType);
/// @notice Borrower address is same as lender address.
error LC_ReusedNote();
/// @notice Zero amount passed in where not allowed.
error LC_ZeroAmount();
/**
* @notice Check collateral is not already used in a active loan.
*
* @param collateralAddress Address of the collateral.
* @param collateralId ID of the collateral token.
*/
error LC_CollateralInUse(address collateralAddress, uint256 collateralId);
/**
* @notice The reported settlements are invalid, and LoanCore would lose tokens
* attempting to perform the requested operations.
*
*
* @param payout Amount of tokens to be paid out.
* @param collected Amount of tokens to collect - should be fewer than payout.
*/
error LC_CannotSettle(uint256 payout, uint256 collected);
/**
* @notice User attempted to withdraw a pending balance that was in excess
* of what is available.
*
* @param amount Amount of tokens to be withdrawn.
* @param available Amount of tokens available to withdraw.
*/
error LC_CannotWithdraw(uint256 amount, uint256 available);
/**
* @notice Two arrays were provided that must be of matching length, but were not.
*
*/
error LC_ArrayLengthMismatch();
/**
* @notice A proposed affiliate split was submitted that is over the maximum.
*
* @param splitBps The proposed affiliate split.
* @param maxSplitBps The maximum allowed affiliate split.
*
*/
error LC_OverMaxSplit(uint96 splitBps, uint96 maxSplitBps);
/**
* @notice Ensure valid loan state for loan lifceycle operations.
*
* @param state Current state of a loan according to LoanState enum.
*/
error LC_InvalidState(LoanLibrary.LoanState state);
/**
* @notice Loan duration has not expired.
*
* @param dueDate Timestamp of the end of the loan duration.
*/
error LC_NotExpired(uint256 dueDate);
/**
* @notice User address and the specified nonce have already been used.
*
* @param user Address of collateral owner.
* @param nonce Represents the number of transactions sent by address.
*/
error LC_NonceUsed(address user, uint160 nonce);
/**
* @notice Protocol attempted to set an affiliate code which already exists. Affiliate
* codes are immutable.
*
* @param affiliateCode The affiliate code being set.
*/
error LC_AffiliateCodeAlreadySet(bytes32 affiliateCode);
/**
* @notice Specified note token ID does not have a redeemable receipt.
*
* @param loanId The loanId being checked.
*/
error LC_NoReceipt(uint256 loanId);
/**
* @notice Only Loan Core contract can call this function.
*/
error LC_CallerNotLoanCore();
/**
* @notice The loan core contract has been irreversibly shut down.
*/
error LC_Shutdown();
// ==================================== Promissory Note ======================================
/// @notice All errors prefixed with PN_, to separate from other contracts in the protocol.
/**
* @notice Zero address passed in where not allowed.
*
* @param addressType The name of the parameter for which a zero address was provided.
*/
error PN_ZeroAddress(string addressType);
/**
* @notice Caller of mint function must have the MINTER_ROLE in AccessControl.
*
* @param caller Address of the function caller.
*/
error PN_MintingRole(address caller);
/**
* @notice Caller of burn function must have the BURNER_ROLE in AccessControl.
*
* @param caller Address of the function caller.
*/
error PN_BurningRole(address caller);
/**
* @notice Non-existant token id provided as argument.
*
* @param tokenId The ID of the token to lookup the URI for.
*/
error PN_DoesNotExist(uint256 tokenId);
// ==================================== Fee Controller ======================================
/// @notice All errors prefixed with FC_, to separate from other contracts in the protocol.
/**
* @notice Caller attempted to set a lending fee which is larger than the global maximum.
*/
error FC_LendingFeeOverMax(bytes32 selector, uint256 fee, uint256 maxFee);
/**
* @notice Caller attempted to set a vault mint fee which is larger than the global maximum.
*/
error FC_VaultMintFeeOverMax(uint256 fee, uint256 maxFee);
// ==================================== ERC721 Permit ======================================
/// @notice All errors prefixed with ERC721P_, to separate from other contracts in the protocol.
/**
* @notice Deadline for the permit has expired.
*
* @param deadline Permit deadline parameter as a timestamp.
*/
error ERC721P_DeadlineExpired(uint256 deadline);
/**
* @notice Address of the owner to also be the owner of the tokenId.
*
* @param owner Owner parameter for the function call.
*/
error ERC721P_NotTokenOwner(address owner);
/**
* @notice Invalid signature.
*
* @param signer Signer recovered from ECDSA sugnature hash.
*/
error ERC721P_InvalidSignature(address signer);
// SPDX-License-Identifier: MIT
pragma solidity 0.8.18;
/**
* @title LoanLibrary
* @author Non-Fungible Technologies, Inc.
*
* Contains all data types used across Arcade lending contracts.
*/
library LoanLibrary {
/**
* @dev Enum describing the current state of a loan.
* State change flow:
* Created -> Active -> Repaid
* -> Defaulted
*/
enum LoanState {
// We need a default that is not 'Created' - this is the zero value
DUMMY_DO_NOT_USE,
// The loan has been initialized, funds have been delivered to the borrower and the collateral is held.
Active,
// The loan has been repaid, and the collateral has been returned to the borrower. This is a terminal state.
Repaid,
// The loan was delinquent and collateral claimed by the lender. This is a terminal state.
Defaulted
}
/**
* @dev The raw terms of a loan.
*/
struct LoanTerms {
// Interest expressed as a rate, unlike V1 gross value.
// Input conversion: 0.01% = (1 * 10**18) , 10.00% = (1000 * 10**18)
// This represents the rate over the lifetime of the loan, not APR.
// 0.01% is the minimum interest rate allowed by the protocol.
uint256 proratedInterestRate;
/// @dev Full-slot variables
// The amount of principal in terms of the payableCurrency.
uint256 principal;
// The token ID of the address holding the collateral.
/// @dev Can be an AssetVault, or the NFT contract for unbundled collateral
address collateralAddress;
/// @dev Packed variables
// The number of seconds representing relative due date of the loan.
/// @dev Max is 94,608,000, fits in 96 bits
uint96 durationSecs;
// The token ID of the collateral.
uint256 collateralId;
// The payable currency for the loan principal and interest.
address payableCurrency;
// Timestamp for when signature for terms expires
uint96 deadline;
// Affiliate code used to start the loan.
bytes32 affiliateCode;
}
/**
* @dev Modification of loan terms, used for signing only.
* Instead of a collateralId, a list of predicates
* is defined by 'bytes' in items.
*/
struct LoanTermsWithItems {
// Interest expressed as a rate, unlike V1 gross value.
// Input conversion: 0.01% = (1 * 10**18) , 10.00% = (1000 * 10**18)
// This represents the rate over the lifetime of the loan, not APR.
// 0.01% is the minimum interest rate allowed by the protocol.
uint256 proratedInterestRate;
/// @dev Full-slot variables
// The amount of principal in terms of the payableCurrency.
uint256 principal;
// The tokenID of the address holding the collateral
address collateralAddress;
/// @dev Packed variables
// The number of seconds representing relative due date of the loan.
/// @dev Max is 94,608,000, fits in 96 bits
uint96 durationSecs;
// An encoded list of predicates, along with their verifiers.
bytes items;
// The payable currency for the loan principal and interest.
address payableCurrency;
// Timestamp for when signature for terms expires
uint96 deadline;
// Affiliate code used to start the loan.
bytes32 affiliateCode;
}
/**
* @dev Predicate for item-based verifications
*/
struct Predicate {
// The encoded predicate, to decoded and parsed by the verifier contract.
bytes data;
// The verifier contract.
address verifier;
}
/**
* @dev Snapshot of lending fees at the time of loan creation.
*/
struct FeeSnapshot {
// The fee taken when lender claims defaulted collateral.
uint16 lenderDefaultFee;
// The fee taken from the borrower's interest repayment.
uint16 lenderInterestFee;
// The fee taken from the borrower's principal repayment.
uint16 lenderPrincipalFee;
}
/**
* @dev The data of a loan. This is stored once the loan is Active
*/
struct LoanData {
/// @dev Packed variables
// The current state of the loan.
LoanState state;
// Start date of the loan, using block.timestamp.
uint160 startDate;
/// @dev Full-slot variables
// The raw terms of the loan.
LoanTerms terms;
// Record of lending fees at the time of loan creation.
FeeSnapshot feeSnapshot;
}
}
// SPDX-License-Identifier: MIT
pragma solidity 0.8.18;
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "./interfaces/IRepaymentController.sol";
import "./interfaces/IPromissoryNote.sol";
import "./interfaces/ILoanCore.sol";
import "./interfaces/IFeeController.sol";
import "./libraries/InterestCalculator.sol";
import "./libraries/FeeLookups.sol";
import "./libraries/LoanLibrary.sol";
import {
RC_ZeroAddress,
RC_CannotDereference,
RC_InvalidState,
RC_OnlyLender
} from "./errors/Lending.sol";
/**
* @title RepaymentController
* @author Non-Fungible Technologies, Inc.
*
* The Repayment Controller is the entry point for all loan lifecycle
* operations in the Arcade.xyz lending protocol once a loan has begun.
* This contract allows a caller to calculate an amount due on a loan,
* repay an open loan, and claim collateral on a defaulted loan. It
* is this contract's responsibility to verify loan conditions before
* calling LoanCore.
*/
contract RepaymentController is IRepaymentController, InterestCalculator, FeeLookups {
using SafeERC20 for IERC20;
// ============================================ STATE ===============================================
ILoanCore private immutable loanCore;
IPromissoryNote private immutable lenderNote;
IFeeController private immutable feeController;
// ========================================= CONSTRUCTOR ============================================
/**
* @notice Creates a new repayment controller contract.
*
* @dev For this controller to work, it needs to be granted the REPAYER_ROLE
* in loan core after deployment.
*
* @param _loanCore The address of the loan core logic of the protocol.
* @param _feeController The address of the fee logic of the protocol.
*/
constructor(address _loanCore, address _feeController) {
if (_loanCore == address(0)) revert RC_ZeroAddress("loanCore");
if (_feeController == address(0)) revert RC_ZeroAddress("feeController");
loanCore = ILoanCore(_loanCore);
lenderNote = loanCore.lenderNote();
feeController = IFeeController(_feeController);
}
// ==================================== LIFECYCLE OPERATIONS ========================================
/**
* @notice Repay an active loan, referenced by borrower note ID (equivalent to loan ID). The interest for a loan
* is calculated, and the principal plus interest is withdrawn from the caller.
* Anyone can repay a loan. Control is passed to LoanCore to complete repayment.
*
* @param loanId The ID of the loan.
*/
function repay(uint256 loanId) external override {
(uint256 amountFromBorrower, uint256 amountToLender) = _prepareRepay(loanId);
// call repay function in loan core - msg.sender will pay the amountFromBorrower
loanCore.repay(loanId, msg.sender, amountFromBorrower, amountToLender);
}
/**
* @notice Repay an active loan, referenced by borrower note ID (equivalent to loan ID). The interest for a loan
* is calculated, and the principal plus interest is withdrawn from the caller. Anyone can repay a loan.
* Using forceRepay will not send funds to the lender: instead, those funds will be made
* available for withdrawal in LoanCore. Can be used in cases where a borrower has funds to repay
* but the lender is not able to receive those tokens (e.g. token blacklist).
*
* @param loanId The ID of the loan.
*/
function forceRepay(uint256 loanId) external override {
(uint256 amountFromBorrower, uint256 amountToLender) = _prepareRepay(loanId);
// call repay function in loan core - msg.sender will pay the amountFromBorrower
loanCore.forceRepay(loanId, msg.sender, amountFromBorrower, amountToLender);
}
/**
* @notice Claim collateral on an active loan, referenced by lender note ID (equivalent to loan ID).
* The loan must be past the due date. No funds are collected
* from the borrower.
*
* @param loanId The ID of the loan.
*/
function claim(uint256 loanId) external override {
LoanLibrary.LoanData memory data = loanCore.getLoan(loanId);
if (data.state == LoanLibrary.LoanState.DUMMY_DO_NOT_USE) revert RC_CannotDereference(loanId);
// make sure that caller owns lender note
// Implicitly checks if loan is active - if inactive, note will not exist
address lender = lenderNote.ownerOf(loanId);
if (lender != msg.sender) revert RC_OnlyLender(lender, msg.sender);
LoanLibrary.LoanTerms memory terms = data.terms;
uint256 interest = getInterestAmount(terms.principal, terms.proratedInterestRate);
uint256 totalOwed = terms.principal + interest;
uint256 claimFee = (totalOwed * data.feeSnapshot.lenderDefaultFee) / BASIS_POINTS_DENOMINATOR;
loanCore.claim(loanId, claimFee);
}
/**
* @notice Redeem a lender note for a completed return in return for funds repaid in an earlier
* transaction via forceRepay. The lender note must be owned by the caller.
*
* @param loanId The ID of the lender note to redeem.
*/
function redeemNote(uint256 loanId, address to) external override {
if (to == address(0)) revert RC_ZeroAddress("to");
LoanLibrary.LoanData memory data = loanCore.getLoan(loanId);
(, uint256 amountOwed) = loanCore.getNoteReceipt(loanId);
if (data.state != LoanLibrary.LoanState.Repaid) revert RC_InvalidState(data.state);
address lender = lenderNote.ownerOf(loanId);
if (lender != msg.sender) revert RC_OnlyLender(lender, msg.sender);
uint256 redeemFee = (amountOwed * feeController.getLendingFee(FL_08)) / BASIS_POINTS_DENOMINATOR;
loanCore.redeemNote(loanId, redeemFee, to);
}
// =========================================== HELPERS ==============================================
/**
* @dev Shared logic to perform validation and calculations for repay and forceRepay.
*
* @param loanId The ID of the loan.
*
* @return amountFromBorrower The amount to collect from the borrower.
* @return amountToLender The amount owed to the lender.
*/
function _prepareRepay(uint256 loanId) internal view returns (uint256 amountFromBorrower, uint256 amountToLender) {
LoanLibrary.LoanData memory data = loanCore.getLoan(loanId);
if (data.state == LoanLibrary.LoanState.DUMMY_DO_NOT_USE) revert RC_CannotDereference(loanId);
if (data.state != LoanLibrary.LoanState.Active) revert RC_InvalidState(data.state);
LoanLibrary.LoanTerms memory terms = data.terms;
uint256 interest = getInterestAmount(terms.principal, terms.proratedInterestRate);
uint256 interestFee = (interest * data.feeSnapshot.lenderInterestFee) / BASIS_POINTS_DENOMINATOR;
uint256 principalFee = (terms.principal * data.feeSnapshot.lenderPrincipalFee) / BASIS_POINTS_DENOMINATOR;
amountFromBorrower = terms.principal + interest;
amountToLender = amountFromBorrower - interestFee - principalFee;
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "../IERC20.sol";
import "../../../utils/Address.sol";
/**
* @title SafeERC20
* @dev Wrappers around ERC20 operations that throw on failure (when the token
* contract returns false). Tokens that return no value (and instead revert or
* throw on failure) are also supported, non-reverting calls are assumed to be
* successful.
* To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract,
* which allows you to call the safe operations as `token.safeTransfer(...)`, etc.
*/
library SafeERC20 {
using Address for address;
function safeTransfer(
IERC20 token,
address to,
uint256 value
) internal {
_callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value));
}
function safeTransferFrom(
IERC20 token,
address from,
address to,
uint256 value
) internal {
_callOptionalReturn(token, abi.encodeWithSelector(token.transferFrom.selector, from, to, value));
}
/**
* @dev Deprecated. This function has issues similar to the ones found in
* {IERC20-approve}, and its usage is discouraged.
*
* Whenever possible, use {safeIncreaseAllowance} and
* {safeDecreaseAllowance} instead.
*/
function safeApprove(
IERC20 token,
address spender,
uint256 value
) internal {
// safeApprove should only be called when setting an initial allowance,
// or when resetting it to zero. To increase and decrease it, use
// 'safeIncreaseAllowance' and 'safeDecreaseAllowance'
require(
(value == 0) || (token.allowance(address(this), spender) == 0),
"SafeERC20: approve from non-zero to non-zero allowance"
);
_callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, value));
}
function safeIncreaseAllowance(
IERC20 token,
address spender,
uint256 value
) internal {
uint256 newAllowance = token.allowance(address(this), spender) + value;
_callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, newAllowance));
}
function safeDecreaseAllowance(
IERC20 token,
address spender,
uint256 value
) internal {
unchecked {
uint256 oldAllowance = token.allowance(address(this), spender);
require(oldAllowance >= value, "SafeERC20: decreased allowance below zero");
uint256 newAllowance = oldAllowance - value;
_callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, newAllowance));
}
}
/**
* @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement
* on the return value: the return value is optional (but if data is returned, it must not be false).
* @param token The token targeted by the call.
* @param data The call data (encoded using abi.encode or one of its variants).
*/
function _callOptionalReturn(IERC20 token, bytes memory data) private {
// We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since
// we're implementing it ourselves. We use {Address.functionCall} to perform this call, which verifies that
// the target address contains contract code and also asserts for success in the low-level call.
bytes memory returndata = address(token).functionCall(data, "SafeERC20: low-level call failed");
if (returndata.length > 0) {
// Return data is optional
require(abi.decode(returndata, (bool)), "SafeERC20: ERC20 operation did not succeed");
}
}
}
{
"compilationTarget": {
"contracts/RepaymentController.sol": "RepaymentController"
},
"evmVersion": "paris",
"libraries": {},
"metadata": {
"bytecodeHash": "none"
},
"optimizer": {
"enabled": true,
"runs": 200
},
"remappings": []
}
[{"inputs":[{"internalType":"address","name":"_loanCore","type":"address"},{"internalType":"address","name":"_feeController","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[{"internalType":"uint256","name":"target","type":"uint256"}],"name":"RC_CannotDereference","type":"error"},{"inputs":[{"internalType":"enum LoanLibrary.LoanState","name":"state","type":"uint8"}],"name":"RC_InvalidState","type":"error"},{"inputs":[{"internalType":"address","name":"lender","type":"address"},{"internalType":"address","name":"caller","type":"address"}],"name":"RC_OnlyLender","type":"error"},{"inputs":[{"internalType":"string","name":"addressType","type":"string"}],"name":"RC_ZeroAddress","type":"error"},{"inputs":[],"name":"BASIS_POINTS_DENOMINATOR","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"FL_01","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"FL_02","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"FL_03","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"FL_04","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"FL_05","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"FL_06","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"FL_07","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"FL_08","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"INTEREST_RATE_DENOMINATOR","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"loanId","type":"uint256"}],"name":"claim","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"loanId","type":"uint256"}],"name":"forceRepay","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"principal","type":"uint256"},{"internalType":"uint256","name":"proratedInterestRate","type":"uint256"}],"name":"getInterestAmount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"loanId","type":"uint256"},{"internalType":"address","name":"to","type":"address"}],"name":"redeemNote","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"loanId","type":"uint256"}],"name":"repay","outputs":[],"stateMutability":"nonpayable","type":"function"}]