The Ultimate Guide To Learn Solidity From Scratch By Building a Lending Dapp Step-by-Step in 2024

Merunas Grincalaitis
31 min readNov 16, 2023

--

It’s time to become a blockchain developer and the best way to do that is to learn solidity, the core language of Ethereum. Whether you have programming experience or not, this is the right tutorial for you.

Blockchain developers earn on average $157k dollars. Now the market is more competitive than it was a few years ago but the salaries stay strong. You could be one of them if you prove your skills. In this guide I’m giving you exactly that.

This guide is huge. It contains a ton of code so you better be prepared because you’re gonna be working by coding. And I want you to code everything by hand, meaning no copy pasting because otherwise you wouldn’t learn as much.

Here’s the end result, a beautiful working fully decentralized web3 application where you can lend and borrow crypto:

Writing code by hand is the most effective learning method.

These are the things you’ll learn by the end of this guide:

  1. How to setup a Smart Contract Solidity project with Hardhat
  2. How to write variables and functions in Solidity
  3. How to build a lending smart contract from scratch (part 1)
  4. How to build a lending smart contract from scratch (part 2)
  5. How to build a lending smart contract from scratch (part 3)
  6. How to test your ethereum solidity smart contracts
  7. How to create the frontend website for your lending dapp (part 1)
  8. How to create the frontend website for your lending dapp (part 2)
  9. Recap on everything you’ve learned in this guide

The best way to learn is to put your knowledge to work with a real use case project. In this case we’ll be building a lending DeFi dapp. It’s one of the most useful use cases and the cornerstone of all DeFi.

Let’s get right into it!

All I’m asking is that if you like this type of content make sure to join my email list for early access to top tier content in crypto: https://forms.gle/NVxcYUtEvRj8aa9G9 and subscribe to my sniper bot at premium.mevdao.org

1. How to setup a Smart Contract Solidity project with Hardhat

The first thing you’re gonna do it create a new folder in your desktop called solidity-learning then you'll setup hardhat, which is a framework for writing dapps.

Open a terminal and run npm init -y. You must have node.js installed, if you haven't done it go ahead and download it to install it from here: https://nodejs.org/en/

The command npm init simply sets up the initial node.js project.

Then install yarn if you haven’t done so with the command npm i -g yarn. Inside the same folder, run: yarn add hardhat. This will install hardhat.

After that, run npx hardhat which will show you a set of questions that you must respond with enter to all of them:

A bunch of files have been created for you.

The hardhat.config.js file contains all the configuration for your project. The folder contracts/ is where your Solidity smart contracts will live and the test/ folder will include the tests to your contracts. Those are the main things.

That’s all you need to do to setup the initial project with hardhat, in the next section you’ll see how to actually write smart contracts and build the lending dapp I’ve promised you.

2. How to write variables and functions in Solidity

Inside your solidity-learning folder there’s one called contracts with a contract called Lock.sol . Delete that file. Create one called Lending.sol .

Then, write the following code:

pragma solidity 0.8.0;contract Lending {
constructor() {}
}

The first line tells the program which version of solidity we want to use, in this case it’s 0.8.0, then we define the contract with the name Lending and create a constructor() which is simply the first function that will be executed when the contract is created.

That’s the basic setup. You can compile your contract with npx hardhat compile which will tell you any errors you’ve made.

In this case it will tell you:

The Solidity version pragma statement in these files doesn’t match any of the configured compilers in your config.

That means your hardhat tool doesn’t know which version of solidity to use. That’s okay, in the configuration we’ll change that.

Open hardhat.config.js which should be at the root of your folder. And change this line:

module.exports = {
solidity: "0.8.17",
};

The first line tells the program which version of solidity we want to use, in this case it’s 0.8.0, then we define the contract with the name Lending and create a constructor() which is simply the first function that will be executed when the contract is created.

Don’t worry too much if there’s a newer version of Solidity. Previous versions work just as well as newer versions which means your contract will be the same regardless. Use the version specified in this guide.

That’s the basic setup. You can compile your contract with npx hardhat compile which will tell you any errors you’ve made.

In this case it will tell you:

The Solidity version pragma statement in these files doesn’t match any of the configured compilers in your config.

That means your hardhat tool doesn’t know which version of solidity to use. That’s okay, in the configuration we’ll change that.

Open hardhat.config.js which should be at the root of your folder. And change this line:

module.exports = {  
solidity: "0.8.17",
};

To solidity: "0.8.0", which will now allow you to compile with npx hardhat compile .

Let’s get into variables.

Variables in solidity are simple, there are all these types:

Numbers

Here’s how they look like: uint256, uint8, uint128, int128 they allow you to define numbers like so:

uint256 public myNumber;

Where public is the visibility and myNumber is the variables name. If you want to update a number variable you simply do:

myNumber = 123;

You will use uint256 99% of the time because it's the largest number storage, meaning you can store almost infinitely big numbers. Almost.

The other types such as uint8, uint128 and so on, are more situational dependant. You'll use uint8 for variables where you don't expect to store large numbers.

Strings

Here’s how they look like: string, bytes both work as strings and allow you to store up to 10,000 characters in one variable like so:

string public myArticle = "this is a test string";

Bytes

Here’s how they look like: bytes32, byte, bytes they allow to store text and simply any binary data if you will.

Where bytes32 is the most used for performance reasons, it will allow you to store about 32 characters. Think of it as exactly the same as string but only for 32 characters.

