Simple Loan

1. Summary

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. The minimum duration of a simple loan is 10 minutes.

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

The expiration of a loan can be extended by a maximum of 30 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.

Inherited contracts, implemented Interfaces and ERCs

Functions

createLOAN

Overview

Users use this function to start a simple loan in the PWN protocol.

This function takes five arguments supplied by the caller:

  • addressloanTermsFactoryContract - Address of a loan terms factory contract. Needs to have SIMPLE_LOAN_TERMS_FACTORY tag in the PWN Hub.

  • bytes calldataloanTermsFactoryData - Encoded data for a loan terms factory

  • bytes calldatasignature - Signed loan factory data. Can be empty if a offer/request was created via an on-chain transaction

  • bytes calldataloanAssetPermit - Permit data for a loan asset signed by a lender

  • bytes calldatacollateralPermit - Permit data for collateral signed by a borrower

Implementation

function createLOAN(
    address loanTermsFactoryContract,
    bytes calldata loanTermsFactoryData,
    bytes calldata signature,
    bytes calldata loanAssetPermit,
    bytes calldata collateralPermit
) external returns (uint256 loanId) {
    // Check that loan terms factory contract is tagged in PWNHub
    if (hub.hasTag(loanTermsFactoryContract, PWNHubTags.SIMPLE_LOAN_TERMS_FACTORY) == false)
        revert CallerMissingHubTag(PWNHubTags.SIMPLE_LOAN_TERMS_FACTORY);

    // Build PWNLOANTerms.Simple by loan factory
    PWNLOANTerms.Simple memory loanTerms = PWNSimpleLoanTermsFactory(loanTermsFactoryContract).createLOANTerms({
        caller: msg.sender,
        factoryData: loanTermsFactoryData,
        signature: signature
    });

    // Check loan asset validity
    if (MultiToken.isValid(loanTerms.asset) == false)
        revert InvalidLoanAsset();

    // Check collateral validity
    if (MultiToken.isValid(loanTerms.collateral) == false)
        revert InvalidCollateralAsset();

    // Mint LOAN token for lender
    loanId = loanToken.mint(loanTerms.lender);

    // Store loan data under loan id
    LOAN storage loan = LOANs[loanId];
    loan.status = 2;
    loan.borrower = loanTerms.borrower;
    loan.expiration = loanTerms.expiration;
    loan.loanAssetAddress = loanTerms.asset.assetAddress;
    loan.loanRepayAmount = loanTerms.loanRepayAmount;
    loan.collateral = loanTerms.collateral;

    emit LOANCreated(loanId, loanTerms);

    // Transfer collateral to Vault
    _permit(loanTerms.collateral, loanTerms.borrower, collateralPermit);
    _pull(loanTerms.collateral, loanTerms.borrower);

    // Permit spending if permit data provided
    _permit(loanTerms.asset, loanTerms.lender, loanAssetPermit);

    uint16 fee = config.fee();
    if (fee > 0) {
        // Compute fee size
        (uint256 feeAmount, uint256 newLoanAmount) = PWNFeeCalculator.calculateFeeAmount(fee, loanTerms.asset.amount);

        if (feeAmount > 0) {
            // Transfer fee amount to fee collector
            loanTerms.asset.amount = feeAmount;
            _pushFrom(loanTerms.asset, loanTerms.lender, config.feeCollector());

            // Set new loan amount value
            loanTerms.asset.amount = newLoanAmount;
        }
    }

    // Transfer loan asset to borrower
    _pushFrom(loanTerms.asset, loanTerms.lender, loanTerms.borrower);
}
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 calldataloanAssetPermit - Permit data for a loan asset signed by a borrower

Implementation

function repayLOAN(
    uint256 loanId,
    bytes calldata loanAssetPermit
) external {
    LOAN storage loan = LOANs[loanId];
    uint8 status = loan.status;

    // Check that loan is not from a different loan contract
    if (status == 0)
        revert NonExistingLoan();
    // Check that loan is running
    else if (status != 2)
        revert InvalidLoanStatus(status);

    // Check that loan is not expired
    if (loan.expiration <= block.timestamp)
        revert LoanDefaulted(loan.expiration);

    // Move loan to repaid state
    loan.status = 3;

    // Transfer repaid amount of loan asset to Vault
    MultiToken.Asset memory repayLoanAsset = MultiToken.Asset({
        category: MultiToken.Category.ERC20,
        assetAddress: loan.loanAssetAddress,
        id: 0,
        amount: loan.loanRepayAmount
    });

    _permit(repayLoanAsset, msg.sender, loanAssetPermit);
    _pull(repayLoanAsset, msg.sender);

    // Transfer collateral back to borrower
    _push(loan.collateral, loan.borrower);

    emit LOANPaidBack(loanId);
}
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) {
        revert NonExistingLoan();
    }
    // Loan has been paid back
    else if (loan.status == 3) {
        MultiToken.Asset memory loanAsset = MultiToken.Asset({
            category: MultiToken.Category.ERC20,
            assetAddress: loan.loanAssetAddress,
            id: 0,
            amount: loan.loanRepayAmount
        });

        // Delete loan data & burn LOAN token before calling safe transfer
        _deleteLoan(loanId);

        // Transfer repaid loan to lender
        _push(loanAsset, msg.sender);

        emit LOANClaimed(loanId, false);
    }
    // Loan is running but expired
    else if (loan.status == 2 && loan.expiration <= block.timestamp) {
            MultiToken.Asset memory collateral = loan.collateral;

        // Delete loan data & burn LOAN token before calling safe transfer
        _deleteLoan(loanId);

        // Transfer collateral to lender
        _push(collateral, msg.sender);

        emit LOANClaimed(loanId, true);
    }
    // Loan is in wrong state or from a different loan contract
    else {
        revert InvalidLoanStatus(loan.status);
    }
}
extendLOANExpirationDate

