// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.4;
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol";
import "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol";
import "./interfaces/IXY3.sol";
import "./interfaces/IDelegateV3.sol";
import "./interfaces/IAddressProvider.sol";
import "./interfaces/IServiceFee.sol";
import {IFlashExecPermits} from "./interfaces/IFlashExecPermits.sol";
import "./DataTypes.sol";
import "./LoanStatus.sol";
import "./Config.sol";
import "./utils/SigningUtils.sol";
import {InterceptorManager} from "./InterceptorManager.sol";
import {SIGNER_ROLE} from "./Roles.sol";
/**
* @title XY3
* @author XY3
* @notice Main contract for XY3 lending.
*/
contract XY3 is
IXY3,
Config,
LoanStatus,
InterceptorManager,
ERC721Holder,
ERC1155Holder
{
using SafeERC20 for IERC20;
/**
* @notice A mapping from a loan's identifier to the loan's terms, represted by the LoanTerms struct.
*/
mapping(uint32 => LoanDetail) public override loanDetails;
/**
* @notice A mapping, (collection address, token Id) -> loan ID.
*/
mapping(address => mapping(uint256 => uint32)) public override loanIds;
/**
* @notice A mapping, (user address , nonce) -> boolean.
*/
mapping(address => mapping(uint256 => bool)) internal _invalidNonce;
/**
* @notice A mapping that takes a user's address and a cancel timestamp.
*
*/
mapping(address => uint256) internal _offerCancelTimestamp;
/**
* modifier
*/
modifier loanIsOpen(uint32 _loanId) {
require(
getLoanState(_loanId).status == StatusType.NEW,
"Loan is not open"
);
_;
}
/**
* @dev Init contract
*
* @param _admin - Initial admin of this contract.
* @param _addressProvider - AddressProvider contract
*/
constructor(
address _admin,
address _addressProvider
)
Config(_admin, _addressProvider)
LoanStatus()
InterceptorManager()
{
}
/**
PUBLIC FUNCTIONS
*/
/**
* @dev The borrower accept a lender's offer to create a loan.
*
* @param _offer - The offer made by the lender.
* @param _nftId - The ID
* @param _isCollectionOffer - Wether the offer is a collection offer.
* @param _lenderSignature - The lender's signature.
* @param _brokerSignature - The broker's signature.
* @param _extraDeal - Create a new loan by getting a NFT colleteral from external contract call.
* The external contract can be lending market or deal market, specially included the restricted repay of myself.
* But should not be the Xy3Nft.mint, though this contract maybe have the permission.
*/
function borrow(
Offer calldata _offer,
uint256 _nftId,
bool _isCollectionOffer,
Signature calldata _lenderSignature,
Signature calldata _brokerSignature,
CallData calldata _extraDeal
) external override whenNotPaused nonReentrant returns (uint32) {
_loanSanityChecks(_offer);
address nftAsset = _offer.nftAsset;
beforeBorrow(nftAsset, _nftId);
LoanDetail memory _loanDetail = _createLoanDetail(
_offer,
_nftId,
_isCollectionOffer
);
_checkBorrow(
_offer,
_nftId,
_isCollectionOffer,
_lenderSignature,
_brokerSignature
);
IAddressProvider addressProvider = getAddressProvider();
IDelegateV3(addressProvider.getTransferDelegate()).erc20Transfer(
_lenderSignature.signer,
msg.sender,
_offer.borrowAsset,
_offer.borrowAmount
);
if (_extraDeal.target != address(0)) {
require(getAgentPermit(_extraDeal.target, _extraDeal.selector), "Not valide agent");
bytes memory data = abi.encodeWithSelector(
_extraDeal.selector,
msg.sender,
_extraDeal.data
);
(bool succ, ) = _extraDeal.target.call(data);
require(succ, "Borrow extra call failed");
}
IDelegateV3(addressProvider.getTransferDelegate()).erc721Transfer(
msg.sender,
address(this),
nftAsset,
_nftId
);
uint32 loanId = _createBorrowNote(
_lenderSignature.signer,
msg.sender,
_loanDetail,
_lenderSignature,
_extraDeal
);
_serviceFee(_offer, loanId, _extraDeal.target);
loanIds[nftAsset][_nftId] = loanId;
afterBorrow(nftAsset, _nftId);
emit BorrowRefferal(loanId, msg.sender, _extraDeal.referral);
return loanId;
}
/**
* @dev Restricted function, only called by self from borrow with target.
* @param _sender The borrow's msg.sender.
* @param _param The borrow CallData's data, encode loadId only.
*/
function repay(address _sender, bytes calldata _param) external {
require(msg.sender == address(this), "Invalide caller");
uint32 loanId = abi.decode(_param, (uint32));
_repay(_sender, loanId);
}
/**
* @dev Public function for anyone to repay a loan, and return the NFT token to origin borrower.
* @param _loanId The loan Id.
*/
function repay(uint32 _loanId) public override nonReentrant {
_repay(msg.sender, _loanId);
}
/**
* @dev Lender ended the load which not paid by borrow and expired.
*
* @param _loanId The loan Id.
*/
function liquidate(
uint32 _loanId
) external override nonReentrant loanIsOpen(_loanId) {
(
address borrower,
address lender,
LoanDetail memory loan
) = _getPartiesAndData(_loanId);
address nftAsset = loan.nftAsset;
uint nftId = loan.nftTokenId;
beforeLiquidate(nftAsset, nftId);
uint256 loanMaturityDate = _loanMaturityDate(loan);
require(block.timestamp > loanMaturityDate, "Loan is not overdue yet");
require(msg.sender == lender, "Only lender can liquidate");
// Emit an event with all relevant details from this transaction.
emit LoanLiquidated(
_loanId,
borrower,
lender,
loan.borrowAmount,
nftId,
loanMaturityDate,
block.timestamp,
nftAsset
);
// nft to lender
IERC721(nftAsset).safeTransferFrom(address(this), lender, nftId);
_resolveLoanNote(_loanId);
delete loanIds[nftAsset][nftId];
afterLiquidate(nftAsset, nftId);
}
/**
* @dev Flash out the colleteral NFT.
*
* @param _loanId The loan Id.
* @param _target The target contract.
* @param _selector The callback selector.
* @param _data The callback data.
*/
function flashExecute(
uint32 _loanId,
address _target,
bytes4 _selector,
bytes memory _data
) external {
(address borrower, , LoanDetail memory loan) = _getPartiesAndData(
_loanId
);
IAddressProvider addressProvider = getAddressProvider();
require(
IFlashExecPermits(addressProvider.getFlashExecPermits())
.isPermitted(_target, _selector),
"Invalid airdrop target"
);
require(block.timestamp <= _loanMaturityDate(loan), "Loan is expired");
require(msg.sender == borrower, "Only borrower");
IERC721(loan.nftAsset).safeTransferFrom(
address(this),
_target,
loan.nftTokenId
);
(bool succ, ) = _target.call(
abi.encodeWithSelector(_selector, msg.sender, _data)
);
require(succ, "External call failed");
address owner = IERC721(loan.nftAsset).ownerOf(loan.nftTokenId);
require(owner == address(this), "Nft not returned");
emit FlashExecute(_loanId, loan.nftAsset, loan.nftTokenId, _target);
}
/**
* @dev A lender or a borrower to cancel all off-chain orders signed that contain this nonce.
* @param _nonce - User nonce
*/
function cancelByNonce(uint256 _nonce) external override {
require(!_invalidNonce[msg.sender][_nonce], "Invalid nonce");
_invalidNonce[msg.sender][_nonce] = true;
emit NonceCancelled(msg.sender, _nonce);
}
/**
* @dev A borrower cancel all offers with timestamp before the _timestamp parameter.
* @param _timestamp - cancelled timestamp
*/
function cancelByTimestamp(uint256 _timestamp) external override {
require(_timestamp < block.timestamp, "Invalid timestamp");
if (_timestamp > _offerCancelTimestamp[msg.sender]) {
_offerCancelTimestamp[msg.sender] = _timestamp;
emit TimeStampCancelled(msg.sender, _timestamp);
}
}
/**
* @dev The amount of ERC20 currency for the loan.
*
* @param _loanId loan Id.
* @return The amount of ERC20 currency.
*/
function getRepayAmount(
uint32 _loanId
) external view override returns (uint256) {
LoanDetail storage loan = loanDetails[_loanId];
return loan.repayAmount;
}
/**
* @notice Check a nonce has been used or not
* @param _user - The user address.
* @param _nonce - The order Id.
*
* @return A bool for used or not.
*/
function getNonceUsed(
address _user,
uint256 _nonce
) external view override returns (bool) {
return _invalidNonce[_user][_nonce];
}
/**
* @dev This function can be used to view the last cancel timestamp a borrower has set.
* @param _user User address
* @return The cancel timestamp
*/
function getTimestampCancelled(
address _user
) external view override returns (uint256) {
return _offerCancelTimestamp[_user];
}
/**
* @dev Claim the ERC20 airdrop by admin timelock.
* @param _to - Receiver address
* @param tokens - Claimed token list
* @param amounts - Clamined amount list
*/
function adminClaimErc20(
address _to,
address[] memory tokens,
uint256[] memory amounts
) external override onlyRole(DEFAULT_ADMIN_ROLE) {
require(_to != address(0x0), "Invalid address");
for (uint i = 0; i < tokens.length; i++) {
address token = tokens[i];
IERC20(token).safeTransfer(_to, amounts[i]);
}
}
/**
* @dev Claim the ERC721 airdrop by admin timelock.
* @param _to - Receiver address
* @param tokens - Claimed token list
* @param tokenIds - Clamined ID list
*/
function adminClaimErc721(
address _to,
address[] memory tokens,
uint256[] memory tokenIds
) external override onlyRole(DEFAULT_ADMIN_ROLE) {
for (uint i = 0; i < tokens.length; i++) {
address token = tokens[i];
uint256 tokenId = tokenIds[i];
uint32 loanId = loanIds[token][tokenId];
if (loanId == 0) {
IERC721(token).safeTransferFrom(
address(this),
_to,
tokenIds[i]
);
}
}
}
/**
* @dev Claim the ERC1155 airdrop by admin timelock.
* @param _to - Receiver address
* @param tokens - Claimed token list
* @param tokenIds - Clamined ID list
* @param amounts - Clamined amount list
*/
function adminClaimErc1155(
address _to,
address[] memory tokens,
uint256[] memory tokenIds,
uint256[] memory amounts
) external override onlyRole(DEFAULT_ADMIN_ROLE) {
for (uint i = 0; i < tokens.length; i++) {
address token = tokens[i];
IERC1155(token).safeTransferFrom(
address(this),
_to,
tokenIds[i],
amounts[i],
""
);
}
}
/**
* @dev ERC165 support
*/
function supportsInterface(
bytes4 interfaceId
)
public
view
virtual
override(AccessControl, ERC1155Receiver)
returns (bool)
{
return
interfaceId == type(IERC721Receiver).interfaceId ||
super.supportsInterface(interfaceId);
}
/**
* @param _loanId Load Id.
*/
function _resolveLoanNote(uint32 _loanId) internal {
resolveLoan(_loanId);
delete loanDetails[_loanId];
}
/**
* @dev Check loan parameters validation
*
*/
function _loanSanityChecks(Offer memory _offer) internal view {
require(getERC20Permit(_offer.borrowAsset), "Invalid currency");
require(getERC721Permit(_offer.nftAsset), "Invalid ERC721 token");
require(
uint256(_offer.borrowDuration) <= maxBorrowDuration,
"Invalid maximum duration"
);
require(
uint256(_offer.borrowDuration) >= minBorrowDuration,
"Invalid minimum duration"
);
require(
_offer.repayAmount >= _offer.borrowAmount,
"Invalid interest rate"
);
}
function _getPartiesAndData(
uint32 _loanId
)
internal
view
returns (address borrower, address lender, LoanDetail memory loan)
{
uint256 xy3NftId = getLoanState(_loanId).xy3NftId;
loan = loanDetails[_loanId];
borrower = IERC721(getAddressProvider().getBorrowerNote()).ownerOf(xy3NftId);
lender = IERC721(getAddressProvider().getLenderNote()).ownerOf(xy3NftId);
}
/**
* @dev Get the payoff amount and admin fee
* @param _loanDetail - Loan parameters
*/
function _payoffAndFee(
LoanDetail memory _loanDetail
) internal pure returns (uint256 adminFee, uint256 payoffAmount) {
uint256 interestDue = _loanDetail.repayAmount -
_loanDetail.borrowAmount;
adminFee = (interestDue * _loanDetail.adminShare) / HUNDRED_PERCENT;
payoffAmount = _loanDetail.repayAmount - adminFee;
}
/**
* @param _offer - Offer parameters
* @param _nftId - NFI ID
* @param _isCollection - is collection or not
* @param _lenderSignature - lender signature
* @param _brokerSignature - broker signature
*/
function _checkBorrow(
Offer memory _offer,
uint256 _nftId,
bool _isCollection,
Signature memory _lenderSignature,
Signature memory _brokerSignature
) internal view {
address _lender = _lenderSignature.signer;
require(
!_invalidNonce[_lender][_lenderSignature.nonce],
"Lender nonce invalid"
);
require(
hasRole(SIGNER_ROLE, _brokerSignature.signer),
"Invalid broker signer"
);
require(
_offerCancelTimestamp[_lender] < _offer.timestamp,
"Offer cancelled"
);
_checkSignatures(
_offer,
_nftId,
_isCollection,
_lenderSignature,
_brokerSignature
);
}
function _createBorrowNote(
address _lender,
address _borrower,
LoanDetail memory _loanDetail,
Signature memory _lenderSignature,
CallData memory _extraDeal
) internal returns (uint32) {
_invalidNonce[_lender][_lenderSignature.nonce] = true;
// Mint ERC721 note to the lender and borrower
uint32 loanId = createLoan(_lender, _borrower);
// Record
loanDetails[loanId] = _loanDetail;
emit LoanStarted(
loanId,
msg.sender,
_lenderSignature.signer,
_lenderSignature.nonce,
_loanDetail,
_extraDeal.target,
_extraDeal.selector
);
return loanId;
}
function _repay(
address payer,
uint32 _loanId
) internal loanIsOpen(_loanId) {
(
address borrower,
address lender,
LoanDetail memory loan
) = _getPartiesAndData(_loanId);
require(block.timestamp <= _loanMaturityDate(loan), "Loan is expired");
address nftAsset = loan.nftAsset;
uint nftId = loan.nftTokenId;
beforeRepay(nftAsset, nftId);
IERC721(nftAsset).safeTransferFrom(address(this), borrower, nftId);
// pay from the payer
_repayAsset(payer, borrower, lender, _loanId, loan);
_resolveLoanNote(_loanId);
delete loanIds[nftAsset][nftId];
afterRepay(nftAsset, nftId);
}
function _repayAsset(
address payer,
address borrower,
address lender,
uint32 _loanId,
LoanDetail memory loan
) internal {
(uint256 adminFee, uint256 payoffAmount) = _payoffAndFee(loan);
IAddressProvider addressProvider = getAddressProvider();
// Paid back to lender
IDelegateV3(addressProvider.getTransferDelegate()).erc20Transfer(
payer,
lender,
loan.borrowAsset,
payoffAmount
);
// Transfer admin fee
IDelegateV3(addressProvider.getTransferDelegate()).erc20Transfer(
payer,
adminFeeReceiver,
loan.borrowAsset,
adminFee
);
emit LoanRepaid(
_loanId,
borrower,
lender,
loan.borrowAmount,
loan.nftTokenId,
payoffAmount,
adminFee,
loan.nftAsset,
loan.borrowAsset
);
}
/**
* @param _offer - Offer parameters
* @param _nftId - NFI ID
* @param _isCollection - is collection or not
* @param _lenderSignature - lender signature
* @param _brokerSignature - broker signature
*/
function _checkSignatures(
Offer memory _offer,
uint256 _nftId,
bool _isCollection,
Signature memory _lenderSignature,
Signature memory _brokerSignature
) private view {
if (_isCollection) {
require(
SigningUtils.offerSignatureIsValid(_offer, _lenderSignature),
"Lender signature is invalid"
);
} else {
require(
SigningUtils.offerSignatureIsValid(
_offer,
_nftId,
_lenderSignature
),
"Lender signature is invalid"
);
}
require(
SigningUtils.offerSignatureIsValid(
_offer,
_nftId,
_brokerSignature
),
"Signer signature is invalid"
);
}
/**
* @param _offer - Offer parameters
* @param _nftId - NFI ID
* @param _isCollection - is collection or not
*/
function _createLoanDetail(
Offer memory _offer,
uint256 _nftId,
bool _isCollection
) internal view returns (LoanDetail memory) {
return
LoanDetail({
borrowAsset: _offer.borrowAsset,
borrowAmount: _offer.borrowAmount,
repayAmount: _offer.repayAmount,
nftAsset: _offer.nftAsset,
nftTokenId: _nftId,
loanStart: uint64(block.timestamp),
loanDuration: _offer.borrowDuration,
adminShare: adminShare,
isCollection: _isCollection
});
}
/**
* @param loan - Loan parameters
*/
function _loanMaturityDate(
LoanDetail memory loan
) private pure returns (uint256) {
return uint256(loan.loanStart) + uint256(loan.loanDuration);
}
function _serviceFee(Offer memory offer, uint32 loanId, address target) internal {
if (target != address(0)) {
IAddressProvider addressProvider = getAddressProvider();
address nftAsset = offer.nftAsset;
uint256 borrowAmount = offer.borrowAmount;
address borrowAsset = offer.borrowAsset;
address serviceFeeAddr = addressProvider.getServiceFee();
uint16 serviceFeeRate = 0;
uint256 fee = 0;
if(serviceFeeAddr != address(0)) {
serviceFeeRate = IServiceFee(serviceFeeAddr).getServiceFee(
target,
msg.sender,
nftAsset
);
if(serviceFeeRate > 0) {
fee = borrowAmount * serviceFeeRate / HUNDRED_PERCENT;
IDelegateV3(addressProvider.getTransferDelegate()).erc20Transfer(
msg.sender,
adminFeeReceiver,
borrowAsset,
fee
);
}
emit ServiceFee(loanId, target, serviceFeeRate, fee);
}
}
}
}