Addresses

Here’s how they look like: address, address payable those are simply ethereum addresses.

Payable addresses are the ones that allow you to receive money from ethereum transfers.

You write addresses without quotes like so:

address public userAccount = 0xdafea492d9c6733ae3d56b7ed1adb60692c98bc5;

Mappings and arrays

Here’s how they look like: mapping, uint256[] they allow you to store large amounts of data.

Mapping is a relationship between a key and a value and you define it like so:

mapping (address => uint256) public myAddresses;

Where the variable types inside determine the key and value.

Then you can add values to the mapping this way:

myAddresses[0xdafea492d9c6733ae3d56b7ed1adb60692c98bc5] = 10

As you can see it’s pretty straightforward.

When it comes to arrays, you simply take the existing types such as bytes32, uint256, address and add the brackets [] like so:

uint256[] public myNumbers;
myNumber[1] = 20;

Mappings are more of a complex topic that we’ll see later on. Arrays can be from pretty much any of types we’ve seen except mapping. You can’t make an array of mappings.

Booleans

A boolean is the simplest type of variable it can either be true or false and you define it this way:

bool public isUserValid;
isUserValid = true;

By default all booleans are false.

We’ll see variables in practice and functions in the next section. Let’s talk about functions now.

Functions

Functions are the main blocks where your code will be executed. Each function has a goal. Think of them as tasks that are completed by the blockchain. Let’s take a look at one.

function thisIsMyFunction(uint256 _myNumber) public onlyOwner returns(uint256) {}

As you can see there’s a lot going on there. However all you need to know right now is that the function name is thisIsMyFunction and the parameters that the function receive are in brackets, in this case a number called _myNumber .

In this case the function thisIsMyFunction will receive a variable number, will do some operations and then give you another number.

In solidity we use the underscore in front of function parameter names to not confuse them with internal and state variables. But you shouldn’t be concerned with that yet, just letting you know.

The public keyword tells us it can be executed by anybody and onlyOwner is a modifier with is simply some code, defined at the beginning of the contract, executed to check some things first. In this case it restricts access to only the owner of the contract.

Don’t think too much about it, I’m just throwing you everything there is so you can have an idea later on.

Finally the returns(uint256) keyword indicates that this function returns a number.

Let’s go ahead and put it all into practice by building a simple yet powerful lending dapp! This will be your introduction to DeFi.

3. How to build a lending smart contract from scratch (part 1)

We’re gonna create a dapp (decentralized application) that allows 1 person to deposit money into a Smart Contract so that others can borrow that money and repay it back with interest after some time.

All of this is decentralized. Compared to traditional lending where a person goes to a lender, usually a bank, and asks for money. He may or may not get the money based on how likely he’s to pay it back.

If he gets the loan he will have to pay back more money than he received, that’s how the lender makes his money.

First let’s create the smart contract file. Inside your contracts/ folder add a new file called Lending.sol. You'll notice that contracts usually are uppercase because the contract name inside is also uppercase.

Open the file and write the basic structure:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.0;
contract Lending {}

As you can see we first start with a comment which indicates the license identifier we want to use for that contract. This is so other people don’t steal your code. Although that’s a bit difficult to enforce in the blockchain.

After that, we define the version of solidity we’re gonna use. In this case is 0.8.0. The version doesn't really matter because all contracts are capable of the same things however the way they are written changes slightly between versions. Keep that in mind.

You can write all your life contracts in the version 0.8.0 and it will continue working 10, even 100 years in the future with no issues. That's the beauty of the blockchain.

Coming back to the topic at hand, we then create the contract with the contract name. In this case it’s called Lending. Inside the curly brackets is where all your contract code lives.

Now let’s add some state variables. State variables are variables that all your functions inside the contract can access and those variables get written into the blockchain permanently.

// SPDX-License-Identifier: MIT
pragma solidity 0.8.0;
contract Lending {
uint256 public availableFunds;
address public depositor;
uint256 public interest = 10;
uint256 public repayTime;
uint256 public repayed;
uint256 public loanStartTime;
uint256 public borrowed;
bool public isLoanActive;
}

As you can see all of them are public and they are the types we’ve seen before. The only variable that we initialize is interest to 10, which represents 10% meaning people that borrow money need to pay 10% more of what they’ve been given to the lender.

The other variables we’ll use later on.

Now let’s define the constructor. The constructor is the function that gets executed right when you create and deploy the smart contract.

// SPDX-License-Identifier: MIT
pragma solidity 0.8.0;
contract Lending {
uint256 public availableFunds;
address public depositor;
uint256 public interest = 10;
uint256 public repayTime;
uint256 public repayed;
uint256 public loanStartTime;
uint256 public borrowed;
bool public isLoanActive;

constructor(uint256 _repayTime, uint256 _interest) payable {
repayTime = _repayTime;
interest = _interest;
depositor = msg.sender;
deposit();
}

function deposit() public payable {
require(msg.sender == depositor, 'You must be the depositor');
availableFunds = availableFunds + msg.value;
}
}

As you can see the constructor doesn’t start with the function keyword. We added the repay time and interest variables as well as the payable modifier so people can send ether to the contract and deposit funds for others to borrow right away.

Inside the constructor I’m simply setting the up the parameters to the variables so they are stored in the blockchain.

