使用 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 合約的詳細指南。總結關鍵要點:
開發階段:
- 使用 Foundry 的測試框架進行全面的單元測試和模糊測試
- 確保所有不變量(Invariant)在各種輸入下都成立
- 參考 OpenZeppelin 的安全庫和最佳實踐
部署階段:
- 先在本地網路(Anvil)完整測試
- 在測試網進行模擬攻擊測試
- 主網部署前必須完成安全審計
- 使用多簽錢包管理合約權限
後續學習方向:
- 實現完整的代幣工廠(ERC-20 代幣部署)
- 添加費用開關和協議收入
- 實現治理機制
- 整合 Chainlink 預言機
- 學習 Uniswap V4 的 Hooks 機制
掌握這些技能後,你將能夠開發、生測試和部署複雜的去中心化金融合約,為以太坊生態系統貢獻高質量的代碼。
重要參考資源
- Foundry 官方文檔:https://book.getfoundry.sh
- OpenZeppelin Contracts:https://docs.openzeppelin.com/contracts
- Uniswap V2 原始碼:https://github.com/Uniswap/v2-core
- Solidity 官方文檔:https://docs.soliditylang.org
- 以太坊安全最佳實踐
相關文章
- DeFi 自動做市商(AMM)數學推導完整指南:從常數乘積到穩定幣模型的深度解析 — 自動做市商(AMM)是 DeFi 生態系統中最具創新性的基礎設施之一。本文從數學視角出發,系統性地推導各類 AMM 模型的定價公式、交易滑點計算、流動性提供者收益模型、以及無常損失的數學證明。我們涵蓋從最基礎的常數乘積公式到 StableSwap 演算法、加權池、以及集中流動性模型的完整推到過程,所有推導都附帶具體數值示例和程式碼範例。
- DeFi 進階合約模式完整指南:從設計模式到 production-ready 程式碼實踐 — 本文深入探討以太坊 DeFi 協議開發中的進階合約模式,這些模式是構建生產級去中心化金融應用的核心技術基礎。相較於基礎的代幣轉帳和簡單借貸,進階 DeFi 協議需要處理複雜的定價邏輯、流動性管理、風險控制和多層次的激勵機制。本文從資深工程師視角出發,提供可直接應用於生產環境的程式碼範例,涵蓋 AMM 深度實現、質押衍生品、借貸協議進階風控、協議治理等關鍵領域。
- 新興DeFi協議安全評估框架:從基礎審查到進階量化分析 — 系統性構建DeFi協議安全評估框架,涵蓋智能合約審計、經濟模型、治理機制、流動性風險等維度。提供可直接使用的Python風險評估代碼、借貸與DEX協議的專門評估方法、以及2024-2025年安全事件數據分析。
- DeFi 借貸協議從零開發實作指南:建立你自己的去中心化借貸市場 — 本指南從工程師視角出發,手把手帶你從零開發一個完整的 DeFi 借貸協議。涵蓋借貸協議的核心機制設計、利率模型的數學原理、清算邏輯的實現、智慧合約的安全考量、以及完整的測試部署流程。使用 Solidity 開發語言和 Hardhat 開發框架,從最基礎的合約開始,逐步構建包含 WadRayMath 數學庫、利率模型合約、價格預言機、核心借貸池等組件的完整系統,並提供可直接運行的測試程式碼。
- 以太坊智能合約開發除錯完整指南:從基礎到生產環境的實戰教學 — 本文提供完整的智能合約開發除錯指南,涵蓋常見漏洞分析(重入攻擊、整數溢位、存取控制)、調試技術(Hardhat/Foundry)、Gas 優化技巧、完整測試方法論,以及動手實驗室單元。幫助開發者從新手成長為能夠獨立開發生產環境就緒合約的工程師。
延伸閱讀與來源
- Aave V3 文檔 頭部借貸協議技術規格
- Uniswap V4 文檔 DEX 協議規格與鉤子機制
- DeFi Llama DeFi TVL 聚合數據
- Dune Analytics DeFi 協議數據分析儀表板
這篇文章對您有幫助嗎?
請告訴我們如何改進:
評論
發表評論
注意:由於這是靜態網站,您的評論將儲存在本地瀏覽器中,不會公開顯示。
目前尚無評論,成為第一個發表評論的人吧!