使用 Foundry 部署 AMM 合約完整步驟指南:從零到一的開發實戰

Foundry 是目前以太坊開發生態系統中最受歡迎的智慧合約開發框架。本文以自動做市商(AMM)合約為例,提供從專案初始化、工廠合約實現、流動性池合約開發、完整測試套件、本地部署到主網部署的完整實戰指南。涵蓋 Foundry 環境配置、AMM 核心合約代碼、模糊測試、以及部署腳本開發等核心技能。

使用 Foundry 部署 AMM 合約完整步驟指南:從零到一的開發實戰

概述

Foundry 是目前以太坊開發生態系統中最受歡迎的智慧合約開發框架之一,相比傳統的 Hardhat 和 Truffle,Foundry 提供了更快的編譯速度、 更強大的測試框架,以及更優秀的開發者體驗。本文將以自動做市商(AMM)合約為例,提供從專案初始化到主網部署的完整實戰指南。

本文涵蓋 Foundry 環境配置、專案結構設計、AMM 核心合約實現(包含工廠合約、流動性池合約)、完整測試套件開發、本地部署與測試網部署、以及最終的主網部署流程。讀者將掌握使用 Foundry 開發、生測試和部署智慧合約的全部技能。

第一章:Foundry 環境配置與專案初始化

1.1 Foundry 簡介與優勢

Foundry 由 Paradigm 開發,是一個用 Rust 編寫的智慧合約開發工具鏈,由四個核心工具組成:

Foundry 工具鏈組成:

┌─────────────────────────────────────────────────────┐
│                                                     │
│   forge      - 編譯、測試、部署智慧合約             │
│   cast       - 與區塊鏈交互的命令行工具            │
│   anvil      - 本地區塊鏈節點(類似 Ganache)       │
│   ch利害     - 智慧合約調試器與開發者工具           │
│                                                     │
└─────────────────────────────────────────────────────┘

Foundry 相比 Hardhat 的核心優勢:

1. 執行速度
   - Solidity 原生編譯(Forge)
   - 測試速度提升 10-100 倍
   - 無需 Node.js 環境

2. 測試框架
   - 內置模糊測試(Fuzzing)
   - 形式化驗證整合
   - 事件日誌斷言

3. 開發體驗
   - 熱重載(Hot Reload)
   - 更好的錯誤訊息
   - 內置合約調試器

4. 部署工具
   - 腳本化部署
   - 多網路支援
   - 私密金鑰管理

1.2 安裝 Foundry

Foundry 可以通過多種方式安裝,以下是最推薦的安裝方法:

# 方法一:使用 foundryup(官方推薦)
curl -L https://foundry.paradigm.xyz | bash
foundryup

# 方法二:使用 cargo 安裝(需要 Rust 環境)
cargo install --git https://github.com/foundry-rs/foundry foundry --profile release --locked

# 驗證安裝
forge --version
cast --version
anvil --version

# 預期輸出:
# forge 0.3.0 (4e26de3 2025-01-14)
# cast 0.3.0 (4e26de3 2025-01-14)
# anvil 0.3.0 (4e26de3 2025-01-14)

1.3 初始化專案

使用 Foundry 初始化一個新的 AMM 專案:

# 建立新專案
forge init my-amm --no-commit

# 進入專案目錄
cd my-amm

# 查看專案結構
tree -L 3

# 預期輸出:
# my-amm/
# ├── lib/                 # 依賴庫(使用 git submodule)
# ├── src/                 # 合約原始碼
# │   └── Counter.sol      # 範例合約
# ├── test/                # 測試檔案
# │   └── Counter.t.sol    # 範例測試
# ├── script/              # 部署腳本
# │   └── Counter.s.sol    # 範例腳本
# ├── foundry.toml         # Foundry 設定檔
# └── .gitignore

1.4 配置文件詳解

Foundry 的配置通過 foundry.toml 檔案管理:

# foundry.toml 完整配置範例

[profile.default]
src = "src"                           # 合約原始碼目錄
out = "out"                           # 編譯輸出目錄
libs = ["lib"]                        # 依賴庫目錄
solc = "0.8.28"                      # Solidity 編譯器版本
optimizer = true                      # 啟用優化器
optimizer_runs = 200                  # 優化器執行次數
via_ir = true                         # 使用 IR 優化管道(記憶體問題時關閉)

[profile.default.foundry]
# Foundry 專用設定
out = "out"
libs = ["lib"]
cache = "cache"
test = "test"

# 測試網路設定
[profile.ci]
# CI/CD 環境配置
fuzz_runs = 10000                     # 模糊測試次數
invocations = 1

# 主網部署配置
[rpc_endpoints]
mainnet = "${MAINNET_RPC_URL}"
sepolia = "${SEPOLIA_RPC_URL}"
goerli = "${GOERLI_RPC_URL}"

[etherscan]
# Etherscan API 配置(用於驗證合約)
mainnet = "${ETHERSCAN_API_KEY}"
sepolia = "${ETHERSCAN_API_KEY}"

1.5 添加依賴庫

AMM 合約需要使用 OpenZeppelin 的安全庫:

# 使用 forge 安裝依賴
forge install OpenZeppelin/openzeppelin-contracts@v5.0.2 --no-commit

# 查看依賴結構
ls lib/

# 預期輸出:
# lib/
# ├── openzeppelin-contracts/
# │   ├── contracts/
# │   │   ├── access/
# │   │   ├── token/
# │   │   ├── utils/
# │ │   └── ...
# │   └── ...

第二章:AMM 核心合約設計

2.1 AMM 數學原理回顧

在開始編碼之前,讓我們回顧 AMM 的核心數學原理。AMM 使用常數乘積公式(Constant Product Formula):

x * y = k

其中:
- x = 代幣 A 的儲備量
- y = 代幣 B 的儲備量
- k = 常數(交易後不變)

