Create a Lending Marketplace dApp on Polygon

Make your own lending marketplace on Polygon
Polygon
AdvancedSmart contractsSolidityTypescript1.5 hours
Written by Devendra Yadav

Introduction

A Lending Marketplace provides a secure, flexible, open-source foundation for a decentralized loan marketplace on the Polygon blockchain. It provides the pieces necessary to create a decentralized lending exchange, including the requisite lending assets, repayments, and collateral infrastructure, enabling third parties to build applications for lending.

Prerequisites

Requirements

  • MetaMask is a browser-based blockchain wallet that can be used to store any kind of digital assets and cryptocurrency.Extension can be installed from here
  • Node.js enables the development of fast web servers in JavaScript by bringing event-driven programming to web servers.Make sure to have NodeJS 12.0.1+ version installed.
  • Truffle, which can be installed with command:
npm install -g truffle
React.js is an open-source JavaScript library that is used to create single-page applications' user interfaces.
Clone this Git Repository and read the Deploying and Debugging Smart Contracts on Polygon tutorial to setup network config inside Truffle and learn the deployment on the Polygon network.
git clone https://github.com/Devilla/cryptolend.eth.git
Go to the repository:
cd cryptolend.eth
Install the required depencencies:
npm i

Introduction to smart contracts

There are two main smart contracts: One to create the loan offer and request, and one to define the contract details of the loan with repayment methods.

Loan creator

In the contracts/Loancreator.sol file there are smart contract methods for both the lender & the borrower to create their loan offer and request respectively. A person might be willing to offer a loan or requesting for a loan.
Importing OpenZeppelin contract functionality:
OpenZeppelin contracts help to minimize risks for end-users and give developers confidence by using well-tested libraries of smart contracts.
pragma solidity ^0.5.0; import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; import "openzeppelin-solidity/contracts/lifecycle/Pausable.sol"; import "./LoanContract.sol";
There are two functions required to support the creation of a new Loan:
  • createNewLoanOffer
  • createNewLoanRequest
