import{expect}from"chai";import{ethers}from"hardhat";describe("PerpetualContractNFTDemo",function(){it("should allow an owner to collateralize an NFT, rent it to a contract, and then have the owner repay the loan",asyncfunction(){const[owner]=awaitethers.getSigners();constPerpetualContractNFTDemo=awaitethers.getContractFactory("PerpetualContractNFTDemo");constdemo=awaitPerpetualContractNFTDemo.deploy("DemoNFT","DNFT");awaitdemo.waitForDeployment();expect(demo.target).to.be.properAddress;// Mint an NFT to the owner
// 将 NFT 铸造给所有者
awaitdemo.mint(1,owner.address);// Owner collateralizes the NFT for a loan
// 所有者抵押 NFT 以获得贷款
constloanAmount=ethers.parseUnits("1","ether");// 1 Ether in Wei. Use Wei to avoid precision error.
constinterest=5;// 5% interest
constexpiration=Math.floor(newDate().getTime()/1000)+3600;// Expire after 60 minutes (3600 seconds), convert it to seconds because `hours` in solidity converted to seconds
awaitdemo.connect(owner).collateralize(1,loanAmount,interest,expiration);// tokenId, loanAmount, interestRate, loanDuration
// Check current user of the NFT (should be the contract address)
// 检查 NFT 的当前用户(应该是合约地址)
expect(awaitdemo.userOf(1)).to.equal(demo.target);// Borrower repays the loan to release the NFT
// 借款人偿还贷款以释放 NFT
constrepayAmountWei=awaitdemo.connect(owner).viewRepayAmount(1);awaitdemo.connect(owner).repayLoan(1,repayAmountWei);// Check if the NFT is returned to the original owner after the loan is repaid
// 检查贷款偿还后 NFT 是否返回给原始所有者
expect(awaitdemo.userOf(1)).to.equal("0x0000000000000000000000000000000000000000");});});
在终端中运行:
npx hardhat test
参考实现
// SPDX-License-Identifier: CC0-1.0
pragmasolidity^0.8.0;//import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import"./IPerpetualContractNFT.sol";import"./ERC4907/ERC4907.sol";contractPerpetualContractNFTisERC4907,IPerpetualContractNFT{structLoanInfo{addressborrower;// Address that borrowed against the NFT
uint256loanAmount;// Amount of funds borrowed
uint256interestRate;// Interest rate for the loan
uint64loanDuration;// Duration of the loan
uint256loanStartTime;// Timestamp when the loan starts
}mapping(uint256=>LoanInfo)internal_loans;//Constructor to initialize the Perpetual Contract NFT contract with the given name and symbo
//构造函数用于使用给定的名称和符号初始化永久合约 NFT 合约
constructor(stringmemoryname_,stringmemorysymbol_)ERC4907(name_,symbol_){}functioncollateralize(uint256tokenId,uint256loanAmount,uint256interestRate,uint64loanDuration)publicoverride{require(ownerOf(tokenId)==msg.sender||isApprovedForAll(ownerOf(tokenId),msg.sender)||getApproved(tokenId)==msg.sender,"Not owner nor approved");LoanInfostorageinfo=_loans[tokenId];info.borrower=msg.sender;// The loan amount should reflect the asset's value as represented by the NFT, considering an appropriate loan-to-value (LTV) ratio.
// 贷款金额应反映 NFT 所代表的资产价值,同时考虑到适当的贷款价值比 (LTV)。
info.loanAmount=loanAmount;info.interestRate=interestRate;info.loanDuration=loanDuration;info.loanStartTime=block.timestamp;setUser(tokenId,address(this),loanDuration);emitCollateralized(tokenId,msg.sender,loanAmount,interestRate,loanDuration);// Further logic can be implemented here to manage the lending of assets
// 可以在此处实现更多逻辑来管理资产的借贷
}functionrepayLoan(uint256tokenId,uint256repayAmount)publicoverride{require(_loans[tokenId].borrower==msg.sender,"Not the borrower.");// Calculate the total amount due for repayment
// 计算偿还所需的总金额
uint256totalDue=viewRepayAmount(tokenId);// Check if the repayAmount is sufficient to cover at least a part of the total due amount
// 检查 repayAmount 是否足以至少支付一部分到期总额
require(repayAmount<=totalDue,"Repay amount exceeds total due.");// Calculate the remaining loan amount after repayment
// 计算偿还后剩余的贷款金额
_loans[tokenId].loanAmount=totalDue-repayAmount;// Resets the user of the NFT to the default state if the entire loan amount is fully repaid
// 如果全部贷款金额已全部偿还,则将 NFT 的用户重置为默认状态
if(_loans[tokenId].loanAmount==0){setUser(tokenId,address(0),0);}emitLoanRepaid(tokenId,msg.sender);}functiongetLoanTerms(uint256tokenId)publicviewoverridereturns(uint256,uint256,uint256,uint256){LoanInfostorageinfo=_loans[tokenId];return(info.loanAmount,info.interestRate,info.loanDuration,info.loanStartTime);}functioncurrentOwner(uint256tokenId)publicviewoverridereturns(address){returnownerOf(tokenId);}functionviewRepayAmount(uint256tokenId)publicviewreturns(uint256){if(_loans[tokenId].loanAmount==0){// If the loan amount is zero, there is nothing to repay
// 如果贷款金额为零,则无需偿还
return0;}// The interest is calculated on an hourly basis, prorated based on the actual duration for which the loan was held.
// If the borrower repays before the loan duration ends, they are charged interest only for the time the loan was held.
// For example, if the annual interest rate is 5% and the borrower repays in half the loan term, they pay only 2.5% interest.
// 利息按小时计算,按贷款实际持有时间按比例计算。
// 如果借款人在贷款期限结束前偿还贷款,则仅收取贷款持有期间的利息。
// 例如,如果年利率为 5%,并且借款人在贷款期限的一半时偿还贷款,则他们仅支付 2.5% 的利息。
uint256elapsed=block.timestamp>(_loans[tokenId].loanStartTime+_loans[tokenId].loanDuration)?_loans[tokenId].loanDuration/1hours:(block.timestamp-_loans[tokenId].loanStartTime)/1hours;// Round up
// 四舍五入
// Example: 15/4 = 3.75
// 示例:15/4 = 3.75
// round((15 + 4 - 1)/4) = 4, round((15/4) = 3)
// round((15 + 4 - 1)/4) = 4, round((15/4) = 3)
uint256interest=((_loans[tokenId].loanAmount*_loans[tokenId].interestRate/100)*elapsed+(_loans[tokenId].loanDuration/1hours)-1)/(_loans[tokenId].loanDuration/1hours);// Calculate the total amount due
// 计算到期总额
uint256totalDue=_loans[tokenId].loanAmount+interest;returntotalDue;}// Additional functions and logic to handle loan defaults, transfers, and other aspects of the NFT lifecycle
// 其他功能和逻辑,用于处理贷款违约、转移和 NFT 生命周期的其他方面
}