交易價格計算:
price = dy/dx = y/x

滑點計算(交易量 dx):
new_y = k / (x + dx)
new_x = k / (y - dy)
dy = y - k / (x + dx)

2.2 合約架構設計

我們的 AMM 將包含以下合約:

合約架構:

┌─────────────────────────────────────────────────────┐
│                                                     │
│   Factory Contract(工廠合約)                     │
│   ├── 建立新的交易對                               │
│   ├── 管理交易對註冊表                             │
│   └── 設定協議費用參數                             │
│                                                     │
│   Pair Contract(交易對合約)                     │
│   ├── 流動性提供                                 │
│   ├── 代幣交換                                    │
│   ├── 費用計算                                    │
│   └── 流動性代幣鑄造/銷毀                        │
│                                                     │
│   Router Contract(路由合約)                     │
│   ├── 多跳交換支援                                │
│   ├── 流動性操作封裝                              │
│   └── 緊急關閉                                     │
│                                                     │
└─────────────────────────────────────────────────────┘

2.3 工廠合約實現

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

import {IUniswapV2Factory} from "./interfaces/IUniswapV2Factory.sol";
import {UniswapV2Pair} from "./UniswapV2Pair.sol";

/// @title Uniswap V2 工廠合約
/// @notice 負責創建和管理所有交易對合約
contract UniswapV2Factory is IUniswapV2Factory {
    
    // --- 狀態變數 ---
    
    /// @notice 每筆交易的費用因子(分子),預設 25 = 0.25%
    /// @dev 費用 = feeFactor / 10000
    uint256 public override feeFactor = 25;
    
    /// @notice 協議費用接收地址
    address public override feeTo;
    
    /// @notice 有權設置費用參數的地址
    address public override feeToSetter;
    
    /// @notice 所有交易對的映射
    mapping(address => mapping(address => address)) 
        public override getPair;
    
    /// @notice 所有交易對地址的陣列
    address[] public override allPairs;
    
    // --- 事件 ---
    
    event PairCreated(
        address indexed token0, 
        address indexed token1, 
        address pair, 
        uint256
    );
    
    // --- 初始化 ---
    
    constructor(address _feeToSetter) {
        feeToSetter = _feeToSetter;
    }
    
    // --- 核心函數 ---
    
    /// @notice 返回交易對數量
    function allPairsLength() external view override returns (uint256) {
        return allPairs.length;
    }
    
    /// @notice 創建新的交易對
    /// @param tokenA 代幣 A 地址
    /// @param tokenB 代幣 B 地址
    /// @return pair 新創建的交易對合約地址
    function createPair(
        address tokenA, 
        address tokenB
    ) external override returns (address pair) {
        // 1. 參數校驗
        require(tokenA != tokenB, "UniswapV2: IDENTICAL_ADDRESSES");
        
        // 2. 排序代幣地址(確保 token0 < token1)
        (address token0, address token1) = tokenA < tokenB
            ? (tokenA, tokenB)
            : (tokenB, tokenA);
        
        require(token0 != address(0), "UniswapV2: ZERO_ADDRESS");
        require(
            getPair[token0][token1] == address(0), 
            "UniswapV2: PAIR_EXISTS"
        );
        
        // 3. 獲取鹽值(用於 CREATE2)
        bytes32 salt = keccak256(abi.encodePacked(token0, token1));
        
        // 4. 部署交易對合約
        bytes memory bytecode = type(UniswapV2Pair).creationCode;
        bytes32 initCodeHash = keccak256(bytecode);
        
        pair = address(
            new UniswapV2Pair{salt: salt}()
        );
        
        // 5. 初始化交易對
        IUniswapV2Pair(pair).initialize(token0, token1);
        
        // 6. 更新狀態
        getPair[token0][token1] = pair;
        getPair[token1][token0] = pair; // 雙向映射
        allPairs.push(pair);
        
        // 7. 發送事件
        emit PairCreated(token0, token1, pair, allPairs.length);
    }
    
    /// @notice 設置費用接收地址
    function setFeeTo(address _feeTo) external override {
        require(msg.sender == feeToSetter, "UniswapV2: FORBIDDEN");
        feeTo = _feeTo;
    }
    
    /// @notice 設置費用設定者
    function setFeeToSetter(address _feeToSetter) external override {
        require(msg.sender == feeToSetter, "UniswapV2: FORBIDDEN");
        feeToSetter = _feeToSetter;
    }
    
    /// @notice 設置費用因子
    function setFeeFactor(uint256 _feeFactor) external override {
        require(msg.sender == feeToSetter, "UniswapV2: FORBIDDEN");
        require(_feeFactor > 0 && _feeFactor <= 10000, "UniswapV2: INVALID_FEE");
        feeFactor = _feeFactor;
    }
    
    // --- 輔助函數 ---
    
    /// @notice 計算交易對的 CREATE2 地址
    function pairFor(
        address factory,
        address tokenA,
        address tokenB
    ) public pure returns (address pair) {
        (address token0, address token1) = tokenA < tokenB
            ? (tokenA, tokenB)
            : (tokenB, tokenA);
        
        bytes32 salt = keccak256(abi.encodePacked(token0, token1));
        bytes memory bytecode = type(UniswapV2Pair).creationCode;
        
        bytes32 initCodeHash = keccak256(bytecode);
        
        pair = address(
            uint160(
                uint256(
                    keccak256(
                        abi.encodePacked(
                            hex"ff",
                            factory,
                            salt,
                            initCodeHash
                        )
                    )
                )
            )
        );
    }
}

2.4 交易對合約實現

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

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
import {IUniswapV2Pair} from "./interfaces/IUniswapV2Pair.sol";
import {IUniswapV2Factory} from "./interfaces/IUniswapV2Factory.sol";

