DeFi 協議程式碼實作完整指南:從智能合約到前端交互

本文提供從智能合約層到前端交互的完整程式碼範例,涵蓋 ERC-20 代幣合約、借貸協議、AMM 交易所、質押協議等主要 DeFi 應用場景,使用 Solidity 和 JavaScript/TypeScript 提供可直接運行的程式碼範例。

DeFi 協議程式碼實作完整指南:從智能合約到底層交互

DeFi 這玩意兒,說起來容易,真正要搞懂卻得折騰好幾層。你知道 Uniswap 的 swapExactTokensForTokens 背後到底呼叫了什麼合約嗎?Aave 的借貸利率是怎麼算出來的?Compound 的 cToken 又是什麼原理?這篇文章就是要把這些問題從頭到尾拆解清楚,帶你從智能合約層一路摸到前端交互。

我踩過太多坑了——一開始以為會用 ethers.js 就懂 DeFi,結果被現實狠狠打臉。後來硬著頭去啃合約原始碼,才發現介面文件和實際實作差了十萬八千里。所以這篇文章不只教你「怎麼用」,更要讓你搞懂「為什麼這樣設計」。

數據截止到 2026 年 3 月,所有合約位址和 API 以太坊主網為準。

以太坊智能合約交互基礎

為什麼要直接交互合約

大部分人接觸 DeFi 是從 MetaMask 錢包 + 網頁介面開始的。點點滑鼠就能swap代幣、質押借貸,看似很簡單。但這種方式有幾個問題:

直接用程式碼交互合約,就像從「用美圖秀秀」變成「用 Photoshop」——自由度高了,但門檻也高了。

ABI:合約的「說明書」

要與智能合約交互,首先得有 ABI(Application Binary Interface)。ABI 就像是合約的 API 文件,告訴你這個合約有什麼函數、參數是什麼類型、返回值是什麼。