The msg.sender keyword contains the address of whoever is executing the contract. In this case it's the address of the creator of the contract. So we simply store that data inside the depositor variable because we'll use it later on.

Notice how I’ve added the deposit() function. This function will be used for the depositor to add some more funds to the contract for people to borrow.

Inside that function, we use require() this is a special function inside solidity that stops the function execution if the condition inside is false.

Let’s say someone with the address 0x2112bb05f70898cd022c5a1452a78a1bf79df5d8 executes the function deposit() to add some ether to the contract.

The require function will look like this:

require(0x2112bb05f70898cd022c5a1452a78a1bf79df5d8 == 0x2112bb05f70898cd022c5a1452a78a1bf79df5d8);

Our depositor is also 0x2112bb05f70898cd022c5a1452a78a1bf79df5d8 since we set that variable right in the constructor. So if the depositor and the executor variable are the same, the contract allows you to continue executing the function.

In this case we’re simply updating the variable availableFunds with msg.value. Where msg.value is the ether you sent along with the function execution.

In ethereum you can execute functions AND send ether along the way at the same time. When that happens, the ether amount is stored in a special fixed variable called msg.value. So if you execute deposit() and attach 1 ether during the execution, the variable msg.value will be equal to 1 ether.

That’s also way we must mark the function as payable, this is to tell ethereum that that particular function allows you to send ether to it.

When that happens, the ether is kept inside the smart contract as if it were a regular person’s wallet. That ether will stay there inside. That’s why you must write functions to extract those funds if they get stuck, but that’s a topic for another day.

That’s the first step towards becoming an ethereum developer, let’s continue in the next section to see how to add the other functions. Keep going!

4. How to build a lending smart contract from scratch (part 2)

Now that you know more about setting up a smart contract from scratch, let’s get into the juicy part. You’ll create the borrow function next. First create the function structure like so:

function borrow(uint256 _amount) public {}

You can put it right below the deposit function. In the borrow function, any user comes in and executes it with the _amount of ETH he wants to borrow.

Let’s write the function body. Always start with adding the require checks on top to make sure the function is executed the right way:

function borrow(uint256 _amount) public {
require(_amount > 0, 'Must borrow something');
require(availableFunds >= _amount, 'No funds available');
require(block.timestamp > loanStartTime + repayTime, 'Loan expired must be repayed first');
}

I forgot to mention that the string inside the require() statement is simply the error that will be shown to the user if that check fails.

So first we check that the amount to borrow is higher than zero. We don’t want the user to borrow 0 ether obviously. If the user executed the function with the _amount being zero, we simply stop the function and show him the message Must borrow something. Pretty clear.

Then we check the available funds to borrow. If you remember in the deposit function we were increasing the availableFunds variable and here is where we use it to make sure people can't borrow more than what's available.

Then we simply check that the loan isn’t expired. Notice the block.timestamp that's a special fixed variable that exists in every function and it contains a timestamp like this: 1664322046 which represents a date. In this case it represents September 27 2022 in seconds.

We use it to check the current date and to see if when the loan started + the time the user has to repay it back, has expired. In this case, if the loan is expired, the user won’t be able to get another loan until the previous expired one is repaid.

Let’s now add the rest of the code for this function:

function borrow(uint256 _amount) public {
require(_amount > 0, 'Must borrow something');
require(availableFunds >= _amount, 'No funds available');
require(block.timestamp > loanStartTime + repayTime, 'Loan expired must be repayed first');

if (borrowed == 0) {
loanStartTime = block.timestamp;
}
availableFunds = availableFunds - _amount;
borrowed = borrowed + _amount;
isLoanActive = true;
payable(msg.sender).transfer(_amount);
}

Here we check if the borrowed amount is zero, meaning the user hasn’t borrowed anything yet, then we set the loan start time.

Then we reduce the available funds, we increased the borrowed state variable and set the loan to active with the boolean isLoanActive state variable.

Finally we transfer the funds to the user. As you can see, every address payable type of variable, in this case msg.sender, has a transfer() function attached. That function receives 1 parameter which is how many ETH you want to send to the address.

Here we take the msg.sender variable (which is address type), we convert it into address payable so we can make transfers. Then we transfer the amount requested by the borrower.

That’s pretty much it, let’s continue with the next important function: the repay loan function!

5. How to build a lending smart contract from scratch (part 3)

Now that people can borrow and lend funds, let’s complete the lending contract with a repay function. This will allow borrowers to repay their loans as they should to continue borrowing in the future.

Of course, this contract is just an introduction and it isn’t perfect. People could simply take loans and never repay them back. That’s up to you, to enforce it in the code by only allowing people you choose to borrow.

You could use a collateral to borrow which is the popular option to make fully decentralized loans. But we won’t cover that in this course. Try to implement it yourself to learn this properly.

Let’s go ahead and create the repay function by starting with the require statements:

function repay() public payable {
require(isLoanActive, 'Must be an active loan');
}

We define the function with the name repay() and make it payable so people can send the funds they borrowed with interest back to the contract.

Then we make sure the loan is active otherwise people won’t be able to repay it.

function repay() public payable {
require(isLoanActive, 'Must be an active loan');
uint256 amountToRepay = borrowed + (borrowed * interest / 100);
uint256 leftToPay = amountToRepay - repayed;
uint256 exceeding = 0;
}

