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
function createLOAN(
ProposalSpec calldata proposalSpec,
LenderSpec calldata lenderSpec,
CallerSpec calldata callerSpec,
bytes calldata extra
) external returns (uint256 loanId) {
// Check provided proposal contract
if (!hub.hasTag(proposalSpec.proposalContract, PWNHubTags.LOAN_PROPOSAL)) {
revert AddressMissingHubTag({ addr: proposalSpec.proposalContract, tag: PWNHubTags.LOAN_PROPOSAL });
}
// Revoke nonce if needed
if (callerSpec.revokeNonce) {
revokedNonce.revokeNonce(msg.sender, callerSpec.nonce);
}
// If refinancing a loan, check that the loan can be repaid
if (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 correct
if (msg.sender != loanTerms.lender && loanTerms.lenderSpecHash != getLenderSpecHash(lenderSpec)) {
revert InvalidLenderSpecHash({ current: loanTerms.lenderSpecHash, expected: getLenderSpecHash(lenderSpec) });
}
// Check minimum loan duration
if (loanTerms.duration < MIN_LOAN_DURATION) {
revert InvalidDuration({ current: loanTerms.duration, limit: MIN_LOAN_DURATION });
}
// Check maximum accruing interest APR
if (loanTerms.accruingInterestAPR > MAX_ACCRUING_INTEREST_APR) {
revert InterestAPROutOfBounds({ 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
});
emit LOANCreated({
loanId: loanId,
proposalHash: proposalHash,
proposalContract: proposalSpec.proposalContract,
refinancingLoanId: callerSpec.refinancingLoanId,
terms: loanTerms,
lenderSpec: lenderSpec,
extra: extra
});
// Execute permit for the caller
if (callerSpec.permitData.length > 0) {
Permit memory permit = abi.decode(callerSpec.permitData, (Permit));
_checkPermit(msg.sender, loanTerms.credit.assetAddress, permit);
_tryPermit(permit);
}
// Settle the loan
if (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
function repayLOAN(
uint256 loanId,
bytes calldata permitData
) external {
LOAN storage loan = LOANs[loanId];
_checkLoanCanBeRepaid(loan.status, loan.defaultTimestamp);
// Update loan to repaid state
_updateRepaidLoan(loanId);
// Execute permit for the caller
if (permitData.length > 0) {
Permit memory permit = abi.decode(permitData, (Permit));
_checkPermit(msg.sender, loan.creditAddress, permit);
_tryPermit(permit);
}
// Transfer the repaid credit to the Vault
uint256 repaymentAmount = loanRepaymentAmount(loanId);
_pull(loan.creditAddress.ERC20(repaymentAmount), msg.sender);
// Transfer collateral back to borrower
_push(loan.collateral, loan.borrower);
// Try to repay directly
try this.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
function claimLOAN(uint256 loanId) external {
LOAN storage loan = LOANs[loanId];
// Check that caller is LOAN token holder
if (loanToken.ownerOf(loanId) != msg.sender)
revert CallerNotLOANTokenHolder();
if (loan.status == 0)
// Loan is not existing or from a different loan contract
revert NonExistingLoan();
else if (loan.status == 3)
// Loan has been paid back
_settleLoanClaim({ loanId: loanId, loanOwner: msg.sender, defaulted: false });
else if (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 state
revert LoanRunning();
}
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
function getStateFingerprint(uint256 tokenId) external view virtual override returns (bytes32) {
LOAN storage loan = LOANs[tokenId];
if (loan.status == 0)
return bytes32(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.
return keccak256(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.