/// @title Uniswap V2 交易對合約
/// @notice 實現 AMM 核心功能:流動性提供和代幣交換
contract UniswapV2Pair is IUniswapV2Pair {
    
    // --- 使用 SafeMath ---
    using SafeERC20 for IERC20;
    
    // --- 常量 ---
    
    uint256 public constant MINIMUM_LIQUIDITY = 10**3;
    bytes4 private constant SELECTOR = bytes4(
        keccak256(abi.encodePacked("transfer(address,uint256)"))
    );
    
    // --- 狀態變數 ---
    
    address public override factory;
    address public override token0;
    address public override token1;
    
    uint112 private reserve0;
    uint112 private reserve1;
    uint32 private blockTimestampLast;
    
    uint256 public override price0CumulativeLast;
    uint256 public override price1CumulativeLast;
    uint256 public override kLast;
    
    uint256 private unlocked = 1;
    
    // --- 修改器 ---
    
    modifier lock() {
        require(unlocked == 1, "UniswapV2: LOCKED");
        unlocked = 0;
        _;
        unlocked = 1;
    }
    
    // --- 初始化 ---
    
    constructor() {
        factory = msg.sender;
    }
    
    /// @notice 初始化交易對(只能調用一次)
    function initialize(address _token0, address _token1) external override {
        require(msg.sender == factory, "UniswapV2: FORBIDDEN");
        require(_token0 != address(0) && _token1 != address(0), "UniswapV2: ZERO_ADDRESS");
        require(_token0 != _token1, "UniswapV2: IDENTICAL_ADDRESSES");
        
        token0 = _token0;
        token1 = _token1;
    }
    
    // --- 核心函數 ---
    
    /// @notice 獲取當前儲備量
    function getReserves() 
        public 
        view 
        override 
        returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) 
    {
        _reserve0 = reserve0;
        _reserve1 = reserve1;
        _blockTimestampLast = blockTimestampLast;
    }
    
    /// @notice 更新儲備量(需配合代幣餘額變動)
    function _update(
        uint256 balance0, 
        uint256 balance1, 
        uint112 _reserve0, 
        uint112 _reserve1
    ) private {
        require(
            balance0 <= type(uint112).max && 
            balance1 <= type(uint112).max,
            "UniswapV2: OVERFLOW"
        );
        
        uint32 blockTimestamp = uint32(block.timestamp % 2**32);
        uint32 timeElapsed = blockTimestamp - blockTimestampLast;
        
        if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) {
            // 防止除以零並累積價格
            price0CumulativeLast += uint256(
               UQ112x112.encode(_reserve1).uqdiv(
                    UQ112x112.encode(_reserve0)
                )
            ) * timeElapsed;
            
            price1CumulativeLast += uint256(
                UQ112x112.encode(_reserve0).uqdiv(
                    UQ112x112.encode(_reserve1)
                )
            ) * timeElapsed;
        }
        
        reserve0 = uint112(balance0);
        reserve1 = uint112(balance1);
        blockTimestampLast = blockTimestamp;
        
        emit Sync(reserve0, reserve1);
    }
    
    /// @notice 鑄造流動性代幣
    function mint(address to) 
        external 
        lock 
        returns (uint256 liquidity) 
    {
        (uint112 _reserve0, uint112 _reserve1, ) = getReserves();
        uint256 balance0 = IERC20(token0).balanceOf(address(this));
        uint256 balance1 = IERC20(token1).balanceOf(address(this));
        
        uint256 amount0 = balance0 - _reserve0;
        uint256 amount1 = balance1 - _reserve1;
        
        uint256 _totalSupply = totalSupply;
        
        if (_totalSupply == 0) {
            // 首次流動性:扣除最小流動性
            liquidity = Math.sqrt(amount0 * amount1) - MINIMUM_LIQUIDITY;
            _mint(address(0), MINIMUM_LIQUIDITY); // 鎖定最小流動性
        } else {
            // 計算流動性份額(按比例)
            liquidity = Math.min(
                amount0 * _totalSupply / _reserve0,
                amount1 * _totalSupply / _reserve1
            );
        }
        
        require(liquidity > 0, "UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED");
        _mint(to, liquidity);
        
        _update(balance0, balance1, _reserve0, _reserve1);
        
        if (kLast != 0) {
            // 檢查 k 是否增加(協議費用檢查)
            require(
                uint256(reserve0) * reserve1 >= kLast,
                "UniswapV2: K"
            );
        }
        
        emit Mint(msg.sender, amount0, amount1);
    }
    
    /// @notice 焚燒流動性代幣並提取代幣
    function burn(address to) 
        external 
        lock 
        returns (uint256 amount0, uint256 amount1) 
    {
        (uint112 _reserve0, uint112 _reserve1, ) = getReserves();
        address _token0 = token0;
        address _token1 = token1;
        
        uint256 balance0 = IERC20(_token0).balanceOf(address(this));
        uint256 balance1 = IERC20(_token1).balanceOf(address(this));
        
        uint256 liquidity = balanceOf[address(this)];
        
        uint256 _totalSupply = totalSupply;
        
        // 按比例計算可提取數量
        amount0 = liquidity * balance0 / _totalSupply;
        amount1 = liquidity * balance1 / _totalSupply;
        
        require(amount0 > 0 && amount1 > 0, "UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED");
        
        _burn(address(this), liquidity);
        
        _safeTransfer(_token0, to, amount0);
        _safeTransfer(_token1, to, amount1);
        
        balance0 = IERC20(_token0).balanceOf(address(this));
        balance1 = IERC20(_token1).balanceOf(address(this));
        
        _update(balance0, balance1, _reserve0, _reserve1);
        
        if (kLast != 0) {
            kLast = uint256(reserve0) * reserve1;
        }
        
        emit Burn(msg.sender, amount0, amount1, to);
    }
    
    /// @notice 代幣交換
    function swap(
        uint256 amount0Out, 
        uint256 amount1Out, 
        address to, 
        bytes calldata data
    ) external override lock {
        require(amount0Out > 0 || amount1Out > 0, "UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT");
        require(amount0Out < reserve0 && amount1Out < reserve1, "UniswapV2: INSUFFICIENT_LIQUIDITY");
        
        (uint112 _reserve0, uint112 _reserve1, ) = getReserves();
        
        require(amount0Out < _reserve0 && amount1Out < _reserve1, "UniswapV2: INSUFFICIENT_LIQUIDITY");
        
        // 處理接收代幣
        if (amount0Out > 0) _safeTransfer(token0, to, amount0Out);
        if (amount1Out > 0) _safeTransfer(token1, to, amount1Out);
        
        // 回調(支援閃電貸)
        if (data.length > 0) {
            IUniswapV2Callee(to).uniswapV2Call(
                msg.sender, 
                amount0Out, 
                amount1Out, 
                data
            );
        }
        
        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");
        
        // 扣礦計算(防止添加流動性後立即套利)
        uint256 balance0Adjusted = balance0 * 1000 - amount0In * 3;
        uint256 balance1Adjusted = balance1 * 1000 - amount1In * 3;
        
        require(
            balance0Adjusted * balance1Adjusted >= uint256(_reserve0) * _reserve1 * 1000**2,
            "UniswapV2: K"
        );
        
        _update(balance0, balance1, _reserve0, _reserve1);
        
        emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
    }
    
    // --- 輔助函數 ---
    
    /// @notice 安全轉帳
    function _safeTransfer(address token, address to, uint256 value) private {
        (bool success, bytes memory data) = token.call(
            abi.encodeWithSelector(SELECTOR, to, value)
        );
        require(
            success && (data.length == 0 || abi.decode(data, (bool))),
            "UniswapV2: TRANSFER_FAILED"
        );
    }
    
    /// @notice 強制同步(用於處理非正常代幣轉移)
    function skim(address to) external lock {
        address _token0 = token0;
        address _token1 = token1;
        _safeTransfer(
            _token0, 
            to, 
            IERC20(_token0).balanceOf(address(this)) - reserve0
        );
        _safeTransfer(
            _token1, 
            to, 
            IERC20(_token1).balanceOf(address(this)) - reserve1
        );
    }
    
    /// @notice 強制同步儲備量
    function sync() external lock {
        _update(
            IERC20(token0).balanceOf(address(this)),
            IERC20(token1).balanceOf(address(this)),
            reserve0,
            reserve1
        );
    }
}