We’ve defined a few variables:

  • amountToRepay is a variable that checks how much you've borrowed and adds the interest
  • leftToPay is a variable that checks how much needs to be payed back and how much has been repaid so far.
  • exceeding is a variable that we'll use later on for any excess ETH the user may sent to the function so we can refund that back.

Keep in mind: in solidity and ethereum you can’t have decimals. Meaning numbers like this are allowed : 12345 but numbers like this aren't: 1234.5382. That's because decimals can lead to precision errors and variable results which is not compatible with ethereum.

Every block, every function execution should be replicable with the same results. That’s mostly why decimals aren’t allowed.

That’s why you can’t have divisions that result in a number lower than zero. Therefore you must always multiply first and divide second so that doesn’t happen.

function repay() public payable {
require(isLoanActive, 'Must be an active loan');
uint256 amountToRepay = borrowed + (borrowed * interest / 100);
uint256 leftToPay = amountToRepay - repayed;
uint256 exceeding = 0;

if (msg.value > leftToPay) {
exceeding = msg.value - leftToPay;
isLoanActive = false;
} else if (msg.value == leftToPay) {
isLoanActive = false;
} else {
repayed = repayed + msg.value;
}
}

This new piece of code simply checks if the user sent too much ether to the function and calculates the excess to refund it later on. So if the user sends enough or more ether than he owes, then the loan can be considered closed, fully paid back.

We do that with the isLoanActive variable.

function repay() public payable {
require(isLoanActive, 'Must be an active loan');
uint256 amountToRepay = borrowed + (borrowed * interest / 100);
uint256 leftToPay = amountToRepay - repayed;
uint256 exceeding = 0;

if (msg.value > leftToPay) {
exceeding = msg.value - leftToPay;
isLoanActive = false;
} else if (msg.value == leftToPay) {
isLoanActive = false;
} else {
repayed = repayed + msg.value;
}

payable(depositor).transfer(msg.value - exceeding);
if (exceeding > 0) {
payable(msg.sender).transfer(exceeding);
}
}

Now I’ve added the transfer of the funds to the depositor (the person that offered the loan). Basically he’ll receive whatever he lent plus the interest.

If there’s an excess, meaning the borrower sent too much money, we simply give it back. That’s all it does there.

function repay() public payable {
require(isLoanActive, 'Must be an active loan');
uint256 amountToRepay = borrowed + (borrowed * interest / 100);
uint256 leftToPay = amountToRepay - repayed;
uint256 exceeding = 0;

if (msg.value > leftToPay) {
exceeding = msg.value - leftToPay;
isLoanActive = false;
} else if (msg.value == leftToPay) {
isLoanActive = false;
} else {
repayed = repayed + msg.value;
}

payable(depositor).transfer(msg.value - exceeding);
if (exceeding > 0) {
payable(msg.sender).transfer(exceeding);
}

// Reset everything
if (!isLoanActive) {
borrowed = 0;
availableFunds = 0;
repayed = 0;
loanStartTime = 0;
}
}

If the loan is closed and fully repaid back, we reset all the state variables so people can borrow funds again with that last piece of code.

That’s about it! The lending contract is ready to be used and deployed! Let’s now make it real by creating a frontend website that allows people to interact with the contract.

But before that, let me show you real quick how to test your contract to make sure it works and it doesn’t have any errors. Let’s go.

6. How to test your ethereum solidity smart contracts

Testing your contracts is extremely important. Not only you’re making sure your contract works as expected but you’re making sure funds from other people are safe.

Unlike traditional apps where you only test them to make sure things properly, in ethereum things are far more delicate. The contracts once deployed will stay there for eternity. That means you have to do everything you can to make sure the code works long after you create it.

To start, open your test/ folder that hardhat created for you in the first step and create a file called Lending.js. Then add the following code:

const { expect } = require('chai')
const { ethers } = require('hardhat')
let lending = null
describe('Lending', function () {})

We’re importing chai and hardhat. Chai is simply a tool to test things easily. After that we define a variable called lending which will contain an instance of the contract we just wrote.

Then we start the test by using the describe() function which is where all the tests will live.

Now we’re gonna add the function to deploy a test contract:

describe('Lending', function () {
beforeEach(async () => {
const Lending = await ethers.getContractFactory('Lending')
lending = await Lending.deploy(30 * 24 * 60 * 60, 10) // 30 days in seconds
})
})

The beforeEach function contains a piece of code that gets executed right before every test. In this case we're accessing the Lending contract and deploying it to a test blockchain created by hardhat. It's a local blockchain that lives in your computer.

Let’s now write a test:

describe('Lending', function () {
beforeEach(async () => {
const Lending = await ethers.getContractFactory('Lending')
lending = await Lending.deploy(30 * 24 * 60 * 60, 10) // 30 days in seconds
})

it('Should deposit successfully', async () => {
const oneEth = ethers.BigNumber.from('1000000000000000000')
await lending.deposit({ value: oneEth })
expect(await lending.availableFunds()).to.eq(oneEth)
})
})

The test starts with the it() function and it starts with a message indicating what you're testing. In this case I'm testing the deposit functionality.

First I create a variable to contain 1 ether. You should know that 1 ether is represented as 1 with 18 zeroes since there are no decimals in solidity.

Therefore the lowest unit 1 is called wei and 1 ether is 100000000000000000 wei so that people have enough space in that number to make calculations.

The variable oneEth is a BigNumber. That's a special type created by the library ethers so that we can deal with large numbers without losing precision.

