Skip to main content

Lending_Barrowing_Compuond

Simplified Compound-Style Lending & Borrowing Pool

This contract is an educational, minimal clone of Compound’s core lending and borrowing mechanism, intended for learning purposes. It uses the OpenZeppelin ERC20 interface and SafeERC20 library to ensure safe token transfers.

// SPDX-License-Identifier: MIT
pragma solidity 0.8.21;


import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

/// Simple pToken (pool share). Only the pool can mint/burn.
contract SimplePToken is ERC20 {
address public pool;
constructor(string memory name_, string memory symbol_, address pool_) ERC20(name_, symbol_) {
pool = pool_;
}
function mint(address to, uint256 amount) external {
require(msg.sender == pool, "only pool");
_mint(to, amount);
}
function burn(address from, uint256 amount) external {
require(msg.sender == pool, "only pool");
_burn(from, amount);
}
}

/// Simplified pool
contract SimpleCompoundDemo is ReentrancyGuard, Ownable {
using SafeERC20 for IERC20;

// Tokens
IERC20 public immutable underlying; // token people supply & borrow (e.g., DAI)
IERC20 public immutable collateralToken; // token used as collateral (e.g., wETH)
SimplePToken public immutable pToken; // pool-share token

// Basic accounting
uint256 public totalBorrows; // total outstanding borrows (raw tokens)
uint256 public borrowIndex = 1e18; // index, scaled by 1e18 (starts 1e18)
uint256 public lastAccrual; // last timestamp interest was accrued
uint256 public borrowRatePerSecond; // scaled 1e18 (APR / seconds-per-year)

// Parameters (scaled by 1e18)
uint256 public collateralFactor = 0.75e18; // 75% LTV
uint256 public liquidationIncentive = 1.08e18; // 8% bonus to liquidator
uint256 public closeFactor = 0.5e18; // repay up to 50% in one liquidation

// Per-user state
mapping(address => uint256) public collateralBalance; // how much collateral each user deposited
struct BorrowSnapshot { uint256 principal; uint256 interestIndex; }
mapping(address => BorrowSnapshot) public accountBorrows;

// Simple oracle (owner sets prices in 1e18 USD units)
mapping(address => uint256) public price1e18;

event Supplied(address indexed who, uint256 amount, uint256 pTokens);
event Redeemed(address indexed who, uint256 pTokens, uint256 amountUnderlying);
event CollateralDeposited(address indexed who, uint256 amount);
event CollateralWithdrawn(address indexed who, uint256 amount);
event Borrowed(address indexed who, uint256 amount);
event Repaid(address indexed who, uint256 amount);
event Liquidated(address indexed liq, address indexed borrower, uint256 repay, uint256 seized);

constructor(IERC20 _underlying, IERC20 _collateral, uint256 _aprMantissa) {
underlying = _underlying;
collateralToken = _collateral;
pToken = new SimplePToken("pToken", "pTOK", address(this));
borrowRatePerSecond = _aprMantissa / 31536000; // APR -> per second
lastAccrual = block.timestamp;

// defaults: $1 each
price1e18[address(_underlying)] = 1e18;
price1e18[address(_collateral)] = 1e18;
}

// ---------- Admin ----------
function setPrice(IERC20 token, uint256 price1e18_) external onlyOwner { price1e18[address(token)] = price1e18_; }
function setBorrowAPR(uint256 aprMantissa) external onlyOwner { borrowRatePerSecond = aprMantissa / 31536000; }

// ---------- Helpers ----------
function getCash() public view returns (uint256) { return underlying.balanceOf(address(this)); }

// Exchange rate: how much underlying each pToken is worth (1e18 scale)
function exchangeRate() public view returns (uint256) {
uint256 supply = pToken.totalSupply();
if (supply == 0) return 1e18;
uint256 total = getCash() + totalBorrows; // no reserves in this simple version
return (total * 1e18) / supply;
}

// Compute user's borrow balance using index snapshot
function borrowBalanceStored(address who) public view returns (uint256) {
BorrowSnapshot memory bs = accountBorrows[who];
if (bs.principal == 0) return 0;
if (bs.interestIndex == 0) return bs.principal;
return (bs.principal * borrowIndex) / bs.interestIndex;
}

// Accrue interest globally (anyone can call)
function accrueInterest() public {
uint256 nowTs = block.timestamp;
uint256 dt = nowTs - lastAccrual;
if (dt == 0) return;
// interest = totalBorrows * ratePerSecond * dt / 1e18
uint256 interest = (totalBorrows * borrowRatePerSecond * dt) / 1e18;
totalBorrows += interest;
// update borrowIndex: newIndex = borrowIndex + borrowIndex * rate * dt / 1e18
borrowIndex = borrowIndex + (borrowIndex * borrowRatePerSecond * dt) / 1e18;
lastAccrual = nowTs;
}

// ---------- Supply / Redeem ----------
// Supply underlying to pool, receive pTokens
function supply(uint256 amount) external nonReentrant {
require(amount > 0, "zero");
accrueInterest();
underlying.transferFrom(msg.sender, address(this), amount);
uint256 ex = exchangeRate();
uint256 pTokens = (amount * 1e18) / ex;
require(pTokens > 0, "mint 0");
pToken.mint(msg.sender, pTokens);
emit Supplied(msg.sender, amount, pTokens);
}

// Redeem underlying by burning pTokens
function redeem(uint256 pTokenAmount) external nonReentrant {
require(pTokenAmount > 0, "zero");
accrueInterest();
uint256 ex = exchangeRate();
uint256 underlyingAmount = (pTokenAmount * ex) / 1e18;
pToken.burn(msg.sender, pTokenAmount);
require(getCash() >= underlyingAmount, "insolvent");
underlying.transfer(msg.sender, underlyingAmount);
emit Redeemed(msg.sender, pTokenAmount, underlyingAmount);
}

// ---------- Collateral ----------
function depositCollateral(uint256 amount) external nonReentrant {
require(amount > 0, "zero");
collateralToken.transferFrom(msg.sender, address(this), amount);
collateralBalance[msg.sender] += amount;
emit CollateralDeposited(msg.sender, amount);
}

function withdrawCollateral(uint256 amount) external nonReentrant {
require(amount > 0, "zero");
require(collateralBalance[msg.sender] >= amount, "not enough");
// ensure user stays solvent after withdraw
uint256 newColl = collateralBalance[msg.sender] - amount;
(uint256 avail, uint256 shortfall) = _computeAccountLiquidity(newColl, borrowBalanceStored(msg.sender));
require(shortfall == 0, "would undercollateralize");
collateralBalance[msg.sender] = newColl;
collateralToken.transfer(msg.sender, amount);
emit CollateralWithdrawn(msg.sender, amount);
}

// ---------- Borrow & Repay ----------
function borrow(uint256 amount) external nonReentrant {
require(amount > 0, "zero");
accrueInterest();
// check liquidity
(uint256 availUSD, uint256 short) = getAccountLiquidity(msg.sender);
require(short == 0, "undercollateralized");
uint256 priceU = price1e18[address(underlying)];
uint256 reqUSD = (amount * priceU) / 1e18;
require(availUSD >= reqUSD, "insufficient collateral");
// update borrow snapshot
uint256 prev = borrowBalanceStored(msg.sender);
BorrowSnapshot storage bs = accountBorrows[msg.sender];
bs.principal = prev + amount;
bs.interestIndex = borrowIndex;
totalBorrows += amount;
require(getCash() >= amount, "pool no liquidity");
underlying.transfer(msg.sender, amount);
emit Borrowed(msg.sender, amount);
}

function repay(uint256 amount) external nonReentrant {
require(amount > 0, "zero");
accrueInterest();
uint256 owed = borrowBalanceStored(msg.sender);
require(owed > 0, "nothing owed");
uint256 pay = amount > owed ? owed : amount;
underlying.transferFrom(msg.sender, address(this), pay);
BorrowSnapshot storage bs = accountBorrows[msg.sender];
bs.principal = owed - pay;
// if fully repaid, clear interestIndex
bs.interestIndex = bs.principal == 0 ? 0 : borrowIndex;
totalBorrows -= pay;
emit Repaid(msg.sender, pay);
}

// ---------- Liquidation ----------
// anyone can call to repay a portion of borrower's debt and seize collateral
function liquidate(address borrower, uint256 repayAmount) external nonReentrant {
require(repayAmount > 0, "zero");
accrueInterest();
( , uint256 shortfall) = getAccountLiquidity(borrower);
require(shortfall > 0, "not liquidatable");
uint256 owed = borrowBalanceStored(borrower);
uint256 maxRepay = (owed * closeFactor) / 1e18;
uint256 actual = repayAmount > maxRepay ? maxRepay : repayAmount;
// liquidator pays underlying to pool
underlying.transferFrom(msg.sender, address(this), actual);
// reduce borrower debt
BorrowSnapshot storage bs = accountBorrows[borrower];
uint256 newOwed = owed > actual ? owed - actual : 0;
bs.principal = newOwed;
bs.interestIndex = newOwed == 0 ? 0 : borrowIndex;
totalBorrows -= actual;

// compute seize amount (in collateral tokens)
uint256 priceU = price1e18[address(underlying)];
uint256 priceC = price1e18[address(collateralToken)];
uint256 repayUSD = (actual * priceU) / 1e18;
uint256 seizeUSD = (repayUSD * liquidationIncentive) / 1e18;
uint256 seizeTokens = (seizeUSD * 1e18) / priceC;

if (seizeTokens > collateralBalance[borrower]) seizeTokens = collateralBalance[borrower];
collateralBalance[borrower] -= seizeTokens;
collateralToken.transfer(msg.sender, seizeTokens);
emit Liquidated(msg.sender, borrower, actual, seizeTokens);
}

// ---------- Liquidity helpers ----------
// returns (availableUSD, shortfallUSD) both in 1e18 scale (USD)
function getAccountLiquidity(address who) public view returns (uint256 availableUSD, uint256 shortfallUSD) {
uint256 collBal = collateralBalance[who];
uint256 borrowBal = borrowBalanceStored(who);
return _computeAccountLiquidity(collBal, borrowBal);
}

function _computeAccountLiquidity(uint256 collBal, uint256 borrowBal) internal view returns (uint256 availableUSD, uint256 shortfallUSD) {
uint256 collPrice = price1e18[address(collateralToken)];
uint256 borrowPrice = price1e18[address(underlying)];
if (collPrice == 0 || borrowPrice == 0) return (0, 0);
uint256 collUSD = (collBal * collPrice) / 1e18;
uint256 borrowUSD = (borrowBal * borrowPrice) / 1e18;
uint256 maxBorrowUSD = (collUSD * collateralFactor) / 1e18;
if (maxBorrowUSD >= borrowUSD) {
availableUSD = maxBorrowUSD - borrowUSD;
shortfallUSD = 0;
} else {
availableUSD = 0;
shortfallUSD = borrowUSD - maxBorrowUSD;
}
}
}