// --- 固定點數學庫(用於價格累積)---

library UQ112x112 {
    uint224 constant Q112 = 2**112;
    
    function encode(uint112 y) internal pure returns (uint224 z) {
        z = uint224(y) * Q112;
    }
    
    function uqdiv(uint224 x, uint112 y) internal pure returns (uint224 z) {
        z = x / y;
    }
}

2.5 路由合約實現

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

import {IUniswapV2Factory} from "../interfaces/IUniswapV2Factory.sol";
import {IUniswapV2Pair} from "../interfaces/IUniswapV2Pair.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {UniswapV2Library} from "./UniswapV2Library.sol";

/// @title Uniswap V2 路由合約
/// @notice 提供便捷的交易和流動性操作介面
contract UniswapV2Router {
    
    using SafeERC20 for IERC20;
    
    // --- 狀態變數 ---
    
    address public immutable factory;
    
    // --- 初始化 ---
    
    constructor(address _factory) {
        factory = _factory;
    }
    
    // --- 輔助函數 ---
    
    /// @notice 處理接收的代幣回退函數
    receive() external payable {}
    
    // --- 交換函數 ---
    
    /// @notice 基礎交換(單一路由)
    function _swap(
        uint256[] memory amounts,
        address[] memory path,
        address _to
    ) private {
        for (uint256 i; i < path.length - 1; i++) {
            (address input, address output) = (path[i], path[i + 1]);
            (address token0, ) = UniswapV2Library.sortTokens(input, output);
            uint256 amountOut = amounts[i + 1];
            
            (uint256 amount0Out, uint256 amount1Out) = input == token0
                ? (uint256(0), amountOut)
                : (amountOut, uint256(0));
            
            address to = i < path.length - 2
                ? UniswapV2Library.pairFor(factory, output, path[i + 2])
                : _to;
            
            IUniswapV2Pair(
                UniswapV2Library.pairFor(factory, input, output)
            ).swap(amount0Out, amount1Out, to, new bytes(0));
        }
    }
    
    /// @notice 交換代幣(指定滑點)
    function swapExactTokensForTokens(
        uint256 amountIn,
        uint256 amountOutMin,
        address[] calldata path,
        address to,
        uint256 deadline
    ) external virtual returns (uint256[] memory amounts) {
        amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);
        require(
            amounts[amounts.length - 1] >= amountOutMin,
            "UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT"
        );
        TransferHelper.safeTransferFrom(
            path[0],
            msg.sender,
            UniswapV2Library.pairFor(factory, path[0], path[1]),
            amounts[0]
        );
        _swap(amounts, path, to);
    }
    
    /// @notice 交換 ETH 換取代幣
    function swapExactETHForTokens(
        uint256 amountOutMin,
        address[] calldata path,
        address to,
        uint256 deadline
    ) external payable virtual returns (uint256[] memory amounts) {
        require(path[0] == WETH, "UniswapV2Router: INVALID_PATH");
        amounts = UniswapV2Library.getAmountsOut(factory, msg.value, path);
        require(
            amounts[amounts.length - 1] >= amountOutMin,
            "UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT"
        );
        IWETH(WETH).deposit{value: amounts[0]}();
        assert(
            IERC20(WETH).transfer(
                UniswapV2Library.pairFor(factory, path[0], path[1]),
                amounts[0]
            )
        );
        _swap(amounts, path, to);
    }
    
    /// @notice 交換代幣換取 ETH
    function swapExactTokensForETH(
        uint256 amountIn,
        uint256 amountOutMin,
        address[] calldata path,
        address to,
        uint256 deadline
    ) external virtual returns (uint256[] memory amounts) {
        require(
            path[path.length - 1] == WETH,
            "UniswapV2Router: INVALID_PATH"
        );
        amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);
        require(
            amounts[amounts.length - 1] >= amountOutMin,
            "UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT"
        );
        TransferHelper.safeTransferFrom(
            path[0],
            msg.sender,
            UniswapV2Library.pairFor(factory, path[0], path[1]),
            amounts[0]
        );
        _swap(amounts, path, address(this));
        IWETH(WETH).withdraw(amounts[amounts.length - 1]);
        TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]);
    }
    
    // --- 流動性函數 ---
    
    /// @notice 添加流動性
    function addLiquidity(
        address tokenA,
        address tokenB,
        uint256 amountADesired,
        uint256 amountBDesired,
        uint256 amountAMin,
        uint256 amountBMin,
        address to,
        uint256 deadline
    ) external virtual returns (
        uint256 amountA,
        uint256 amountB,
        uint256 liquidity
    ) {
        (amountA, amountB) = _addLiquidity(
            tokenA,
            tokenB,
            amountADesired,
            amountBDesired,
            amountAMin,
            amountBMin
        );
        
        address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
        TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA);
        TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB);
        liquidity = IUniswapV2Pair(pair).mint(to);
    }
    
    /// @notice 添加流動性(ETH)
    function addLiquidityETH(
        address token,
        uint256 amountTokenDesired,
        uint256 amountTokenMin,
        uint256 amountETHMin,
        address to,
        uint256 deadline
    ) external payable virtual returns (
        uint256 amountToken,
        uint256 amountETH,
        uint256 liquidity
    ) {
        (amountToken, amountETH) = _addLiquidity(
            token,
            WETH,
            amountTokenDesired,
            msg.value,
            amountTokenMin,
            amountETHMin
        );
        
        address pair = UniswapV2Library.pairFor(factory, token, WETH);
        TransferHelper.safeTransferFrom(token, msg.sender, pair, amountToken);
        IWETH(WETH).deposit{value: amountETH}();
        assert(IERC20(WETH).transfer(pair, amountETH));
        liquidity = IUniswapV2Pair(pair).mint(to);
        
        // 退還多餘的 ETH
        if (msg.value > amountETH) {
            TransferHelper.safeTransferETH(msg.sender, msg.value - amountETH);
        }
    }
    
    /// @notice 移除流動性
    function removeLiquidity(
        address tokenA,
        address tokenB,
        uint256 liquidity,
        uint256 amountAMin,
        uint256 amountBMin,
        address to,
        uint256 deadline
    ) public returns (uint256 amountA, uint256 amountB) {
        address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
        IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity);
        (uint256 amount0, uint256 amount1) = IUniswapV2Pair(pair).burn(to);
        
        (address token0, ) = UniswapV2Library.sortTokens(tokenA, tokenB);
        (amountA, amountB) = tokenA == token0
            ? (amount0, amount1)
            : (amount1, amount0);
        
        require(amountA >= amountAMin, "UniswapV2Router: INSUFFICIENT_A_AMOUNT");
        require(amountB >= amountBMin, "UniswapV2Router: INSUFFICIENT_B_AMOUNT");
    }
    
    /// @notice 移除流動性(ETH)
    function removeLiquidityETH(
        address token,
        uint256 liquidity,
        uint256 amountTokenMin,
        uint256 amountETHMin,
        address to,
        uint256 deadline
    ) public returns (uint256 amountToken, uint256 amountETH) {
        (amountToken, amountETH) = removeLiquidity(
            token,
            WETH,
            liquidity,
            amountTokenMin,
            amountETHMin,
            address(this),
            deadline
        );
        TransferHelper.safeTransfer(token, to, amountToken);
        IWETH(WETH).withdraw(amountETH);
        TransferHelper.safeTransferETH(to, amountETH);
    }
    
    // --- 私有輔助函數 ---
    
    function _addLiquidity(
        address tokenA,
        address tokenB,
        uint256 amountADesired,
        uint256 amountBDesired,
        uint256 amountAMin,
        uint256 amountBMin
    ) private returns (uint256 amountA, uint256 amountB) {
        (uint256 reserveA, uint256 reserveB) = UniswapV2Library.getReserves(
            factory,
            tokenA,
            tokenB
        );
        
        if (reserveA == 0 && reserveB == 0) {
            (amountA, amountB) = (amountADesired, amountBDesired);
        } else {
            uint256 amountBOptimal = UniswapV2Library.quote(
                amountADesired,
                reserveA,
                reserveB
            );
            
            if (amountBOptimal <= amountBDesired) {
                require(
                    amountBOptimal >= amountBMin,
                    "UniswapV2Router: INSUFFICIENT_B_AMOUNT"
                );
                (amountA, amountB) = (amountADesired, amountBOptimal);
            } else {
                uint256 amountAOptimal = UniswapV2Library.quote(
                    amountBDesired,
                    reserveB,
                    reserveA
                );
                assert(amountAOptimal <= amountADesired);
                require(
                    amountAOptimal >= amountAMin,
                    "UniswapV2Router: INSUFFICIENT_A_AMOUNT"
                );
                (amountA, amountB) = (amountAOptimal, amountBDesired);
            }
        }
    }
}

