去中心化借貸協議

去中心化借貸是 DeFi 最核心的應用場景之一。Aave 和 Compound 這兩個協議鎖定了數百億美元的資產,讓使用者可以:

  • 存款:將資產存入協議賺取利息
  • 借款:抵押資產後借出其他資產
  • 清算:當抵押率不足時,清算人可以償還借款並獲得折扣的抵押品

借貸協議的核心概念

抵押率 (Collateral Ratio)

$$\text{抵押率} = \frac{\text{抵押品價值}}{\text{借款金額}}$$

  • 你的抵押率必須高於最低抵押率(如 150%)
  • 如果抵押率低於清算門檻(如 130%),就會被清算

範例

  1. 你存入 1 ETH(價值 $3000)
  2. 最大可借款 = $3000 / 150% = $2000
  3. 你借出 1000 USDC
  4. 如果 ETH 跌到 $1300:抵押率 = $1300 / $1000 = 130% → 觸發清算

實作借貸合約

// 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;  // 抵押品代幣(如 ETH)
    IERC20 public loanToken;        // 借款代幣(如 USDC)
    
    // 利率參數
    uint256 public baseRate = 5;           // 基礎年利率 5%
    uint256 public utilizationRate = 0;    // 資金利用率
    
    // 抵押與清算參數
    uint256 public constant COLLATERAL_RATIO = 150;  // 最低抵押率 150%
    uint256 public constant LIQUIDATION_THRESHOLD = 130;  // 清算門檻 130%
    uint256 public constant LIQUIDATION_BONUS = 10;  // 清算獎勵 10%
    
    // 使用者資料
    struct UserInfo {
        uint256 depositedAmount;   // 存入的抵押品數量
        uint256 borrowedAmount;    // 借款數量
        uint256 lastInterestTime;  // 最後更新利息的時間
    }
    
    mapping(address => UserInfo) public users;
    
    uint256 public totalDeposits;  // 總存款
    uint256 public totalLoans;     // 總借款
    
    // 事件
    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);
    }
    
    // === 存款(抵押品) ===
    function deposit(uint256 _amount) external nonReentrant {
        require(_amount > 0, "金額必須大於 0");
        
        // 更新使用者的利息
        updateInterest(msg.sender);
        
        // 轉入抵押品
        collateralToken.transferFrom(msg.sender, address(this), _amount);
        
        users[msg.sender].depositedAmount += _amount;
        totalDeposits += _amount;
        
        emit Deposited(msg.sender, _amount);
    }
    
    // === 提領抵押品 ===
    function withdraw(uint256 _amount) external nonReentrant {
        require(_amount > 0, "金額必須大於 0");
        
        updateInterest(msg.sender);
        
        UserInfo storage user = users[msg.sender];
        require(user.depositedAmount >= _amount, "餘額不足");
        
        // 檢查提領後抵押率是否足夠
        uint256 remainingDeposit = user.depositedAmount - _amount;
        if (user.borrowedAmount > 0) {
            uint256 currentRatio = (remainingDeposit * 100) / user.borrowedAmount;
            require(currentRatio >= COLLATERAL_RATIO, "提領後抵押率不足");
        }
        
        user.depositedAmount = remainingDeposit;
        totalDeposits -= _amount;
        
        collateralToken.transfer(msg.sender, _amount);
        
        emit Withdrawn(msg.sender, _amount);
    }
    
    // === 借款 ===
    function borrow(uint256 _amount) external nonReentrant {
        require(_amount > 0, "金額必須大於 0");
        require(loanToken.balanceOf(address(this)) >= _amount, "協議流動性不足");
        
        updateInterest(msg.sender);
        
        UserInfo storage user = users[msg.sender];
        require(user.depositedAmount > 0, "請先存入抵押品");
        
        // 檢查抵押率
        uint256 maxBorrow = (user.depositedAmount * 100) / COLLATERAL_RATIO;
        require(user.borrowedAmount + _amount <= maxBorrow, "超過最大可借款金額");
        
        user.borrowedAmount += _amount;
        totalLoans += _amount;
        
        loanToken.transfer(msg.sender, _amount);
        
        emit Borrowed(msg.sender, _amount);
    }
    
    // === 還款 ===
    function repay(uint256 _amount) external nonReentrant {
        require(_amount > 0, "金額必須大於 0");
        
        updateInterest(msg.sender);
        
        UserInfo storage user = users[msg.sender];
        require(user.borrowedAmount >= _amount, "借款餘額不足");
        
        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);
    }
    
    // === 清算 ===
    function liquidate(address _user) external nonReentrant {
        UserInfo storage user = users[_user];
        require(user.borrowedAmount > 0, "沒有借款");
        
        // 計算抵押率
        uint256 currentRatio = (user.depositedAmount * 100) / user.borrowedAmount;
        require(currentRatio < LIQUIDATION_THRESHOLD, "尚未達到清算門檻");
        
        updateInterest(_user);
        
        // 清算人可以償還借款,獲得抵押品(含獎勵)
        uint256 repayAmount = user.borrowedAmount;
        uint256 collateralToLiquidate = (repayAmount * (100 + LIQUIDATION_BONUS)) / 100;
        
        if (collateralToLiquidate > user.depositedAmount) {
            collateralToLiquidate = user.depositedAmount;
        }
        
        // 清算人償還借款
        loanToken.transferFrom(msg.sender, address(this), repayAmount);
        
        // 清算人獲得抵押品
        user.depositedAmount -= collateralToLiquidate;
        user.borrowedAmount = 0;
        totalLoans -= repayAmount;
        
        collateralToken.transfer(msg.sender, collateralToLiquidate);
        
        emit Liquidated(_user, msg.sender, collateralToLiquidate);
    }
    
    // === 利息計算 ===
    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;
        // 簡單年利率計算
        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;
    }
    
    // === 查看使用者狀態 ===
    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;
    }
}

