DeFi 智能合約實作範例新手教程:從借貸到交易的完整程式碼指南

本文帶你深入了解 DeFi 核心功能的智慧合約實現,包括自動做市商(AMM)的常數乘積公式與程式碼範例、借貸協議的利率模型與清算機制、流動性挖礦的激勵設計,以及如何在以太坊上實際操作這些協議。所有範例都提供完整的 Solidity 程式碼,幫助讀者從理論到實踐全面掌握 DeFi 開發。

DeFi 智能合約實作範例新手教程:從借貸到交易的完整程式碼指南

前言:為什麼要學習 DeFi 程式碼?

去中心化金融(DeFi)是以太坊生態系統中最激動人心的應用領域之一。通過智慧合約,人們可以在不需要傳統金融機構的情況下,借貸、交易、存款、借款。但很多新手只知道「如何使用」DeFi 協議,卻不理解它們背後的運作原理。

本文將帶你深入了解 DeFi 核心功能的智慧合約實現。我們會用簡單的語言解釋複雜的概念,並提供完整的 Solidity 程式碼範例。閱讀完本文後,你將能夠理解:

這篇文章假設你已經了解以太坊的基本概念(地址、交易、Gas),但不要求你會編程。

第一章:自動做市商(AMM)基礎

1.1 什麼是 AMM?

傳統金融市場使用「訂單簿」模式:買方和賣方各自下單,交易所負責匹配。例如:

但這種模式需要足夠的流動性——有足夠多的買家和賣家。如果市場冷門,可能找不到交易對手。

AMM 採用完全不同的方式:使用數學公式自動定價,不需要傳統的買家和賣家。流動性提供者(LP)將資金存入「流動性池」,交易者直接與池子交易。價格由演算法自動計算。

1.2 常數乘積公式

AMM 最核心的概念是「常數乘積公式」:

x × y = k

其中:

這個公式的意義是:交易前後,x 和 y 的乘積必須保持不變。

舉例說明

假設 ETH/USDT 流動性池有:

此時 k = 100 × 200,000 = 20,000,000

現在有人想用 USDT 買 ETH:

注意:這個交易改變了 ETH 的價格!從 200,000/100 = 2000 USDT/ETH,變成 220,000/90.91 ≈ 2420 USDT/ETH。

1.3 AMM 合約程式碼範例

以下是一個簡化的 AMM 智慧合約:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

/**
 * @title SimpleAMM
 * @dev 簡化的自動做市商合約
 * 
 * 這個合約實現了基本的 x * y = k 公式
 * 僅供教學用途,生產環境需要更多安全考量
 */