The reason being the fact that javascript has difficulty dealing with large numbers, it approximates and misses decimals when the number is too large.

After setting the variable, we execute the deposit() function and we pass it 1 eth since the function is payable which means it can receive ether.

Then we simply check that the function has executed properly by checking if the availableFunds in the smart contract has a value of 1 ether, which is exactly what we sent to it.

To run the test, go to your terminal or command line and type:

npx hardhat test

It will execute the test and show it passing. If it doesn’t complete successfully it means your contract has some error that you need to fix to make it run. That’s exactly what you look for so you can improve your code.

Let’s add another test:

it('Should borrow funds successfully', async () => {
const [, borrower] = await ethers.getSigners()
const oneEth = ethers.BigNumber.from('1000000000000000000')
await lending.deposit({ value: oneEth })
expect(await lending.availableFunds()).to.eq(oneEth)

const balance1 = await ethers.provider.getBalance(borrower.address)
lending = lending.connect(borrower)
const tx = await lending.borrow(oneEth.div(2))
const receipt = await tx.wait()
const gasUsed = receipt.gasUsed.mul(receipt.effectiveGasPrice)
const balance2 = await ethers.provider.getBalance(borrower.address)

expect(await lending.availableFunds()).to.eq(oneEth.div(2))
expect(balance2.add(gasUsed)).to.eq(balance1.add(oneEth.div(2)))
})

There’s a lot going on so let me go step-by-step:

  1. We are gonna get the borrower address. We do it with the getSigners() method. By using an array we simply request the second account. Don't think too much about it.
  2. Then we repeat the previous test with the deposit functionality. Don’t be afraid to repeat code in tests and write “improperly” because the code won’t be used outside testing your contract.
  3. Now the real test begins, we get the balance of the borrower account. We do that with the getBalance method from the ethers provider. We'll use it later on.
  4. In order for the borrower to interact with the contract and execute the functions, we need to let the app know we want that with the lending.connect() function, which receives the entire borrower object, meaning without accessing the address parameter. We simply store that connection into the same lending variable because we want to interact with the smart contract as the borrower.
  5. Now we execute the borrow function from the lending smart contract. While at the same time storing the transaction in a variable called tx. In this case we're borrowing half an ether.
  6. Then we wait() for the transaction to be processed by the blockchain in order to get the receipt which is simply a bunch of data about the transaction from the blockchain. The only reason we do this is so we can...
  7. Check the gas used. The way we get the gas consumed by the transaction is by multiplying the gasUsed by the effectiveGasBalance. You don't have to understand it deeply, only know that this is how we get the gas value used. We need it for later.
  8. Then we check the balance of the borrower again to see how it changed after executing the borrow() from the smart contract, to see if we actually got the ether or not.
  9. Finally we check the availableFunds variable and see if it's equal to half an ether. If it is, that means the borrow function executed correctly. We also check the final borrower's balance, we add the gas consumed and compare it to the previous balance plus half an ether. That way we can see the balance increasing.

That was a lot. But hey, I’m sure you learned quite a lot.

Now let’s add a final test to make sure the repay() function from your contract works as expected.

Here’s the entire test:

it('Should repay a loan partially', async () => {
const [lender, borrower] = await ethers.getSigners()
const oneEth = ethers.BigNumber.from('1000000000000000000')
await lending.deposit({ value: oneEth })
expect(await lending.availableFunds()).to.eq(oneEth)

const balance1 = await ethers.provider.getBalance(borrower.address)
lending = lending.connect(borrower)
const tx = await lending.borrow(oneEth.div(2))
const receipt = await tx.wait()
const gasUsed = receipt.gasUsed.mul(receipt.effectiveGasPrice)
const balance2 = await ethers.provider.getBalance(borrower.address)

expect(await lending.availableFunds()).to.eq(oneEth.div(2))
expect(balance2.add(gasUsed)).to.eq(balance1.add(oneEth.div(2)))

const balance3 = await ethers.provider.getBalance(lender.address)
await lending.repay({ value: oneEth.div(10) })
const balance4 = await ethers.provider.getBalance(lender.address)
expect(balance4).to.eq(balance3.add(oneEth.div(10)))
})

As you can see this time we’re getting the address of the lender and the borrower because we'll need both to simulate the entire deposit, borrow and repay process.

Most of the code is the same as the previous test, the only thing changed is this portion:

const balance3 = await ethers.provider.getBalance(lender.address)
await lending.repay({ value: oneEth.div(10) })
const balance4 = await ethers.provider.getBalance(lender.address)
expect(balance4).to.eq(balance3.add(oneEth.div(10)))

Where we basically check the lender balance first, then we run the repay() function to repay a portion of the loan, 10% of 1 ether. Then we check that it's repaid correctly with the final expect() check.

That’s about it! You know now how to test smart contracts. It’s a simply but long process that needs to be done if you want to run any kind of public smart contract.

As an exercise, I want you to add one last test to test the repayment of the entire loan with interest.

Once you finish that, continue reading because we’re gonna create the website that interacts with the smart contract. The most engaging part of the whole process.

7. How to create the frontend website for your lending dapp (part 1)

The Smart Contract are ready. You implemented the right functions and tested the properly to verify they function as you want. Now it’s time to create the website which will be used to interact with the Smart Contract.

First inside your project folder create a new folder called website/ you can call it however you want.