How this simplified code works — step-by-step (plain language)

1. Roles & core pieces

  • Underlying token — the asset people supply to earn yield and that others borrow (e.g., DAI).
  • Collateral token — the asset a borrower deposits to back their loan (e.g., wETH).
  • pToken — a simple pool-share token you get when supplying underlying (like cToken). 1 pTOK represents a share of the pool.

2. Key mechanics (short)

  • Supply: call supply(amount) → pool takes amount underlying and mints pTokens to you. pTokens track your share; as the pool earns interest, each pToken becomes redeemable for more underlying.
  • Redeem: call redeem(pTokenAmount) → pool burns your pTokens and sends the underlying by pTokenAmount × exchangeRate.
  • Deposit collateral: call depositCollateral(amount) to put collateral into the pool (used to allow borrowing).
  • Borrow: call borrow(amount) — pool checks your collateral value (via simple price mapping) and ensures you have enough borrowing capacity (collateralValue × collateralFactor - currentBorrow >= requested). If allowed, it transfers amount underlying to you and updates your borrow snapshot.
  • Repay: call repay(amount) — you pay back underlying and your borrow decreases.
  • Liquidate: If a borrower becomes undercollateralized (borrow > allowed), anyone can call liquidate(borrower, repayAmount) to repay part of their debt (up to closeFactor portion) and receive collateral at a discount (liquidation incentive).

