Difficulty: Intermediate
Decentralized lending protocols like Aave, Compound, and MakerDAO allow users to lend their crypto assets to earn interest and borrow against their deposited collateral without intermediaries. These protocols have become the cornerstone of DeFi, with billions of dollars locked across multiple chains. Understanding their mechanics is essential for anyone working in blockchain development or DeFi engineering.
The lending side is straightforward: suppliers deposit tokens into a lending pool and receive interest-bearing tokens in return (aTokens in Aave, cTokens in Compound). These receipt tokens continuously accrue interest, meaning their value increases over time relative to the underlying asset. Interest comes from borrowers who pay a variable rate determined algorithmically based on supply and demand. The protocol takes a small spread (reserve factor) as revenue.
Borrowing in DeFi is always over-collateralized, meaning borrowers must deposit more value than they borrow. This is necessary because there are no credit checks or legal enforcement in decentralized systems. Each asset has a Loan-to-Value (LTV) ratio that determines the maximum borrowing power. For example, if ETH has an 80% LTV, depositing $1000 worth of ETH allows borrowing up to $800 worth of stablecoins. The difference between collateral value and borrowed value is the safety margin that protects lenders.
Liquidation is the enforcement mechanism that keeps the protocol solvent. When a borrower's collateral value drops relative to their debt (due to price movements), their position becomes eligible for liquidation. Third-party liquidators can repay a portion of the borrower's debt and receive the borrower's collateral at a discount (the liquidation bonus, typically 5-15%). This incentivizes rapid liquidation and protects lenders from bad debt. Borrowers lose their collateral but have their debt reduced.
Interest rates in lending protocols are determined by utilization rate: the ratio of total borrowed to total supplied. When utilization is low (plenty of supply, little demand), rates are low to encourage borrowing. As utilization increases, rates rise to incentivize more supply and discourage excess borrowing. Most protocols use a kinked interest rate model with two slopes: a gentle slope below optimal utilization (e.g., 80%) and a steep slope above it. The steep slope above the kink rapidly makes borrowing expensive, preventing the pool from being fully utilized and ensuring depositors can always withdraw.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract SimpleLendingPool {
IERC20 public immutable lendingToken; // e.g., USDC
IERC20 public immutable collateralToken; // e.g., WETH
// Interest rate model parameters
uint256 public constant OPTIMAL_UTILIZATION = 80; // 80%
uint256 public constant BASE_RATE = 2; // 2% APY at 0% utilization
uint256 public constant SLOPE1 = 4; // +4% at optimal utilization
uint256 public constant SLOPE2 = 75; // +75% above optimal (steep)
uint256 public constant LTV = 75; // 75% loan-to-value
uint256 public constant LIQUIDATION_THRESHOLD = 80; // Liquidate at 80%
uint256 public constant LIQUIDATION_BONUS = 5; // 5% bonus for liquidators
// Pool state
uint256 public totalDeposits;
uint256 public totalBorrows;
struct UserDeposit {
uint256 amount;
uint256 timestamp;
}
struct Borrow {
uint256 borrowed; // Amount borrowed
uint256 collateral; // Collateral deposited
uint256 timestamp; // When borrowed
}
mapping(address => UserDeposit) public deposits;
mapping(address => Borrow) public borrows;
// Simplified oracle: price of collateral in lending token units
uint256 public collateralPrice = 2000e18; // e.g., 1 ETH = 2000 USDC
event Deposited(address indexed user, uint256 amount);
event Withdrawn(address indexed user, uint256 amount);
event Borrowed(address indexed user, uint256 amount, uint256 collateral);
event Repaid(address indexed user, uint256 amount);
event Liquidated(address indexed user, address indexed liquidator, uint256 debtRepaid, uint256 collateralSeized);
constructor(address _lendingToken, address _collateralToken) {
lendingToken = IERC20(_lendingToken);
collateralToken = IERC20(_collateralToken);
}
// === LENDING ===
function deposit(uint256 amount) external {
lendingToken.transferFrom(msg.sender, address(this), amount);
deposits[msg.sender].amount += amount;
deposits[msg.sender].timestamp = block.timestamp;
totalDeposits += amount;
emit Deposited(msg.sender, amount);
}
function withdraw(uint256 amount) external {
require(deposits[msg.sender].amount >= amount, "Insufficient deposit");
require(totalDeposits - totalBorrows >= amount, "Insufficient liquidity");
deposits[msg.sender].amount -= amount;
totalDeposits -= amount;
lendingToken.transfer(msg.sender, amount);
emit Withdrawn(msg.sender, amount);
}
// === BORROWING ===
function borrow(uint256 borrowAmount, uint256 collateralAmount) external {
// Transfer collateral
collateralToken.transferFrom(msg.sender, address(this), collateralAmount);
// Check LTV: collateral value * LTV% >= borrow amount
uint256 collateralValue = (collateralAmount * collateralPrice) / 1e18;
uint256 maxBorrow = (collateralValue * LTV) / 100;
require(borrowAmount <= maxBorrow, "Exceeds LTV");
require(borrowAmount <= totalDeposits - totalBorrows, "Insufficient liquidity");
borrows[msg.sender].borrowed += borrowAmount;
borrows[msg.sender].collateral += collateralAmount;
borrows[msg.sender].timestamp = block.timestamp;
totalBorrows += borrowAmount;
lendingToken.transfer(msg.sender, borrowAmount);
emit Borrowed(msg.sender, borrowAmount, collateralAmount);
}
function repay(uint256 amount) external {
Borrow storage userBorrow = borrows[msg.sender];
require(userBorrow.borrowed >= amount, "Repaying too much");
lendingToken.transferFrom(msg.sender, address(this), amount);
userBorrow.borrowed -= amount;
totalBorrows -= amount;
// If fully repaid, return collateral
if (userBorrow.borrowed == 0) {
uint256 collateral = userBorrow.collateral;
userBorrow.collateral = 0;
collateralToken.transfer(msg.sender, collateral);
}
emit Repaid(msg.sender, amount);
}
// === LIQUIDATION ===
function liquidate(address borrower) external {
Borrow storage userBorrow = borrows[borrower];
require(userBorrow.borrowed > 0, "No active borrow");
uint256 collateralValue = (userBorrow.collateral * collateralPrice) / 1e18;
uint256 healthFactor = (collateralValue * 100) / userBorrow.borrowed;
require(healthFactor < LIQUIDATION_THRESHOLD, "Position is healthy");
// Liquidator repays 50% of debt
uint256 debtToRepay = userBorrow.borrowed / 2;
lendingToken.transferFrom(msg.sender, address(this), debtToRepay);
// Liquidator receives collateral + bonus
uint256 collateralToSeize = (debtToRepay * 1e18) / collateralPrice;
uint256 bonus = (collateralToSeize * LIQUIDATION_BONUS) / 100;
uint256 totalSeized = collateralToSeize + bonus;
if (totalSeized > userBorrow.collateral) {
totalSeized = userBorrow.collateral;
}
userBorrow.borrowed -= debtToRepay;
userBorrow.collateral -= totalSeized;
totalBorrows -= debtToRepay;
collateralToken.transfer(msg.sender, totalSeized);
emit Liquidated(borrower, msg.sender, debtToRepay, totalSeized);
}
// === INTEREST RATE ===
function getUtilizationRate() public view returns (uint256) {
if (totalDeposits == 0) return 0;
return (totalBorrows * 100) / totalDeposits;
}
function getBorrowRate() public view returns (uint256) {
uint256 utilization = getUtilizationRate();
if (utilization <= OPTIMAL_UTILIZATION) {
return BASE_RATE + (SLOPE1 * utilization) / OPTIMAL_UTILIZATION;
} else {
uint256 excessUtilization = utilization - OPTIMAL_UTILIZATION;
uint256 maxExcess = 100 - OPTIMAL_UTILIZATION;
return BASE_RATE + SLOPE1 + (SLOPE2 * excessUtilization) / maxExcess;
}
}
// Owner function to update oracle price (simplified)
function setCollateralPrice(uint256 _price) external {
collateralPrice = _price;
}
}
This simplified lending pool demonstrates the core mechanics: depositing to earn yield, over-collateralized borrowing with LTV checks, liquidation when health factor drops below the threshold, and a kinked interest rate model based on utilization.
Collateral, Liquidation, Interest Rate, Utilization