// --- 常量定義(需在實際部署時替換)---

address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;

第三章:測試合約開發

3.1 測試框架結構

Foundry 提供了強大的測試框架,支持 Solidity 編寫的測試:

// test/UniswapV2Pair.t.sol

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

import {Test, console} from "forge-std/Test.sol";
import {UniswapV2Pair} from "../src/UniswapV2Pair.sol";
import {UniswapV2Factory} from "../src/UniswapV2Factory.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";

/// @title Mock ERC20 代幣(用於測試)
contract MockERC20 is ERC20 {
    uint8 private _decimals;
    
    constructor(
        string memory name,
        string memory symbol,
        uint8 decimals_
    ) ERC20(name, symbol) {
        _decimals = decimals_;
    }
    
    function decimals() public view override returns (uint8) {
        return _decimals;
    }
    
    function mint(address to, uint256 amount) external {
        _mint(to, amount);
    }
}

/// @title UniswapV2Pair 測試
contract UniswapV2PairTest is Test {
    
    // --- 合約實例 ---
    UniswapV2Factory public factory;
    UniswapV2Pair public pair;
    MockERC20 public token0;
    MockERC20 public token1;
    
    // --- 測試地址 ---
    address public user1 = address(0x1);
    address public user2 = address(0x2);
    
    // --- 初始化 ---
    
    function setUp() public {
        // 部署工廠合約
        factory = new UniswapV2Factory(address(this));
        
        // 部署測試代幣
        token0 = new MockERC20("Token A", "TKNA", 18);
        token1 = new MockERC20("Token B", "TKNB", 18);
        
        // 創建交易對
        pair = UniswapV2Pair(
            factory.createPair(address(token0), address(token1))
        );
        
        // 給測試用戶分發代幣
        token0.mint(user1, 1000 ether);
        token1.mint(user1, 1000 ether);
        token0.mint(user2, 1000 ether);
        token1.mint(user2, 1000 ether);
    }
    
    // --- 基礎測試 ---
    
    function testInitialize() public {
        assertEq(pair.token0(), address(token0));
        assertEq(pair.token1(), address(token1));
        assertEq(pair.factory(), address(factory));
    }
    
    function testGetReserves() public {
        (uint112 reserve0, uint112 reserve1, ) = pair.getReserves();
        assertEq(reserve0, 0);
        assertEq(reserve1, 0);
    }
    
    // --- 流動性測試 ---
    
    function testMintInitialLiquidity() public {
        // 用戶 1 添加流動性
        token0.transfer(address(pair), 100 ether);
        token1.transfer(address(pair), 100 ether);
        
        vm.prank(user1);
        pair.mint(user1);
        
        // 檢查流動性代幣鑄造
        assertGt(pair.balanceOf(user1), 0);
        assertEq(pair.totalSupply(), pair.balanceOf(user1));
    }
    
    function testMintRevertIfZeroLiquidity() public {
        token0.transfer(address(pair), 1);
        token1.transfer(address(pair), 1);
        
        vm.prank(user1);
        vm.expectRevert("UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED");
        pair.mint(user1);
    }
    
    function testLiquidityProvidedCorrectly() public {
        // 準備:添加初始流動性
        _addLiquidity(user1, 100 ether, 100 ether);
        
        // 記錄初始狀態
        uint256 initialBalance = pair.balanceOf(user1);
        uint256 initialReserve0 = pair.reserve0();
        
        // 用戶 2 添加相同比例流動性
        _addLiquidity(user2, 50 ether, 50 ether);
        
        // 驗證:用戶 2 應該獲得正確比例的流動性代幣
        uint256 user2Balance = pair.balanceOf(user2);
        
        // user2 的流動性應該是 user1 的一半(因為添加量減半)
        assertEq(user2Balance * 2, initialBalance);
    }
    
    // --- 交換測試 ---
    
    function testSwap() public {
        // 準備:添加流動性
        _addLiquidity(user1, 100 ether, 100 ether);
        
        // 用戶 2 用 token0 交換 token1
        uint256 amountIn = 10 ether;
        uint256 expectedOut = _getAmountOut(amountIn, 100 ether, 100 ether);
        
        // 準備交換
        token0.transfer(address(pair), amountIn);
        
        vm.prank(user2);
        pair.swap(0, expectedOut, user2, new bytes(0));
        
        // 驗證:用戶 2 應該收到正確數量的 token1
        assertEq(token1.balanceOf(user2), 1000 ether - 50 ether + expectedOut);
    }
    
    function testSwapRevertIfInsufficientLiquidity() public {
        _addLiquidity(user1, 100 ether, 100 ether);
        
        vm.prank(user2);
        vm.expectRevert("UniswapV2: INSUFFICIENT_LIQUIDITY");
        pair.swap(0, 101 ether, user2, new bytes(0));
    }
    
    function testSwapRevertIfNoInput() public {
        _addLiquidity(user1, 100 ether, 100 ether);
        
        vm.prank(user2);
        vm.expectRevert("UniswapV2: INSUFFICIENT_INPUT_AMOUNT");
        pair.swap(0, 1 ether, user2, new bytes(0));
    }
    
    // --- 燃燒流動性測試 ---
    
    function testBurn() public {
        // 添加流動性
        _addLiquidity(user1, 100 ether, 100 ether);
        uint256 liquidity = pair.balanceOf(user1);
        
        // 燃燒流動性
        vm.prank(user1);
        pair.burn(user1);
        
        // 驗證:流動性代幣被燒毀
        assertEq(pair.balanceOf(user1), 0);
    }
    
    // --- 輔助函數 ---
    
    function _addLiquidity(
        address provider,
        uint256 amountA,
        uint256 amountB
    ) internal {
        token0.transfer(address(pair), amountA);
        token1.transfer(address(pair), amountB);
        
        vm.prank(provider);
        pair.mint(provider);
    }
    
    function _getAmountOut(
        uint256 amountIn,
        uint256 reserveIn,
        uint256 reserveOut
    ) internal pure returns (uint256) {
        uint256 amountInWithFee = amountIn * 997;
        uint256 numerator = amountInWithFee * reserveOut;
        uint256 denominator = reserveIn * 1000 + amountInWithFee;
        return numerator / denominator;
    }
}