createNewLoanOffer:
Taking input from the user who wants to loan their cryptocurrency, this function will include the loan amount, duration, data about the collateral accepted in case the loan isn’t paid, referencing the same to the loan contract address.
function createNewLoanOffer(uint256 _loanAmount, uint128 _duration, string memory _acceptedCollateralsMetadata) public returns(address _loanContractAddress) { _loanContractAddress = address (new LoanContract(_loanAmount, _duration, _acceptedCollateralsMetadata, 0, address(0), 0, 0, 0, address(0), msg.sender, LoanContract.LoanStatus.OFFER)); loans.push(_loanContractAddress); emit LoanOfferCreated(msg.sender, _loanContractAddress); return _loanContractAddress;
The data is appended into the array named loans. The loan offer is created then sent as a message to that loan contract address.
createNewLoanRequest:
This function is a request from the borrower, the user who is asking for a loan. It includes the loan amount and duration; interest the user is willing to pay; data about the collateral such as the collateral address & collateral amount; the cryptocurrency being requested as a loan; price of the collateral in the specific cryptocurrency & finally the loan contract address.
The input is appended into the array named loans. The loan request is created then sent as a message to that loan contract address.
function createNewLoanRequest(uint256 _loanAmount, uint128 _duration, uint256 _interest, address _collateralAddress, uint256 _collateralAmount, uint256 _collateralPriceInETH) public returns(address _loanContractAddress) { _loanContractAddress = address (new LoanContract(_loanAmount, _duration, "", _interest, _collateralAddress, _collateralAmount, _collateralPriceInETH, 50, msg.sender, address(0), LoanContract.LoanStatus.REQUEST)); loans.push(_loanContractAddress); emit LoanRequestCreated(msg.sender, _loanContractAddress); return _loanContractAddress; } function getAllLoans() public view returns(address[] memory) { return loans; }
The second contract that we're creating is the LoanContract.sol.
Importing the dependencies for our contract:
Importing OpenZeppelin contract functionality. OpenZeppelin contracts help to minimize risks for end-users and give developers confidence by using well-tested libraries of smart contracts.
LoanMath is a library created for our mathematical functions (it can be found in the libs directory), it consists of all the financial-related functions being used in our smart contract.
String is a library for our string functions, it converts any type bytes32 into a string.
pragma solidity ^0.5.0; import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; import "openzeppelin-solidity/contracts/lifecycle/Pausable.sol"; import "openzeppelin-solidity/contracts/token/ERC20/IERC20.sol"; import "./libs/LoanMath.sol"; import "./libs/String.sol";
Contracts in Solidity are similar to classes in object-oriented languages. Calling a function on a different contract (instance) will perform an EVM function call and thus switch the context such that state variables in the calling contract are inaccessible.
Then we start creating our contract by declaring the data types we will use. The wallet address & admin address corresponding to our contract are also defined.
contract LoanContract { using SafeMath for uint256; uint256 constant PLATFORM_FEE_RATE = 100; address constant WALLET_1 = 0x88347aeeF7b66b743C46Cb9d08459784FA1f6908; uint256 constant SOME_THINGS = 105; address admin = 0x95FfeBC06Bb4b7DeDfF961769055C335542E1dBF;
Next, we'll make two enumerated lists: LoanStatus & CollateralStatus, which define user options to select from for both the lender & the loan requester.
enum LoanStatus { OFFER, REQUEST, ACTIVE, FUNDED, REPAID, DEFAULT } enum CollateralStatus { WAITING, ARRIVED, RETURNED, DEFAULT }
Generating records for the CollateralData & LoanData with struct types. Structs are used to represent a record, a data type with more than one member of different data types. The struct types we’re creating here have data about the collateral being provided by the loan requester & the details of the loan being created.
struct CollateralData { address collateralAddress; uint256 collateralAmount; uint256 collateralPrice; // will have to subscribe to oracle uint256 ltv; CollateralStatus collateralStatus; } struct LoanData { uint256 loanAmount; uint256 loanCurrency; uint256 interestRate; // will be updated on acceptance in case of loan offer string acceptedCollateralsMetadata; // json string uint128 duration; uint256 createdOn; uint256 startedOn; mapping (uint256 => bool) repayments; address borrower; address lender; LoanStatus loanStatus; CollateralData collateral; // will be updated on accepance in case of loan offer }
A function enrich loan is created to provide the details inside, once our loan is sanctioned.
function enrichLoan(uint256 _interestRate, address _collateralAddress, uint256 _collateralAmount, uint256 _collateralPriceInETH, uint256 _ltv) public { loan.interestRate = _interestRate; loan.collateral.collateralAddress = _collateralAddress; loan.collateral.collateralPrice = _collateralPriceInETH; loan.collateral.collateralAmount = _collateralAmount; loan.collateral.collateralStatus = CollateralStatus.WAITING; loan.collateral.ltv = _ltv; emit LoanContractUpdated(_interestRate, _collateralAddress, _collateralPriceInETH, _collateralAmount, _ltv); }
Below we are declaring our events to store the arguments passed to the respective functions in transaction logs, these logs are stored on the blockchain & can be accessed using the address of the contract. These events reference the collateral transfer, funds transfer, collateral return on complete loan repayment, collateral seizure in case of loan non-payment & in case any update is made to the loan contract.
event CollateralTransferToLoanFailed(address, uint256); event CollateralTransferToLoanSuccessful(address, uint256, uint256); event FundTransferToLoanSuccessful(address, uint256); event FundTransferToBorrowerSuccessful(address, uint256); event LoanRepaid(address, uint256); event LoanStarted(uint256 _value); // watch for this event event CollateralTransferReturnedToBorrower(address, uint256); event CollateralClaimedByLender(address, uint256); event CollateralSentToLenderForDefaultedRepayment(uint256,address,uint256); event LoanContractUpdated(uint256, address, uint256, uint256, uint256);
Here we’re declaring the constructor function to be executed for our solidity contract. Post execution, the final code of the contract is stored on the blockchain.
constructor(uint256 _loanAmount, uint128 _duration, string memory _acceptedCollateralsMetadata,uint256 _interestRate, address _collateralAddress,uint256 _collateralAmount, uint256 _collateralPriceInETH, uint256 _ltv, address _borrower, address _lender, LoanStatus _loanstatus) public { loan.loanAmount = _loanAmount; loan.duration = _duration; loan.acceptedCollateralsMetadata = _acceptedCollateralsMetadata; loan.interestRate = _interestRate; loan.createdOn = now; loan.borrower = _borrower; loan.lender = _lender; loan.loanStatus = _loanstatus; remainingCollateralAmount = _collateralAmount; loan.collateral = CollateralData(_collateralAddress, _collateralAmount, _collateralPriceInETH, _ltv, CollateralStatus.WAITING);
Later this will be filled when a borrower accepts the loan.
functions used:
  • transferFundsToLoan – to transfer funds to loan address after loan is sanctioned
  • toString – converts the address into a string to which loan is being sent
  • transferCollateralToLoan – transfers the collateral after the loan request is created
// after loan offer created function transferFundsToLoan() public payable OnlyLender { require(msg.value >= loan.loanAmount, "Sufficient funds not transferred"); loan.loanStatus = LoanStatus.FUNDED; // status changed OFFER -> FUNDED emit FundTransferToLoanSuccessful(msg.sender, msg.value); } function toString(address x) public returns (string memory) { bytes memory b = new bytes(20); for (uint i = 0; i < 20; i++) b[i] = byte(uint8(uint(x) / (2**(8*(19 - i))))); return string(b); } // after loan request created function transferCollateralToLoan() payable public OnlyBorrower { ERC20 = IERC20(loan.collateral.collateralAddress); LoanStatus prevStatus = loan.loanStatus; if(loan.collateral.collateralAmount > ERC20.allowance(msg.sender, address(this))) { emit CollateralTransferToLoanFailed(msg.sender, loan.collateral.collateralAmount); revert(); } loan.collateral.collateralStatus = CollateralStatus.ARRIVED;
An 'CollateralTransferToLoanSuccessful' is emitted, it stores the arguments passed in transaction logs. These logs are stored on blockchain and are accessible using address of the contract till the contract is present on the blockchain.
emit CollateralTransferToLoanSuccessful(msg.sender, loan.collateral.collateralAmount, loan.collateral.collateralPrice)
Some more functions created being used in our loan contract:
acceptLoanOffer calls an event that keeps track of the acceptance of the loan offered by the requester
function acceptLoanOffer(uint256 _interestRate, address _collateralAddress, uint256 _collateralAmount, uint256 _collateralPriceInETH, uint256 _ltv) public { require(loan.loanStatus == LoanStatus.FUNDED, "Incorrect loan status"); loan.borrower = msg.sender; /* This will call setters and enrich loan data */ enrichLoan(_interestRate,_collateralAddress,_collateralAmount, _collateralPriceInETH,_ltv); }
approveLoanRequest: This calls an event to show that the loan has been approved. The date and time when a loan is started will be stored and used to keep track of repayments.
function approveLoanRequest() public payable { require(msg.value >= loan.loanAmount, "Sufficient funds not transferred"); require(loan.loanStatus == LoanStatus.REQUEST, "Incorrect loan status"); loan.lender = msg.sender; loan.loanStatus = LoanStatus.FUNDED; emit LoanStarted(loan.startedOn); // We monitor this event and block time it was fired. every duration interval apart, we call function to make a call for potentially failed repayments emit FundTransferToLoanSuccessful(msg.sender, msg.value); loan.startedOn = now; address(uint160(loan.borrower)).transfer(loan.loanAmount); emit FundTransferToBorrowerSuccessful(loan.borrower, loan.loanAmount); }
getLoanData: This function will publicly view the loan details- the amount left, collateral status, loan status, addresses of the borrower & lender. At each repayment of the loan, this function feeds the values into the blockchain.
function getLoanData() view public returns ( uint256 _loanAmount, uint128 _duration, uint256 _interest, string memory _acceptedCollateralsMetadata, uint256 startedOn, LoanStatus _loanStatus, address _collateralAddress, uint256 _collateralAmount, uint256 _collateralPrice, uint256 _ltv, CollateralStatus _collateralStatus, uint256 _remainingCollateralAmount, address _borrower, address _lender) { return (loan.loanAmount, loan.duration, loan.interestRate, loan.acceptedCollateralsMetadata, loan.startedOn, loan.loanStatus, loan.collateral.collateralAddress, loan.collateral.collateralAmount, loan.collateral.collateralPrice, loan.collateral.ltv, loan.collateral.collateralStatus, remainingCollateralAmount, loan.borrower, loan.lender); }
getCurrentRepaymentNumber:The number of the current installment is returned as well as tracked throughout the repayment process.
function getCurrentRepaymentNumber() view public returns(uint256) { return LoanMath.getRepaymentNumber(loan.startedOn, loan.duration); }
getRepaymentAmount: The amount of each installment required for repayment is calculated based on the installment number & the interest being levied.
function getRepaymentAmount(uint256 repaymentNumber) view public returns(uint256 amount, uint256 monthlyInterest, uint256 fees){ uint256 totalLoanRepayments = LoanMath.getTotalNumberOfRepayments(loan.duration); monthlyInterest = LoanMath.getAverageMonthlyInterest(loan.loanAmount, loan.interestRate, totalLoanRepayments); if(repaymentNumber == 1){ fees = LoanMath.getPlatformFeeAmount(loan.loanAmount, PLATFORM_FEE_RATE); }else{ fees = 0; } amount = LoanMath.calculateRepaymentAmount(loan.loanAmount, monthlyInterest, fees, totalLoanRepayments); return (amount, monthlyInterest, fees); }
makeFailedRepayments: Based on the duration, it is triggered when we pass a repayment number from the UI.
function makeFailedRepayments(uint256 _repaymentNumberMissed) public OnlyAdmin { uint256 repaymentNumber = _repaymentNumberMissed; require(loan.repayments[repaymentNumber] == false,"repayment was already paid"); (uint256 _repayAmount,uint256 interest,uint256 fees) = getRepaymentAmount(repaymentNumber); uint256 collateralAmountToTrasnfer = LoanMath.calculateCollateralAmountToDeduct((_repayAmount.sub(fees)).mul(SOME_THINGS.div(100)),loan.collateral.collateralPrice); ERC20 = IERC20(loan.collateral.collateralAddress); ERC20.transfer(loan.lender, collateralAmountToTrasnfer); emit CollateralSentToLenderForDefaultedRepayment(repaymentNumber,loan.lender,collateralAmountToTrasnfer); }
repayLoan: Tracks if the loan has been completely paid & emits the event to be stored on the blockchain. The installment number of the repayment is also logged.
function repayLoan() public payable { require(now <= loan.startedOn + loan.duration * 1 minutes, "Loan Duration Expired"); uint256 repaymentNumber = LoanMath.getRepaymentNumber(loan.startedOn, loan.duration); (uint256 amount, , uint256 fees) = getRepaymentAmount(repaymentNumber); require(msg.value >= amount, "Required amount not transferred"); if(fees != 0){ transferToWallet1(fees); } uint256 toTransfer = amount.sub(fees); loan.repayments[repaymentNumber] = true; address(uint160(loan.lender)).transfer(toTransfer); emit LoanRepaid(msg.sender, amount); }
transferCollateralToWallet1: The collateral will be transferred in the wallet provided by the contract owner, for a fair use policy of the contract.
function transferToWallet1(uint256 fees) private { address(uint160(WALLET_1)).transfer(fees); } function transferCollateralToWallet1 (uint256 fees) private { uint256 feesInCollateralAmount = LoanMath.calculateCollateralAmountToDeduct(fees, loan.collateral.collateralPrice); ERC20 = IERC20(loan.collateral.collateralAddress); ERC20.transfer(WALLET_1, feesInCollateralAmount); }

Compile and migrate using truffle

LoanContract and LoanCreator are being compiled and migrated here along with a standard ERC-20 token.
Open truffle console to run a local blockchain in your terminal at localhost:9545:
truffle develop
This will start the Truffle development blockchain and display Account addresses along with their Private Keys and Mnemonics required for deploying the smart contracts.
In the truffle console compile the smart contracts:
truffle(develop)> compile Compiling your contracts... =========================== > Compiling .\contracts\LoanContract.sol > Compiling .\contracts\LoanCreator.sol > Compiling .\contracts\Migrations.sol > Compiling .\contracts\StandardToken.sol > Compiling .\contracts\libs\DateTime\DateTime.sol > Compiling .\contracts\libs\DateTime\api.sol > Compiling .\contracts\libs\LoanMath.sol > Compiling .\contracts\libs\LoanMethods.sol > Compiling .\contracts\libs\String.sol > Compiling openzeppelin-solidity\contracts\GSN\Context.sol > Compiling openzeppelin-solidity\contracts\access\Roles.sol > Compiling openzeppelin-solidity\contracts\access\roles\PauserRole.sol > Compiling openzeppelin-solidity\contracts\lifecycle\Pausable.sol > Compiling openzeppelin-solidity\contracts\math\SafeMath.sol > Compiling openzeppelin-solidity\contracts\ownership\Ownable.sol > Compiling openzeppelin-solidity\contracts\token\ERC20\IERC20.sol > Compilation warnings encountered: > Artifacts written to C:\Users\hp\cryptolend\build\contracts > Compiled successfully using: - solc: 0.5.0+commit.1d4f565a.Emscripten.clang
Now we can migrate (deploy) the compiled smart contracts to the locally running Truffle development blockchain:
truffle(develop)> migrate Starting migrations... ====================== > Network name: 'develop' > Network id: 5777 > Block gas limit: 6721975 (0x6691b7) 2_deploy_contracts.js ===================== Deploying 'LoanCreator' ----------------------- > transaction hash: 0x232be40e9171c62f74585c52e15492a8a8653b8a65eb9f97f6e57ccdcb0eec66 > Blocks: 0 Seconds: 0 > contract address: 0xc7Eb239cA1e53093B645A50d70B4a895AAD94cb0 > block number: 4 > block timestamp: 1629372448 > account: 0x2F3CeD6f849630301feC1dD613869E8cc3857665 > balance: 99.990053476 > gas used: 2460473 (0x258b39) > gas price: 2 gwei > value sent: 0 ETH > total cost: 0.004920946 ETH Deploying 'LoanContract' ----------------------- > transaction hash: 0x657we40e9575c62f74585c52e15492a8a8663b8a65eb9f97f6e57ccdcb0eec435 > Blocks: 0 Seconds: 0 > contract address: 0xb8b239cC1e53093D645A50c70B4a995BBC9acb > block number: 4 > block timestamp: 1629372448 > account: 0x2F3CeD6f849630301feC1dD613869E8cc3857665 > balance: 99.990053476 > gas used: 2460473 (0x258b39) > gas price: 2 gwei > value sent: 0 ETH > total cost: 0.004920946 ETH Deploying 'StandardToken' ------------------------- > transaction hash: 0xa74cf285dc8e80b73b91a9334304a408c973a67ecd9d8e700559c7c7d8e321d8 > Blocks: 0 Seconds: 0 > contract address: 0xBD613f04E9Fd211b95A608776620B6C49f11A421 > block number: 5 > block timestamp: 1629372449 > account: 0x2F3CeD6f849630301feC1dD613869E8cc3857665 > balance: 99.988584512 > gas used: 734482 (0xb3512) > gas price: 2 gwei > value sent: 0 ETH > total cost: 0.001468964 ETH > Saving migration to chain. > Saving artifacts Summary ======= > Total deployments: 2 > Final cost: 0.010922144 ETH - Saving migration to chain.

Using UI in the browser

Clone this Git Repository
git clone https://github.com/Devilla/cryptolend.ui.git
Browse the project directory:
cd cryptolend.ui
Install dependencies required for the project:
npm i
Run the server to be able to access the application UI in your browser:
npm start
In the metamask supported browser create a Custom RPC in the Networks as follows:
Open the lending dapp in browser and go to the /myloans path and connect to Polygon Network in the Metamask extention. Now feel free to go throgh the various features of thhe lending marketplace like creating a loan request on /request path and loan offer on /offer, which can also be browsed from the Navigation bar.

Conclusion

Now you know about creating a Lending Marketplace with Truffle Suite and ReactJS on the Polygon network.

About the authors

This tutorial is created by Devendra Yadav (Blockchain Dev) and Prince Rana (Data Specialist).

References

Table of Contents