Introduction
In this tutorial, we will explore the Loan Management Contract, a decentralized smart contract built on the Celo blockchain using Solidity. We will learn how to create loans, lend funds, and repay loans in a secure and transparent manner. The contract allows borrowers to define loan parameters such as principal amount, interest rate, due date, and penalty rate. Lenders can then fund these loans, and borrowers can easily repay them. By leveraging features like access control modifiers and the integration of the OpenZeppelin library for secure math operations and ERC20 token interactions, we ensure a robust and reliable loan management system. Join us on this tutorial journey to understand how to participate in decentralized lending and borrowing using the Loan Management Contract.
Requirements
In order to complete this tutorial, you will require:
-
A code editor or text editor such as Remix that you can access.
-
A dependable internet connection and web browser.
Prerequisites
-
A fundamental understanding of Javascript.
-
Familiarity with the workings of Blockchain.
-
Basic knowledge of the Solidity programming language.
Now lets get started!
Let’s start creating our smart contract.
Here is the full code:
// SPDX-License-Identifier: MIT
pragma solidity >=0.7.0 <0.9.0;
import { SafeMath } from "@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 LoanManagementContract {
using SafeMath for uint256;
uint256 private loansCount = 0;
address private cUsdTokenAddress =
0x874069Fa1Eb16D44d622F2e0Ca25eeA172369bC1;
struct Loan {
address borrower;
address lender;
uint256 principal;
uint256 interestRate;
uint256 dueDate;
uint256 penaltyRate;
bool isExtended;
bool isRepaid;
}
mapping(uint256 => Loan) private loans;
mapping(uint256 => bool) private exists;
modifier exist(uint256 _index) {
require(exists[_index], "Query of non-existent loan");
_;
}
modifier onlyLoanParticipant(uint256 _index) {
require(
msg.sender == loans[_index].borrower ||
msg.sender == loans[_index].lender,
"Unauthorized participant"
);
_;
}
modifier onlyBorrower(uint256 _index) {
require(
msg.sender == loans[_index].borrower,
"Only borrower can perform this operation"
);
_;
}
modifier onlyLender(uint256 _index) {
require(
msg.sender == loans[_index].lender,
"Only lender can perform this operation"
);
_;
}
modifier notRepaid(uint256 _index) {
require(!loans[_index].isRepaid, "Loan has already been repaid");
_;
}
modifier notExtended(uint256 _index) {
require(!loans[_index].isExtended, "Loan has already been extended");
_;
}
function createLoan(
address _lender,
uint256 _principal,
uint256 _interestRate,
uint256 _dueDate,
uint256 _penaltyRate
) external {
require(_principal > 0, "Principal amount must be greater than zero");
require(
_interestRate > 0,
"Interest rate must be greater than zero"
);
require(_dueDate > block.timestamp, "Invalid due date");
loans[loansCount] = Loan(
msg.sender,
_lender,
_principal,
_interestRate,
_dueDate,
_penaltyRate,
false,
false
);
exists[loansCount] = true;
loansCount++;
}
function repayLoan(uint256 _index)
external
payable
exist(_index)
onlyBorrower(_index)
notRepaid(_index)
{
Loan storage loan = loans[_index];
uint256 repaymentAmount = loan.principal.add(
calculateInterest(loan.principal, loan.interestRate)
);
require(
msg.value >= repaymentAmount,
"Insufficient funds to repay the loan"
);
loan.isRepaid = true;
if (msg.value > repaymentAmount) {
payable(msg.sender).transfer(msg.value.sub(repaymentAmount));
}
}
function calculateInterest(uint256 _principal, uint256 _interestRate)
private
pure
returns (uint256)
{
return _principal.mul(_interestRate).div(100);
}
function getLoanDetails(uint256 _index)
external
view
exist(_index)
returns (
address borrower,
address lender,
uint256 principal,
uint256 interestRate,
uint256 dueDate,
uint256 penaltyRate,
bool isExtended,
bool isRepaid
)
{
Loan memory loan = loans[_index];
borrower = loan.borrower;
lender = loan.lender;
principal = loan.principal;
interestRate = loan.interestRate;
dueDate = loan.dueDate;
penaltyRate = loan.penaltyRate;
isExtended = loan.isExtended;
isRepaid = loan.isRepaid;
}
function extendLoan(uint256 _index)
external
exist(_index)
onlyBorrower(_index)
notExtended(_index)
{
Loan storage loan = loans[_index];
require(!loan.isRepaid, "Cannot extend a repaid loan");
loan.isExtended = true;
loan.dueDate = loan.dueDate.add(30 days);
}
function assessPenalty(uint256 _index)
external
exist(_index)
onlyLoanParticipant(_index)
notRepaid(_index)
{
Loan storage loan = loans[_index];
require(block.timestamp > loan.dueDate, "No penalty is due");
uint256 penaltyAmount = calculateInterest(
loan.principal,
loan.penaltyRate
);
require(
IERC20Token(cUsdTokenAddress).transferFrom(
msg.sender,
loan.lender,
penaltyAmount
),
"Transfer failed."
);
}
}
Code Analysis:
// SPDX-License-Identifier: MIT
pragma solidity >=0.7.0 <0.9.0;
import { SafeMath } from "@openzeppelin/contracts/utils/math/SafeMath.sol";
The line SPDX-License-Identifier: MIT
is where we specify the license under which the code is distributed. In this case, we use the MIT License, which is a permissive open-source license.
Moving to the next line, we declare the version of the Solidity programming language we are using in our smart contract. By stating pragma solidity >=0.7.0 <0.9.0
, we ensure that the code is compatible with Solidity version 0.7.0
or any newer version up until, but not including, 0.9.0
. This way, we define the language version we are working with, providing clarity on the expected compatibility and behavior of the contract.
And also we import the SafeMath
library from the OpenZeppelin library to ensure secure mathematical operations. By utilizing this library, we can prevent common vulnerabilities such as integer overflow and underflow.
Moving forward, we will include the ERC20 token interface in our smart contract.
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
);
}
The IERC20Token
interface is a standard interface that we can use to interact with ERC-20 tokens in Solidity. By implementing this interface in our smart contracts, we can access a set of predefined functions and events that allow us to manage and transfer tokens.
With this interface, we can use functions like transfer
, approve
, and transferFrom
to perform token transfers between addresses. These functions enable us to move tokens from one account to another, either directly or on behalf of another address.
We also have access to functions like totalSupply
, balanceOf
, and allowance
. These functions allow us to retrieve information about the total supply of tokens, check the balance of a specific address, and determine the amount of tokens that a spender is allowed to transfer on behalf of an owner.
Furthermore, the interface defines two events: Transfer
and Approval
. These events are emitted when token transfers and approvals occur, providing us with a way to track and listen to these events within our smart contracts.
By leveraging the IERC20Token
interface, we can seamlessly integrate ERC-20 tokens into our Solidity projects and build applications that can interact with various tokens in a standardized manner.
Moving ahead, we will name our contract as LoanManagementContract
.
contract LoanManagementContract {
using SafeMath for uint256;
uint256 private loansCount = 0;
address private cUsdTokenAddress =
0x874069Fa1Eb16D44d622F2e0Ca25eeA172369bC1;
struct Loan {
address borrower;
address lender;
uint256 principal;
uint256 interestRate;
uint256 dueDate;
uint256 penaltyRate;
bool isExtended;
bool isRepaid;
}
mapping(uint256 => Loan) private loans;
mapping(uint256 => bool) private exists;
modifier exist(uint256 _index) {
require(exists[_index], "Query of non-existent loan");
_;
}
modifier onlyLoanParticipant(uint256 _index) {
require(
msg.sender == loans[_index].borrower ||
msg.sender == loans[_index].lender,
"Unauthorized participant"
);
_;
}
modifier onlyBorrower(uint256 _index) {
require(
msg.sender == loans[_index].borrower,
"Only borrower can perform this operation"
);
_;
}
modifier onlyLender(uint256 _index) {
require(
msg.sender == loans[_index].lender,
"Only lender can perform this operation"
);
_;
}
modifier notRepaid(uint256 _index) {
require(!loans[_index].isRepaid, "Loan has already been repaid");
_;
}
modifier notExtended(uint256 _index) {
require(!loans[_index].isExtended, "Loan has already been extended");
_;
}
In the LoanManagementContract
smart contract, we manage a collection of loans. Let’s explore its structure and modifiers.
The contract utilizes the SafeMath
library to perform secure mathematical operations with uint256
values.
We keep track of the total number of loans using the loansCount
variable, which is initially set to 0
.
The cUsdTokenAddress
variable stores the address of a specific token, presumably a USD stablecoin, which will be used in penalty assessments.
The Loan
struct defines the properties of a loan, including the borrower’s and lender’s addresses, principal amount, interest rate, due date, penalty rate, and flags indicating if the loan is extended or repaid.
Two mappings, loans
and exists
, are used to store and retrieve loan information based on their index.
Several modifiers are implemented to enforce specific conditions:
- The
exist
modifier ensures that a loan with a given index exists. - The
onlyLoanParticipant
modifier restricts certain operations to authorized participants, either the borrower or the lender. - The
onlyBorrower
modifier allows only the borrower to execute specific operations. - The
onlyLender
modifier restricts certain operations to the lender. - The
notRepaid
modifier ensures that a loan has not already been repaid. - The
notExtended
modifier verifies that a loan has not been extended previously.
These modifiers help to enforce the necessary conditions for loan management within the contract.
To enhance the functionality of our smart contract, we will begin adding various functions. The first function we will introduce is the createLoan
function.
function createLoan(
address _lender,
uint256 _principal,
uint256 _interestRate,
uint256 _dueDate,
uint256 _penaltyRate
) external {
require(_principal > 0, "Principal amount must be greater than zero");
require(
_interestRate > 0,
"Interest rate must be greater than zero"
);
require(_dueDate > block.timestamp, "Invalid due date");
loans[loansCount] = Loan(
msg.sender,
_lender,
_principal,
_interestRate,
_dueDate,
_penaltyRate,
false,
false
);
exists[loansCount] = true;
loansCount++;
}
The createLoan
function allows us to create a new loan by providing the necessary details. Here’s a brief explanation of how it works:
- We pass the address of the
_lender
, the loan’s_principal
amount,_interestRate
,_dueDate
, and_penaltyRate
as function arguments. - Before creating the loan, we have a series of
require
statements to validate the input parameters. The principal amount must be greater than zero, the interest rate must be greater than zero, and the due date must be in the future. - If all the validations pass, we proceed to create the loan.
- We assign the loan’s details, including the borrower’s address (
msg.sender
), the lender’s address (_lender
), the principal amount, interest rate, due date, penalty rate, and initial flags indicating that the loan is not extended and not repaid. - The loan is stored in the
loans
mapping using theloansCount
as the index. - We set the corresponding index in the
exists
mapping totrue
to mark the loan’s existence. - Finally, we increment the
loansCount
to prepare for the creation of the next loan.
By calling the createLoan
function, we can easily create a new loan by providing the necessary details and ensuring the required conditions are met.
Next we add a function repayLoan
.
function repayLoan(uint256 _index)
external
payable
exist(_index)
onlyBorrower(_index)
notRepaid(_index)
{
Loan storage loan = loans[_index];
uint256 repaymentAmount = loan.principal.add(
calculateInterest(loan.principal, loan.interestRate)
);
require(
msg.value >= repaymentAmount,
"Insufficient funds to repay the loan"
);
loan.isRepaid = true;
if (msg.value > repaymentAmount) {
payable(msg.sender).transfer(msg.value.sub(repaymentAmount));
}
}
The repayLoan
function allows us to repay a specific loan by providing its _index
as an argument. Here’s a brief explanation of how it works:
- We specify that this function can be called externally and requires a payment (
payable
). - We apply several modifiers (
exist
,onlyBorrower
,notRepaid
) to ensure that the loan exists, the caller is the borrower, and the loan has not been repaid. - Inside the function, we retrieve the loan object from the
loans
mapping based on the given_index
. - We calculate the
repaymentAmount
by adding the loan’s principal amount and the interest calculated using thecalculateInterest
function. - We require that the payment (
msg.value
) is greater than or equal to therepaymentAmount
, ensuring that sufficient funds are provided to repay the loan. - If the payment exceeds the
repaymentAmount
, we refund the excess amount to the borrower using thetransfer
function. - Finally, we mark the loan as repaid by setting the
isRepaid
flag totrue
.
By calling the repayLoan
function, we can repay a loan by providing the loan’s index and making the necessary payment. It ensures that the loan exists, the caller is the borrower, and the required amount is provided for repayment.
Furthermore, we add the calculateInterest
function.
function calculateInterest(uint256 _principal, uint256 _interestRate)
private
pure
returns (uint256)
{
return _principal.mul(_interestRate).div(100);
}
function getLoanDetails(uint256 _index)
external
view
exist(_index)
returns (
address borrower,
address lender,
uint256 principal,
uint256 interestRate,
uint256 dueDate,
uint256 penaltyRate,
bool isExtended,
bool isRepaid
)
{
Loan memory loan = loans[_index];
borrower = loan.borrower;
lender = loan.lender;
principal = loan.principal;
interestRate = loan.interestRate;
dueDate = loan.dueDate;
penaltyRate = loan.penaltyRate;
isExtended = loan.isExtended;
isRepaid = loan.isRepaid;
}
The calculateInterest
function calculates the interest amount for a given principal and interest rate. Here’s a brief explanation:
- This function is
private
, meaning it can only be accessed within the smart contract. - It is marked as
pure
, indicating that it doesn’t modify any state variables. - The function takes two parameters:
_principal
(the loan principal) and_interestRate
(the interest rate). - Inside the function, we multiply the principal by the interest rate and divide the result by 100 to obtain the interest amount.
- The function returns the calculated interest amount.
The getLoanDetails
function allows us to retrieve the details of a specific loan based on its _index
. Here’s a brief explanation:
- This function can be called externally (
external
) and only provides a read-only view of the data (view
). - We apply the
exist
modifier to ensure that the loan exists. - Inside the function, we create a
Loan
memory variable and assign it the loan object stored in theloans
mapping based on the given_index
. - We then extract the individual details of the loan (borrower, lender, principal, interest rate, due date, penalty rate, whether it is extended, and whether it is repaid) and assign them to the respective variables.
- Finally, we return these loan details as a tuple.
By using the calculateInterest
function, we can determine the interest amount for a loan. With the getLoanDetails
function, we can retrieve the various details of a loan, allowing us to access and display specific information about a loan when needed.
In addition, we add the extendLoan
function.
function extendLoan(uint256 _index)
external
exist(_index)
onlyBorrower(_index)
notExtended(_index)
{
Loan storage loan = loans[_index];
require(!loan.isRepaid, "Cannot extend a repaid loan");
loan.isExtended = true;
loan.dueDate = loan.dueDate.add(30 days);
}
In our contract, we have a function called extendLoan
that allows the borrower to extend the due date of their loan. Here’s how it works:
- We can call this function from outside the smart contract (
external
). - Before extending the loan, we apply several modifiers to ensure the loan exists (
exist
), the caller is the borrower (onlyBorrower
), and the loan hasn’t been previously extended (notExtended
). - Inside the function, we retrieve the loan object from the
loans
mapping using the provided index and store it in aLoan
storage variable namedloan
. - We check that the loan hasn’t been repaid yet by verifying that the
isRepaid
flag is false. If the loan is already repaid, we throw an error message. - If the loan is eligible for extension, we set the
isExtended
flag to true, indicating that the loan has been extended. - Additionally, we increase the due date of the loan by adding 30 days to the existing due date using the
add
function from the SafeMath library. - By extending the loan, the borrower is given more time to repay the loan amount.
By implementing the extendLoan
function, we provide borrowers with the flexibility to extend their loan’s due date when needed, ensuring they have sufficient time to meet their repayment obligations.
Finally we add the assessPenalty
function
function assessPenalty(uint256 _index)
external
exist(_index)
onlyLoanParticipant(_index)
notRepaid(_index)
{
Loan storage loan = loans[_index];
require(block.timestamp > loan.dueDate, "No penalty is due");
uint256 penaltyAmount = calculateInterest(
loan.principal,
loan.penaltyRate
);
require(
IERC20Token(cUsdTokenAddress).transferFrom(
msg.sender,
loan.lender,
penaltyAmount
),
"Transfer failed."
);
}
}
In our contract, we have a function called assessPenalty
that allows us to apply a penalty to a loan if it has not been repaid by the due date. Here’s how it works:
- We can call this function from outside the smart contract (
external
). - Before assessing the penalty, we apply several modifiers to ensure the loan exists (
exist
), and the caller is either the borrower or the lender (onlyLoanParticipant
). - Inside the function, we retrieve the loan object from the
loans
mapping using the provided index and store it in aLoan
storage variable namedloan
. - We check if the current timestamp (
block.timestamp
) is greater than the due date of the loan. If the due date has not yet passed, we throw an error message indicating that no penalty is due. - If a penalty is due, we calculate the penalty amount by applying the
penaltyRate
to the loan’s principal using thecalculateInterest
function. - We then attempt to transfer the penalty amount from the borrower (
msg.sender
) to the lender (loan.lender
) using thetransferFrom
function of thecUsdTokenAddress
contract (assumed to be an ERC20 token contract). - If the transfer is successful, it means the penalty has been paid, and the lender receives the penalty amount.
- If the transfer fails, we throw an error message indicating that the transfer failed.
By implementing the assessPenalty
function, we can enforce penalties on loans that are not repaid by their due dates, ensuring that borrowers have an incentive to repay on time.
Click here to go to the code repo of this project
Contract Deployment
To deploy the loan smart contract on the Celo blockchain, a few things are required:
To deploy the Loan smart contract on the Celo blockchain, you will need to first download and install the Celo Extension Wallet from the Google Chrome store. After installation, create a wallet and make sure to securely store your key phrase. Click here to intall the celo extension wallet
To fund your wallet, you can use the Celo Faucet. Simply copy the address of your wallet and paste it into the faucet. Then confirm your request, and your wallet will be funded. Click here to access celo faucet
Celo Remix Plugin: Open Remix and create a new Solidity file, paste the Loan contract code into the file, and ensure the Solidity compiler is set to version 0.8.7 or later. Click here to access to access the remix ide
Compile the contract by clicking the Compile loan.sol
button in the Solidity Compiler tab in Remix.
In the Deploy & Run Transactions
tab, select the Celo network from the dropdown menu, connect your wallet to Remix by clicking Connect to wallet
, and select Loan
from the Contract
dropdown menu.
Click the Deploy
button, confirm the transaction in your wallet, and wait for the transaction to be confirmed on the Celo blockchain.
Once the transaction is confirmed, the Loan contract will be deployed on the Celo blockchain and you can interact with it using Remix.
Conclusion
In this tutorial, we have covered the basic functionality of a Loan Management Contract written in Solidity, a programming language for Celo smart contracts. We explored various functions and modifiers that allow us to create loans, repay loans, extend loan due dates, and assess penalties.
The contract utilizes the SafeMath
library for safe arithmetic operations and includes a data structure called Loan
to store loan details. We used mappings to associate loan IDs with their corresponding loan objects and implemented modifiers to enforce access control and loan status conditions.
Throughout the tutorial, we emphasized the importance of security and validation checks, such as verifying loan existence, participant authorization, repayment status, and due dates. Additionally, we made use of the IERC20Token
interface to interact with an external token contract for penalty transfers.
By following this tutorial, we have gained insights into building a basic loan management system on the Celo blockchain. This contract can be further expanded and customized to suit specific lending scenarios, allowing for efficient and secure loan management within decentralized applications.
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, you can reach out to me on twitter by clicking this link. Happy learning!
About the author
My name is Ogoyi Thompson, and I’m a web3 developer based in Nigeria. I am enthusiastic about working with blockchain technology.