3.2 模糊測試(Fuzz Testing)

Foundry 支持 Solidity 原生的模糊測試:

// test/UniswapV2PairFuzz.t.sol

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

import {Test, console} from "forge-std/Test.sol";
import {UniswapV2Pair} from "../src/UniswapV2Pair.sol";
import {UniswapV2Factory} from "../src/UniswapV2Factory.sol";
import {MockERC20} from "./UniswapV2Pair.t.sol";

contract UniswapV2PairFuzzTest is Test {
    
    UniswapV2Factory public factory;
    UniswapV2Pair public pair;
    MockERC20 public token0;
    MockERC20 public token1;
    
    address public user = address(0x1);
    
    function setUp() public {
        factory = new UniswapV2Factory(address(this));
        token0 = new MockERC20("Token A", "TKNA", 18);
        token1 = new MockERC20("Token B", "TKNB", 18);
        
        pair = UniswapV2Pair(
            factory.createPair(address(token0), address(token1))
        );
        
        token0.mint(user, type(uint256).max);
        token1.mint(user, type(uint256).max);
    }
    
    /// @notice 模糊測試:任意數量的流動性添加
    function testFuzz_AddLiquidity(
        uint256 amountA,
        uint256 amountB
    ) public {
        vm.assume(amountA > 0 && amountA < type(uint256).max / 2);
        vm.assume(amountB > 0 && amountB < type(uint256).max / 2);
        
        token0.transfer(address(pair), amountA);
        token1.transfer(address(pair), amountB);
        
        vm.prank(user);
        uint256 liquidity = pair.mint(user);
        
        assertGt(liquidity, 0);
        assertGe(pair.totalSupply(), liquidity);
    }
    
    /// @notice 模糊測試:任意數量的交換
    function testFuzz_Swap(
        uint256 amountA,
        uint256 amountB,
        uint256 swapAmount
    ) public {
        vm.assume(amountA > 1000 && amountB > 1000);
        vm.assume(swapAmount > 0 && swapAmount < amountA / 2);
        
        // 添加流動性
        token0.transfer(address(pair), amountA);
        token1.transfer(address(pair), amountB);
        pair.mint(user);
        
        // 嘗試交換
        uint256 balance0Before = token0.balanceOf(user);
        uint256 balance1Before = token1.balanceOf(user);
        
        token0.transfer(address(pair), swapAmount);
        
        vm.prank(user);
        uint256 amountOut = swapAmount * 997 * amountB / 
            (amountA * 1000 + swapAmount * 997);
        
        if (amountOut > 0) {
            vm.prank(user);
            pair.swap(amountOut, 0, user, new bytes(0));
            
            // 驗證交換後餘額變化
            assertLt(token0.balanceOf(user), balance0Before);
        }
    }
    
    /// @notice 測試不變量:K 值必須保持或增加
    function testFuzz_KRemainConstant(
        uint256 amountA,
        uint256 amountB,
        uint256 swapAmount
    ) public {
        vm.assume(amountA > 1000 && amountB > 1000);
        vm.assume(swapAmount > 0 && swapAmount < amountA / 10);
        
        // 添加流動性
        token0.transfer(address(pair), amountA);
        token1.transfer(address(pair), amountB);
        pair.mint(user);
        
        (uint112 reserve0Before, uint112 reserve1Before, ) = pair.getReserves();
        uint256 kBefore = uint256(reserve0Before) * reserve1Before;
        
        // 交換
        token0.transfer(address(pair), swapAmount);
        
        vm.prank(user);
        uint256 amountOut = swapAmount * 997 * reserve1Before / 
            (reserve0Before * 1000 + swapAmount * 997);
        
        vm.prank(user);
        pair.swap(amountOut, 0, user, new bytes(0));
        
        (uint112 reserve0After, uint112 reserve1After, ) = pair.getReserves();
        uint256 kAfter = uint256(reserve0After) * reserve1After;
        
        // K' 應該 >= K(扣除費用後)
        assertGe(kAfter, kBefore);
    }
}

