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 pTOKrepresents a share of the pool. 
2. Key mechanics (short)
- Supply: call 
supply(amount)→ pool takesamountunderlying and mintspTokensto you.pTokenstrack your share; as the pool earns interest, eachpTokenbecomes redeemable for more underlying. - Redeem: call 
redeem(pTokenAmount)→ pool burns yourpTokensand sends the underlying bypTokenAmount × 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 transfersamountunderlying 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 tocloseFactorportion) and receive collateral at a discount (liquidation incentive). 
3. Interest & borrow accounting (why borrowIndex?)
- The pool accrues interest globally with 
accrueInterest():interest = totalBorrows * rate * timeDeltatotalBorrowsincreases byinterest.borrowIndexincreases proportionally (used to scale each borrower's snapshot).
 - Each borrower’s debt is tracked as a 
principalsnapshot and theinterestIndexat snapshot time.- Current debt = 
principal × borrowIndex / interestIndex. - This avoids updating every borrower every time interest accrues.
 
 - Current debt = 
 
4. Exchange rate (how suppliers earn)
exchangeRate = (poolCash + totalBorrows) / totalPTokenSupply- If borrowers pay interest, 
totalBorrowsgoes up →exchangeRateincreases → eachpTokenredeems for more underlying. 
example
- 
Setup
- Alice deposits 1000 COLL (collateral token price = $1).
 - Lender supplies 2000 underlying (DAI) to pool and receives pTOK.
 - Pool has liquidity: 2000 DAI.
 
 - 
Alice borrows
- collateralUSD = 1000 × $1 = $1000.
 - borrow limit = 1000 × 0.75 = $750.
 - Alice borrows 600 DAI (allowed).
 - Now 
totalBorrowsincreases by 600. 
 - 
Accrue 1 day of interest (APR = 10%)
- daily interest on borrow = 
600 * 0.10 / 365 ≈ 0.1644 DAI. - totalBorrows & borrowIndex increase slightly.
 
 - daily interest on borrow = 
 - 
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.
 
 - 
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.
 
 - Liquidator repays up to 
 
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 
borrowIndexto scale per-account debt without touching every borrower on each accrual — we mimic that withborrowIndexand 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 
price1e18mapping to illustrate how price changes affect health.