You’re gonna learn how to create the website in HTML, CSS and vanilla javascript. Start by creating a file called index.html inside website/. Add the following code:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Lending page</title>
<style>
.box {
display: none;
max-width: 700px;
margin: auto;
}
</style>
<!-- CSS only -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-iYQeCzEYFbKjA/T2uDLTpkwGzCiq6soy8tYaI1GyVh/UjpbCx/TYkiZhlZB6+fzT" crossorigin="anonymous">
</head>
<body>

</body>
</html>

That’s the basic html structure. The <title> tag contains the name of the website which will be shown at the top of your browser like in the image below:

In the <style> tag is where we store all of our CSS. CSS is just a way to edit how your website looks and HTML is the structure, the text content of it.

Finally you’ve added a <Link> to import Bootstrap which is a library, an utility to speed up the website creation process with predefined styles. Sounds confusing? It's just a shortcut to design the website.

This is how the dapp will look like:

Let’s add the design elements so it actually looks like that. Copy the following code by hand and put it in between the <body> </body> tags:

<!-- 1 -->
<nav class="navbar navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="/">Lending DeFi dApp</a>
</div>
</nav>

<!-- 2 -->
<div class="container text-center">
<div class="row">
<div class="col">
As a lender: Deploy a new lending contract, deposit some test ETH and share the contract address with your friend.
</div>
</div>
<div class="row">
<div class="col">
As a borrower: Paste the contract address in the box and borrow some funds. Then make sure to repay them before the agreed date.
</div>
</div>
</div>

<!-- 3 -->
<div class="container text-center">
<div class="row">
<div class="col">
<button onclick="showLend()" type="button" class="btn btn-secondary">I want to lend crypto</button>
</div>
<div class="col">
<button onclick="showBorrow()" type="button" class="btn btn-secondary">I want to borrow crypto</button>
</div>
<div class="col">
<button onclick="showRepay()" type="button" class="btn btn-secondary">I want to repay my loan</button>
</div>
</div>
</div>

<!-- 4 -->
<form id="lend" class="box" onsubmit="deployAndDeposit(event)" >
<div class="mb-3">
<label for="repay-time" class="form-label">Repay time in days</label>
<input type="number" oninput="validateRepayInput(event)" class="form-control" id="repay-time" aria-describedby="emailHelp">
<div id="emailHelp" class="form-text">How many days the borrower has to pay you back</div>
</div>
<div class="mb-3">
<label for="interest-percentage" class="form-label">Interest percentage</label>
<input type="number" oninput="validateInterest(event)" class="form-control" id="interest-percentage">
<div class="form-text">Between 0 and 100</div>
</div>
<div class="mb-3">
<label for="deposit-to-lend" class="form-label">Max amount to lend</label>
<input type="number" step="0.0001" oninput="validateDeposit(event)" class="form-control" id="deposit-to-lend">
<div class="form-text">In ETH</div>
</div>
<button type="submit" class="btn btn-primary">Deploy Lending Smart Contract & Deposit</button>
<div class="mb-3">
<br/>
<p id="deployed-contract"></p>
</div>
</form>

<!-- 5 -->
<form id="borrow" class="box" onsubmit="borrow(event)" >
<div class="mb-3">
<label for="lending-borrow-contract" class="form-label">Lending contract address</label>
<input type="text" class="form-control" id="lending-borrow-contract" aria-describedby="emailHelp">
<div id="emailHelp" class="form-text">The deployed lending contract</div>
</div>
<div class="mb-3">
<label for="lending-borrow-input" class="form-label">Borrow amount</label>
<input type="number" step="0.0001" oninput="validateBorrowAmount(event)" class="form-control" id="lending-borrow-input">
<div class="form-text">In ETH</div>
</div>
<button type="submit" class="btn btn-primary">Borrow</button>
</form>

<!-- 6 -->
<form id="repay" class="box" onsubmit="repay(event)" >
<div class="mb-3">
<label for="lending-repay-contract" class="form-label">Lending contract address</label>
<input type="text" class="form-control" id="lending-repay-contract" aria-describedby="emailHelp">
<div id="emailHelp" class="form-text">The deployed lending contract</div>
</div>
<div class="mb-3">
<label for="repay-input" class="form-label">Repay amount</label>
<input type="number" step="0.0001" class="form-control" id="repay-input">
</div>
<button type="submit" class="btn btn-primary">Repay</button>
</form>

I’ve marked out each section with a comment. Comments in html look like this: <!-- 1 --> so that information between those special tags won't be shown anywhere, only shown to you as a coder.

If you haven’t done so already, install Metamask.io to your browser and setup a blockchain wallet. This will be your door to the world of crypto. You’ll need it to use the dapp you’re creating.