3. Interest & borrow accounting (why borrowIndex?)

  • The pool accrues interest globally with accrueInterest():
    • interest = totalBorrows * rate * timeDelta
    • totalBorrows increases by interest.
    • borrowIndex increases proportionally (used to scale each borrower's snapshot).
  • Each borrower’s debt is tracked as a principal snapshot and the interestIndex at snapshot time.
    • Current debt = principal × borrowIndex / interestIndex.
    • This avoids updating every borrower every time interest accrues.

4. Exchange rate (how suppliers earn)

  • exchangeRate = (poolCash + totalBorrows) / totalPTokenSupply
  • If borrowers pay interest, totalBorrows goes up → exchangeRate increases → each pToken redeems for more underlying.

example

  1. Setup

    • Alice deposits 1000 COLL (collateral token price = $1).
    • Lender supplies 2000 underlying (DAI) to pool and receives pTOK.
    • Pool has liquidity: 2000 DAI.
  2. Alice borrows

    • collateralUSD = 1000 × $1 = $1000.
    • borrow limit = 1000 × 0.75 = $750.
    • Alice borrows 600 DAI (allowed).
    • Now totalBorrows increases by 600.
  3. Accrue 1 day of interest (APR = 10%)

    • daily interest on borrow = 600 * 0.10 / 365 ≈ 0.1644 DAI.
    • totalBorrows & borrowIndex increase slightly.
  4. Price shock

    • COLL price drops to $0.80.
    • collateralUSD = 1000 * 0.8 = $800.
    • borrow limit = $800 * 0.75 = $600.
    • Alice's debt ≈ 600.1644 > 600 → shortfall ≈ 0.1644 USD → liquidatable.
  5. Liquidation

    • Liquidator repays up to closeFactor (50% of owed).
    • If liquidator repays 300.082 DAI, seize collateral worth 300.082 * 1.08 ≈ $324.09 → that's ~405 COLL at $0.8 each.
    • Collateral is moved from Alice to liquidator; Alice's debt is reduced.

How this reflects real-world protocols (Compound / Aave) — mapping

  • pTokens / cTokens: real protocols mint pool-share tokens to track supplier shares. Exchange rate rises as interest accrues. (We implement pToken & exchangeRate().)
  • Comptroller / risk checks: real systems use a central comptroller to calculate liquidity & enforce collateral factors. Our _computeAccountLiquidity + parameters (collateralFactor, closeFactor) play that role.
  • Index-based borrow accounting: Compound uses a borrowIndex to scale per-account debt without touching every borrower on each accrual — we mimic that with borrowIndex and per-account snapshots.
  • Liquidation mechanics: repay portion of debt + seize discounted collateral (liquidation incentive) — same idea implemented.
  • Oracle & prices: real protocols use secure oracles (Chainlink, etc.). We use a simple owner-set price1e18 mapping to illustrate how price changes affect health.