測試借貸功能

// scripts/test_lending.js
async function main() {
  const [deployer, user1, liquidator] = await hre.ethers.getSigners();
  
  // 部署代幣
  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();
  
  // 部署借貸合約
  const SimpleLending = await hre.ethers.getContractFactory("SimpleLending");
  const lending = await SimpleLending.deploy(
    await collateral.getAddress(),
    await loanToken.getAddress()
  );
  await lending.waitForDeployment();
  
  console.log("借貸合約地址:", await lending.getAddress());
  
  // 給 user1 一些抵押品
  const depositAmount = hre.ethers.parseEther("1000");
  await collateral.transfer(user1.address, depositAmount);
  
  // 給借貸合約一些借款代幣
  const loanAmount = hre.ethers.parseEther("10000");
  await loanToken.transfer(await lending.getAddress(), loanAmount);
  
  // user1 存入抵押品
  await collateral.connect(user1).approve(await lending.getAddress(), depositAmount);
  await lending.connect(user1).deposit(depositAmount);
  console.log("\nuser1 存入 1000 抵押品");
  
  // user1 借款
  const borrowAmount = hre.ethers.parseEther("500");
  await lending.connect(user1).borrow(borrowAmount);
  console.log("user1 借款 500");
  
  // 查詢健康狀態
  const health = await lending.getUserHealth(user1.address);
  console.log(`\n健康狀態: 存款=${hre.ethers.formatEther(health[0])}, ` +
    `借款=${hre.ethers.formatEther(health[1])}, ` +
    `比率=${health[2]}, 健康=${health[3]}`);
}

main().catch(console.error);

使用 Vibe Coding 建借貸協議

🔥 【借貸協議詠唱範例】 「請幫我實作一個去中心化借貸合約: 1. 支援存入 ETH 作為抵押品,借出 ERC-20 代幣。 2. 最低抵押率 150%,清算門檻 120%。 3. 利息根據資金利用率動態調整(利用率越高,利率越高)。 4. 清算獎勵 5%(清算人可以低價買到抵押品)。 5. 需要防止重入攻擊 (ReentrancyGuard)。 6. 寫一個完整的測試流程。」

本日總結

在本章中,你學到了:

  1. 借貸協議核心機制:抵押存款、借款、還款、清算
  2. 抵押率計算:如何確保協議的安全性
  3. 清算機制:當抵押率不足時的處理流程
  4. 利息計算:基於時間的簡單利率模型
  5. 使用者健康狀態:監控使用者的抵押率

下一章,我們將建立一個 DApp 前端來與這些合約互動!

解鎖完整教學內容

本章為付費內容。加入專案即可解鎖超過 5000 字的深度解析,包含 10 個以上神級 Prompt 與真實 Source Code 範例!