Overview

Holders of LOAN tokens (lenders) can decide to extend an expiration date of a loan by up to 30 days from the time the transaction is included in a block.

This function takes two arguments supplied by the caller:

  • uint256loanId - ID of the loan that is being extended

  • uint40extendedExpirationDate - New expiration (maturity) date of the loan. Has to be in the future and maximally 30 days from the previous expiration

Implementation

function extendLOANExpirationDate(uint256 loanId, uint40 extendedExpirationDate) external {
    // Check that caller is LOAN token holder
    // This prevents from extending non-existing loans
    if (loanToken.ownerOf(loanId) != msg.sender)
        revert CallerNotLOANTokenHolder();

    LOAN storage loan = LOANs[loanId];

    // Check extended expiration date
    if (extendedExpirationDate > uint40(block.timestamp + MAX_EXPIRATION_EXTENSION)) // to protect lender
        revert InvalidExtendedExpirationDate();
    if (extendedExpirationDate <= uint40(block.timestamp)) // have to extend expiration futher in time
        revert InvalidExtendedExpirationDate();
    if (extendedExpirationDate <= loan.expiration) // have to be later than current expiration date
        revert InvalidExtendedExpirationDate();

    // Extend expiration date
    loan.expiration = extendedExpirationDate;

    emit LOANExpirationDateExtended(loanId, extendedExpirationDate);
}

View Functions

getLOAN

Overview

Returns a Loan struct with information about a supplied loan ID.

This function takes one argument supplied by the caller:

  • uint256loanId - ID of the loan to get parameters for

Implementation

function getLOAN(uint256 loanId) external view returns (LOAN memory loan) {
    loan = LOANs[loanId];
    loan.status = _getLOANStatus(loanId);
}
loanMetadataUri

Overview

Returns a metadata URI for LOAN tokens. This URI is defined in PWN Config.

This function doesn't take any arguments.

Implementation

function loanMetadataUri() override external view returns (string memory) {
    return config.loanMetadataUri(address(this));
}
getStateFingerprint

Overview

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, expiration
    // Status is updated for expired loans based on block.timestamp.
    // 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.expiration
    ));
}

Events

The PWN Simple Loan contract defines one event and no custom errors.

event LOANCreated(uint256 indexed loanId, PWNLOANTerms.Simple terms);
event LOANPaidBack(uint256 indexed loanId);
event LOANClaimed(uint256 indexed loanId, bool indexed defaulted);
event LOANExpirationDateExtended(uint256 indexed loanId, uint40 extendedExpirationDate);
LOANCreated

LOANCreated is emitted when a new simple loan is started.

This event has two parameters:

  • uint256 indexedloanId - ID of the LOAN token that is associated with the created loan

  • PWNLOANTerms.Simpleterms - Struct with the parameters of the created loan. See PWN Loan Terms for more information

LOANPaidBack

LOANPaidBack event is emitted when a borrower repays a simple loan.

This event has one parameter:

  • uint256 indexedloanId - ID of the LOAN token that is associated with the repaid loan

LOANClaimed

LOANClaimed event is emitted when a lender claims repaid asset or defaulted collateral.

This event has two parameters:

  • uint256 indexedloanId - ID of the LOAN token that is associated with the claimed loan

  • bool indexeddefaulted - Boolean determining if the claimed loan was defaulted or properly repaid

LOANExpirationDateExtended

LOANExpirationDateExtended event is emitted when a lender extends the loan maturity date.

This event has two parameters:

  • uint256 indexedloanId - ID of the LOAN token that is associated with the loan being extended

  • uint40extendedExpirationDate - New expiration (maturity) date of the loan

Simple Loan Struct

TypeNameComment

uint8

status

0 -> None/Dead 2 -> Running/accepted offer/accepted request 3 -> Repaid 4 -> Expired

address

borrower

Address of a borrower

uint40

expiration

Unix timestamp (in seconds) setting up a maturity date

address

loanAssetAddress

Asset used as a loan credit. See MultiToken for more infomation about the Asset struct.

uint256

loanRepayAmount

Amount of asset to be repaid.

MultiToken.Asset

collateral

Asset used as a loan collateral. See MultiToken for more infomation about the Asset struct.

Last updated