Building a Library Management System with Solidity: A Step-by-Step Guide
Welcome to this blog post where I will guide you through building a library management system using Solidity. Don’t worry if you’re new to Solidity, as I’ll help you learn it step-by-step .
To start with, we’ll explore the two main roles in the system, namely users and librarians. After this, we will define the system and outline its requirements. Along the way, we’ll also discuss any potential loopholes in the system and identify areas for future improvement.
So let’s dive in and start building!
Basic requirements ⚒️
- The system has two roles: Librarian and User.
- Users stake tokens to borrow books, and if they lose a book, the tokens are taken away.
- The librarian is responsible for managing the inventory and approving transactions.
- To increase security and fairness, the system integrates stablecoins to ensure that the value of the tokens remains stable and secure.
Learn more about stablecoins here.
Code 🧑💻
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin-contracts/blob/master/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
We import two Solidity libraries: IERC20 for interacting with ERC-20 tokens(DAI to be specific) and ReentrancyGuard for preventing reentrancy attacks. We are going to work with Solidity version 0.8.0
contract BookLendingSystem {
struct Book
{
string title;
string author;
uint256 inventory;
bool isAvailable;
}
mapping(uint256 => Book) public books;
mapping(address => mapping(uint256 => bool)) public borrowed;
mapping(address => mapping(uint256 => bool)) public bookReturnRequested;
uint256 public numBooks;
uint256 public tokenAmount;
address public admin;
address[] public librarians;
mapping(address => bool) public librarianExists;
IERC20 public dai;
event BookAdded(string title, string author, uint256 inventory);
event BookBorrowed(uint256 bookId, address user);
event LibrarianAdded(address librarian);
event BookReturnApprovalRequested(uint256 bookId, address user);
event BookReturnApproved(uint256 bookId, address librarian);
event BookReturnDenied(uint256 bookId, address librarian);
event BookReturned(uint256 bookId, address user);
We have a struct called Book
that contains information about each book, including the book’s title, author, inventory, and availability.
There are several mappings that the smart contract uses to keep track of information about books, including the books
mapping, which maps a book’s ID to its information, the borrowed
mapping, which maps a user’s address to the books they have borrowed, and the bookReturnRequested
mapping, which maps a user’s address to whether they have requested approval to return a book.
The tokenAmount
variable represents the minimum amount of DAI that a user must stake in order to borrow a book.We have defined appropriate events to track the borrowing process.
constructor(IERC20 _dai, uint256 _tokenAmount) {
dai = _dai;
tokenAmount = _tokenAmount;
admin = msg.sender;
}
modifier onlyAdmin {
require(msg.sender == admin, "Only an admin can perform this action");
_;
}
modifier onlyLibrarian {
require(librarianExists[msg.sender], "Only a librarian can perform this action");
_;
}
modifier bookIdExists(uint256 _bookId) {
require(_bookId < numBooks, "This book ID does not exist");
_;
}
The constructor function takes in two parameters and initialises three variables. The modifier functions check if the caller of the function is the admin or a librarian and if the book ID exists in the library before executing the function.
function addBook(string memory _title, string memory _author, uint256 _inventory) public onlyLibrarian {
for (uint256 i = 0; i < numBooks; i++) {
if (keccak256(bytes(books[i].title)) == keccak256(bytes(_title)) &&
keccak256(bytes(books[i].author)) == keccak256(bytes(_author))) {
// If a book with the same title and author already exists, increase its inventory
books[i].inventory += _inventory;
books[i].isAvailable = true;
emit BookAdded(_title, _author, _inventory);
return;
}
}
// If a book with the same title and author does not exist, add a new book
books[numBooks] = Book(_title, _author, _inventory, true);
emit BookAdded(_title, _author, _inventory);
numBooks++;
}
This function allows a librarian to add a new book to the library system. If the book with the same title and author already exists, it will increase its inventory. Otherwise, it will add a new book. It emits the BookAdded
event with the title, author, and inventory of the added book. The function can only be called by a librarian, as specified by the onlyLibrarian
modifier.
function borrowBook(uint256 _bookId) public bookIdExists nonReentrant{
require(books[_bookId].isAvailable, "This book is not available");
require(!borrowed[msg.sender][_bookId], "You have already borrowed this book");
require(dai.balanceOf(msg.sender) >= tokenAmount, "You do not have enough DAI to borrow a book");
require(dai.allowance(msg.sender, address(this)) >= tokenAmount, "You must approve DAI to borrow a book");
dai.transferFrom(msg.sender, address(this), tokenAmount);
borrowed[msg.sender][_bookId] = true;
books[_bookId].inventory--;
if(books[_bookId].inventory == 0) {
books[_bookId].isAvailable = false;
}
emit BookBorrowed(_bookId, msg.sender);
}
The function borrowBook
allows a user to borrow a book if the book is available, the user has not already borrowed the book, and the user has approved enough DAI to cover the transfer. If all the conditions are met, the function transfers the approved DAI from the user to the contract, marks the book as borrowed, and reduces the inventory count. If the inventory of the book becomes zero, it is marked as unavailable. Finally, the function emits a BookBorrowed
event.
function requestApproval(uint256 _bookId) public bookIdExists{
require(borrowed[msg.sender][_bookId], "You have not borrowed this book");
bookReturnRequested[msg.sender][_bookId] = true;
emit BookReturnApprovalRequested(_bookId,msg.sender);
}
The requestApproval
function allows the borrower to request approval for returning a borrowed book. It checks whether the book has been borrowed by the user, sets the bookReturnRequested
mapping to true
, and emits the BookReturnApprovalRequested
event with the book ID and the borrower's address as parameters. The event can be used to notify the Librarian :)
function approvalFromLibrarian(uint256 _bookId,address _user,bool _approved) public bookIdExists onlyLibrarian {
require(bookReturnRequested[_user][_bookId], "No approval needed for this book");
// if book in good condition
if(_approved){
bookReturnRequested[_user][_bookId] = false;
emit BookReturnApproved(_bookId,msg.sender);
returnBook(_bookId,_user);
}
// if book in bad condition
else{
bookReturnRequested[_user][_bookId] = false;
emit BookReturnDenied(_bookId,msg.sender);
}
}
This code defines a function that can be called by a librarian to approve or deny a user’s request to return a borrowed book. The function takes in the book ID, the user’s address, and a boolean value indicating whether the book is in good condition or not. If the book is in good condition, the function updates the bookReturnRequested
mapping, emits a BookReturnApproved
event, and calls the returnBook
function. If the book is in bad condition, the function updates the bookReturnRequested
mapping and emits a BookReturnDenied
event.
function returnBook(uint256 _bookId,address _user) internal bookIdExists{
require(borrowed[_user][_bookId], "You have not borrowed this book");
borrowed[_user][_bookId] = false;
books[_bookId].inventory++;
if(books[_bookId].inventory > 0) {
books[_bookId].isAvailable = true;
}
dai.transfer(_user, tokenAmount);
emit BookReturned(_bookId, _user);
}
This function is used to return a borrowed book by a user. It checks if the user has actually borrowed the book and updates the inventory of the book accordingly. If the inventory of the book becomes greater than 0 after the return, the book is marked as available for borrowing. It then transfers the token amount back to the user and emits a BookReturned
event.
function addLibrarian(address _newLibrarian) public onlyAdmin{
require(!librarianExists[_newLibrarian], "This address is already a librarian");
librarians.push(_newLibrarian);
librarianExists[_newLibrarian] = true;
emit LibrarianAdded(_newLibrarian);
}
This function adds a new librarian to the system. It requires that only the admin can perform this action, and that the address of the new librarian is not already in the system. The address of the new librarian is added to the array of librarians and its existence is marked as true in the mapping. Finally, an event is emitted to signal the addition of the new librarian.
Note : Do not use this code in production, there are certain flaws in the system!
We have an issue❗️ : The contract does not impose time limit on book returns, thus there’s no automatic mechanism to reclaim tokens or mark books as lost in case of non-return. This could result in indefinite hold on books by borrowers, preventing others from borrowing them.
How decentralised is this system 🧐?
This system is not fully decentralised since there is still a central entity, the librarian, who is responsible for managing the inventory and approving transactions. However, the use of tokens and the multi-signature feature help to decentralise the system to some extent.
That being said, there are still potential loopholes and risks that should be addressed to ensure the security and fairness of the system. Some of these include:
- Sybil attacks: Since users are able to create multiple accounts, there is a risk of Sybil attacks, where one user creates multiple accounts to gain an unfair advantage. To mitigate this risk, the system could implement a reputation system that takes into account the behaviour of all of a user’s accounts, or require some form of identity verification to create an account.
- Code complexity : As we incorporate additional features into the system, the code complexity increases, which in turn leads to a rise in the gas fee required to utilise the system.
Future Scope 🔮
- Use ZK technology to enhance privacy.
- Store book data in a database and perform computations only on the blockchain.
- Invest staked tokens (DAI) to obtain a fair return and share a portion of the interest with the librarian as an incentive.
- Allow borrowing of multiple books at the same time.
- Stake different amounts of DAI on the basis of price of the book.
- Implement a penalty for late book returns.
- Add a function in the smart contract to enable the librarian to remove books.
I really hope this post was informative and helped you understand new concepts in solidity. Thank you so much for taking the time to read it! If you found this post useful, please consider sharing it with your friends and colleagues and give me a follow. Your support means a lot to me 🥳.