contract SimpleAMM {
    // 事件記錄
    event Swap(
        address indexed user,
        address indexed tokenIn,
        address indexed tokenOut,
        uint256 amountIn,
        uint256 amountOut
    );
    event AddLiquidity(
        address indexed provider,
        uint256 amountA,
        uint256 amountB
    );
    event RemoveLiquidity(
        address indexed provider,
        uint256 amountA,
        uint256 amountB
    );

    // 兩種代幣的地址
    address public tokenA;
    address public tokenB;

    // 流動性池中的代幣數量
    uint256 public reserveA;
    uint256 public reserveB;

    // 流動性代幣總供應量
    uint256 public totalSupply;

    // 用戶持有的流動性代幣數量
    mapping(address => uint256) public balanceOf;

    constructor(address _tokenA, address _tokenB) {
        tokenA = _tokenA;
        tokenB = _tokenB;
    }

    /**
     * @dev 計算交易輸出數量
     * 使用常數乘積公式
     */
    function getAmountOut(uint256 amountIn, uint256 reserveIn, uint256 reserveOut) 
        public pure returns (uint256) 
    {
        require(amountIn > 0, "Invalid input amount");
        require(reserveIn > 0 && reserveOut > 0, "Insufficient liquidity");

        // 考慮 0.3% 的交易費用
        uint256 amountInWithFee = amountIn * 997;
        uint256 numerator = amountInWithFee * reserveOut;
        uint256 denominator = reserveIn * 1000 + amountInWithFee;

        return numerator / denominator;
    }

    /**
     * @dev 兌換函數:將代幣 A 換成代幣 B
     */
    function swap(address tokenIn, uint256 amountIn) external returns (uint256 amountOut) {
        require(tokenIn == tokenA || tokenIn == tokenB, "Invalid token");
        
        (uint256 reserveIn, uint256 reserveOut) = tokenIn == tokenA 
            ? (reserveA, reserveB) 
            : (reserveB, reserveA);

        // 計算輸出數量
        amountOut = getAmountOut(amountIn, reserveIn, reserveOut);
        require(amountOut > 0, "Insufficient output amount");

        // 更新儲備量
        if (tokenIn == tokenA) {
            reserveA += amountIn;
            reserveB -= amountOut;
        } else {
            reserveB += amountIn;
            reserveA -= amountOut;
        }

        // 轉入輸入代幣
        IERC20(tokenIn).transferFrom(msg.sender, address(this), amountIn);
        // 轉出輸出代幣
        IERC20(tokenIn == tokenA ? tokenB : tokenA).transfer(msg.sender, amountOut);

        emit Swap(msg.sender, tokenIn, tokenIn == tokenA ? tokenB : tokenA, amountIn, amountOut);
    }

    /**
     * @dev 添加流動性
     */
    function addLiquidity(uint256 amountA, uint256 amountB) external returns (uint256 liquidity) {
        // 根據現有比例計算
        uint256 ratio;
        if (totalSupply == 0) {
            // 首次添加,使用幾何平均數
            liquidity = sqrt(amountA * amountB);
        } else {
            // 根據比例計算
            uint256 liquidityA = (amountA * totalSupply) / reserveA;
            uint256 liquidityB = (amountB * totalSupply) / reserveB;
            liquidity = liquidityA < liquidityB ? liquidityA : liquidityB;
        }

        require(liquidity > 0, "Insufficient liquidity minted");

        // 更新狀態
        balanceOf[msg.sender] += liquidity;
        totalSupply += liquidity;
        reserveA += amountA;
        reserveB += amountB;

        // 轉入代幣
        IERC20(tokenA).transferFrom(msg.sender, address(this), amountA);
        IERC20(tokenB).transferFrom(msg.sender, address(this), amountB);

        emit AddLiquidity(msg.sender, amountA, amountB);
    }

    /**
     * @dev 移除流動性
     */
    function removeLiquidity(uint256 liquidity) external returns (uint256 amountA, uint256 amountB) {
        require(liquidity > 0, "Invalid liquidity amount");
        require(balanceOf[msg.sender] >= liquidity, "Insufficient balance");

        // 計算贖回數量
        amountA = (liquidity * reserveA) / totalSupply;
        amountB = (liquidity * reserveB) / totalSupply;

        // 更新狀態
        balanceOf[msg.sender] -= liquidity;
        totalSupply -= liquidity;
        reserveA -= amountA;
        reserveB -= amountB;

        // 轉出代幣
        IERC20(tokenA).transfer(msg.sender, amountA);
        IERC20(tokenB).transfer(msg.sender, amountB);

        emit RemoveLiquidity(msg.sender, amountA, amountB);
    }

    // 輔助函數:平方根計算
    function sqrt(uint256 y) internal pure returns (uint256 z) {
        if (y > 3) {
            z = y;
            uint256 x = y / 2 + 1;
            while (x < z) {
                z = x;
                x = (y / x + x) / 2;
            }
        } else if (y != 0) {
            z = 1;
        }
    }
}

// 簡化的 ERC20 接口
interface IERC20 {
    function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
    function transfer(address recipient, uint256 amount) external returns (bool);
    function balanceOf(address account) external view returns (uint256);
}

1.4 代數定價曲線

實際的 AMM(如 Uniswap)使用更複雜的定價曲線,稱為「代數定價」:

/**
 * @title ConstantProductAMMWithFee
 * @dev 帶費用的常數乘積 AMM
 */
contract ConstantProductAMMWithFee {
    uint256 public constant FEE = 3; // 0.3% 費用

    function getAmountOut(
        uint256 amountIn,
        uint256 reserveIn,
        uint256 reserveOut
    ) public pure returns (uint256) {
        require(amountIn > 0, "amountIn must be greater than 0");
        require(reserveIn > 0 && reserveOut > 0, "reserves must be greater than 0");

        // 扣除費用
        uint256 amountInWithFee = amountIn * (1000 - FEE);
        
        // 常數乘積公式
        // (x + dx) * (y - dy) = x * y
        // y - dy = x * y / (x + dx)
        // dy = y - x * y / (x + dx)
        // dy = y * dx / (x + dx)
        
        uint256 numerator = amountInWithFee * reserveOut;
        uint256 denominator = (reserveIn * 1000) + amountInWithFee;
        
        return numerator / denominator;
    }

    // 範例:計算 ETH 換 USDT 的輸出
    function getETHToUSDTOutput(uint256 ethAmountIn, uint256 ethReserve, uint256 usdtReserve)
        external pure returns (uint256)
    {
        return getAmountOut(ethAmountIn, ethReserve, usdtReserve);
    }

    // 範例:計算 USDT 換 ETH 的輸出
    function getUSDTToETHOutput(uint256 usdtAmountIn, uint256 usdtReserve, uint256 ethReserve)
        external pure returns (uint256)
    {
        return getAmountOut(usdtAmountIn, usdtReserve, ethReserve);
    }
}

