PWNSimpleLoan.sol is written in Solidity version 0.8.16
Features
Manages simple loan flow
Creation
Repayment
Claim
Implements an option for the lender to extend the expiration (maturity) date of a loan
Acts as a vault for all assets used in simple loans
The expiration of a loan can be extended by a maximum of 90 days into the future. This is a measure to protect lenders from accidentally extending a loan maturity date too far. Lenders can extend a loan expiration date an unlimited amount of times meaning a loan expiration date can be extended indefinitely.
Minumum duration of a simple loan is 10 minutes.
Inherited contracts, implemented Interfaces and ERCs
bytes calldataextra - Auxiliary data that are emitted in the LOANCreated event. They are not used in the contract logic.
Implementation
functioncreateLOAN(ProposalSpeccalldata proposalSpec,LenderSpeccalldata lenderSpec,CallerSpeccalldata callerSpec,bytescalldata extra) externalreturns (uint256 loanId) {// Check provided proposal contractif (!hub.hasTag(proposalSpec.proposalContract, PWNHubTags.LOAN_PROPOSAL)) {revertAddressMissingHubTag({ addr: proposalSpec.proposalContract, tag: PWNHubTags.LOAN_PROPOSAL }); }// Revoke nonce if neededif (callerSpec.revokeNonce) { revokedNonce.revokeNonce(msg.sender, callerSpec.nonce); }// If refinancing a loan, check that the loan can be repaidif (callerSpec.refinancingLoanId !=0) { LOAN storage loan = LOANs[callerSpec.refinancingLoanId];_checkLoanCanBeRepaid(loan.status, loan.defaultTimestamp); }// Accept proposal and get loan terms (bytes32 proposalHash, Terms memory loanTerms) =PWNSimpleLoanProposal(proposalSpec.proposalContract) .acceptProposal({ acceptor: msg.sender, refinancingLoanId: callerSpec.refinancingLoanId, proposalData: proposalSpec.proposalData, proposalInclusionProof: proposalSpec.proposalInclusionProof, signature: proposalSpec.signature });// Check that provided lender spec is correctif (msg.sender != loanTerms.lender && loanTerms.lenderSpecHash !=getLenderSpecHash(lenderSpec)) {revertInvalidLenderSpecHash({ current: loanTerms.lenderSpecHash, expected:getLenderSpecHash(lenderSpec) }); }// Check minimum loan durationif (loanTerms.duration < MIN_LOAN_DURATION) {revertInvalidDuration({ current: loanTerms.duration, limit: MIN_LOAN_DURATION }); }// Check maximum accruing interest APRif (loanTerms.accruingInterestAPR > MAX_ACCRUING_INTEREST_APR) {revertInterestAPROutOfBounds({ current: loanTerms.accruingInterestAPR, limit: MAX_ACCRUING_INTEREST_APR }); }if (callerSpec.refinancingLoanId ==0) {// Check loan credit and collateral validity_checkValidAsset(loanTerms.credit);_checkValidAsset(loanTerms.collateral); } else {// Check refinance loan terms_checkRefinanceLoanTerms(callerSpec.refinancingLoanId, loanTerms); }// Create a new loan loanId =_createLoan({ loanTerms: loanTerms, lenderSpec: lenderSpec });emitLOANCreated({ loanId: loanId, proposalHash: proposalHash, proposalContract: proposalSpec.proposalContract, refinancingLoanId: callerSpec.refinancingLoanId, terms: loanTerms, lenderSpec: lenderSpec, extra: extra });// Execute permit for the callerif (callerSpec.permitData.length >0) { Permit memory permit = abi.decode(callerSpec.permitData, (Permit));_checkPermit(msg.sender, loanTerms.credit.assetAddress, permit);_tryPermit(permit); }// Settle the loanif (callerSpec.refinancingLoanId ==0) {// Transfer collateral to Vault and credit to borrower_settleNewLoan(loanTerms, lenderSpec); } else {// Update loan to repaid state_updateRepaidLoan(callerSpec.refinancingLoanId);// Repay the original loan and transfer the surplus to the borrower if any_settleLoanRefinance({ refinancingLoanId: callerSpec.refinancingLoanId, loanTerms: loanTerms, lenderSpec: lenderSpec }); }}
repayLOAN
Overview
Borrowers use this function to repay simple loans in the PWN Protocol.
This function takes two arguments supplied by the caller:
uint256loanId - ID of the loan that is being repaid
bytes calldatapermitData - Permit data for a loan asset signed by borrower
Implementation
functionrepayLOAN(uint256 loanId,bytescalldata permitData) external { LOAN storage loan = LOANs[loanId];_checkLoanCanBeRepaid(loan.status, loan.defaultTimestamp);// Update loan to repaid state_updateRepaidLoan(loanId);// Execute permit for the callerif (permitData.length >0) { Permit memory permit = abi.decode(permitData, (Permit));_checkPermit(msg.sender, loan.creditAddress, permit);_tryPermit(permit); }// Transfer the repaid credit to the Vaultuint256 repaymentAmount =loanRepaymentAmount(loanId);_pull(loan.creditAddress.ERC20(repaymentAmount), msg.sender);// Transfer collateral back to borrower_push(loan.collateral, loan.borrower);// Try to repay directlytrythis.tryClaimRepaidLOAN(loanId, repaymentAmount, loanToken.ownerOf(loanId)) {} catch {// Note: Safe transfer or supply to a pool can fail. In that case leave the LOAN token in repaid state and// wait for the LOAN token owner to claim the repaid credit. Otherwise lender would be able to prevent// borrower from repaying the loan. }}
claimLOAN
Overview
Holders of LOAN tokens (lenders) use this function to claim a repaid loan or defaulted collateral. The claimed asset is transferred to the LOAN token holder and the LOAN token is burned.
This function takes one argument supplied by the caller:
uint256loanId - ID of the loan that is being claimed
Implementation
functionclaimLOAN(uint256 loanId) external { LOAN storage loan = LOANs[loanId];// Check that caller is LOAN token holderif (loanToken.ownerOf(loanId) != msg.sender)revertCallerNotLOANTokenHolder();if (loan.status ==0)// Loan is not existing or from a different loan contractrevertNonExistingLoan();elseif (loan.status ==3)// Loan has been paid back_settleLoanClaim({ loanId: loanId, loanOwner: msg.sender, defaulted:false });elseif (loan.status ==2&& loan.defaultTimestamp <= block.timestamp)// Loan is running but expired_settleLoanClaim({ loanId: loanId, loanOwner: msg.sender, defaulted:true });else// Loan is in wrong staterevertLoanRunning();}
extendLOAN
Overview
This function extends loan default date with signed extension proposal signed by borrower or the LOAN token owner (usually the lender).
This function takes three arguments supplied by the caller:
This function returns the current token state fingerprint for a supplied token ID. See ERC-5646 standard specification for more detailed information.
This function takes one argument supplied by the caller:
uint256tokenId - ID of the LOAN token to get a fingerprint for
Implementation
functiongetStateFingerprint(uint256 tokenId) externalviewvirtualoverridereturns (bytes32) { LOAN storage loan = LOANs[tokenId];if (loan.status ==0)returnbytes32(0);// The only mutable state properties are:// - status: updated for expired loans based on block.timestamp// - defaultTimestamp: updated when the loan is extended// - fixedInterestAmount: updated when the loan is repaid and waiting to be claimed// - accruingInterestAPR: updated when the loan is repaid and waiting to be claimed// Others don't have to be part of the state fingerprint as it does not act as a token identification.returnkeccak256(abi.encode(_getLOANStatus(tokenId), loan.defaultTimestamp, loan.fixedInterestAmount, loan.accruingInterestAPR ));}
Events
The PWN Simple Loan contract defines one event and no custom errors.
Fixed interest amount in credit asset tokens. It is the minimum amount of interest which has to be paid by a borrower
uint24
accruingInterestAPR
Accruing interest APR with 2 decimals
bytes32
lenderSpecHash
Hash of a lender specification struct
bytes32
borrowerSpecHash
Hash of a borrower specification struct
ProposalSpec Struct
Type
Name
Comment
address
proposalContract
Address of a loan proposal contract
bytes
proposalData
Encoded proposal data that is passed to the loan proposal contract
bytes32[]
proposalInclusionProof
Inclusion proof of the proposal in the proposal contract
bytes
signature
Signature of the proposal
LenderSpec Struct
Type
Name
Comment
address
sourceOfFunds
Address of a source of funds. This can be the lenders address, if the loan is funded directly, or a pool address from which the funds are withdrawn on the lenders behalf
CallerSpec Struct
Type
Name
Comment
uint256
refinancingLoanId
ID of a loan to be refinanced. Zero if creating a new loan
bool
revokeNonce
Flag if the callers nonce should be revoked
uint256
nonce
Callers nonce to be revoked. Nonce is revoked from the current nonce space
bytes
permitData
Callers permit data for a loans credit asset
LOAN Struct
Type
Name
Comment
uint8
status
0 -> None/Dead
2 -> Running
3 -> Repaid
4 -> Expired
address
creditAddress
Address of an asset used as a loan credit
address
originalSourceOfFunds
Address of a source of funds that was used to fund the loan
uint40
startTimestamp
Unix timestamp (in seconds) of a start date
uint40
defaultTimestamp
Unix timestamp (in seconds) of a default date
address
borrower
Address of a borrower
address
originalLender
Address of a lender that funded the loan
uint24
accruingInterestAPR
Accruing interest APR with 2 decimals
uint256
fixedInterestAmount
Fixed interest amount in credit asset tokens. It is the minimum amount of interest which has to be paid by a borrower. This property is reused to store the final interest amount if the loan is repaid and waiting to be claimed.