Decentralized Lending Protocol
Decentralized lending is one of the most essential DeFi applications. Protocols like Aave and Compound have locked billions of dollars in assets, enabling users to:
- Deposit: Supply assets to the protocol and earn interest
- Borrow: Collateralize assets to borrow other assets
- Liquidation: When collateral drops below the threshold, liquidators repay debt and earn discounted collateral
Lending Protocol Core Concepts
Collateral Ratio
$$\text{Collateral Ratio} = \frac{\text{Collateral Value}}{\text{Loan Amount}}$$
- Your collateral ratio must stay above the minimum (e.g. 150%)
- If the ratio drops below the liquidation threshold (e.g. 130%), liquidation occurs
Example
- You deposit 1 ETH (worth $3,000)
- Max borrowable = $3,000 / 150% = $2,000
- You borrow 1,000 USDC
- If ETH drops to $1,300: ratio = $1,300 / $1,000 = 130% โ liquidation triggered
Implementing the Lending Contract
// contracts/SimpleLending.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract SimpleLending is Ownable, ReentrancyGuard {
IERC20 public collateralToken; // Collateral token (e.g. ETH)
IERC20 public loanToken; // Loan token (e.g. USDC)
// Interest rate parameters
uint256 public baseRate = 5; // Base annual rate 5%
uint256 public utilizationRate = 0; // Funds utilization rate
// Collateral and liquidation parameters
uint256 public constant COLLATERAL_RATIO = 150; // Min collateral ratio 150%
uint256 public constant LIQUIDATION_THRESHOLD = 130; // Liquidation threshold 130%
uint256 public constant LIQUIDATION_BONUS = 10; // Liquidation bonus 10%
// User data
struct UserInfo {
uint256 depositedAmount; // Collateral amount deposited
uint256 borrowedAmount; // Amount borrowed
uint256 lastInterestTime; // Last interest update timestamp
}
mapping(address => UserInfo) public users;
uint256 public totalDeposits; // Total deposits
uint256 public totalLoans; // Total loans outstanding
// Events
event Deposited(address indexed user, uint256 amount);
event Withdrawn(address indexed user, uint256 amount);
event Borrowed(address indexed user, uint256 amount);
event Repaid(address indexed user, uint256 amount);
event Liquidated(address indexed user, address indexed liquidator, uint256 amount);
constructor(address _collateralToken, address _loanToken) Ownable(msg.sender) {
collateralToken = IERC20(_collateralToken);
loanToken = IERC20(_loanToken);
}
// === Deposit (Collateral) ===
function deposit(uint256 _amount) external nonReentrant {
require(_amount > 0, "Amounts must be greater than 0");
// Update user interest
updateInterest(msg.sender);
// Transfer collateral
collateralToken.transferFrom(msg.sender, address(this), _amount);
users[msg.sender].depositedAmount += _amount;
totalDeposits += _amount;
emit Deposited(msg.sender, _amount);
}
// === Withdraw Collateral ===
function withdraw(uint256 _amount) external nonReentrant {
require(_amount > 0, "Amounts must be greater than 0");
updateInterest(msg.sender);
UserInfo storage user = users[msg.sender];
require(user.depositedAmount >= _amount, "Insufficient balance");
// Check collateral ratio after withdrawal
uint256 remainingDeposit = user.depositedAmount - _amount;
if (user.borrowedAmount > 0) {
uint256 currentRatio = (remainingDeposit * 100) / user.borrowedAmount;
require(currentRatio >= COLLATERAL_RATIO, "Collateral ratio too low after withdrawal");
}
user.depositedAmount = remainingDeposit;
totalDeposits -= _amount;
collateralToken.transfer(msg.sender, _amount);
emit Withdrawn(msg.sender, _amount);
}
// === Borrow ===
function borrow(uint256 _amount) external nonReentrant {
require(_amount > 0, "Amounts must be greater than 0");
require(loanToken.balanceOf(address(this)) >= _amount, "Insufficient protocol liquidity");
updateInterest(msg.sender);
UserInfo storage user = users[msg.sender];
require(user.depositedAmount > 0, "Deposit collateral first");
// Check collateral ratio
uint256 maxBorrow = (user.depositedAmount * 100) / COLLATERAL_RATIO;
require(user.borrowedAmount + _amount <= maxBorrow, "Exceeds max borrowable");
user.borrowedAmount += _amount;
totalLoans += _amount;
loanToken.transfer(msg.sender, _amount);
emit Borrowed(msg.sender, _amount);
}
// === Repay ===
function repay(uint256 _amount) external nonReentrant {
require(_amount > 0, "Amounts must be greater than 0");
updateInterest(msg.sender);
UserInfo storage user = users[msg.sender];
require(user.borrowedAmount >= _amount, "Insufficient loan balance");
uint256 interest = calculateInterest(msg.sender);
uint256 totalPayment = _amount + interest;
user.borrowedAmount -= _amount;
totalLoans -= _amount;
loanToken.transferFrom(msg.sender, address(this), totalPayment);
emit Repaid(msg.sender, _amount);
}
// === Liquidation ===
function liquidate(address _user) external nonReentrant {
UserInfo storage user = users[_user];
require(user.borrowedAmount > 0, "No active loan");
// Calculate current collateral ratio
uint256 currentRatio = (user.depositedAmount * 100) / user.borrowedAmount;
require(currentRatio < LIQUIDATION_THRESHOLD, "Not below liquidation threshold");
updateInterest(_user);
// Liquidator repays the loan and receives discounted collateral (bonus)
uint256 repayAmount = user.borrowedAmount;
uint256 collateralToLiquidate = (repayAmount * (100 + LIQUIDATION_BONUS)) / 100;
if (collateralToLiquidate > user.depositedAmount) {
collateralToLiquidate = user.depositedAmount;
}
// Liquidator repays the loan
loanToken.transferFrom(msg.sender, address(this), repayAmount);
// Liquidator receives the discounted collateral
user.depositedAmount -= collateralToLiquidate;
user.borrowedAmount = 0;
totalLoans -= repayAmount;
collateralToken.transfer(msg.sender, collateralToLiquidate);
emit Liquidated(_user, msg.sender, collateralToLiquidate);
}
// === Interest Calculation ===
function calculateInterest(address _user) public view returns (uint256) {
UserInfo storage user = users[_user];
if (user.borrowedAmount == 0) return 0;
uint256 timeElapsed = block.timestamp - user.lastInterestTime;
// Simple annual interest: principal * rate * time / 365 days
return (user.borrowedAmount * baseRate * timeElapsed) / (365 days * 100);
}
function updateInterest(address _user) internal {
UserInfo storage user = users[_user];
if (user.borrowedAmount > 0 && user.lastInterestTime > 0) {
uint256 interest = calculateInterest(_user);
if (interest > 0) {
user.borrowedAmount += interest;
totalLoans += interest;
}
}
user.lastInterestTime = block.timestamp;
}
// === View User Health ===
function getUserHealth(address _user) public view returns (
uint256 depositValue,
uint256 borrowValue,
uint256 ratio,
bool isHealthy
) {
UserInfo storage user = users[_user];
depositValue = user.depositedAmount;
borrowValue = user.borrowedAmount;
if (borrowValue == 0) {
return (depositValue, 0, 0, true);
}
ratio = (depositValue * 100) / borrowValue;
isHealthy = ratio >= LIQUIDATION_THRESHOLD;
}
}
Testing the Lending Protocol
// scripts/test_lending.js
async function main() {
const [deployer, user1, liquidator] = await hre.ethers.getSigners();
// Deploy tokens
const VibeToken = await hre.ethers.getContractFactory("VibeToken");
const collateral = await VibeToken.deploy(deployer.address);
await collateral.waitForDeployment();
const loanToken = await VibeToken.deploy(deployer.address);
await loanToken.waitForDeployment();
// Deploy the lending contract
const SimpleLending = await hre.ethers.getContractFactory("SimpleLending");
const lending = await SimpleLending.deploy(
await collateral.getAddress(),
await loanToken.getAddress()
);
await lending.waitForDeployment();
console.log("Lending contract deployed at:", await lending.getAddress());
// Give user1 some collateral tokens
const depositAmount = hre.ethers.parseEther("1000");
await collateral.transfer(user1.address, depositAmount);
// Give the lending contract some loan tokens
const loanAmount = hre.ethers.parseEther("10000");
await loanToken.transfer(await lending.getAddress(), loanAmount);
// user1 deposits collateral
await collateral.connect(user1).approve(await lending.getAddress(), depositAmount);
await lending.connect(user1).deposit(depositAmount);
console.log("\nUser1 deposited 1000 collateral");
// user1 borrows
const borrowAmount = hre.ethers.parseEther("500");
await lending.connect(user1).borrow(borrowAmount);
console.log("User1 borrowed 500");
// Check user health
const health = await lending.getUserHealth(user1.address);
console.log(`\nHealth Status: deposit=${hre.ethers.formatEther(health[0])}, ` +
`borrow=${hre.ethers.formatEther(health[1])}, ` +
`ratio=${health[2]}, healthy=${health[3]}`);
}
main().catch(console.error);
How to Use Vibe Coding to Build a Lending Protocol
๐ฅ Vibe Coding Prompt for Lending Protocol "Implement a decentralized lending contract with these features:
- Support depositing ETH as collateral and borrowing ERC-20 tokens
- Minimum collateral ratio: 150%, liquidation threshold: 120%
- Interest rate adjusts dynamically based on utilization (higher utilization = higher rate)
- Liquidation bonus: 5% (liquidators get discounted collateral)
- Must include ReentrancyGuard for security
- Write a complete test script covering deposit, borrow, repay, and liquidation"
Summary
In this chapter, you built a fully functional decentralized lending protocol โ the same architecture used by Aave, Compound, and Morpho:
- What you built: A lending smart contract supporting collateralized deposits, borrowing with interest, loan repayment, and automated liquidation
- Why it matters: Lending is one of the two pillars of DeFi (alongside DEXs). Over $20 billion is currently locked in lending protocols across all chains
- How it works: Users deposit collateral โ borrow against it at a safe ratio โ pay interest based on utilization โ get liquidated if the ratio drops too low
Key takeaways:
- Collateral Ratio: Always maintain a buffer (150% minimum) to absorb price volatility
- Liquidation: When the ratio drops below the threshold, anyone can liquidate โ this keeps the protocol solvent
- Interest Rate Model: Higher utilization = higher rates. This incentivizes depositors (more supply) and discourages borrowers (less demand), creating a self-balancing market
- User Health: The
getUserHealth()function lets users monitor their position and add collateral before liquidation - ReentrancyGuard: Essential for any contract handling multiple token transfers in a single transaction
- Liquidation Bonus: The 5-10% bonus incentivizes liquidators to act quickly, protecting the protocol from bad debt
What Is Next: DApp Frontend
Now that the smart contracts are complete โ tokens, NFTs, DEX, and lending โ the next chapter shows you how to build a complete DApp frontend that connects everything to real users. You will build a React dashboard with wallet connection (MetaMask), real-time balance displays, token swap interfaces, and lending dashboard โ all styled with Tailwind CSS and connected to Sepolia testnet.