1.5 滑點的概念

「滑點」是指你預期獲得的價格與實際成交價格之間的差異。

在 AMM 中,交易規模越大,價格影響越大,滑點越高。

例如:

這就是為什麼大額交易會造成巨大滑點。聰明的交易者會分批執行大額訂單。

第二章:借貸協議

2.1 借貸協議的基本原理

DeFi 借貸協議允許用戶:

借款需要超額抵押——借出的價值必須低於抵押的價值。這確保了協議的安全性。

2.2 簡化借貸合約

以下是一個基礎的借貸智慧合約:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

/**
 * @title SimpleLendingProtocol
 * @dev 簡化版借貸協議
 * 
 * 核心功能:
 * - 存款獲取利息
 * - 抵押借款
 * - 清算機制
 */
contract SimpleLendingProtocol {
    // 事件
    event Deposit(address indexed user, address indexed token, uint256 amount);
    event Borrow(address indexed user, address indexed token, uint256 amount, uint256 collateralAmount);
    event Repay(address indexed user, address indexed token, uint256 amount);
    event Liquidate(
        address indexed liquidator,
        address indexed borrower,
        address indexed collateralToken,
        uint256 debtPaid,
        uint256 collateralReceived
    );

    // 市場狀態
    struct Market {
        uint256 totalDeposits;      // 總存款
        uint256 totalBorrows;       // 總借款
        uint256 depositRate;        // 存款利率(每秒)
        uint256 borrowRate;         // 借款利率(每秒)
        uint256 lastUpdateTime;     // 上次更新時間
        uint256 collateralFactor;   // 抵押因子(最大借款額/抵押價值)
    }

    // 用戶存款餘額
    mapping(address => mapping(address => uint256)) public deposits;
    
    // 用戶借款餘額
    mapping(address => mapping(address => uint256)) public borrows;
    
    // 用戶的抵押資產
    mapping(address => mapping(address => uint256)) public collateral;
    
    // 市場資訊
    mapping(address => Market) public markets;

    // 利率模型參數
    uint256 public constant BASE_RATE = 0.000001 ether; // 基礎利率
    uint256 public constant UTILIZATION_RATE = 0.5 ether; // 目標利用率

    /**
     * @dev 存款函數
     */
    function deposit(address token, uint256 amount) external {
        require(amount > 0, "Deposit amount must be greater than 0");

        // 更新市場利率
        _updateInterest(token);

        // 累積用戶存款
        deposits[msg.sender][token] += amount;
        
        // 更新市場總存款
        markets[token].totalDeposits += amount;

        // 將代幣轉入合約
        IERC20(token).transferFrom(msg.sender, address(this), amount);

        emit Deposit(msg.sender, token, amount);
    }

    /**
     * @dev 借款函數
     */
    function borrow(address token, uint256 amount, address collateralToken) external {
        require(amount > 0, "Borrow amount must be greater than 0");

        Market storage market = markets[token];
        
        // 檢查是否有足夠存款
        require(market.totalDeposits - market.totalBorrows >= amount, "Insufficient liquidity");

        // 計算健康度
        uint256 maxBorrow = collateral[msg.sender][collateralToken] * market.collateralFactor / 1e18;
        require(borrows[msg.sender][token] + amount <= maxBorrow, "Insufficient collateral");

        // 更新市場利率
        _updateInterest(token);

        // 記錄借款
        borrows[msg.sender][token] += amount;
        market.totalBorrows += amount;

        // 轉出借款
        IERC20(token).transfer(msg.sender, amount);

        emit Borrow(msg.sender, token, amount, collateral[msg.sender][collateralToken]);
    }

    /**
     * @dev 償還借款
     */
    function repay(address token, uint256 amount) external {
        require(amount > 0, "Repay amount must be greater than 0");

        // 更新市場利率
        _updateInterest(token);

        uint256 debt = borrows[msg.sender][token];
        uint256 repayAmount = amount > debt ? debt : amount;

        // 更新借款餘額
        borrows[msg.sender][token] -= repayAmount;
        markets[token].totalBorrows -= repayAmount;

        // 轉入還款
        IERC20(token).transferFrom(msg.sender, address(this), repayAmount);

        emit Repay(msg.sender, token, repayAmount);
    }

    /**
     * @dev 抵押資產
     */
    function addCollateral(address token, uint256 amount) external {
        require(amount > 0, "Collateral amount must be greater than 0");

        collateral[msg.sender][token] += amount;
        IERC20(token).transferFrom(msg.sender, address(this), amount);
    }

    /**
     * @dev 清算函數
     * 當健康度低於閾值時,任何人可以清算借款人
     */
    function liquidate(
        address borrower,
        address debtToken,
        address collateralToken,
        uint256 debtAmount
    ) external {
        Market storage market = markets[debtToken];
        
        // 計算健康度
        uint256 maxBorrow = collateral[borrower][collateralToken] * market.collateralFactor / 1e18;
        uint256 currentDebt = borrows[borrower][debtToken];
        
        require(currentDebt > 0, "No debt to liquidate");
        require(currentDebt * 1e18 / collateral[borrower][collateralToken] < market.collateralFactor, 
            "Health factor is good");

        // 計算清算數量
        uint256 repayAmount = debtAmount > currentDebt ? currentDebt : debtAmount;
        
        // 清算獎勵(通常有折扣)
        uint256 bonus = repayAmount / 10; // 10% 獎勵

        // 更新餘額
        borrows[borrower][debtToken] -= repayAmount;
        market.totalBorrows -= repayAmount;
        
        collateral[borrower][collateralToken] -= (repayAmount + bonus);

        // 轉帳
        IERC20(debtToken).transferFrom(msg.sender, address(this), repayAmount);
        IERC20(collateralToken).transfer(msg.sender, repayAmount + bonus);

        emit Liquidate(msg.sender, borrower, collateralToken, repayAmount, repayAmount + bonus);
    }

    /**
     * @dev 更新利率
     */
    function _updateInterest(address token) internal {
        Market storage market = markets[token];
        
        if (market.totalDeposits == 0) return;

        uint256 timePassed = block.timestamp - market.lastUpdateTime;
        if (timePassed == 0) return;

        // 計算利用率
        uint256 utilization = market.totalBorrows * 1e18 / market.totalDeposits;

        // 借款利率 = 基礎利率 + 利用率 * 係數
        market.borrowRate = BASE_RATE + (utilization * 2);
        
        // 存款利率 = 借款利率 * 利用率 * 0.7(70% 給存款者)
        market.depositRate = market.borrowRate * utilization * 7 / 10 / 1e18;

        // 累積利息
        uint256 borrowInterest = market.totalBorrows * market.borrowRate * timePassed / 1e18;
        market.totalBorrows += borrowInterest;

        uint256 depositInterest = market.totalDeposits * market.depositRate * timePassed / 1e18;
        market.totalDeposits += depositInterest;

        market.lastUpdateTime = block.timestamp;
    }
}