3.3 執行測試

# 執行所有測試
forge test

# 執行特定測試
forge test --match-test testSwap

# 執行模糊測試
forge test --fuzz-runs 10000

# 查看詳細輸出
forge test -vvvv

# 生成覆蓋率報告
forge coverage

第四章:部署腳本開發

4.1 本地網路部署

// script/DeployLocal.s.sol

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

import {Script, console} from "forge-std/Script.sol";
import {UniswapV2Factory} from "../src/UniswapV2Factory.sol";
import {UniswapV2Router} from "../src/UniswapV2Router.sol";
import {MockERC20} from "../test/UniswapV2Pair.t.sol";

/// @title 本地網路部署腳本
contract DeployLocal is Script {
    
    function run() public {
        // 讀取部署私鑰
        uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
        
        vm.startBroadcast(deployerPrivateKey);
        
        // 部署工廠合約
        UniswapV2Factory factory = new UniswapV2Factory(msg.sender);
        console.log("Factory deployed at:", address(factory));
        
        // 部署路由合約
        UniswapV2Router router = new UniswapV2Router(address(factory));
        console.log("Router deployed at:", address(router));
        
        // 部署測試代幣
        MockERC20 tokenA = new MockERC20("Token A", "TKNA", 18);
        MockERC20 tokenB = new MockERC20("Token B", "TKNB", 18);
        console.log("TokenA deployed at:", address(tokenA));
        console.log("TokenB deployed at:", address(tokenB));
        
        // 創建交易對
        address pair = factory.createPair(address(tokenA), address(tokenB));
        console.log("Pair created at:", pair);
        
        vm.stopBroadcast();
        
        // 輸出部署信息供後續使用
        console.log("");
        console.log("=== Deployment Summary ===");
        console.log("Factory:", address(factory));
        console.log("Router:", address(router));
        console.log("TokenA:", address(tokenA));
        console.log("TokenB:", address(tokenB));
        console.log("Pair:", pair);
    }
}