This is not a web development course therefore I won’t get too deep into how this code works or what every little thing means. In any case, I’ll go through each part so you understand what you’re writing. Keep up by checking the comments marked from 1 to 6:

  1. In this first block we’re adding the navbar, the top black bar you see on top of the website with a link to the home page.
  2. Here we’re simply adding 2 descriptive texts to let people know how to use this dapp.
  3. Here we’re adding 3 buttons to let people choose what they want to do. Once each button is clicked, a javascript function is executed as you can see in the onclick="" attribute.
  4. This block contains the first part of the website which is creating a form where depositors can input the repay time and the interest percentage. Once the user clicks on the button Deploy Lending Smart Contract & Deposit the function deployAndDeposit(event) will be executed. We'll add that function later on. Once executed, the user will see a Metamask popup to let the user execute the Smart Contract creation. This is where the constructor() of your Lending smart contract will be executed as we talked in previous sections.
  5. After the contract is deployed, the user will see the contract address which will be shared with the person that wants to borrow funds. This fifth block will be shown after the user clicks on the button I want to borrow crypto. This block will be a form where the borrowers come and input the deployed lending smart contract and choose how much they want from the available lending funds.
  6. This is the repay block. It will be displayed after the user clicks on I want to repay my loan . The user will input the contract address and the amount to repay. Once those inputs are set, the user will click on Repay which will trigger a blockchain transaction that will be shown in metamask and the borrower will simply repay the loan, either partially or fully.

That was a lot, wasn’t it? It’s all good, you simply have to copy the code by hand and write it down in your code editor.

Continue reading for the next section so you can implement the javascript functions.

8. How to create the frontend website for your lending dapp (part 2)

Now let’s get into the final part. You’ll implement the javascript part which is the most exciting section. I personally love javascript because it’s a proper programming language and you can create pretty much any application you can think of once you master it.

I’ll write down the entire javascript section and you’ll copy it by hand, function by function. Then I’ll explain the code.

Right after your </body> close tag, before your </html> closing tag, add this code:

<script type="module">
// 1
import { ethers } from "./ethers.js"
window.ethers = ethers
import lendingJson from './artifacts/contracts/Lending.sol/Lending.json' assert { type: 'json' }
const lendingAbi = lendingJson.abi
const lendingBytecode = lendingJson.bytecode

// 2
let repayTime = null
let interest = null
let deposit = null
let eth = {
provider: null,
address: null,
signer: null,
lendingContract: null,
borrowAmount: null,
}

// 3
const setup = async () => {
eth.provider = new ethers.providers.Web3Provider(window.ethereum)
eth.address = (await eth.provider.send("eth_requestAccounts", []))[0]
eth.signer = eth.provider.getSigner(eth.address)
}

// 4
// In type="module" functions are not global by default that's why we gotta use window
window.showLend = () => {
[...document.querySelectorAll('.box')].map(box => box.style.display = 'none')
document.querySelector('#lend').style.display = 'block'
}
window.showBorrow = () => {
[...document.querySelectorAll('.box')].map(box => box.style.display = 'none')
document.querySelector('#borrow').style.display = 'block'
}
window.showRepay = () => {
[...document.querySelectorAll('.box')].map(box => box.style.display = 'none')
document.querySelector('#repay').style.display = 'block'
}

// 5
window.validateRepayInput = event => {
event.target.value = event.target.value.replace(/[^0-9]*/g,'');
repayTime = Number(event.target.value)
}
window.validateInterest = event => {
event.target.value = event.target.value.replace(/[^0-9]*/g,'');
interest = Number(event.target.value)
}
window.validateDeposit = event => {
deposit = event.target.value
}
window.validateBorrowAmount = e => {
eth.borrowAmount = e.target.value
}

// 6
window.deployAndDeposit = async e => {
e.preventDefault()
if (!repayTime) return alert('Must set the repay time in days')
if (!interest) return alert('Must set the interest percentage')
if (interest < 0 || interest > 100) return alert('Interest percentage must be between 0 and 100')
if (!deposit) return alert('Must set the deposit amount')

const factory = new ethers.ContractFactory(lendingAbi, lendingBytecode, eth.signer)
eth.lendingContract = await factory.deploy(repayTime, interest, {
value: window.ethers.FixedNumber.from(deposit) // This converts the number to wei
})
console.log('eth.lendingContract', eth.lendingContract)
document.querySelector('#deployed-contract').innerHTML = 'Deploying contract...: ' + eth.lendingContract.address
console.log('deploying...')
await eth.lendingContract.deployTransaction.wait()
console.log('eth.lendingContract', eth.lendingContract)
console.log('deployed!')
alert('deployed!')
}

// 7
window.borrow = async e => {
e.preventDefault()
const lendingAddress = document.querySelector('#lending-borrow-contract').value
const borrowAmount = Number(document.querySelector('#lending-borrow-input').value)
if (borrowAmount <= 0) return alert('Must set the borrow amount')
try {
eth.lendingContract = new ethers.Contract(lendingAddress, lendingAbi, eth.signer)
} catch (e) {
return alert('Invalid lending address')
}
const tx = await eth.lendingContract.borrow(
window.ethers.FixedNumber.from(String(borrowAmount))
)
await tx.wait()
console.log('Borrow successful.')
alert('Borrow successful!')
}

// 8
window.repay = async e => {
e.preventDefault()

const lendingAddress = document.querySelector('#lending-repay-contract').value
const repayAmount = Number(document.querySelector('#repay-input').value)
if (repayAmount <= 0) return alert('Must set the repay amount')
try {
eth.lendingContract = new ethers.Contract(lendingAddress, lendingAbi, eth.signer)
} catch (e) {
return alert('Invalid lending address')
}
const tx = await eth.lendingContract.repay({
value: window.ethers.FixedNumber.from(String(repayAmount))
})
await tx.wait()
console.log('Repay successful.')
alert('Repay successful!')
}

// 9
setup()
</script>

