Introduction
Welcome to our peer-to-peer lending tutorial! In this tutorial, we will learn how to build a decentralized lending platform on the Ethereum blockchain using Solidity.
Our lending platform allows borrowers and lenders to connect directly, eliminating the need for intermediaries. By leveraging smart contracts, we ensure secure and transparent lending transactions.
In this tutorial, we will cover the following key features:
- Loan Creation: Borrowers can request loans by specifying the loan amount, interest rate, and duration.
- Loan Funding: Lenders can fund loan requests by directly sending the requested amount to the borrower’s address.
- Loan Repayment: Borrowers can repay their loans by sending the loan amount plus interest to the contract. Once the repayment is received, the loan is marked as completed.
- Interest Calculation: The contract automatically calculates the interest amount based on the loan parameters and remaining duration.
We will also integrate an ERC20 token, such as cUSD, for seamless transfer of loan amounts between borrowers and lenders.
By the end of this tutorial, you will have a solid understanding of building a peer-to-peer lending platform on the Ethereum blockchain. Let’s get started!
REQUIREMENTS
To follow this tutorial, you will require:
- A code editor or text editor such as Remix.
- An internet browser and a stable internet connection.
PREREQUISITES
To successfully complete this tutorial, it is recommended that you have:
- Familiarity with Javascript programming language.
- A basic understanding of Blockchain technology and its functioning.
- Basic knowledge of the Solidity programming language used for smart contract development on the blockchain.
We will begin by using the Remix IDE to write our smart contract. Let’s get started!
The complete code:
// SPDX-License-Identifier: MIT
pragma solidity >=0.7.0 <0.9.0;
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
interface IERC20Token {
function transfer(address, uint256) external returns (bool);
function approve(address, uint256) external returns (bool);
function transferFrom(address, address, uint256) external returns (bool);
function totalSupply() external view returns (uint256);
function balanceOf(address) external view returns (uint256);
function allowance(address, address) external view returns (uint256);
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
}
contract PeerToPeerLending {
using SafeMath for uint256;
address private cUsdTokenAddress = 0x874069Fa1Eb16D44d622F2e0Ca25eeA172369bC1;
struct Loan {
address payable borrower;
uint256 amount;
uint256 interestRate;
uint256 duration;
uint256 endTime;
bool isActive;
bool isCompleted;
}
uint256 private loanIdCounter = 0;
mapping(uint256 => Loan) private loans;
modifier onlyActiveLoan(uint256 loanId) {
require(loans[loanId].isActive, "Loan is not active");
_;
}
function createLoan(uint256 amount, uint256 interestRate, uint256 duration) external {
require(amount > 0, "Loan amount must be greater than zero");
require(interestRate > 0, "Interest rate must be greater than zero");
require(duration > 0, "Loan duration must be greater than zero");
Loan storage newLoan = loans[loanIdCounter];
newLoan.borrower = payable(msg.sender);
newLoan.amount = amount;
newLoan.interestRate = interestRate;
newLoan.duration = duration;
newLoan.endTime = block.timestamp.add(duration);
newLoan.isActive = true;
newLoan.isCompleted = false;
loanIdCounter++;
}
function getLoan(uint256 loanId) public view returns (address payable, uint256, uint256, uint256, uint256, bool, bool) {
Loan storage loan = loans[loanId];
return (
loan.borrower,
loan.amount,
loan.interestRate,
loan.duration,
loan.endTime,
loan.isActive,
loan.isCompleted
);
}
function fundLoan(uint256 loanId) external payable onlyActiveLoan(loanId) {
Loan storage loan = loans[loanId];
require(msg.value == loan.amount, "Incorrect loan amount");
IERC20Token(cUsdTokenAddress).transferFrom(msg.sender, loan.borrower, loan.amount);
loan.isActive = false;
}
function interestAmount(Loan storage loan) internal view returns (uint256) {
uint256 calculatedInterestAmount = loan.amount.mul(loan.interestRate).div(100);
uint256 remainingDays = loan.endTime.sub(block.timestamp).div(1 days);
return calculatedInterestAmount.mul(remainingDays).div(365);
}
}
Code Analysis:
Step 1: Setting up the Smart Contract and Importing Dependencies
// SPDX-License-Identifier: MIT
pragma solidity >=0.7.0 <0.9.0;
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
In this step, we will set up the initial structure of our smart contract, Thriftbooks
, and import the necessary dependencies. We will be using the Solidity programming language and the OpenZeppelin library.
To begin, we specify the SPDX-License-Identifier
to indicate the license under which our contract is released. In this case, we use the MIT license
.
We then define our contract, Thriftbooks
, which is marked with the pragma directive specifying the Solidity version to be used. We indicate that the version should be greater than or equal to 0.7.0
and less than 0.9.0
.
Next, we import the SafeMath
library from the OpenZeppelin
library. SafeMath provides arithmetic operations that prevent common vulnerabilities such as integer overflow or underflow.
By including these dependencies, we ensure that our contract has access to safe mathematical operations and is compliant with the specified version of Solidity.
Setting up the contract and importing necessary dependencies are the first steps in building our Thriftbooks marketplace. These initial preparations lay the foundation for implementing the functionalities that will be added in subsequent steps.
Step 2: Defining the ERC20 Token Interface
interface IERC20Token {
function transfer(address, uint256) external returns (bool);
function approve(address, uint256) external returns (bool);
function transferFrom(address, address, uint256) external returns (bool);
function totalSupply() external view returns (uint256);
function balanceOf(address) external view returns (uint256);
function allowance(address, address) external view returns (uint256);
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
}
In this step, we define the ERC20Token
interface, which allows our smart contract to interact with ERC20-compliant tokens. ERC20 is a widely adopted standard for fungible tokens on the Celo blockchain.
The interface lists a set of functions
and events
that a token contract should implement to be considered ERC20 compliant.
-
The
transfer
function allows the token contract to transfer a certain amount of tokens from one address to another. It takes two parameters: the recipient’s address and the amount of tokens to be transferred. -
The
approve
function allows an address to approve another address to spend a specified amount of tokens on its behalf. It takes two parameters: the spender’s address and the amount of tokens approved. -
The
transferFrom
function allows an address to transfer tokens on behalf of another address, given that the spender has been approved to spend the tokens. It takes three parameters: theowner's
address, therecipient's
address, and the amount of tokens to be transferred. -
The
totalSupply
function returns the total supply of tokens in circulation. -
The
balanceOf
function returns the balance of tokens owned by a specific address. -
The
allowance
function returns the amount of tokens that the owner has approved the spender to spend. -
The
Transfer
event is emitted when tokens are transferred from one address to another. It includes the sender’s address, the recipient’s address, and the amount of tokens transferred. -
The
Approval
event is emitted when an address approves another address to spend a certain amount of tokens on its behalf. It includes the owner’s address, the spender’s address, and the amount of tokens approved.
By defining the ERC20Token interface, we establish a common set of functions and events that our smart contract can utilize to interact with ERC20 tokens. This allows for seamless integration with existing ERC20 token contracts and ensures compatibility with the broader Celo ecosystem.
Step 3: Creating the PeerToPeerLending Contract
contract PeerToPeerLending {
using SafeMath for uint256;
address private cUsdTokenAddress = 0x874069Fa1Eb16D44d622F2e0Ca25eeA172369bC1;
struct Loan {
address payable borrower;
uint256 amount;
uint256 interestRate;
uint256 duration;
uint256 endTime;
bool isActive;
bool isCompleted;
}
uint256 private loanIdCounter = 0;
mapping(uint256 => Loan) private loans;
modifier onlyActiveLoan(uint256 loanId) {
require(loans[loanId].isActive, "Loan is not active");
_;
}
In this step, we create the PeerToPeerLending
contract, which serves as the core smart contract for our peer-to-peer lending platform.
The contract begins by importing the SafeMath
library from the OpenZeppelin library. SafeMath provides arithmetic operations with safety checks to prevent overflow and underflow vulnerabilities.
Inside the contract, we define a private
variable called cUsdTokenAddress
, which represents the address of the cUSD token. This token is used as the payment currency for the loans in our lending platform.
Next, we define a struct
called Loan
. This struct represents a single loan request made by a borrower. It contains the following fields: borrower (an address payable)
, amount (the loan amount in cUSD)
, interestRate (the interest rate for the loan)
, duration (the duration of the loan in seconds)
, endTime (the timestamp when the loan ends)
, isActive (a boolean indicating if the loan is active)
, and isCompleted (a boolean indicating if the loan is completed)
.
We also declare a private
variable called loanIdCounter
, which will be used to assign unique identifiers to each loan request. This variable is initialized to 0
.
To store and manage loan requests, we create a mapping
called loans. The mapping maps loan identifiers (uint256)
to Loan structs. By using the loanIdCounter
, we can easily access loan details based on their unique identifiers.
To ensure certain conditions are met, we define a modifier called onlyActiveLoan
. This modifier can be applied to functions and requires that the loan specified by the loanId is active. If the condition is not met, the function call will be reverted with an error message.
With the PeerToPeerLending
contract and its necessary components in place, we are now ready to proceed with implementing the functionalities of our lending platform.
Step 4: Creating a Loan Request
function createLoan(uint256 amount, uint256 interestRate, uint256 duration) external {
require(amount > 0, "Loan amount must be greater than zero");
require(interestRate > 0, "Interest rate must be greater than zero");
require(duration > 0, "Loan duration must be greater than zero");
Loan storage newLoan = loans[loanIdCounter];
newLoan.borrower = payable(msg.sender);
newLoan.amount = amount;
newLoan.interestRate = interestRate;
newLoan.duration = duration;
newLoan.endTime = block.timestamp.add(duration);
newLoan.isActive = true;
newLoan.isCompleted = false;
loanIdCounter++;
}
In this step, we implement the createLoan
function in the PeerToPeerLending
contract. This function allows borrowers to create loan requests on the platform.
The createLoan
function is declared as external
, meaning it can be called by external accounts. It takes three parameters: amount
(the loan amount in cUSD), interestRate
(the interest rate for the loan), and duration
(the duration of the loan in seconds).
Before creating the loan request, we add several require
statements to validate the input parameters. We check that the loan amount, interest rate, and duration are all greater than zero
. If any of these conditions fail, the function call will be reverted with an appropriate error message.
If the validation passes, we create a new Loan struct
called newLoan
and assign it to the loans mapping using the loanIdCounter
as the key. We use the loanIdCounter
to generate a unique identifier for the loan request.
Inside the newLoan
struct, we set the borrower to the address of the caller (msg.sender)
, the amount to the provided amount, the interestRate to the provided interestRate, the duration to the provided duration, and the endTime
to the current block timestamp plus the duration. We also set isActive
to true to indicate that the loan is active and isCompleted
to false since the loan is not yet completed.
Finally, we increment the loanIdCounter
to prepare for the next loan request.
With the createLoan
function implemented, borrowers can now create loan requests by providing the loan amount, interest rate, and duration. These loan requests will be stored in the loans mapping with unique identifiers, and the necessary validations are in place to ensure the input parameters are valid.
Step 5: Retrieving Loan Details
function getLoan(uint256 loanId) public view returns (address payable, uint256, uint256, uint256, uint256, bool, bool) {
Loan storage loan = loans[loanId];
return (
loan.borrower,
loan.amount,
loan.interestRate,
loan.duration,
loan.endTime,
loan.isActive,
loan.isCompleted
);
}
In this step, we implement the getLoan
function in the PeerToPeerLending
contract. This function allows users to retrieve the details of a specific loan by providing the loan ID.
The getLoan
function is declared as public and view, meaning it can be called by any external account and does not modify the contract’s state.
The function takes one parameter, loanId
, which represents the unique identifier of the loan we want to retrieve.
Inside the function, we retrieve the Loan struct
associated with the given loanId from the loans mapping and assign it to the loan variable using the storage keyword.
Then, we return a tuple containing the following loan details in the specified order:
borrower
: the address of the borrower (payable)amount
: the loan amountinterestRate
: the interest rate for the loanduration
: the duration of the loanendTime
: the timestamp indicating when the loan endsisActive
: a boolean indicating whether the loan is activeisCompleted
: a boolean indicating whether the loan is completed
By using the getLoan function, users can easily fetch the details of a specific loan by providing its unique loan ID. This provides transparency and allows interested parties to access relevant information about a loan, such as the borrower’s address, loan amount, interest rate, duration, and status.
Step 6: Funding a Loan and Calculating Interest
In this step, we will cover two functions: fundLoan
and interestAmount
in the PeerToPeerLending
contract.
The fundLoan
function allows lenders to fund a specific loan by providing the loan ID
and sending the loan amount as Celo along with the transaction. It is declared as external
and payable
, indicating that it can be called externally and requires the caller to send Celo to fund the loan.
Inside the function, we first retrieve
the Loan struct
associated with the given loanId
from the loans mapping and assign it to the loan variable using the storage keyword.
We then use a require
statement to verify that the amount sent with the transaction matches the loan amount. This ensures that the correct loan amount is funded.
Next, we call the transferFrom
function of the IERC20Token
interface, using the cUsdTokenAddress
, to transfer the loan amount from the lender (msg.sender)
to the borrower (loan.borrower)
. This assumes that the contract has been granted the necessary approval to transfer funds on behalf of the lender.
Finally, we set the isActive
flag of the loan to false
, indicating that the loan is no longer active.
The interestAmount
function is an internal
view function that calculates the interest amount for a given loan. It takes a Loan struct as a parameter and returns the calculated interest amount as a uint256 value.
Inside the function, we calculate the interest amount by multiplying the loan amount with the interest rate and dividing the result by 100
. This gives us the interest amount based on the loan amount.
We then calculate the remaining number of days until the loan’s endTime by subtracting the current block timestamp
from the loan’s endTime
and dividing it by the number of seconds in a day (1 days).
Finally, we multiply the calculated interest amount by the remaining number of days and divide it by 365
to get the proportional interest amount based on the remaining loan duration.
These two functions allow lenders to fund loans and borrowers to calculate the interest amount owed on their loans. This completes the implementation of the core functionalities of our peer-to-peer lending platform.
Click here to get the complete code for this session
CONTRACT DEPLOYMENT
To deploy the peer-to-peer lending platform smart contract on the Celo blockchain, follow the steps below:
-
Install Celo Extension Wallet: Download and install the Celo Extension Wallet from the Google Chrome store. Create a wallet and securely store your key phrase.
-
Fund your wallet: Copy your wallet address and paste it into the Celo Faucet. Confirm the transaction to receive Celo tokens in your wallet.
-
Open Remix and create a new Solidity file: Paste the insurance contract code into the file. Ensure that the Solidity compiler is set to version 0.8.7 or later.
-
Compile the contract: Click the
loan.sol
button in the Solidity Compiler tab in Remix. -
Deploy the contract: In the
Deploy & Run Transactions
tab, select the Celo network from the dropdown menu. Connect your wallet to Remix by clickingConnect to wallet
. Selectinsurance
from theContract
dropdown menu. Click theDeploy
button, confirm the transaction in your wallet, and wait for the transaction to be confirmed on the Celo blockchain. -
Interact with the contract: Once the transaction is confirmed, the
laon
contract will be deployed on the Celo blockchain. You can interact with it using Remix.
CONCLUSION
In this tutorial, we explored the implementation of a peer-to-peer lending platform using Solidity on the Celo blockchain. We covered key steps such as defining the loan structure
, creating loans
, retrieving loan details
, funding loans
, calculating interest amounts
, and more.
By leveraging smart contracts and blockchain technology, we eliminated the need for intermediaries, enabling direct lending between borrowers and lenders. This offers a transparent, secure, and efficient alternative to traditional lending systems.
The PeerToPeerLending
contract showcased the power of Solidity and demonstrated how various functions and modifiers can be used to facilitate lending transactions, track loan details, and ensure the integrity of the platform.
Through this tutorial, you gained insights into building a decentralized lending platform and learned about the essential concepts and code snippets involved in the process. This knowledge can serve as a foundation for further exploration and enhancement of peer-to-peer lending applications on the blockchain.
NEXT STEPS
Great job! It’s always helpful to provide additional resources for further learning. Don’t hesitate to reach out if you have any more questions or if you need further assistance. Happy learning!
About the author
My name is Ogoyi Thompson, and I’m a web3 developer based in Nigeria, you can connect with me on twitter. I am enthusiastic about working with blockchain technology.