How to code your own Lending Protocol

Alexandre Ramos
Coinmonks
6 min readMar 4, 2022

--

Build a decentralized lending protocol from scratch with hardhat, Solidity and Typescript.

Photo by Kanchanara on Unsplash

Introduction

Today we are going to learn how to build your own lending protocol, using Solidity, Typescript and Openzeppelin.

First, I have to explain exactly what we are going to build and then jump right into the code.

Second, this protocol is for study purposes only, it’s not sustainable and is not production ready.

Lending Protocol

What we are going to build is simple.

A lender can deposit DAI into the protocol’s pool and receive a bDAI token (interest bearing DAI) in return.

Subsequently the protocol deposits that amount of DAI inside AAVE Lending Pool to earn interest while waiting for a borrower or the lender to withdraw.

Borrowers can deposit ether as collateral and borrow DAI up to 80% of its collateral value.

Borrowers can be liquidated when the value of its debt surpasses 80% of its collateral value.

Note: The ether deposited as collateral is also deposited into AAVE’s lending pool to earn interest.

The protocol also has a reserve, from time to time, collects all interests from AAVE’s lending pool and deposits on the reserve.

The reserve also receives all the fees from the loan fees when a borrower pays the loan.

About the liquidation, when a liquidation occurs, the ether liquidated is swapped for DAI using Uniswap and deposited on the reserve.

The Math

Now let’s talk about the math behind the protocol.

The formulas I’m using are inspired by Anchor protocol and Compound finance.

All the loans are algorithmically determined by the borrow rate.

The borrow rate constantly varies based on the market supply and demand (utilization ratio).

Utilization ratio

The utilization ratio quantifies a borrow demand relative to the amount deposited of DAI.

Borrow Rate

The borrow rate increases proportionally with the utilization ratio.

Some parameters values initially configured (fixed annual borrow rate and base rate).

Deposit Rate

Also the deposit rate increases proportionally the utilization ratio.

This indicators makes rates variable based on protocol usage. When the protocol has a lower utilization ratio, it lower the deposit/borrow rate to attract more borrowers than depositors.

When the protocol usage is high, it increase the borrow/deposit rates, to attract deposits and drive borrowers away.

bDAI

Each bDAI is convertible into a increasing quantity of DAI as interest accrues, the exchange rate between bDAI and DAI is calculated using this formula:

Getting started

Let’s jump right to code, if you are not familiar to hardhat i recommend read its documentation.

Requirements

  • Git
  • Node.JS
  • Typescript
  • NPM

The quickest way to get everything is to just do the following:

git clone https://github.com/AlexRamos93/solidity-defi
cd solidity-defi
npm install

Inside the contract folder, open the file called BondToken.sol and should look like the code below.

I'm not going to walk through the whole code, but i will highlight the key parts to help you understand what’s going on.

As you can see, we have some interfaces, let me explain each one of them.

  • ILendingPool: Is the Aave lending contract that we need to interact in order to send the deposits and collaterals.
  • IWETHGateway: Since we are dealing with native ETH, it must be first wrapped into WETH in order to interact with Aave protocol, so the WETHGateway is a helper contract to easily wrap and unwrap ETH.
  • ISwapRouter: Is the Uniswap contract for swapping tokens, when we need to swap the wrapped eth into DAI.
  • AggregatorV3Interface: Chainlink data feed contract, we need to get the ether market price from the real-world.

In line 47, we instantiate our contract with a couple of inheritances.

  • ERC20Burnable: Openzeppelin implementation of a ERC20 contract with burn functionality.
  • Ownable: Access control contract
  • Math: A contract to help us do calculations.

As you can see we have a couple contract variables, I won’t walk through all of them, they are very straightforward.

Then we instantiate the external contracts that we will need to call.

Now let's talk about some important functions.

These functions serve as deposit and withdrawal for lenders.

In order for the bondAsset function to work, first the lender needs to approve the same amount of DAI for the contract be able to spend that DAI.

After the contract gets the DAI, it increases the totalDeposit amount and send it to Aave lending pool, afterwards, it gets the exchangeRate and mint bDAI for the msg.sender.

The unbondAsset function is simple as well, what it does is, multiple the _amount of DAI the user wants to withdraw by the current exchangeRate , after, it decreases the totalDeposit , burn the amount in bDAI and then withdraw from Aave and send it to the user.

Other key functions in the contract is, addCollateral and removeCollateral .

addCollateral has nothing special on it, is a payable function to be able to receive ether from the msg, it adds the amount in ether to the usersCollateral and send that amount to Aave protocol.

Note: The contract can't just send ether to Aave, first calls the WETHGateway to wrap that ether and then deposit on Aave's pool.

At removeCollateral function, as you can see it can be a little tricky.

First, get the market price of ether from the oracle (Chainlink). Then maps the users collateral and borrow amount, and checks if there's any amount left to remove.

If the remove amount is lower than the left amount, the contract removes that amount of collateral and sends it back to the user.

Note: Again, since the Aave can't handle native ether, we need to use the WETHGateway to unwrap the ether and send it back.

Last but not least, the borrow, repay and liquidation functions.

The borrow function is very simple, just checks if the msg.sender has enough collateral and then removes the DAI from Aave and sends it to the borrower.

For repay to work, the caller needs to approve the amount of DAI it wants to pay back. After that, the contract calculates the borrowing fee.

The borrowing fee goes to the reserve and the rest goes back to the deposits.

The liquidation function, different from the other functions, can only be called by the contract owner.

What the liquidation function does is, basically, checks if the loan value is higher than 80% of the collateral in USD. If that is true, that collateral (ether) is swapped to DAI, and if that amount of DAI is higher than the loan, the extra amount is sent to the reserve and the rest goes back to the deposits.

Conclusion

This was a brief walk through in how to code your own lending pool.

The rest of the code can be found in this repository on github.

Like a said before, this contract is not suitable for production and has a lot improvements to be made.

You won't find unit/integration tests on this repository, i'm still learning how to mock external contracts, but if you know any good resources or tutorials on this subject, please leave on the comments.

References

--

--

Alexandre Ramos
Coinmonks

Software Engineer, Blockchain Enthusiast & Crypto Investor