I’ve marked down each section with a comment. Comments in javascript start with a double slash // simply find each number to see what I'm referring to:

  1. First we’re using the <script type="module"> tag which is telling the browser that we're adding javascript code. Type module attribute is important.
  2. Then we’re importing ethers.js which is the library we use to communicate with the blockchain from the browser, in order to do that, go here: https://cdnjs.cloudflare.com/ajax/libs/ethers/5.7.1/ethers.esm.js, copy the entire code (not by hand) and create a file inside website/ called ethers.js. Paste the code there. Once imported, we're getting the Lending Smart Contract ABI. The ABI is simply a short document explaining the browser which functions your contract can execute. You need it. To get it, open a terminal in your folder and type npm hardhat compile which will generate an artifacts/ folder. Simply copy that entire folder inside website/ and you'll be able to access the ABI.
  3. Next, we’re setting up some variables we’ll use later on. Variables in javascript are declared with let and const and you don't have to specify the type. The variable eth here is an object, which contains some variables inside we'll need.
  4. Then we’re creating the setup() function. As you can see we create it with that syntax. All you need to know there is that we're connecting to the blockchain by requesting the window.ethereum variable which is injected into the website by metamask. I know, it sounds confusing.
  5. After that we create several functions called showLend(), showBorrow() and showRepay(). Their only purpose is to show and hide the right form for the user. Right after clicking on one of the 3 buttons.
  6. Then we create several validate variables whose only purpose is to get the data inputted by the user and store it into variables while also checking that it's correctly formatted.
  7. Here we create the deployAndDeposit() function which is triggered after the depositor clicks on the Deploy Lending Smart Contract & Deposit button. This function checks that the right data is in place and deploys a new version of the smart contract. I've also added some logs with console.log() which can be seen once you open the website on your browser if you right click anywhere to select Inspect once which will open up the javascript console with the logs.
  8. You guessed it, this is the borrow function and it receives the contract address which is used to execute the borrow() function from the smart contract. We use window.ethers.FixedNumber.from() to convert the number the user input into a number that ethers.js can understand.
  9. This is the repay function which again receives the contract address, creates a contract instance and executes the repay() function from the smart contract. Notice we're passing value to the function which is how we send ether to the smart contract along with the function execution.
  10. Finally we execute the setup function which will run the moment the website loads. Remember setup has been written before.

That’s about it! The dapp is ready. You can now open the index.html file in your browser or by simply double clicking on it and interact with the app. However it won't fully yet.

You must setup a local server to open the app so it works with metamask without errors. To do that, install http-server like so:

npm i -g http-server

Then, open a terminal in your project folder and do:

http-server website/

Which will serve the website folder, meaning it will create a local server that works in your computer and make it accessible via a url. Simply open the browser and navigate to http://localhost:8080 and you'll see your dapp work.

Feel free to interact with it see how data is stored into the blockchain permanently.

Here’s how it should look like:

The guide is now complete. To wrap it up, continue reading the next section where we’ll summarise everything you’ve learned in each section so you can come back to it in the future and go straight to what you need to remember.

9. Recap on everything you’ve learned in this guide

There was a lot of content in this guide so you probably want to know where all the key information is, in case you want to come back and re-learn some concepts. Let’s go part by part.

  1. The Ultimate Guide To Learn Ethereum Development From Scratch By Building a Lending Dapp Step-by-Step: Here you simply have an index about what you’re about to learn, a simple introduction.
  2. How to setup a Smart Contract Solidity project with Hardhat: Here you learn how to install and setup a project with hardhat which is one of the most popular ethereum project utilities.
  3. How to write variables and functions in Solidity: Here you learn the types of variables there are in Solidity, how they look like, how to interact with them and how functions work.
  4. How to build a lending smart contract from scratch (part 1): Here you learn how to start creating a smart contract with the basic structure, how to write state variables, how a constructor works and you start to write functions that work. You also learn how to organize your smart contract to make a lending dapp possible and use global variables like msg.sender and msg.value.
  5. How to build a lending smart contract from scratch (part 2): Here you learn how to create the borrow function, how require functions work, payable functions and global variables like block.timestamp.
  6. How to build a lending smart contract from scratch (part 3): Here you learn how to create the repay() function, how decimals are treated in ethereum and how to refund excess ether sent to the contract.
  7. How to test your ethereum solidity smart contracts: Here you learn how to start testing your smart contract fully including how to use ethers, hardhat and chai.
  8. How to create the frontend website for your lending dapp (part 1): Here you learn how to create the frontend facing website by creating the initial HTML and CSS structure including importing bootstrap and using inline javascript functions.
  9. How to create the frontend website for your lending dapp (part 2): Here you learn to implement javascript into your application with the purpose of connecting your buttons and inputs with the smart contract so all the information is stored in the blockchain.

That’s about it, hope you learned some good value and apply it! If you need anything just make sure to re-read the content until it makes sense and compliment your learning with other tutorials.

Have a blessed decade.

Clap this article 50 times if it helped you!

If you liked this guide make sure to join my email list for early access to top tier content in crypto: https://forms.gle/NVxcYUtEvRj8aa9G9 and subscribe to my sniper bot at premium.mevdao.org

--

--

Merunas Grincalaitis
Merunas Grincalaitis

Written by Merunas Grincalaitis

Blockchain expert. Join my email list here to receive new articles every few months https://merunasgrincalaitis.medium.com/subscribe

Responses (2)