interface IERC20 {
    function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
    function transfer(address recipient, uint256 amount) external returns (bool);
    function balanceOf(address account) external view returns (uint256);
}

2.3 利率模型詳解

借貸協議的利率模型通常包含以下要素:

基礎利率:即使沒有人借款,也會產生的最低利率。這保證了協議的基本運作。

利用率:借款總額 / 存款總額。利用率越高,說明資金越緊張,借款利率應該越高。

借款利率公式

借款利率 = 基礎利率 + 利用率 × 斜率係數

存款利率公式

存款利率 = 借款利率 × 利用率 × 分給存款者的比例

這種設計確保了:

2.4 健康因子與清算

「健康因子」是衡量借款人抵押是否充足的指標:

健康因子 = 抵押價值 × 抵押因子 / 借款價值

例如:

當健康因子低於 1 時,抵押品價值不足以覆蓋借款。這時任何人都可以執行「清算」:

這就是為什麼 DeFi 借貸協議能夠維持償付能力——即使抵押品價格下跌,清算機制會自動處理不良債務。

第三章:流動性挖礦

3.1 什麼是流動性挖礦?

流動性挖礦(Yield Farming)是 DeFi 中的一種激勵機制。協議會分發自己的代幣,獎勵提供流動性的用戶。

例如:

這創造了額外的收益來源,但同時也帶來了複雜的風險。

3.2 流動性挖礦合約範例

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

/**
 * @title SimpleYieldFarming
 * @dev 簡化的流動性挖礦合約
 */