4.2 測試網部署腳本

// script/DeployTestnet.s.sol

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

import {Script} from "forge-std/Script.sol";
import {UniswapV2Factory} from "../src/UniswapV2Factory.sol";
import {UniswapV2Router} from "../src/UniswapV2Router.sol";

/// @title 測試網部署腳本
contract DeployTestnet is Script {
    
    // Sepolia 測試網配置
    // WETH: 0xfff9976782d46cc05630d1f6ebab18b2324d6b14
    // RPC: https://rpc.sepolia.org
    
    function run() public {
        // 讀取環境變數
        uint256 deployerPrivateKey = vm.envUint("SEPOLIA_PRIVATE_KEY");
        string memory rpcUrl = vm.envString("SEPOLIA_RPC_URL");
        address deployer = vm.addr(deployerPrivateKey);
        
        console.log("Deployer address:", deployer);
        
        vm.startBroadcast(deployerPrivateKey);
        
        // 部署工廠合約
        UniswapV2Factory factory = new UniswapV2Factory(deployer);
        console.log("Factory deployed at:", address(factory));
        
        // 部署路由合約
        UniswapV2Router router = new UniswapV2Router(address(factory));
        console.log("Router deployed at:", address(router));
        
        vm.stopBroadcast();
        
        // 保存部署地址
        console.log("");
        console.log("=== Save these addresses ===");
        console.log("FACTORY=", address(factory));
        console.log("ROUTER=", address(router));
    }
}

4.3 主網部署腳本

// script/DeployMainnet.s.sol

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

import {Script} from "forge-std/Script.sol";
import {UniswapV2Factory} from "../src/UniswapV2Factory.sol";
import {UniswapV2Router} from "../src/UniswapV2Router.sol";

/// @title 主網部署腳本
/// @notice 此腳本用於以太坊主網部署,需要額外安全措施
contract DeployMainnet is Script {
    
    // 主網配置
    // WETH: 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
    // RPC: https://eth.llamarpc.com
    
    function run() public {
        // 嚴格檢查部署環境
        uint256 deployerPrivateKey = vm.envUint("MAINNET_PRIVATE_KEY");
        string memory rpcUrl = vm.envString("MAINNET_RPC_URL");
        
        // 確認不是在測試網
        uint256 chainId = block.chainid;
        require(chainId == 1, "Must deploy on mainnet!");
        
        vm.startBroadcast(deployerPrivateKey);
        
        // 部署工廠合約(費用接收地址設為 deployer)
        UniswapV2Factory factory = new UniswapV2Factory(msg.sender);
        
        // 部署路由合約
        UniswapV2Router router = new UniswapV2Router(address(factory));
        
        vm.stopBroadcast();
        
        // 輸出部署信息
        console.log("Factory deployed at:", address(factory));
        console.log("Router deployed at:", address(router));
        
        /*
         * 部署後必須執行的操作:
         * 1. 在 Etherscan 上驗證合約原始碼
         * 2. 設置管理員權限
         * 3. 進行安全審計
         * 4. 建立緊急暫停機制
         */
    }
}

第五章:執行部署

5.1 本地部署

# 啟動本地節點
anvil

# 在另一個終端執行部署
forge script script/DeployLocal.s.sol:DeployLocal \
    --rpc-url http://localhost:8545 \
    --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \
    --broadcast

# 查看部署結果
# 輸出應包含工廠、路由、合約地址

5.2 測試網部署

# 設置環境變數
export SEPOLIA_RPC_URL="https://rpc.sepolia.org"
export SEPOLIA_PRIVATE_KEY="your_private_key_here"

# 執行部署
forge script script/DeployTestnet.s.sol:DeployTestnet \
    --rpc-url $SEPOLIA_RPC_URL \
    --broadcast \
    --verify \
    --etherscan-api-key $ETHERSCAN_API_KEY

# 驗證合約
forge verify-contract <CONTRACT_ADDRESS> \
    --chain sepolia \
    --num-of-optimizations 200 \
    --compiler-version 0.8.28 \
    src/UniswapV2Factory.sol:UniswapV2Factory

5.3 主網部署(謹慎執行)

# 設置環境變數
export MAINNET_RPC_URL="https://eth.llamarpc.com"
export MAINNET_PRIVATE_KEY="your_mainnet_private_key"
export ETHERSCAN_API_KEY="your_etherscan_api_key"

# 先在測試網完整測試後再執行主網部署
# 建議使用多簽錢包部署

forge script script/DeployMainnet.s.sol:DeployMainnet \
    --rpc-url $MAINNET_RPC_URL \
    --broadcast \
    --verify \
    --etherscan-api-key $ETHERSCAN_API_KEY \
    --slow

# --slow 標誌會在交易廣播前暫停,允許最後檢查

結論:最佳實踐與後續學習

本文提供了使用 Foundry 部署完整 AMM 合約的詳細指南。總結關鍵要點:

開發階段

  1. 使用 Foundry 的測試框架進行全面的單元測試和模糊測試
  2. 確保所有不變量(Invariant)在各種輸入下都成立
  3. 參考 OpenZeppelin 的安全庫和最佳實踐

部署階段

  1. 先在本地網路(Anvil)完整測試
  2. 在測試網進行模擬攻擊測試
  3. 主網部署前必須完成安全審計
  4. 使用多簽錢包管理合約權限

後續學習方向

  1. 實現完整的代幣工廠(ERC-20 代幣部署)
  2. 添加費用開關和協議收入
  3. 實現治理機制
  4. 整合 Chainlink 預言機
  5. 學習 Uniswap V4 的 Hooks 機制

掌握這些技能後,你將能夠開發、生測試和部署複雜的去中心化金融合約,為以太坊生態系統貢獻高質量的代碼。

重要參考資源

延伸閱讀與來源

這篇文章對您有幫助嗎?

評論

發表評論

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

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