PWNSimpleLoan.sol contract manages the simple loan type in the PWN protocol. This contract also acts as a Vault for all assets used in simple loans.
2. Important links
3. Contract details
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.
A LoanNotRunning error is thrown when managed loan is not running.
This error has doesn't define any parameters.
LoanRunning
A LoanRunning error is thrown when managed loan is running.
This error has doesn't define any parameters.
LoanRepaid
A LoanRepaid error is thrown when managed loan is repaid.
This error has doesn't define any parameters.
LoanDefaulted
A NonceAlreadyRevoked error is thrown when managed loan is defaulted.
This error has one parameter:
uint40 - Default timestamp of the loan
NonExistingLoan
A NonExistingLoan error is thrown when loan doesn't exist.
This error has doesn't define any parameters.
CallerNotLOANTokenHolder
A CallerNotLOANTokenHolder error is thrown when caller is not a LOAN token holder.
This error has doesn't define any parameters.
RefinanceBorrowerMismatch
A RefinanceBorrowerMismatch error is thrown when refinancing loan terms have different borrower than the original loan.
This error has two parameters:
addresscurrentBorrower
addressnewBorrower
RefinanceCreditMismatch
A RefinanceCreditMismatch error is thrown when refinancing loan terms have different credit asset than the original loan.
This error has doesn't define any parameters.
RefinanceCollateralMismatch
A RefinanceCollateralMismatch error is thrown when refinancing loan terms have different collateral asset than the original loan.
This error has doesn't define any parameters.
InvalidLenderSpecHash
A InvalidLenderSpecHash error is thrown when hash of provided lender spec doesn't match the one in loan terms.
This error has two parameters:
bytes32current - Provided lender spec hash
bytes32expected - Expected lender spec hash
InvalidDuration
A InvalidDuration error is thrown when loan duration is below the minimum (10 minutes).
This error has two parameters:
uint256current - Provided loan duration
uint256limit - Provided loan duration
InterestAPROutOfBounds
A InterestAPROutOfBounds error is thrown when accruing interest APR is above the maximum.
This error has two parameters:
uint256current - Current accrued interest
uint256limit - Maximum accrued interest
CallerNotVault
A CallerNotVault error is thrown when caller is not a vault.
This error has doesn't define any parameters.
InvalidSourceOfFunds
A InvalidSourceOfFunds error is thrown when pool based source of funds doesn't have a registered adapter.
This error has one parameter:
addresssourceOfFunds
InvalidExtensionCaller
A InvalidExtensionCaller error is thrown when caller is not a loan borrower or lender.
This error has doesn't define any parameters.
InvalidExtensionSigner
A InvalidExtensionSigner error is thrown when signer is not a loan extension proposer.
This error has two parameters:
addressallowed
addresscurrent
InvalidExtensionDuration
A InvalidExtensionDuration error is thrown when loan extension duration is out of bounds.
This error has two parameters:
uint256duration
uint256limit
InvalidMultiTokenAsset
A InvalidMultiTokenAsset error is thrown when MultiToken Asset struct is invalid which can happen because of invalid category, address, id or amount.
See MultiToken for more information about the Asset struct.
This error has four parameters:
uint8category
addressaddr
uint256id
uint256amount
Terms Struct
Type
Name
Comment
address
lender
Address of a lender
address
borrower
Address of a borrower
uint32
duration
Loan duration in seconds
collateral
Asset used as a loan collateral
credit
Asset used as a loan credit
uint256
fixedInterestAmount
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.
uint256
principalAmount
Principal amount in credit asset tokens
collateral
Asset used as a loan collateral
ExtensionProposal Struct
Type
Name
Comment
uint256
loanId
ID of a loan to be extended
address
compensationAddress
Address of a compensation asset
uint256
compensationAmount
Amount of a compensation asset that a borrower has to pay to a lender
uint40
duration
Duration of the extension in seconds
uint40
expiration
Unix timestamp (in seconds) of an expiration date
address
proposer
Address of a proposer that signed the extension proposal