Foundry与Uniswap V2实战开发指南
Foundry + Uniswap V2 实战完整技术指南
目录
- 环境准备与搭建
- Windows WSL Ubuntu 安装
- 运行环境配置
- 编译器版本管理
- 测试工具配置
- 部署工具配置
- 项目初始化与配置
- 创建项目结构
- 配置文件设置
- 依赖管理
- Uniswap V2 交互合约开发
- 代币查询合约
- 代币兑换合约
- 流动性管理合约
- 测试策略与实施
- 单元测试编写
- 集成测试编写
- 分叉测试编写
- 测试网部署与测试
- Goerli 测试网部署
- 测试网功能验证
- 升级测试
- 安全审计
- 静态分析工具
- 手动代码审查
- 常见漏洞检查
- 正式网部署
- 主网部署准备
- 主网合约部署
- 验证与监控
- 维护与升级
- 监控与警报
- 紧急响应计划
- 合约升级策略
环境准备与搭建
Windows WSL Ubuntu 安装
-
启用WSL功能
# 以管理员身份打开PowerShell dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart
-
安装Ubuntu
# 下载并安装Ubuntu 20.04 LTS wsl --install -d Ubuntu-20.04
-
初始设置
# 启动Ubuntu并设置用户名和密码 # 更新系统包 sudo apt update && sudo apt upgrade -y sudo apt install build-essential git curl -y
运行环境配置
-
安装Foundry
# 安装Foundry curl -L https://foundry.paradigm.xyz | bash source ~/.bashrc foundryup
-
安装Node.js和npm
# 使用Node Version Manager (nvm) 安装Node.js curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash source ~/.bashrc nvm install 18 nvm use 18
-
安装Python和必要工具
sudo apt install python3 python3-pip python3-venv -y
编译器版本管理
-
安装Solidity版本管理器
# 安装svm (Solidity Version Manager) curl -L https://foundry.paradigm.xyz | bash source ~/.bashrc foundryup
-
配置多版本编译器
# 安装多个Solidity版本 svm install 0.8.19 svm install 0.8.20 svm install 0.8.21# 设置默认版本 svm use 0.8.21
-
在Foundry中配置编译器
# foundry.toml [profile.default] solc = "0.8.21"[fmt] solc = "0.8.21"
测试工具配置
-
安装测试工具
# 安装Foundry标准测试库 forge install foundry-rs/forge-std# 安装其他测试工具 npm install -g mocha chai pip3 install slither-analyzer
-
配置测试环境
# 创建测试环境配置文件 mkdir -p config/test echo "TEST_NETWORK=goerli" > config/test/.env.test
部署工具配置
-
安装部署工具
# 安装Hardhat(可选,作为Foundry的补充) npm install -g hardhat# 安装Truffle(可选) npm install -g truffle# 安装部署脚本依赖 npm install dotenv @nomiclabs/hardhat-ethers ethers
-
配置多网络部署
// hardhat.config.js (可选配置) require('@nomiclabs/hardhat-ethers'); require('dotenv').config();module.exports = {networks: {goerli: {url: process.env.GOERLI_RPC_URL,accounts: [process.env.PRIVATE_KEY]},mainnet: {url: process.env.MAINNET_RPC_URL,accounts: [process.env.PRIVATE_KEY]}} };
项目初始化与配置
创建项目结构
# 创建项目目录结构
mkdir -p uniswapv2-integration
cd uniswapv2-integration
forge init --force
mkdir -p scripts test config deployments audit
配置文件设置
- 设置环境变量
# 创建环境模板文件 cat > .env.example << EOF # 网络配置 MAINNET_RPC_URL=https://eth-mainnet.g.alchemy.com/v2/your-api-key GOERLI_RPC_URL=https://eth-goerli.g.alchemy.com/v2/your-api-key POLYGON_RPC_URL=https://polygon-mainnet.g.alchemy.com/v2/your-api-key# 钱包配置 PRIVATE_KEY=your-private-key-without-0x DEPLOYER_ADDRESS=your-deployer-address# 验证配置 ETHERSCAN_API_KEY=your-etherscan-api-key POLYGONSCAN_API_KEY=your-polygonscan-api-key# 其他配置 GAS_LIMIT=3000000 GAS_PRICE=50 EOF# 复制为实际环境文件 cp .env.example .env
注:1.获取网络配置
-
Infura Infura 是一个提供 Ethereum RPC
端点的云服务,非常适合开发者和项目。以下是获取 Infura 端点的步骤:1)访问 Infura (https://www.infura.io/zh)官网 并注册一个账号。
2)登录后,你会看到一个“API Keys”的选项。点击“Create New Key”。
3)为你的新密钥命名,选择一个计划(通常是免费的“Mainnet”计划)。
4) 创建密钥后,你将看到一个类似于https://mainnet.infura.io/v3/YOUR_PROJECT_ID 的 URL,其中 YOUR_PROJECT_ID 是你的项目标识符。
2.获取钱包配置
3.获取验证配置 -
配置Foundry
# foundry.toml [profile.default] src = "src" out = "out" libs = ["lib"] solc = "0.8.21" optimizer = true optimizer_runs = 200 gas_reports = ["*"][rpc_endpoints] mainnet = "${MAINNET_RPC_URL}" goerli = "${GOERLI_RPC_URL}" polygon = "${POLYGON_RPC_URL}"[etherscan] mainnet = { key = "${ETHERSCAN_API_KEY}" } goerli = { key = "${ETHERSCAN_API_KEY}" } polygon = { key = "${POLYGONSCAN_API_KEY}" }[fmt] line_length = 120 tab_width = 2 bracket_spacing = true[fuzz] runs = 256
依赖管理
-
安装项目依赖
# 安装Uniswap V2接口 forge install Uniswap/uniswap-v2-core forge install Uniswap/uniswap-v2-periphery# 安装OpenZeppelin合约 forge install OpenZeppelin/openzeppelin-contracts# 安装其他有用库 forge install foundry-rs/forge-std forge install PaulRBerg/prb-math
-
更新remappings.txt
# remappings.txt @openzeppelin/=lib/openzeppelin-contracts/ @uniswap/v2-core/=lib/uniswap-v2-core/ @uniswap/v2-periphery/=lib/uniswap-v2-periphery/ @prb/math/=lib/prb-math/src/ forge-std/=lib/forge-std/src/
Uniswap V2 交互合约开发
代币查询合约
创建 src/UniswapV2Query.sol
:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;import "@openzeppelin/contracts/utils/math/SafeMath.sol";
import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol";
import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol";/*** @title UniswapV2Query* @dev 用于查询Uniswap V2相关信息的工具合约* @notice 这个合约提供了一系列查询功能,包括代币价格、储备量等*/
contract UniswapV2Query {using SafeMath for uint256;// Uniswap V2 Factory地址 (主网)address public constant UNISWAP_V2_FACTORY = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f;// Uniswap V2 Router地址 (主网)address public constant UNISWAP_V2_ROUTER = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D;// WETH地址 (主网)address public constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;/*** @dev 根据两个代币地址计算Pair合约地址* @param tokenA 代币A地址* @param tokenB 代币B地址* @return pairAddress Pair合约地址*/function getPairAddress(address tokenA, address tokenB) public pure returns (address pairAddress) {// 对代币地址进行排序,确保一致性(address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);// 计算salt (代币地址的keccak256哈希)bytes32 salt = keccak256(abi.encodePacked(token0, token1));// Uniswap V2的init code hashbytes32 initCodeHash = hex"96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f";// 使用CREATE2方式计算地址pairAddress = address(uint160(uint256(keccak256(abi.encodePacked(hex"ff",UNISWAP_V2_FACTORY,salt,initCodeHash)))));}/*** @dev 查询指定交易对的储备量* @param tokenA 代币A地址* @param tokenB 代币B地址* @return reserveA 代币A的储备量* @return reserveB 代币B的储备量*/function getReserves(address tokenA, address tokenB) public view returns (uint256 reserveA, uint256 reserveB) {address pairAddress = getPairAddress(tokenA, tokenB);IUniswapV2Pair pair = IUniswapV2Pair(pairAddress);(uint112 reserve0, uint112 reserve1, ) = pair.getReserves();(address token0, ) = sortTokens(tokenA, tokenB);(reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0);}/*** @dev 计算代币价格 (基于储备量)* @param tokenIn 输入代币地址* @param tokenOut 输出代币地址* @param amountIn 输入数量* @return amountOut 预计输出数量*/function getPrice(address tokenIn, address tokenOut, uint256 amountIn) public view returns (uint256 amountOut) {(uint256 reserveIn, uint256 reserveOut) = getReserves(tokenIn, tokenOut);amountOut = getAmountOut(amountIn, reserveIn, reserveOut);}/*** @dev 根据输入数量和储备量计算输出数量 (Uniswap V2公式)* @param amountIn 输入数量* @param reserveIn 输入代币储备量* @param reserveOut 输出代币储备量* @return amountOut 输出数量*/function getAmountOut(uint256 amountIn, uint256 reserveIn, uint256 reserveOut) public pure returns (uint256 amountOut) {require(amountIn > 0, "UniswapV2Query: INSUFFICIENT_INPUT_AMOUNT");require(reserveIn > 0 && reserveOut > 0, "UniswapV2Query: INSUFFICIENT_LIQUIDITY");uint256 amountInWithFee = amountIn.mul(997);uint256 numerator = amountInWithFee.mul(reserveOut);uint256 denominator = reserveIn.mul(1000).add(amountInWithFee);amountOut = numerator / denominator;}/*** @dev 对两个代币地址进行排序* @param tokenA 代币A地址* @param tokenB 代币B地址* @return token0 排序后的第一个代币地址* @return token1 排序后的第二个代币地址*/function sortTokens(address tokenA, address tokenB) public pure returns (address token0, address token1) {require(tokenA != tokenB, "UniswapV2Query: IDENTICAL_ADDRESSES");(token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);require(token0 != address(0), "UniswapV2Query: ZERO_ADDRESS");}/*** @dev 检查交易对是否存在* @param tokenA 代币A地址* @param tokenB 代币B地址* @return 是否存在*/function pairExists(address tokenA, address tokenB) public view returns (bool) {address pairAddress = getPairAddress(tokenA, tokenB);uint256 size;assembly {size := extcodesize(pairAddress)}return size > 0;}
}
代币兑换合约
创建 src/UniswapV2Swapper.sol
:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;import "@openzeppelin/contracts/utils/math/SafeMath.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router02.sol";
import "./UniswapV2Query.sol";/*** @title UniswapV2Swapper* @dev 用于在Uniswap V2上进行代币兑换的合约* @notice 这个合约提供了安全的代币兑换功能,包括滑点保护和价格验证*/
contract UniswapV2Swapper is UniswapV2Query {using SafeMath for uint256;using SafeERC20 for IERC20;// 事件:代币兑换event Swap(address indexed sender,address tokenIn,address tokenOut,uint256 amountIn,uint256 amountOut,uint256 timestamp);// 事件:兑换失败event SwapFailed(address indexed sender,address tokenIn,address tokenOut,uint256 amountIn,string reason,uint256 timestamp);// 最大滑点容忍度 (基础点数,100 = 1%)uint256 public maxSlippageBps = 100;/*** @dev 设置最大滑点容忍度* @param slippageBps 滑点基础点数 (100 = 1%)*/function setMaxSlippage(uint256 slippageBps) external {require(slippageBps <= 500, "UniswapV2Swapper: SLIPPAGE_TOO_HIGH"); // 最大5%滑点maxSlippageBps = slippageBps;}/*** @dev 执行代币兑换* @param tokenIn 输入代币地址* @param tokenOut 输出代币地址* @param amountIn 输入数量* @param minAmountOut 最小输出数量* @param deadline 交易截止时间* @return amountOut 实际输出数量*/function swapExactTokensForTokens(address tokenIn,address tokenOut,uint256 amountIn,uint256 minAmountOut,uint256 deadline) external returns (uint256 amountOut) {// 验证截止时间require(deadline >= block.timestamp, "UniswapV2Swapper: EXPIRED");// 验证交易对是否存在require(pairExists(tokenIn, tokenOut), "UniswapV2Swapper: PAIR_NOT_EXISTS");// 计算预计输出数量uint256 expectedAmountOut = getPrice(tokenIn, tokenOut, amountIn);// 计算最小输出数量(考虑滑点)uint256 calculatedMinAmountOut = minAmountOut > 0 ? minAmountOut : expectedAmountOut.mul(10000 - maxSlippageBps).div(10000);// 验证最小输出数量require(expectedAmountOut >= calculatedMinAmountOut,"UniswapV2Swapper: INSUFFICIENT_OUTPUT_AMOUNT");// 转移输入代币到本合约IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn);// 授权Router使用代币IERC20(tokenIn).safeApprove(UNISWAP_V2_ROUTER, amountIn);// 设置兑换路径address[] memory path = new address[](2);path[0] = tokenIn;path[1] = tokenOut;try IUniswapV2Router02(UNISWAP_V2_ROUTER).swapExactTokensForTokens(amountIn,calculatedMinAmountOut,path,msg.sender,deadline) returns (uint[] memory amounts) {amountOut = amounts[1];emit Swap(msg.sender, tokenIn, tokenOut, amountIn, amountOut, block.timestamp);} catch Error(string memory reason) {// 返还代币IERC20(tokenIn).safeTransfer(msg.sender, amountIn);emit SwapFailed(msg.sender, tokenIn, tokenOut, amountIn, reason, block.timestamp);revert(string(abi.encodePacked("UniswapV2Swapper: ", reason)));} catch {// 返还代币IERC20(tokenIn).safeTransfer(msg.sender, amountIn);emit SwapFailed(msg.sender, tokenIn, tokenOut, amountIn, "Unknown error", block.timestamp);revert("UniswapV2Swapper: UNKNOWN_ERROR");}}/*** @dev 执行ETH兑换代币* @param tokenOut 输出代币地址* @param minAmountOut 最小输出数量* @param deadline 交易截止时间* @return amountOut 实际输出数量*/function swapExactETHForTokens(address tokenOut,uint256 minAmountOut,uint256 deadline) external payable returns (uint256 amountOut) {// 验证截止时间require(deadline >= block.timestamp, "UniswapV2Swapper: EXPIRED");// 验证交易对是否存在require(pairExists(WETH, tokenOut), "UniswapV2Swapper: PAIR_NOT_EXISTS");// 计算预计输出数量uint256 expectedAmountOut = getPrice(WETH, tokenOut, msg.value);// 计算最小输出数量(考虑滑点)uint256 calculatedMinAmountOut = minAmountOut > 0 ? minAmountOut : expectedAmountOut.mul(10000 - maxSlippageBps).div(10000);// 验证最小输出数量require(expectedAmountOut >= calculatedMinAmountOut,"UniswapV2Swapper: INSUFFICIENT_OUTPUT_AMOUNT");// 设置兑换路径address[] memory path = new address[](2);path[0] = WETH;path[1] = tokenOut;try IUniswapV2Router02(UNISWAP_V2_ROUTER).swapExactETHForTokens{value: msg.value}(calculatedMinAmountOut,path,msg.sender,deadline) returns (uint[] memory amounts) {amountOut = amounts[1];emit Swap(msg.sender, WETH, tokenOut, msg.value, amountOut, block.timestamp);} catch Error(string memory reason) {// 返还ETHpayable(msg.sender).transfer(msg.value);emit SwapFailed(msg.sender, WETH, tokenOut, msg.value, reason, block.timestamp);revert(string(abi.encodePacked("UniswapV2Swapper: ", reason)));} catch {// 返还ETHpayable(msg.sender).transfer(msg.value);emit SwapFailed(msg.sender, WETH, tokenOut, msg.value, "Unknown error", block.timestamp);revert("UniswapV2Swapper: UNKNOWN_ERROR");}}/*** @dev 紧急提取代币(仅限合约所有者)* @param token 代币地址* @param amount 提取数量*/function emergencyWithdraw(address token, uint256 amount) external {// 注意:在实际合约中,应该添加onlyOwner修饰符IERC20(token).safeTransfer(msg.sender, amount);}// 接收ETHreceive() external payable {}
}
流动性管理合约
创建 src/UniswapV2LiquidityManager.sol
:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;import "@openzeppelin/contracts/utils/math/SafeMath.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router02.sol";
import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol";
import "./UniswapV2Query.sol";/*** @title UniswapV2LiquidityManager* @dev 用于管理Uniswap V2流动性的合约* @notice 这个合约提供了添加和移除流动性的功能,包括自动计算最优数量*/
contract UniswapV2LiquidityManager is UniswapV2Query {using SafeMath for uint256;using SafeERC20 for IERC20;// 事件:流动性添加event LiquidityAdded(address indexed provider,address tokenA,address tokenB,uint256 amountA,uint256 amountB,uint256 liquidity,uint256 timestamp);// 事件:流动性移除event LiquidityRemoved(address indexed provider,address tokenA,address tokenB,uint256 amountA,uint256 amountB,uint256 liquidity,uint256 timestamp);/*** @dev 添加流动性* @param tokenA 代币A地址* @param tokenB 代币B地址* @param amountADesired 期望的代币A数量* @param amountBDesired 期望的代币B数量* @param amountAMin 最小的代币A数量* @param amountBMin 最小的代币B数量* @param deadline 交易截止时间* @return amountA 实际添加的代币A数量* @return amountB 实际添加的代币B数量* @return liquidity 获得的流动性代币数量*/function addLiquidity(address tokenA,address tokenB,uint256 amountADesired,uint256 amountBDesired,uint256 amountAMin,uint256 amountBMin,uint256 deadline) external returns (uint256 amountA, uint256 amountB, uint256 liquidity) {// 验证截止时间require(deadline >= block.timestamp, "UniswapV2LiquidityManager: EXPIRED");// 转移代币到本合约IERC20(tokenA).safeTransferFrom(msg.sender, address(this), amountADesired);IERC20(tokenB).safeTransferFrom(msg.sender, address(this), amountBDesired);// 授权Router使用代币IERC20(tokenA).safeApprove(UNISWAP_V2_ROUTER, amountADesired);IERC20(tokenB).safeApprove(UNISWAP_V2_ROUTER, amountBDesired);// 调用Router添加流动性(amountA, amountB, liquidity) = IUniswapV2Router02(UNISWAP_V2_ROUTER).addLiquidity(tokenA,tokenB,amountADesired,amountBDesired,amountAMin,amountBMin,msg.sender,deadline);// 返还剩余代币if (amountADesired > amountA) {IERC20(tokenA).safeTransfer(msg.sender, amountADesired - amountA);}if (amountBDesired > amountB) {IERC20(tokenB).safeTransfer(msg.sender, amountBDesired - amountB);}emit LiquidityAdded(msg.sender, tokenA, tokenB, amountA, amountB, liquidity, block.timestamp);}/*** @dev 添加ETH流动性* @param token 代币地址* @param amountTokenDesired 期望的代币数量* @param amountTokenMin 最小的代币数量* @param amountETHMin 最小的ETH数量* @param deadline 交易截止时间* @return amountToken 实际添加的代币数量* @return amountETH 实际添加的ETH数量* @return liquidity 获得的流动性代币数量*/function addLiquidityETH(address token,uint256 amountTokenDesired,uint256 amountTokenMin,uint256 amountETHMin,uint256 deadline) external payable returns (uint256 amountToken, uint256 amountETH, uint256 liquidity) {// 验证截止时间require(deadline >= block.timestamp, "UniswapV2LiquidityManager: EXPIRED");// 转移代币到本合约IERC20(token).safeTransferFrom(msg.sender, address(this), amountTokenDesired);// 授权Router使用代币IERC20(token).safeApprove(UNISWAP_V2_ROUTER, amountTokenDesired);// 调用Router添加ETH流动性(amountToken, amountETH, liquidity) = IUniswapV2Router02(UNISWAP_V2_ROUTER).addLiquidityETH{value: msg.value}(token,amountTokenDesired,amountTokenMin,amountETHMin,msg.sender,deadline);// 返还剩余代币if (amountTokenDesired > amountToken) {IERC20(token).safeTransfer(msg.sender, amountTokenDesired - amountToken);}// 返还剩余ETHif (msg.value > amountETH) {payable(msg.sender).transfer(msg.value - amountETH);}emit LiquidityAdded(msg.sender, token, WETH, amountToken, amountETH, liquidity, block.timestamp);}/*** @dev 移除流动性* @param tokenA 代币A地址* @param tokenB 代币B地址* @param liquidity 要移除的流动性数量* @param amountAMin 最小的代币A数量* @param amountBMin 最小的代币B数量* @param deadline 交易截止时间* @return amountA 实际获得的代币A数量* @return amountB 实际获得的代币B数量*/function removeLiquidity(address tokenA,address tokenB,uint256 liquidity,uint256 amountAMin,uint256 amountBMin,uint256 deadline) external returns (uint256 amountA, uint256 amountB) {// 验证截止时间require(deadline >= block.timestamp, "UniswapV2LiquidityManager: EXPIRED");// 获取Pair地址address pairAddress = getPairAddress(tokenA, tokenB);// 转移流动性代币到本合约IERC20(pairAddress).safeTransferFrom(msg.sender, address(this), liquidity);// 授权Router使用流动性代币IERC20(pairAddress).safeApprove(UNISWAP_V2_ROUTER, liquidity);// 调用Router移除流动性(amountA, amountB) = IUniswapV2Router02(UNISWAP_V2_ROUTER).removeLiquidity(tokenA,tokenB,liquidity,amountAMin,amountBMin,msg.sender,deadline);emit LiquidityRemoved(msg.sender, tokenA, tokenB, amountA, amountB, liquidity, block.timestamp);}/*** @dev 移除ETH流动性* @param token 代币地址* @param liquidity 要移除的流动性数量* @param amountTokenMin 最小的代币数量* @param amountETHMin 最小的ETH数量* @param deadline 交易截止时间* @return amountToken 实际获得的代币数量* @return amountETH 实际获得的ETH数量*/function removeLiquidityETH(address token,uint256 liquidity,uint256 amountTokenMin,uint256 amountETHMin,uint256 deadline) external returns (uint256 amountToken, uint256 amountETH) {// 验证截止时间require(deadline >= block.timestamp, "UniswapV2LiquidityManager: EXPIRED");// 获取Pair地址address pairAddress = getPairAddress(token, WETH);// 转移流动性代币到本合约IERC20(pairAddress).safeTransferFrom(msg.sender, address(this), liquidity);// 授权Router使用流动性代币IERC20(pairAddress).safeApprove(UNISWAP_V2_ROUTER, liquidity);// 调用Router移除ETH流动性(amountToken, amountETH) = IUniswapV2Router02(UNISWAP_V2_ROUTER).removeLiquidityETH(token,liquidity,amountTokenMin,amountETHMin,msg.sender,deadline);emit LiquidityRemoved(msg.sender, token, WETH, amountToken, amountETH, liquidity, block.timestamp);}/*** @dev 计算最优的添加流动性数量* @param tokenA 代币A地址* @param tokenB 代币B地址* @param amountADesired 期望的代币A数量* @param amountBDesired 期望的代币B数量* @return amountA 最优的代币A数量* @return amountB 最优的代币B数量*/function quoteOptimalAmounts(address tokenA,address tokenB,uint256 amountADesired,uint256 amountBDesired) public view returns (uint256 amountA, uint256 amountB) {(uint256 reserveA, uint256 reserveB) = getReserves(tokenA, tokenB);if (reserveA == 0 && reserveB == 0) {// 如果池子是空的,使用期望的数量return (amountADesired, amountBDesired);}uint256 amountBOptimal = quote(amountADesired, reserveA, reserveB);if (amountBOptimal <= amountBDesired) {// 使用代币A的数量计算最优的代币B数量return (amountADesired, amountBOptimal);} else {// 使用代币B的数量计算最优的代币A数量uint256 amountAOptimal = quote(amountBDesired, reserveB, reserveA);assert(amountAOptimal <= amountADesired);return (amountAOptimal, amountBDesired);}}/*** @dev 根据储备量计算最优数量 (Uniswap V2公式)* @param amountA 输入数量* @param reserveA 输入代币储备量* @param reserveB 输出代币储备量* @return amountB 输出数量*/function quote(uint256 amountA, uint256 reserveA, uint256 reserveB) public pure returns (uint256 amountB) {require(amountA > 0, "UniswapV2LiquidityManager: INSUFFICIENT_AMOUNT");require(reserveA > 0 && reserveB > 0, "UniswapV2LiquidityManager: INSUFFICIENT_LIQUIDITY");amountB = amountA.mul(reserveB) / reserveA;}
}
测试策略与实施
单元测试编写
创建 test/UniswapV2Query.t.sol
:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;import "forge-std/Test.sol";
import "../src/UniswapV2Query.sol";/*** @title UniswapV2QueryTest* @dev UniswapV2Query合约的单元测试*/
contract UniswapV2QueryTest is Test {UniswapV2Query public query;// 主网地址address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;address constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F;function setUp() public {// 创建主网分叉vm.createSelectFork(vm.rpcUrl("mainnet"));// 部署查询合约query = new UniswapV2Query();}function testGetPairAddress() public {// 测试获取WETH/DAI交易对地址address pairAddress = query.getPairAddress(WETH, DAI);// 已知的WETH/DAI交易对地址address knownPair = 0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11;// 验证地址计算正确assertEq(pairAddress, knownPair);}function testGetReserves() public {// 测试获取WETH/DAI储备量(uint256 reserveWETH, uint256 reserveDAI) = query.getReserves(WETH, DAI);// 验证储备量大于0assertGt(reserveWETH, 0);assertGt(reserveDAI, 0);// 输出储备量信息console.log("WETH Reserve:", reserveWETH);console.log("DAI Reserve:", reserveDAI);}function testGetPrice() public {// 测试获取价格 (1 WETH = ? DAI)uint256 amountIn = 1 ether; // 1 WETHuint256 amountOut = query.getPrice(WETH, DAI, amountIn);// 验证输出数量大于0assertGt(amountOut, 0);// 输出价格信息console.log("1 WETH =", amountOut / 1e18, "DAI");}function testPairExists() public {// 测试检查交易对是否存在bool exists = query.pairExists(WETH, DAI);// 验证交易对存在assertTrue(exists);// 测试不存在的交易对bool notExists = query.pairExists(WETH, address(0x123));// 验证交易对不存在assertFalse(notExists);}function testSortTokens() public {// 测试代币地址排序(address token0, address token1) = query.sortTokens(WETH, DAI);// 验证排序正确assertEq(token0, DAI);assertEq(token1, WETH);// 测试反向排序(address token0Reverse, address token1Reverse) = query.sortTokens(DAI, WETH);// 验证排序一致assertEq(token0, token0Reverse);assertEq(token1, token1Reverse);}
}
集成测试编写
创建 test/UniswapV2Swapper.t.sol
:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;import "forge-std/Test.sol";
import "../src/UniswapV2Swapper.sol";/*** @title UniswapV2SwapperTest* @dev UniswapV2Swapper合约的集成测试*/
contract UniswapV2SwapperTest is Test {UniswapV2Swapper public swapper;// 主网地址address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;address constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F;// 测试用户address user = address(0x123);function setUp() public {// 创建主网分叉vm.createSelectFork(vm.rpcUrl("mainnet"));// 部署Swapper合约swapper = new UniswapV2Swapper();// 给测试用户一些WETHdeal(WETH, user, 10 ether);}function testSwapExactTokensForTokens() public {// 切换至测试用户vm.startPrank(user);// 授权Swapper合约使用WETHIERC20(WETH).approve(address(swapper), 1 ether);// 记录初始余额uint256 initialWETH = IERC20(WETH).balanceOf(user);uint256 initialDAI = IERC20(DAI).balanceOf(user);// 执行兑换uint256 amountOut = swapper.swapExactTokensForTokens(WETH,DAI,1 ether, // 1 WETH0, // 最小输出数量block.timestamp + 1 hours);// 记录最终余额uint256 finalWETH = IERC20(WETH).balanceOf(user);uint256 finalDAI = IERC20(DAI).balanceOf(user);// 验证WETH减少assertEq(initialWETH - finalWETH, 1 ether);// 验证DAI增加assertGt(finalDAI, initialDAI);assertEq(finalDAI - initialDAI, amountOut);// 输出兑换信息console.log("WETH spent:", 1 ether / 1e18);console.log("DAI received:", amountOut / 1e18);vm.stopPrank();}function testSwapExactETHForTokens() public {// 切换至测试用户vm.startPrank(user);// 记录初始余额uint256 initialETH = user.balance;uint256 initialDAI = IERC20(DAI).balanceOf(user);// 执行ETH兑换uint256 amountOut = swapper.swapExactETHForTokens{value: 1 ether}(DAI,0, // 最小输出数量block.timestamp + 1 hours);// 记录最终余额uint256 finalETH = user.balance;uint256 finalDAI = IERC20(DAI).balanceOf(user);// 验证ETH减少assertEq(initialETH - finalETH, 1 ether);// 验证DAI增加assertGt(finalDAI, initialDAI);assertEq(finalDAI - initialDAI, amountOut);// 输出兑换信息console.log("ETH spent:", 1 ether / 1e18);console.log("DAI received:", amountOut / 1e18);vm.stopPrank();}function testSwapWithSlippageProtection() public {// 切换至测试用户vm.startPrank(user);// 设置较低的滑点容忍度 (0.1%)swapper.setMaxSlippage(10);// 授权Swapper合约使用WETHIERC20(WETH).approve(address(swapper), 1 ether);// 计算预计输出uint256 expectedOut = swapper.getPrice(WETH, DAI, 1 ether);// 执行兑换(应该会失败,因为滑点太低)vm.expectRevert();swapper.swapExactTokensForTokens(WETH,DAI,1 ether,0, // 最小输出数量block.timestamp + 1 hours);vm.stopPrank();}function testEmergencyWithdraw() public {// 给Swapper合约一些代币deal(DAI, address(swapper), 1000 ether);// 记录初始余额uint256 initialBalance = IERC20(DAI).balanceOf(address(this));// 执行紧急提取swapper.emergencyWithdraw(DAI, 1000 ether);// 记录最终余额uint256 finalBalance = IERC20(DAI).balanceOf(address(this));// 验证代币已提取assertEq(finalBalance - initialBalance, 1000 ether);}
}
分叉测试编写
创建 test/ForkTest.t.sol
:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;import "forge-std/Test.sol";
import "../src/UniswapV2LiquidityManager.sol";/*** @title ForkTest* @dev 主网分叉测试*/
contract ForkTest is Test {UniswapV2LiquidityManager public liquidityManager;// 主网地址address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;address constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F;// 测试用户address user = address(0x123);function setUp() public {// 创建主网分叉vm.createSelectFork(vm.rpcUrl("mainnet"));// 部署流动性管理器liquidityManager = new UniswapV2LiquidityManager();// 给测试用户一些代币deal(USDC, user, 10000 * 10**6); // 10,000 USDCdeal(DAI, user, 10000 ether); // 10,000 DAIdeal(WETH, user, 10 ether); // 10 WETH}function testAddLiquidity() public {// 切换至测试用户vm.startPrank(user);// 授权流动性管理器使用代币IERC20(USDC).approve(address(liquidityManager), 1000 * 10**6);IERC20(DAI).approve(address(liquidityManager), 1000 ether);// 记录初始余额uint256 initialUSDC = IERC20(USDC).balanceOf(user);uint256 initialDAI = IERC20(DAI).balanceOf(user);// 添加流动性(uint256 amountA, uint256 amountB, uint256 liquidity) = liquidityManager.addLiquidity(USDC,DAI,1000 * 10**6, // 1000 USDC1000 ether, // 1000 DAI0, // 最小USDC数量0, // 最小DAI数量block.timestamp + 1 hours);// 记录最终余额uint256 finalUSDC = IERC20(USDC).balanceOf(user);uint256 finalDAI = IERC20(DAI).balanceOf(user);// 验证代币减少assertEq(initialUSDC - finalUSDC, amountA);assertEq(initialDAI - finalDAI, amountB);// 验证流动性大于0assertGt(liquidity, 0);// 输出流动性信息console.log("USDC added:", amountA / 10**6);console.log("DAI added:", amountB / 1e18);console.log("Liquidity received:", liquidity);vm.stopPrank();}function testAddLiquidityETH() public {// 切换至测试用户vm.startPrank(user);// 授权流动性管理器使用代币IERC20(USDC).approve(address(liquidityManager), 1000 * 10**6);// 记录初始余额uint256 initialUSDC = IERC20(USDC).balanceOf(user);uint256 initialETH = user.balance;// 添加ETH流动性(uint256 amountToken, uint256 amountETH, uint256 liquidity) = liquidityManager.addLiquidityETH{value: 1 ether}(USDC,1000 * 10**6, // 1000 USDC0, // 最小USDC数量0, // 最小ETH数量block.timestamp + 1 hours);// 记录最终余额uint256 finalUSDC = IERC20(USDC).balanceOf(user);uint256 finalETH = user.balance;// 验证代币减少assertEq(initialUSDC - finalUSDC, amountToken);assertEq(initialETH - finalETH, amountETH);// 验证流动性大于0assertGt(liquidity, 0);// 输出流动性信息console.log("USDC added:", amountToken / 10**6);console.log("ETH added:", amountETH / 1e18);console.log("Liquidity received:", liquidity);vm.stopPrank();}function testRemoveLiquidity() public {// 首先添加流动性testAddLiquidity();// 切换至测试用户vm.startPrank(user);// 获取Pair地址address pairAddress = liquidityManager.getPairAddress(USDC, DAI);// 获取流动性数量uint256 liquidity = IERC20(pairAddress).balanceOf(user);// 授权流动性管理器使用流动性代币IERC20(pairAddress).approve(address(liquidityManager), liquidity);// 记录初始余额uint256 initialUSDC = IERC20(USDC).balanceOf(user);uint256 initialDAI = IERC20(DAI).balanceOf(user);// 移除流动性(uint256 amountA, uint256 amountB) = liquidityManager.removeLiquidity(USDC,DAI,liquidity,0, // 最小USDC数量0, // 最小DAI数量block.timestamp + 1 hours);// 记录最终余额uint256 finalUSDC = IERC20(USDC).balanceOf(user);uint256 finalDAI = IERC20(DAI).balanceOf(user);// 验证代币增加assertEq(finalUSDC - initialUSDC, amountA);assertEq(finalDAI - initialDAI, amountB);// 输出移除流动性信息console.log("USDC received:", amountA / 10**6);console.log("DAI received:", amountB / 1e18);vm.stopPrank();}function testQuoteOptimalAmounts() public {// 计算最优数量(uint256 amountA, uint256 amountB) = liquidityManager.quoteOptimalAmounts(USDC,DAI,1000 * 10**6, // 1000 USDC1000 ether // 1000 DAI);// 验证数量大于0assertGt(amountA, 0);assertGt(amountB, 0);// 输出最优数量信息console.log("Optimal USDC:", amountA / 10**6);console.log("Optimal DAI:", amountB / 1e18);}
}
运行测试:
# 运行单元测试
forge test --match-test testGetPairAddress -vvv# 运行集成测试
forge test --match-test testSwapExactTokensForTokens -vvv# 运行分叉测试
forge test --match-test testAddLiquidity -vvv# 运行所有测试
forge test -vvv
测试网部署与测试
Goerli 测试网部署
-
配置测试网环境
# 创建测试网部署脚本 mkdir -p script/deploy touch script/deploy/DeployGoerli.s.sol
-
编写部署脚本
// SPDX-License-Identifier: MIT pragma solidity ^0.8.21;import "forge-std/Script.sol"; import "../../src/UniswapV2Query.sol"; import "../../src/UniswapV2Swapper.sol"; import "../../src/UniswapV2LiquidityManager.sol";/*** @title DeployGoerli* @dev 部署合约到Goerli测试网*/ contract DeployGoerli is Script {function run() external {// 从环境变量获取部署者私钥uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");// 开始广播交易vm.startBroadcast(deployerPrivateKey);// 部署UniswapV2QueryUniswapV2Query query = new UniswapV2Query();console.log("UniswapV2Query deployed at:", address(query));// 部署UniswapV2SwapperUniswapV2Swapper swapper = new UniswapV2Swapper();console.log("UniswapV2Swapper deployed at:", address(swapper));// 部署UniswapV2LiquidityManagerUniswapV2LiquidityManager liquidityManager = new UniswapV2LiquidityManager();console.log("UniswapV2LiquidityManager deployed at:", address(liquidityManager));// 停止广播vm.stopBroadcast();} }
-
执行部署
# 部署到Goerli测试网 forge script script/deploy/DeployGoerli.s.sol:DeployGoerli --rpc-url goerli --broadcast --verify -vvvv# 如果需要验证合约,添加Etherscan API密钥 ETHERSCAN_API_KEY=your-etherscan-api-key forge script script/deploy/DeployGoerli.s.sol:DeployGoerli --rpc-url goerli --broadcast --verify -vvvv
测试网功能验证
-
创建验证脚本
// SPDX-License-Identifier: MIT pragma solidity ^0.8.21;import "forge-std/Script.sol";/*** @title VerifyGoerli* @dev 验证Goerli测试网上的合约功能*/ contract VerifyGoerli is Script {// Goerli测试网上的合约地址address constant QUERY_ADDRESS = 0x...;address constant SWAPPER_ADDRESS = 0x...;address constant LIQUIDITY_MANAGER_ADDRESS = 0x...;// Goerli测试网上的代币地址address constant WETH = 0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6;address constant DAI = 0x...; // 需要部署测试DAI代币function run() external {// 从环境变量获取测试用户私钥uint256 userPrivateKey = vm.envUint("TEST_PRIVATE_KEY");// 开始广播交易vm.startBroadcast(userPrivateKey);// 测试查询功能UniswapV2Query query = UniswapV2Query(QUERY_ADDRESS);address pairAddress = query.getPairAddress(WETH, DAI);console.log("WETH/DAI Pair Address:", pairAddress);// 测试兑换功能UniswapV2Swapper swapper = UniswapV2Swapper(SWAPPER_ADDRESS);// 授权Swapper使用WETHIERC20(WETH).approve(SWAPPER_ADDRESS, 0.1 ether);// 执行兑换uint256 amountOut = swapper.swapExactTokensForTokens(WETH,DAI,0.1 ether,0,block.timestamp + 1 hours);console.log("DAI received:", amountOut);// 停止广播vm.stopBroadcast();} }
-
执行验证
# 运行验证脚本 forge script script/deploy/VerifyGoerli.s.sol:VerifyGoerli --rpc-url goerli --broadcast -vvvv
升级测试
-
创建可升级合约版本
// SPDX-License-Identifier: MIT pragma solidity ^0.8.21;import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import "./UniswapV2Query.sol";/*** @title UniswapV2SwapperUpgradeable* @dev 可升级版本的UniswapV2Swapper*/ contract UniswapV2SwapperUpgradeable is Initializable, OwnableUpgradeable, UniswapV2Query {using SafeMath for uint256;using SafeERC20 for IERC20;// 最大滑点容忍度uint256 public maxSlippageBps;// 事件:代币兑换event Swap(address indexed sender,address tokenIn,address tokenOut,uint256 amountIn,uint256 amountOut,uint256 timestamp);/*** @dev 初始化函数*/function initialize() public initializer {__Ownable_init();maxSlippageBps = 100; // 默认1%滑点}/*** @dev 设置最大滑点容忍度*/function setMaxSlippage(uint256 slippageBps) external onlyOwner {require(slippageBps <= 500, "SLIPPAGE_TOO_HIGH");maxSlippageBps = slippageBps;}// 其他函数与UniswapV2Swapper类似... }
-
创建升级部署脚本
// SPDX-License-Identifier: MIT pragma solidity ^0.8.21;import "forge-std/Script.sol"; import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import "../../src/upgradeable/UniswapV2SwapperUpgradeable.sol";/*** @title DeployUpgradeable* @dev 部署可升级合约到Goerli测试网*/ contract DeployUpgradeable is Script {function run() external {uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");vm.startBroadcast(deployerPrivateKey);// 部署实现合约UniswapV2SwapperUpgradeable implementation = new UniswapV2SwapperUpgradeable();// 部署代理合约ERC1967Proxy proxy = new ERC1967Proxy(address(implementation),abi.encodeWithSelector(UniswapV2SwapperUpgradeable.initialize.selector));console.log("Implementation deployed at:", address(implementation));console.log("Proxy deployed at:", address(proxy));vm.stopBroadcast();} }
-
测试升级功能
// SPDX-License-Identifier: MIT pragma solidity ^0.8.21;import "forge-std/Script.sol"; import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import "../../src/upgradeable/UniswapV2SwapperUpgradeable.sol"; import "../../src/upgradeable/UniswapV2SwapperUpgradeableV2.sol";/*** @title TestUpgrade* @dev 测试合约升级功能*/ contract TestUpgrade is Script {address constant PROXY_ADDRESS = 0x...;function run() external {uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");vm.startBroadcast(deployerPrivateKey);// 创建代理合约实例ERC1967Proxy proxy = ERC1967Proxy(payable(PROXY_ADDRESS));UniswapV2SwapperUpgradeable swapper = UniswapV2SwapperUpgradeable(address(proxy));// 检查当前版本console.log("Current max slippage:", swapper.maxSlippageBps());// 部署新版本实现合约UniswapV2SwapperUpgradeableV2 implementationV2 = new UniswapV2SwapperUpgradeableV2();// 升级代理合约swapper.upgradeTo(address(implementationV2));// 初始化新版本swapper.initializeV2();// 检查新功能console.log("New feature value:", swapper.newFeature());vm.stopBroadcast();} }
安全审计
静态分析工具
-
使用Slither进行静态分析
# 安装Slither pip3 install slither-analyzer# 分析合约 slither src/UniswapV2Swapper.sol
-
使用Mythril进行安全分析
# 安装Mythril pip3 install mythril# 分析合约 myth analyze src/UniswapV2Swapper.sol --solc-json remappings.json
-
使用Foundry内置安全检查
# 使用Foundry进行安全检查 forge inspect src/UniswapV2Swapper.sol:UniswapV2Swapper storage-layout forge inspect src/UniswapV2Swapper.sol:UniswapV2Swapper methods
手动代码审查
-
重入攻击检查
// 检查所有外部调用是否遵循"检查-效果-交互"模式 function swapExactTokensForTokens(address tokenIn,address tokenOut,uint256 amountIn,uint256 minAmountOut,uint256 deadline ) external returns (uint256 amountOut) {// 检查阶段require(deadline >= block.timestamp, "EXPIRED");require(pairExists(tokenIn, tokenOut), "PAIR_NOT_EXISTS");// 效果阶段uint256 expectedAmountOut = getPrice(tokenIn, tokenOut, amountIn);uint256 calculatedMinAmountOut = minAmountOut > 0 ? minAmountOut : expectedAmountOut.mul(10000 - maxSlippageBps).div(10000);require(expectedAmountOut >= calculatedMinAmountOut,"INSUFFICIENT_OUTPUT_AMOUNT");// 交互阶段IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn);IERC20(tokenIn).safeApprove(UNISWAP_V2_ROUTER, amountIn);// ... }
-
整数溢出检查
// 使用SafeMath防止整数溢出 using SafeMath for uint256;function getAmountOut(uint256 amountIn, uint256 reserveIn, uint256 reserveOut) public pure returns (uint256 amountOut) {require(amountIn > 0, "INSUFFICIENT_INPUT_AMOUNT");require(reserveIn > 0 && reserveOut > 0, "INSUFFICIENT_LIQUIDITY");// 使用SafeMath进行数学运算uint256 amountInWithFee = amountIn.mul(997);uint256 numerator = amountInWithFee.mul(reserveOut);uint256 denominator = reserveIn.mul(1000).add(amountInWithFee);amountOut = numerator / denominator; }
-
访问控制检查
// 添加onlyOwner修饰符保护关键功能 function setMaxSlippage(uint256 slippageBps) external onlyOwner {require(slippageBps <= 500, "SLIPPAGE_TOO_HIGH");maxSlippageBps = slippageBps; }function emergencyWithdraw(address token, uint256 amount) external onlyOwner {IERC20(token).safeTransfer(msg.sender, amount); }
常见漏洞检查
-
价格操纵攻击
// 添加滑点保护 function swapExactTokensForTokens(address tokenIn,address tokenOut,uint256 amountIn,uint256 minAmountOut,uint256 deadline ) external returns (uint256 amountOut) {// 计算预计输出uint256 expectedAmountOut = getPrice(tokenIn, tokenOut, amountIn);// 应用滑点保护uint256 calculatedMinAmountOut = minAmountOut > 0 ? minAmountOut : expectedAmountOut.mul(10000 - maxSlippageBps).div(10000);require(expectedAmountOut >= calculatedMinAmountOut,"INSUFFICIENT_OUTPUT_AMOUNT");// ... }
-
前端运行攻击防护
// 设置合理的截止时间 function swapExactTokensForTokens(address tokenIn,address tokenOut,uint256 amountIn,uint256 minAmountOut,uint256 deadline ) external returns (uint256 amountOut) {// 验证截止时间require(deadline >= block.timestamp, "EXPIRED");// 设置合理的截止时间(不要太长)require(deadline <= block.timestamp + 1 hours, "DEADLINE_TOO_LONG");// ... }
-
拒绝服务攻击防护
// 避免在循环中进行外部调用 function batchSwap(address[] calldata tokensIn,address[] calldata tokensOut,uint256[] calldata amountsIn,uint256[] calldata minAmountsOut,uint256 deadline ) external {require(tokensIn.length == tokensOut.length, "ARRAY_LENGTH_MISMATCH");require(tokensIn.length == amountsIn.length, "ARRAY_LENGTH_MISMATCH");require(tokensIn.length == minAmountsOut.length, "ARRAY_LENGTH_MISMATCH");// 限制批量操作的大小require(tokensIn.length <= 10, "BATCH_TOO_LARGE");for (uint256 i = 0; i < tokensIn.length; i++) {swapExactTokensForTokens(tokensIn[i],tokensOut[i],amountsIn[i],minAmountsOut[i],deadline);} }
正式网部署
主网部署准备
-
最终安全审查
# 运行最终安全检查 slither src/ --exclude-dependencies myth analyze src/UniswapV2Swapper.sol forge test --fork-url mainnet -vvv
-
部署检查清单
# 部署检查清单 echo "主网部署检查清单:" echo "1. 合约已通过所有测试" echo "2. 安全审计已完成" echo "3. 环境变量已正确设置" echo "4. 部署脚本已测试" echo "5. 备份计划已准备"
-
配置主网部署脚本
// SPDX-License-Identifier: MIT pragma solidity ^0.8.21;import "forge-std/Script.sol"; import "../src/UniswapV2Query.sol"; import "../src/UniswapV2Swapper.sol"; import "../src/UniswapV2LiquidityManager.sol";/*** @title DeployMainnet* @dev 部署合约到以太坊主网*/ contract DeployMainnet is Script {function run() external {// 记录开始时间console.log("开始主网部署...");console.log("区块号:", block.number);console.log("时间戳:", block.timestamp);// 从环境变量获取部署者私钥uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");address deployer = vm.addr(deployerPrivateKey);console.log("部署者地址:", deployer);// 检查Gas价格uint256 gasPrice = block.gasprice;console.log("当前Gas价格:", gasPrice);// 开始广播交易vm.startBroadcast(deployerPrivateKey);// 部署UniswapV2Queryconsole.log("正在部署UniswapV2Query...");UniswapV2Query query = new UniswapV2Query();console.log("UniswapV2Query已部署:", address(query));// 部署UniswapV2Swapperconsole.log("正在部署UniswapV2Swapper...");UniswapV2Swapper swapper = new UniswapV2Swapper();console.log("UniswapV2Swapper已部署:", address(swapper));// 部署UniswapV2LiquidityManagerconsole.log("正在部署UniswapV2LiquidityManager...");UniswapV2LiquidityManager liquidityManager = new UniswapV2LiquidityManager();console.log("UniswapV2LiquidityManager已部署:", address(liquidityManager));// 停止广播vm.stopBroadcast();// 记录部署完成console.log("主网部署完成!");console.log("总Gas消耗:", tx.gasprice * tx.gaslimit);} }
主网合约部署
-
执行主网部署
# 部署到以太坊主网 forge script script/deploy/DeployMainnet.s.sol:DeployMainnet --rpc-url mainnet --broadcast --verify -vvvv# 使用更高的Gas价格 forge script script/deploy/DeployMainnet.s.sol:DeployMainnet --rpc-url mainnet --broadcast --verify --gas-price 1000000000 -vvvv
-
验证合约
# 验证已部署的合约 forge verify-contract <CONTRACT_ADDRESS> src/UniswapV2Swapper.sol:UniswapV2Swapper --etherscan-api-key $ETHERSCAN_API_KEY --chain-id 1# 批量验证所有合约 ./scripts/verify-all.sh
验证与监控
-
创建验证脚本
// SPDX-License-Identifier: MIT pragma solidity ^0.8.21;import "forge-std/Script.sol";/*** @title VerifyMainnet* @dev 验证主网上的合约功能*/ contract VerifyMainnet is Script {// 主网合约地址address constant QUERY_ADDRESS = 0x...;address constant SWAPPER_ADDRESS = 0x...;address constant LIQUIDITY_MANAGER_ADDRESS = 0x...;// 主网代币地址address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;address constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F;function run() external {// 从环境变量获取测试用户私钥uint256 userPrivateKey = vm.envUint("TEST_PRIVATE_KEY");address user = vm.addr(userPrivateKey);console.log("测试用户地址:", user);// 开始广播交易vm.startBroadcast(userPrivateKey);// 测试查询功能UniswapV2Query query = UniswapV2Query(QUERY_ADDRESS);address pairAddress = query.getPairAddress(WETH, DAI);console.log("WETH/DAI交易对地址:", pairAddress);(uint256 reserveWETH, uint256 reserveDAI) = query.getReserves(WETH, DAI);console.log("WETH储备量:", reserveWETH);console.log("DAI储备量:", reserveDAI);uint256 price = query.getPrice(WETH, DAI, 1 ether);console.log("1 WETH价格:", price / 1e18, "DAI");// 测试兑换功能UniswapV2Swapper swapper = UniswapV2Swapper(SWAPPER_ADDRESS);// 授权Swapper使用WETHIERC20(WETH).approve(SWAPPER_ADDRESS, 0.01 ether);console.log("已授权Swapper使用WETH");// 记录初始余额uint256 initialWETH = IERC20(WETH).balanceOf(user);uint256 initialDAI = IERC20(DAI).balanceOf(user);console.log("初始WETH余额:", initialWETH / 1e18);console.log("初始DAI余额:", initialDAI / 1e18);// 执行兑换uint256 amountOut = swapper.swapExactTokensForTokens(WETH,DAI,0.01 ether, // 0.01 WETH0, // 最小输出数量block.timestamp + 1 hours);console.log("兑换完成,获得DAI:", amountOut / 1e18);// 记录最终余额uint256 finalWETH = IERC20(WETH).balanceOf(user);uint256 finalDAI = IERC20(DAI).balanceOf(user);console.log("最终WETH余额:", finalWETH / 1e18);console.log("最终DAI余额:", finalDAI / 1e18);// 验证余额变化require(finalWETH == initialWETH - 0.01 ether, "WETH余额不正确");require(finalDAI == initialDAI + amountOut, "DAI余额不正确");console.log("所有测试通过!");// 停止广播vm.stopBroadcast();} }
-
设置监控和警报
# 创建监控脚本 touch scripts/monitor.sh chmod +x scripts/monitor.sh
#!/bin/bash # scripts/monitor.sh# 监控合约余额 CONTRACT_ADDRESS="0x..." ABI='[{"constant":true,"inputs":[],"name":"getBalance","outputs":[{"name":"","type":"uint256"}],"type":"function"}]'# 获取合约余额 BALANCE=$(cast call $CONTRACT_ADDRESS "getBalance()" --rpc-url $MAINNET_RPC_URL) echo "合约余额: $BALANCE"# 检查余额是否低于阈值 if [ $BALANCE -lt 1000000000000000000 ]; thenecho "警告: 合约余额过低!"# 发送通知curl -X POST -H "Content-Type: application/json" \-d '{"text":"合约余额过低: '$BALANCE'"}' \$SLACK_WEBHOOK_URL fi
-
设置定时监控任务
# 添加cron任务 (每小時执行一次) crontab -e # 添加以下行 0 * * * * /bin/bash /path/to/uniswapv2-integration/scripts/monitor.sh
维护与升级
监控与警报
-
设置事件监控
// 在合约中添加事件 event AdminWithdraw(address indexed admin, address token, uint256 amount, uint256 timestamp); event SlippageUpdated(uint256 oldSlippage, uint256 newSlippage, uint256 timestamp);function emergencyWithdraw(address token, uint256 amount) external onlyOwner {uint256 balance = IERC20(token).balanceOf(address(this));require(amount <= balance, "Insufficient balance");IERC20(token).safeTransfer(owner(), amount);// 记录管理员提取事件emit AdminWithdraw(owner(), token, amount, block.timestamp); }function setMaxSlippage(uint256 slippageBps) external onlyOwner {require(slippageBps <= 500, "SLIPPAGE_TOO_HIGH");// 记录滑点更新事件emit SlippageUpdated(maxSlippageBps, slippageBps, block.timestamp);maxSlippageBps = slippageBps; }
-
创建事件监听脚本
// scripts/monitor-events.js const { ethers } = require("ethers"); require("dotenv").config();const provider = new ethers.providers.JsonRpcProvider(process.env.MAINNET_RPC_URL); const contractAddress = process.env.CONTRACT_ADDRESS;// 合约ABI (只需要事件部分) const abi = ["event AdminWithdraw(address indexed admin, address token, uint256 amount, uint256 timestamp)","event SlippageUpdated(uint256 oldSlippage, uint256 newSlippage, uint256 timestamp)" ];const contract = new ethers.Contract(contractAddress, abi, provider);// 监听AdminWithdraw事件 contract.on("AdminWithdraw", (admin, token, amount, timestamp) => {console.log(`管理员提取: ${admin} 提取了 ${amount} 个代币 ${token}`);// 发送警报sendAlert(`管理员提取警报: ${admin} 提取了 ${amount} 个代币 ${token}`); });// 监听SlippageUpdated事件 contract.on("SlippageUpdated", (oldSlippage, newSlippage, timestamp) => {console.log(`滑点更新: 从 ${oldSlippage} 更新为 ${newSlippage}`);// 发送警报sendAlert(`滑点更新警报: 从 ${oldSlippage} 更新为 ${newSlippage}`); });function sendAlert(message) {// 发送到Slack或其他通知服务console.log("发送警报:", message); }console.log("开始监听合约事件...");
紧急响应计划
-
创建紧急暂停功能
// 在合约中添加暂停功能 bool public paused;modifier whenNotPaused() {require(!paused, "CONTRACT_PAUSED");_; }function pause() external onlyOwner {paused = true;emit ContractPaused(block.timestamp); }function unpause() external onlyOwner {paused = false;emit ContractUnpaused(block.timestamp); }// 在关键函数中添加暂停检查 function swapExactTokensForTokens(address tokenIn,address tokenOut,uint256 amountIn,uint256 minAmountOut,uint256 deadline ) external whenNotPaused returns (uint256 amountOut) {// 函数逻辑... }
-
创建紧急响应脚本
#!/bin/bash # scripts/emergency-pause.sh# 紧急暂停合约 echo "执行紧急暂停..."# 调用合约的pause函数 cast send $CONTRACT_ADDRESS "pause()" \--rpc-url $MAINNET_RPC_URL \--private-key $EMERGENCY_KEYecho "合约已暂停"# 发送通知 curl -X POST -H "Content-Type: application/json" \-d '{"text":"合约已紧急暂停"}' \$SLACK_WEBHOOK_URL
合约升级策略
-
使用可升级合约模式
// 可升级合约示例 // SPDX-License-Identifier: MIT pragma solidity ^0.8.21;import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol";contract UniswapV2SwapperUpgradeable is Initializable, OwnableUpgradeable, PausableUpgradeable {// 初始化函数function initialize() public initializer {__Ownable_init();__Pausable_init();}// 新版本添加的功能function newFeature() public pure returns (string memory) {return "This is a new feature added in upgrade";}// 升级后初始化函数function initializeV2() public reinitializer(2) {// 新版本的初始化逻辑} }
-
创建升级管理脚本
#!/bin/bash # scripts/upgrade-contract.sh# 合约升级脚本 echo "开始合约升级..."# 部署新实现合约 NEW_IMPL=$(forge create src/UniswapV2SwapperUpgradeable.sol:UniswapV2SwapperUpgradeable \--rpc-url $MAINNET_RPC_URL \--private-key $DEPLOYER_KEY \| grep "Deployed to:" | awk '{print $3}')echo "新实现合约已部署: $NEW_IMPL"# 升级代理合约 cast send $PROXY_ADDRESS "upgradeTo(address)" $NEW_IMPL \--rpc-url $MAINNET_RPC_URL \--private-key $UPGRADER_KEYecho "代理合约已升级"# 初始化新版本 cast send $PROXY_ADDRESS "initializeV2()" \--rpc-url $MAINNET_RPC_URL \--private-key $UPGRADER_KEYecho "新版本已初始化"# 验证升级 cast call $PROXY_ADDRESS "newFeature()" --rpc-url $MAINNET_RPC_URLecho "合约升级完成"
-
测试升级过程
# 在测试网上测试升级 forge script script/upgrade/TestUpgrade.s.sol:TestUpgrade --rpc-url goerli --broadcast -vvvv# 验证升级后功能 forge script script/upgrade/VerifyUpgrade.s.sol:VerifyUpgrade --rpc-url goerli -vvvv
这份完整的技术指南涵盖了从环境搭建到正式网部署的全过程,包括详细的代码注释、测试策略、安全审计和升级方案。在实际应用中,请根据具体需求调整和完善各个部分。