contract SimpleYieldFarming {
    // 獎勵代幣
    IERC20 public rewardToken;
    
    // 抵押的代幣(流動性代幣)
    IERC20 public stakingToken;

    // 每秒產出的獎勵數量
    uint256 public rewardRate;

    // 上次更新獎勵的時間
    uint256 public lastUpdateTime;

    // 每個質押代幣的累積獎勵
    uint256 public rewardPerTokenStored;

    // 用戶已領取的獎勵
    mapping(address => uint256) public userRewardPaid;

    // 用戶的質押餘額
    mapping(address => uint256) public stakingBalance;

    // 總質押量
    uint256 public totalSupply;

    // 事件
    event Staked(address indexed user, uint256 amount);
    event Withdrawn(address indexed user, uint256 amount);
    event RewardClaimed(address indexed user, uint256 reward);

    constructor(address _rewardToken, address _stakingToken, uint256 _rewardRate) {
        rewardToken = IERC20(_rewardToken);
        stakingToken = IERC20(_stakingToken);
        rewardRate = _rewardRate;
        lastUpdateTime = block.timestamp;
    }

    /**
     * @dev 質押函數
     */
    function stake(uint256 amount) external {
        require(amount > 0, "Cannot stake 0");

        // 更新獎勵
        _updateReward(msg.sender);

        // 質押
        stakingBalance[msg.sender] += amount;
        totalSupply += amount;

        // 轉入代幣
        stakingToken.transferFrom(msg.sender, address(this), amount);

        emit Staked(msg.sender, amount);
    }

    /**
     * @dev 提取質押
     */
    function withdraw(uint256 amount) external {
        require(amount > 0, "Cannot withdraw 0");
        require(stakingBalance[msg.sender] >= amount, "Insufficient balance");

        // 更新獎勵
        _updateReward(msg.sender);

        // 提取
        stakingBalance[msg.sender] -= amount;
        totalSupply -= amount;

        // 轉出代幣
        stakingToken.transfer(msg.sender, amount);

        emit Withdrawn(msg.sender, amount);
    }

    /**
     * @dev 領取獎勵
     */
    function getReward() external {
        _updateReward(msg.sender);

        uint256 reward = earned(msg.sender);
        if (reward > 0) {
            userRewardPaid[msg.sender] = rewardPerTokenStored;
            rewardToken.transfer(msg.sender, reward);
            emit RewardClaimed(msg.sender, reward);
        }
    }

    /**
     * @dev 計算用戶已賺取的獎勵
     */
    function earned(address account) public view returns (uint256) {
        uint256 currentRewardPerToken = rewardPerToken();
        
        return (
            stakingBalance[account] * (currentRewardPerToken - userRewardPaid[account]) / 1e18
        );
    }

    /**
     * @dev 計算當前的每代幣獎勵
     */
    function rewardPerToken() public view returns (uint256) {
        if (totalSupply == 0) {
            return rewardPerTokenStored;
        }

        uint256 timeDelta = block.timestamp - lastUpdateTime;
        uint256 rewardDelta = timeDelta * rewardRate;

        return rewardPerTokenStored + (rewardDelta * 1e18 / totalSupply);
    }

    /**
     * @dev 更新獎勵
     */
    function _updateReward(address account) internal {
        rewardPerTokenStored = rewardPerToken();
        lastUpdateTime = block.timestamp;
        
        // 補發歷史獎勵
        if (account != address(0)) {
            uint256 earnedRewards = stakingBalance[account] * 
                (rewardPerTokenStored - userRewardPaid[account]) / 1e18;
            // 這裡可以選擇立即發放或累積
        }
    }
}

interface IERC20 {
    function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
    function transfer(address recipient, uint256 amount) external returns (bool);
    function balanceOf(address account) external view returns (uint256);
}

3. 3 收益優化策略

聰明的 DeFi 用戶會使用各種策略最大化收益:

複利策略:將收益再投入,實現利滾利。例如:

收益聚合器:使用 Yearn、Beefy 等協議,自動優化收益。

風險分散:不要把雞蛋放在同一個籃子裡,分散到多個協議。

第四章:實際操作範例

4.1 部署 AMM 合約

如果你想部署自己的 AMM 合約,需要以下步驟:

  1. 編寫合約程式碼(如上面的 SimpleAMM)
  2. 使用 Hardhat 或 Truffle 編譯
  3. 部署到測試網絡(如 Sepolia)
  4. 驗證合約
  5. 添加流動性
  6. 與合約交互

4.2 與現有 DeFi 協議交互

實際操作 DeFi 的典型流程:

準備錢包

存款

借款

交易


結語

DeFi 智慧合約的世界博大精深。本文涵蓋的只是冰山一角。真正的 DeFi 世界還包括:

建議讀者在理解原理後,親自嘗試使用這些協議。記住:


延伸閱讀


本文為以太坊 DeFi 開發系列文章之一,適合對智慧合約有基本了解的讀者。

延伸閱讀與來源

這篇文章對您有幫助嗎?

評論

發表評論

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

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