// Uniswap V2 Router 的 ABI 片段
const uniswapV2RouterABI = [
  {
    "inputs": [
      { "internalType": "uint256", "name": "amountIn", "type": "uint256" },
      { "internalType": "uint256", "name": "amountOutMin", "type": "uint256" },
      { "internalType": "address[]", "name": "path", "type": "address[]" },
      { "internalType": "address", "name": "to", "type": "address" },
      { "internalType": "uint256", "name": "deadline", "type": "uint256" }
    ],
    "name": "swapExactTokensForTokens",
    "outputs": [
      { "internalType": "uint256[]", "name": "amounts", "type": "uint256[]" }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "factory",
    "outputs": [
      { "internalType": "address", "name": "", "type": "address" }
    ],
    "stateMutability": "view",
    "type": "function"
  }
];

ABI 可以從 Etherscan 上直接複製。點進合約頁面,點「Contract」標籤頁,然後點「ABI」就能看到了。

與合約交互的基本流程

// 使用 ethers.js 與合約交互
const { ethers } = require('ethers');

async function interactWithContract() {
  // 1. 連接到區塊鏈
  const provider = new ethers.providers.JsonRpcProvider(
    'https://mainnet.infura.io/v3/YOUR_PROJECT_ID'
  );
  
  // 2. 建立合約實例
  // 參數:合約位址、ABI、provider(唯讀)或 signer(可寫入)
  const uniswapRouter = new ethers.Contract(
    '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D',  // Uniswap V2 Router
    uniswapV2RouterABI,
    provider
  );
  
  // 3. 唯讀呼叫(不發交易)
  const factory = await uniswapRouter.factory();
  console.log('Uniswap Factory:', factory);
  
  // 4. 發送交易(需要簽名)
  const signer = new ethers.Wallet('YOUR_PRIVATE_KEY', provider);
  const signerContract = uniswapRouter.connect(signer);
  
  // 這裡是交易呼叫,需要 gas
  const tx = await signerContract.swapExactTokensForTokens(
    amountIn,
    amountOutMin,
    path,
    toAddress,
    deadline
  );
  
  // 等待交易確認
  const receipt = await tx.wait();
  console.log('Transaction hash:', receipt.transactionHash);
}

Uniswap:AMM 交換協議

Uniswap V2 交換原理

Uniswap 是以太坊上最經典的 AMM(自動做市商)協議。它的核心公式很簡單:

$$x \times y = k$$

其中 $x$ 和 $y$ 是池子裡兩種代幣的數量,$k$ 是常數。交易時,池子會保證這個等式成立。

// Uniswap V2 Pair 合約核心邏輯
// 這個合約管理一個流動性池的代幣交換
contract UniswapV2Pair {
    // 兩個代幣的位址
    address public token0;
    address public token1;
    
    // 儲備量(用於定價)
    uint112 private reserve0;
    uint112 private reserve1;
    
    // 交換函數
    function swap(
        uint256 amount0Out, 
        uint256 amount1Out, 
        address to, 
        bytes calldata data
    ) external {
        // 1. 獲取當前儲備量
        (uint112 _reserve0, uint112 _reserve1,) = getReserves();
        
        // 2. 驗證輸出金額
        require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');
        require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');
        
        // 3. 計算新的儲備量
        uint256 balance0 = IERC20(token0).balanceOf(address(this));
        uint256 balance1 = IERC20(token1).balanceOf(address(this));
        
        uint256 amount0In = balance0 > _reserve0 - amount0Out 
            ? balance0 - (_reserve0 - amount0Out) 
            : 0;
        uint256 amount1In = balance1 > _reserve1 - amount1Out 
            ? balance1 - (_reserve1 - amount1Out) 
            : 0;
            
        require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT');
        
        // 4. 執行交換並更新儲備量
        _update(balance0, balance1, _reserve0, _reserve1);
        
        // 5. 轉帳給接收者
        if (amount0Out > 0) _safeTransfer(token0, to, amount0Out);
        if (amount1Out > 0) _safeTransfer(token1, to, amount1Out);
    }
}

計算交換價格與滑點

在區塊鏈上執行交換前,你得先計算出你能拿到多少代幣、滑點是多少:

# Python 實現 Uniswap V2 交換計算
class UniswapV2SwapCalculator:
    """
    計算 Uniswap V2 的交換價格和滑點
    """
    
    @staticmethod
    def get_amount_out(amount_in, reserve_in, reserve_out, fee=997, fee_denom=1000):
        """
        計算輸出金額
        公式:amountOut = (amountIn * fee * reserveOut) / (reserveIn * feeDenom + amountIn * fee)
        
        fee = 997/1000 表示 0.3% 的交易費用
        """
        if amount_in <= 0:
            raise ValueError("amount_in must be greater than 0")
        if reserve_in <= 0 or reserve_out <= 0:
            raise ValueError("Reserves must be greater than 0")
            
        # 考慮交易費用
        amount_in_with_fee = amount_in * fee
        numerator = amount_in_with_fee * reserve_out
        denominator = reserve_in * fee_denom + amount_in_with_fee
        
        amount_out = numerator // denominator  # 整數除法,避免浮點誤差
        return amount_out
    
    @staticmethod
    def get_amount_in(amount_out, reserve_in, reserve_out, fee=997, fee_denom=1000):
        """
        計算達到目標輸出需要的輸入金額
        """
        if amount_out <= 0:
            raise ValueError("amount_out must be greater than 0")
        if reserve_in <= 0 or reserve_out <= 0:
            raise ValueError("Reserves must be greater than 0")
            
        numerator = reserve_in * amount_out * fee_denom
        denominator = (reserve_out - amount_out) * fee
        
        amount_in = (numerator // denominator) + 1
        return amount_in
    
    @staticmethod
    def calculate_price_impact(amount_in, reserve_in, reserve_out, fee=997, fee_denom=1000):
        """
        計算價格影響(Price Impact)
        
        公式:
        - 市場價格 = reserveOut / reserveIn
        - 實際價格 = amountOut / amountIn
        - 滑點 = (市場價格 - 實際價格) / 市場價格
        """
        market_price = reserve_out / reserve_in
        amount_out = UniswapV2SwapCalculator.get_amount_out(
            amount_in, reserve_in, reserve_out, fee, fee_denom
        )
        actual_price = amount_out / amount_in
        
        price_impact = (market_price - actual_price) / market_price
        return price_impact  # 例如:0.005 表示 0.5% 的滑點
    
    @staticmethod
    def simulate_swap(path, amounts, reserves):
        """
        模擬多跳路徑的交換
        
        path: 代幣位址陣列,例如 [WETH, USDC, DAI]
        amounts: amounts[0] 是輸入金額
        reserves: 每個池子的儲備量
        
        例如:要用 WETH 換 DAI,可能路徑是 WETH -> USDC -> DAI
        """
        amount = amounts[0]
        
        for i in range(len(path)):
            if i == 0:
                # 第一跳:amount 是輸入
                continue
            elif i == 1:
                # 第二跳:amount 是第一跳的輸出
                amount = UniswapV2SwapCalculator.get_amount_out(
                    amount,
                    reserves[i-1][0],  # 第一個池的 token0 儲備
                    reserves[i-1][1],  # 第一個池的 token1 儲備
                )
            else:
                amount = UniswapV2SwapCalculator.get_amount_out(
                    amount,
                    reserves[i-1][0],
                    reserves[i-1][1],
                )
        
        return amount  # 最終輸出金額


# 使用範例
calculator = UniswapV2SwapCalculator()

# 池子參數(需要從區塊鏈讀取)
reserve_in = 5000 * 10**18   # 5000 ETH (假設是 WETH/DAI 池)
reserve_out = 15_000_000 * 10**18  # 1500萬 DAI
amount_in = 10 * 10**18      # 10 ETH

# 計算輸出
amount_out = calculator.get_amount_out(amount_in, reserve_in, reserve_out)
print(f"預期輸出: {amount_out / 10**18} DAI")

# 計算滑點
price_impact = calculator.calculate_price_impact(
    amount_in, reserve_in, reserve_out
)
print(f"價格影響: {price_impact * 100:.3f}%")

完整的 Swap 交易程式碼

// 完整的 Uniswap V2 Swap 交易流程
const { ethers } = require('ethers');
const { Token, CurrencyAmount, Trade, Route, Pair } = require('@uniswap/sdk');

// 合約 ABI
const UNISWAP_V2_ROUTER_ABI = [
  'function getAmountsOut(uint amountIn, address[] memory path) public view returns (uint[] memory amounts)',
  'function swapExactTokensForTokens(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) returns (uint[] memory amounts)',
  'function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline) payable returns (uint[] memory amounts)',
  'function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) returns (uint[] memory amounts)',
  'function WETH() external pure returns (address)'
];

// 合約位址
const UNISWAP_V2_ROUTER = '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D';
const WETH = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2';
const USDC = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48';
const DAI = '0x6B175474E89094C44Da98b954EedeAC495271d0F';

class UniswapV2Swapper {
  constructor(privateKey, rpcUrl) {
    this.provider = new ethers.providers.JsonRpcProvider(rpcUrl);
    this.wallet = new ethers.Wallet(privateKey, this.provider);
    this.router = new ethers.Contract(
      UNISWAP_V2_ROUTER,
      UNISWAP_V2_ROUTER_ABI,
      this.wallet
    );
  }

  async getAmountsOut(amountIn, path) {
    return await this.router.getAmountsOut(amountIn, path);
  }

  async swapExactTokensForTokens({
    amountIn,
    amountOutMin,
    path,
    deadline = Math.floor(Date.now() / 1000) + 60 * 20  // 20 分鐘後
  }) {
    // 檢查代幣授權
    const tokenIn = new ethers.Contract(path[0], [
      'function allowance(address owner, address spender) view returns (uint256)',
      'function approve(address spender, uint256 amount) returns (bool)'
    ], this.wallet);

    const signerBalance = await tokenIn.balanceOf(this.wallet.address);
    console.log(`錢包餘額: ${ethers.utils.formatUnits(signerBalance, 18)}`);
    
    if (signerBalance.lt(amountIn)) {
      throw new Error('餘額不足');
    }

    const currentAllowance = await tokenIn.allowance(
      this.wallet.address, 
      UNISWAP_V2_ROUTER
    );
    
    if (currentAllowance.lt(amountIn)) {
      console.log('需要授權...');
      const approveTx = await tokenIn.approve(
        UNISWAP_V2_ROUTER, 
        ethers.constants.MaxUint256  // 授權最大值
      );
      await approveTx.wait();
      console.log('授權成功');
    }

    // 執行交換
    console.log('開始交換...');
    const tx = await this.router.swapExactTokensForTokens(
      amountIn,
      amountOutMin,
      path,
      this.wallet.address,
      deadline
    );
    
    const receipt = await tx.wait();
    console.log(`交換成功!交易雜湊: ${receipt.transactionHash}`);
    
    return receipt;
  }

  // 估算最佳交換路徑
  async findBestPath(amountIn, tokenIn, tokenOut) {
    // 常見的直接路徑和橋接路徑
    const commonPaths = [
      [tokenIn, tokenOut],  // 直接路徑
      [tokenIn, WETH, tokenOut],  // 透過 WETH
    ];
    
    let bestPath = null;
    let bestAmountOut = 0;
    
    for (const path of commonPaths) {
      try {
        const amounts = await this.getAmountsOut(amountIn, path);
        const amountOut = amounts[amounts.length - 1];
        
        if (amountOut.gt(bestAmountOut)) {
          bestAmountOut = amountOut;
          bestPath = path;
        }
      } catch (e) {
        // 這個路徑可能不存在
        continue;
      }
    }
    
    return { bestPath, bestAmountOut };
  }
}

// 使用範例
async function main() {
  const swapper = new UniswapV2Swapper(
    process.env.PRIVATE_KEY,
    'https://eth-mainnet.alchemyapi.io/v2/YOUR_API_KEY'
  );
  
  // 交換 1 ETH 等值的 USDC 到 DAI
  const wethAmount = ethers.utils.parseUnits('1', 18);
  
  // 找最佳路徑
  const { bestPath, bestAmountOut } = await swapper.findBestPath(
    wethAmount,
    WETH,
    DAI
  );
  
  console.log(`最佳路徑: ${bestPath.join(' -> ')}`);
  console.log(`預期輸出: ${ethers.utils.formatUnits(bestAmountOut, 18)} DAI`);
  
  // 執行交換(設定 0.5% 滑點)
  const amountOutMin = bestAmountOut.mul(995).div(1000);
  
  const receipt = await swapper.swapExactTokensForTokens({
    amountIn: wethAmount,
    amountOutMin: amountOutMin,
    path: bestPath
  });
}

main().catch(console.error);

Aave V3:去中心化借貸協議

Aave 借貸核心概念

Aave 是以太坊生態中最成功的借貸協議。它的運作方式是:存款人把代幣存入池子獲取利息,借款人用自己的資產作為抵押品借走其他代幣

核心參數:

// Aave V3 Pool 合約核心介面
// 這是與 Aave V3 交互的主要合約
interface IPool {
    // 存款
    function supply(
        address asset,
        uint256 amount,
        address onBehalfOf,
        uint16 referralCode
    ) external;
    
    // 借款
    function borrow(
        address asset,
        uint256 amount,
        uint256 interestRateMode,
        uint16 referralCode,
        address onBehalfOf
    ) external;
    
    // 還款
    function repay(
        address asset,
        uint256 amount,
        uint256 rateMode,
        address onBehalfOf
    ) external;
    
    // 提款
    function withdraw(
        address asset,
        uint256 amount,
        address to
    ) external;
    
    // 獲取使用者帳戶數據
    function getUserAccountData(address user)
        external
        view
        returns (
            uint256 totalCollateralBase,
            uint256 totalDebtBase,
            uint256 availableBorrowsBase,
            uint256 currentLiquidationThreshold,
            uint256 ltv,
            uint256 healthFactor
        );
}

// Aave V3 Data Types
struct ReserveData {
    // 基本參數
    ReserveConfigurationMap configuration;
    uint128 liquidityIndex;
    uint128 currentLiquidityRate;       // 存款 APY (in ray)
    uint128 variableBorrowIndex;        // 浮動借款利率指數
    uint128 variableBorrowRate;         // 浮動借款 APY (in ray)
    uint128 stableBorrowRate;           // 固定借款 APY (in ray)
    uint40 lastUpdateTimestamp;
    // 合約位址
    address aTokenAddress;
    address stableDebtTokenAddress;
    address variableDebtTokenAddress;
    address interestRateStrategyAddress;
    // 其他參數
    uint128 reservesTupleIdentification;
    uint128 unbackedMintCap;
    uint128 debtCeiling;
    uint128 data;
}

計算健康因子

健康因子是 Aave 借貸系統的核心風控指標:

$$HF = \frac{\sum (Collaterali \times LTVi)}{\sum (Debt_i)}$$

# Python 實現健康因子計算
class AaveHealthFactorCalculator:
    """
    計算 Aave 健康因子
    
    公式:HF = Σ(抵押品價值 × LTV) / Σ(借款價值)
    
    健康因子 > 1.0:安全
    健康因子 < 1.0:可被清算
    健康因子 = 1.0:清算門檻
    """
    
    # Aave V3 主要資產的 LTV(Loan-to-Value)配置
    LTV_CONFIG = {
        'ETH': 0.80,    # 80% LTV
        'WBTC': 0.70,  # 70% LTV
        'USDC': 0.80,  # 80% LTV(作為抵押品)
        'DAI': 0.75,   # 75% LTV
    }
    
    @staticmethod
    def calculate_health_factor(collaterals, debts, prices):
        """
        計算健康因子
        
        Args:
            collaterals: Dict[str, float] - 抵押品數量 {代幣: 數量}
            debts: Dict[str, float] - 借款數量 {代幣: 數量}
            prices: Dict[str, float] - 代幣價格 {代幣: USD價格}
        
        Returns:
            float - 健康因子
        """
        weighted_collateral_value = 0.0
        total_debt_value = 0.0
        
        # 計算加權抵押品價值
        for token, amount in collaterals.items():
            if token in prices and token in AaveHealthFactorCalculator.LTV_CONFIG:
                collateral_value = amount * prices[token]
                ltv = AaveHealthFactorCalculator.LTV_CONFIG[token]
                weighted_collateral_value += collateral_value * ltv
        
        # 計算總借款價值
        for token, amount in debts.items():
            if token in prices:
                total_debt_value += amount * prices[token]
        
        if total_debt_value == 0:
            return float('inf')  # 無借款,健康因子無限大
        
        health_factor = weighted_collateral_value / total_debt_value
        return health_factor
    
    @staticmethod
    def calculate_liquidation_threshold(collaterals, debts, prices):
        """
        計算清算門檻
        
        清算發生在 health_factor < 1.0
        """
        health_factor = AaveHealthFactorCalculator.calculate_health_factor(
            collaterals, debts, prices
        )
        return health_factor < 1.0
    
    @staticmethod
    def calculate_max_borrow(collateral_amount, collateral_price, ltv, debt_price=1.0):
        """
        計算最大可借款金額
        
        根據抵押品數量和 LTV 計算最大借款量
        """
        max_borrow = (collateral_amount * collateral_price * ltv) / debt_price
        return max_borrow
    
    @staticmethod
    def simulate_price_drop(collaterals, debts, prices, drop_percentage):
        """
        模擬抵押品價格下跌後的健康因子
        
        這個函數用來評估市場下跌時的風險
        """
        # 複製價格(避免修改原始數據)
        new_prices = prices.copy()
        
        # 應用價格下跌
        for token in collaterals:
            if token in new_prices:
                new_prices[token] *= (1 - drop_percentage / 100)
        
        return AaveHealthFactorCalculator.calculate_health_factor(
            collaterals, debts, new_prices
        )


# 使用範例
calculator = AaveHealthFactorCalculator()

# 假設用戶的持倉
collaterals = {
    'ETH': 10.0,      # 10 ETH
    'WBTC': 0.5,     # 0.5 WBTC
}

debts = {
    'USDC': 15000.0,  # 借了 15000 USDC
}

prices = {
    'ETH': 3000.0,    # ETH = $3000
    'WBTC': 60000.0,  # WBTC = $60000
    'USDC': 1.0,      # USDC = $1
}

# 計算健康因子
health_factor = calculator.calculate_health_factor(collaterals, debts, prices)
print(f"當前健康因子: {health_factor:.3f}")

# 模擬 ETH 價格下跌 20%
new_hf = calculator.simulate_price_drop(collaterals, debts, prices, 20)
print(f"ETH 下跌 20% 後健康因子: {new_hf:.3f}")

# 模擬 ETH 價格下跌 30%
new_hf = calculator.simulate_price_drop(collaterals, debts, prices, 30)
print(f"ETH 下跌 30% 後健康因子: {new_hf:.3f}")

# 計算清算門檻
is_liquidation = calculator.calculate_liquidation_threshold(collaterals, debts, prices)
print(f"是否處於清算邊緣: {is_liquidation}")

與 Aave V3 交互

// 與 Aave V3 完整交互流程
const { ethers } = require('ethers');

// Aave V3 Pool 合約 ABI(精簡版)
const AAVE_V3_POOL_ABI = [
  // 供應(存款)
  'function supply(address asset, uint256 amount, address onBehalfOf, uint16 referralCode)',
  // 借款
  'function borrow(address asset, uint256 amount, uint256 interestRateMode, uint16 referralCode, address onBehalfOf)',
  // 還款
  'function repay(address asset, uint256 amount, uint256 rateMode, address onBehalfOf) returns (uint256)',
  // 提款
  'function withdraw(address asset, uint256 amount, address to) returns (uint256)',
  // 獲取帳戶數據
  'function getUserAccountData(address user) view returns (uint256, uint256, uint256, uint256, uint256, uint256)',
  // 獲取儲備數據
  'function getReserveData(address asset) view returns ((...), address aToken)',
];

// 合約位址(以太坊主網)
const AAVE_V3_POOL = '0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2';
const WETH = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2';
const USDC = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48';
const aUSDC = '00a9B...';  // aUSDC 合約位址

class AaveV3Lender {
  constructor(privateKey, rpcUrl) {
    this.provider = new ethers.providers.JsonRpcProvider(rpcUrl);
    this.wallet = new ethers.Wallet(privateKey, this.provider);
    this.pool = new ethers.Contract(AAVE_V3_POOL, AAVE_V3_POOL_ABI, this.wallet);
  }

  // 供應(存款)ETH 到 Aave
  async supplyETH(amount) {
    // ETH 需要用 payable 方式傳入
    const tx = await this.pool.supply(WETH, amount, this.wallet.address, 0, {
      value: amount  // 直接把 ETH 轉入
    });
    const receipt = await tx.wait();
    console.log(`存款成功: ${ethers.utils.formatUnits(amount, 18)} ETH`);
    return receipt;
  }

  // 借款 USDC
  async borrowUSDC(amount, interestRateMode = 2) {
    // interestRateMode: 1 = 固定利率, 2 = 浮動利率
    
    const tx = await this.pool.borrow(
      USDC,           // 借款幣種
      amount,         // 借款數量
      interestRateMode,  // 利率模式
      0,              // referral code
      this.wallet.address  // 借款去向
    );
    const receipt = await tx.wait();
    console.log(`借款成功: ${ethers.utils.formatUnits(amount, 6)} USDC`);
    return receipt;
  }

  // 還款
  async repayUSDC(amount, rateMode = 2) {
    // rateMode: 1 = 固定利率債務, 2 = 浮動利率債務
    
    // 需要先授權 USDC
    const usdc = new ethers.Contract(USDC, [
      'function approve(address spender, uint256 amount) returns (bool)'
    ], this.wallet);
    
    await usdc.approve(AAVE_V3_POOL, amount);
    
    const tx = await this.pool.repay(USDC, amount, rateMode, this.wallet.address);
    const receipt = await tx.wait();
    console.log(`還款成功`);
    return receipt;
  }

  // 查詢帳戶數據
  async getAccountData() {
    const data = await this.pool.getUserAccountData(this.wallet.address);
    
    return {
      totalCollateralBase: data[0],      // 總抵押品(以 USD 計算)
      totalDebtBase: data[1],            // 總債務
      availableBorrowsBase: data[2],      // 可借款額度
      currentLiquidationThreshold: data[3], // 當前清算門檻
      ltv: data[4],                       // LTV
      healthFactor: data[5]              // 健康因子
    };
  }

  // 計算清算風險
  async assessLiquidationRisk() {
    const accountData = await this.getAccountData();
    const healthFactor = parseFloat(ethers.utils.formatUnits(accountData.healthFactor, 18));
    
    let riskLevel;
    if (healthFactor > 2.0) {
      riskLevel = 'LOW';
    } else if (healthFactor > 1.5) {
      riskLevel = 'MEDIUM';
    } else if (healthFactor > 1.0) {
      riskLevel = 'HIGH';
    } else {
      riskLevel = 'CRITICAL - LIQUIDATION IMMINENT';
    }
    
    console.log(`
      ===== 清算風險評估 =====
      健康因子: ${healthFactor.toFixed(3)}
      風險等級: ${riskLevel}
      總抵押品: $${parseFloat(ethers.utils.formatUnits(accountData.totalCollateralBase, 8)).toLocaleString()}
      總債務: $${parseFloat(ethers.utils.formatUnits(accountData.totalDebtBase, 8)).toLocaleString()}
      可借款額度: $${parseFloat(ethers.utils.formatUnits(accountData.availableBorrowsBase, 8)).toLocaleString()}
    `);
    
    return { healthFactor, riskLevel, ...accountData };
  }
}

// 使用範例
async function main() {
  const aave = new AaveV3Lender(
    process.env.PRIVATE_KEY,
    'https://eth-mainnet.alchemyapi.io/v2/YOUR_API_KEY'
  );
  
  // 評估清算風險
  await aave.assessLiquidationRisk();
  
  // 借款
  const borrowAmount = ethers.utils.parseUnits('1000', 6); // 1000 USDC
  await aave.borrowUSDC(borrowAmount);
  
  // 再次評估
  await aave.assessLiquidationRisk();
}

main().catch(console.error);

Compound III:cToken 機制詳解

cToken 的運作原理

Compound 採用了一種很巧妙的設計:存款得到 cToken,cToken 餘額代表你在池子中的份額

公式是:

$$cTokenAmount = \frac{suppliedAmount}{cTokenExchangeRate}$$

Exchange Rate 會隨著存款利息累積而不斷增加。

// Compound III cToken 合約核心邏輯
contract cToken {
    // 累計利率指數(每個區塊都在增加)
    uint256 public accrualBlockNumber;
    uint256 public borrowIndex;  // 借款利率累計指數
    
    // 總借款量
    uint256 public totalBorrows;
    
    // 用戶的借款記錄
    mapping(address => BorrowSnapshot) internal accountBorrows;
    
    struct BorrowSnapshot {
        uint256 principal;
        uint256 interestIndex;
    }
    
    // 存款利率計算
    function interestRateModel() public view returns (address) {
        return address(interestRateModel);
    }
    
    // 獲取當前的存款利率
    function supplyRatePerBlock() public view returns (uint256) {
        uint256 utilization = utilizationRate();
        return interestRateModel.getSupplyRate(utilization, 0, 0);
    }
    
    // 獲取借款利率
    function borrowRatePerBlock() public view returns (uint256) {
        uint256 utilization = utilizationRate();
        return interestRateModel.getBorrowRate(utilization, 0, 0);
    }
    
    // 利用率計算
    function utilizationRate() public view returns (uint256) {
        uint256 cash = getCash();
        uint256 borrows = totalBorrows;
        uint256 reserves = totalReserves;
        
        if (borrows == 0) return 0;
        
        return borrows * 1e18 / (cash + borrows - reserves);
    }
    
    // 存款(mint cToken)
    function mint(uint mintAmount) external returns (uint) {
        // 1. 計算將獲得的 cToken 數量
        uint exchangeRate = exchangeRateCurrent();
        uint mintTokens;
        
        // 2. 計算 cToken 數量 = mintAmount / exchangeRate
        mintTokens = div_(mintAmount, exchangeRate);
        
        // 3. 轉入 underlying token
        require(EIP20(token).transferFrom(msg.sender, address(this), mintAmount));
        
        // 4. 更新內部記錄
        totalSupply = totalSupply + mintTokens;
        accountTokens[msg.sender] = accountTokens[msg.sender] + mintTokens;
        
        emit Mint(msg.sender, mintAmount, mintTokens);
        return 0;
    }
    
    // 贖回(redeem cToken)
    function redeem(uint redeemTokens) external returns (uint) {
        // 計算可贖回的 underlying token 數量
        uint exchangeRate = exchangeRateCurrent();
        uint redeemAmount = mul_(redeemTokens, exchangeRate);
        
        // 驗證流動性
        require(accrueInterest() == 0);
        require(getCash(msg.sender) >= redeemAmount);
        
        // 更新記錄
        totalSupply = totalSupply - redeemTokens;
        accountTokens[msg.sender] = accountTokens[msg.sender] - redeemTokens;
        
        // 轉出代幣
        require(EIP20(token).transfer(msg.sender, redeemAmount));
        
        return 0;
    }
}

借款利率模型

Compound 使用分段線性利率模型:

 Utilization Rate (U)      Supply Rate      Borrow Rate
      0% ────────────────  0%              0%
                            (免費借錢!)
                                            
      80% ────────────────  8%              10%
                            (線性增加)
                                            
      100% ────────────────  20%             40%
                            (陡峭增加)
# Compound 利率模型實現
class CompoundInterestRateModel:
    """
    Compound 的分段線性利率模型
    
    Borrow Rate = 
        0%, if U < U_optimal
        kink_rate + (U - U_optimal) * (max_borrow_rate - kink_rate) / (1 - U_optimal), otherwise
    """
    
    def __init__(self, kink=0.8, base_rate=0.0, multiplier=0.1, jump_multiplier=2.5):
        """
        Args:
            kink: 轉折點利用率,默認 80%
            base_rate: 基礎借款利率,默認 0%
            multiplier: 轉折點前的斜率,默認 10%
            jump_multiplier: 超過轉折點後的倍數,默認 2.5x
        """
        self.kink = kink
        self.base_rate = base_rate
        self.multiplier = multiplier
        self.jump_multiplier = jump_multiplier
    
    def get_borrow_rate(self, utilization):
        """
        計算借款利率
        
        Args:
            utilization: 利用率 (0-1)
        
        Returns:
            float: 年化借款利率
        """
        if utilization <= self.kink:
            # 低利用率區間:線性增加
            return self.base_rate + utilization * self.multiplier
        else:
            # 高利用率區間:陡峭增加
            normal_rate = self.base_rate + self.kink * self.multiplier
            excess_util = utilization - self.kink
            jump_rate = excess_util * self.multiplier * (self.jump_multiplier - 1) / (1 - self.kink)
            return normal_rate + jump_rate
    
    def get_supply_rate(self, utilization):
        """
        計算存款利率
        
        存款利率 = 借款利率 × 利用率 × (1 - 儲備係數)
        """
        if utilization == 0:
            return 0.0
        
        borrow_rate = self.get_borrow_rate(utilization)
        reserve_factor = 0.15  # Compound 默認 15% 進入儲備金
        
        # 存款利率 = 借款利率 × 利用率 × (1 - 儲備係數)
        supply_rate = borrow_rate * utilization * (1 - reserve_factor)
        return supply_rate
    
    def calculate_apr(self, utilization, periods_per_year=2628000):
        """
        計算 APY(年化收益率)
        
        Args:
            utilization: 利用率
            periods_per_year: 每年區塊數(以太坊約 12 秒/區塊)
        
        Returns:
            float: 年化收益率
        """
        rate_per_block = self.get_supply_rate(utilization)
        apy = (1 + rate_per_block / periods_per_year) ** periods_per_year - 1
        return apy


# 使用範例
model = CompoundInterestRateModel()

print("利用率 | 借款 APR | 存款 APR | 存款 APY")
print("-" * 50)

for u in [0.0, 0.2, 0.4, 0.6, 0.8, 0.9, 0.95]:
    borrow_apr = model.get_borrow_rate(u)
    supply_apr = model.get_supply_rate(u)
    supply_apy = model.calculate_apr(u)
    print(f"{u*100:6.1f}% | {borrow_apr*100:7.2f}% | {supply_apr*100:7.2f}% | {supply_apy*100:7.2f}%")

結語:從代碼到理解

折騰 DeFi 合約交互這麼久,最大的感悟是:不要只會用前端,要搞懂底層邏輯

每次你點擊「Swap」按鈕,背後其實是:

  1. 你的代幣被 approve 轉移到合約
  2. 合約執行了 swapExactTokensForTokens
  3. 根據 AMM 公式計算輸出金額
  4. 扣除 0.3% 的交易費用
  5. 把代幣轉回你的錢包

理解這些之後,你就能做很多前端做不到的事:

DeFi 世界的門檻是高,但不代表不能學。只要願意折騰,遲早能搞懂。


本網站內容僅供教育與資訊目的,不構成任何技術建議或投資建議。在進行任何 DeFi 操作前,請自行研究並諮詢專業人士意見。所有智能合約操作均涉及風險,請謹慎評估。

數據截止:2026 年 3 月

技術參考來源:

COMMIT: Add DeFi protocol code examples guide with extensive Solidity and Python code

延伸閱讀與來源

這篇文章對您有幫助嗎?

評論

發表評論

注意:由於這是靜態網站,您的評論將儲存在本地瀏覽器中,不會公開顯示。

目前尚無評論,成為第一個發表評論的人吧!