引言构建一个去中心化交易所DEX是从零理解DeFi世界的最佳实践路径。本文将完整记录从合约设计、核心模块开发到前端集成的全过程基于Uniswap V2架构实现一个功能完整的DEX DEMO涵盖AMM交易模型、流动性池、Swap交易逻辑、Router/Factory/Pair核心合约、钱包连接及链上交互等全部核心模块。一、技术选型与项目初始化1.1 技术栈层级技术选型智能合约Solidity ^0.8.20 OpenZeppelin合约开发框架Foundry推荐或 Hardhat前端框架React 18 TypeScript链上交互ethers.js v6构建工具ViteUI组件Ant DesignFoundry是当前Solidity开发的最佳实践工具链以Rust编写测试速度极快。若选择Hardhat则更适合JavaScript/TypeScript生态的开发者-。1.2 项目结构dex-demo/ ├── contracts/ │ ├── core/ │ │ ├── DEXFactory.sol # 工厂合约 │ │ ├── DEXPair.sol # 交易对合约 │ │ └── DEXERC20.sol # LP代币 │ ├── periphery/ │ │ ├── DEXRouter.sol # 路由合约 │ │ └── WETH.sol # 封装ETH │ └── test/ │ └── TestToken.sol # 测试代币 ├── frontend/ │ ├── src/ │ │ ├── hooks/ # 自定义Hooks │ │ ├── components/ # UI组件 │ │ └── utils/ # 工具函数 └── test/ └── DEX.t.sol # Foundry测试二、智能合约层Core-Periphery架构DEX的核心采用Core-Periphery模式—— Core合约提供基础安全和核心逻辑Periphery合约提供用户友好的交互接口-。2.1 核心合约Pair交易对Pair合约是AMM的核心实现了恒定乘积做市商模型。恒定乘积公式Pair合约核心实现// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import openzeppelin/contracts/token/ERC20/IERC20.sol; import openzeppelin/contracts/security/ReentrancyGuard.sol; contract DEXPair is ReentrancyGuard { address public factory; address public token0; address public token1; uint112 private reserve0; uint112 private reserve1; uint32 private blockTimestampLast; uint public constant MINIMUM_LIQUIDITY 10**3; mapping(address uint) public balanceOf; uint public totalSupply; constructor() { factory msg.sender; } // 初始化池子仅首次添加流动性时调用 function initialize(address _token0, address _token1) external { require(msg.sender factory, DEX: FORBIDDEN); token0 _token0; token1 _token1; } // 添加流动性 - mint LP代币 function mint(address to) external nonReentrant returns (uint liquidity) { (uint112 _reserve0, uint112 _reserve1, ) getReserves(); uint balance0 IERC20(token0).balanceOf(address(this)); uint balance1 IERC20(token1).balanceOf(address(this)); uint amount0 balance0 - _reserve0; uint amount1 balance1 - _reserve1; uint _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, DEX: INSUFFICIENT_LIQUIDITY_MINTED); _mint(to, liquidity); _update(balance0, balance1, _reserve0, _reserve1); } // 移除流动性 - burn LP代币 function burn(address to) external nonReentrant returns (uint amount0, uint amount1) { (uint112 _reserve0, uint112 _reserve1, ) getReserves(); uint balance0 IERC20(token0).balanceOf(address(this)); uint balance1 IERC20(token1).balanceOf(address(this)); uint liquidity balanceOf[address(this)]; uint _totalSupply totalSupply; amount0 liquidity * balance0 / _totalSupply; amount1 liquidity * balance1 / _totalSupply; require(amount0 0 amount1 0, DEX: INSUFFICIENT_LIQUIDITY_BURNED); _burn(address(this), liquidity); _safeTransfer(token0, to, amount0); _safeTransfer(token1, to, amount1); _update(balance0 - amount0, balance1 - amount1, _reserve0, _reserve1); } // 核心Swap函数 function swap(uint amount0Out, uint amount1Out, address to) external nonReentrant { require(amount0Out 0 || amount1Out 0, DEX: INSUFFICIENT_OUTPUT_AMOUNT); (uint112 _reserve0, uint112 _reserve1, ) getReserves(); require(amount0Out _reserve0 amount1Out _reserve1, DEX: INSUFFICIENT_LIQUIDITY); uint balance0 IERC20(token0).balanceOf(address(this)); uint balance1 IERC20(token1).balanceOf(address(this)); if (amount0Out 0) _safeTransfer(token0, to, amount0Out); if (amount1Out 0) _safeTransfer(token1, to, amount1Out); uint balance0Adjusted balance0 - amount0Out; uint balance1Adjusted balance1 - amount1Out; // 恒定乘积检查包含0.3%手续费 require( balance0Adjusted * balance1Adjusted uint(_reserve0) * uint(_reserve1) * 1000**2, DEX: K ); _update(balance0, balance1, _reserve0, _reserve1); } // 更新储备 function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private { reserve0 uint112(balance0); reserve1 uint112(balance1); blockTimestampLast uint32(block.timestamp % 2**32); } }Pair合约的关键设计要点使用nonReentrant防止重入攻击-1采用CEIChecks-Effects-Interactions模式-1最小流动性锁定机制防止价格操控手续费0.3%通过调整k值隐式收取2.2 工厂合约FactoryFactory合约负责创建和管理所有交易对使用CREATE2实现确定性地址部署。contract DEXFactory { mapping(address mapping(address address)) public getPair; address[] public allPairs; event PairCreated(address indexed token0, address indexed token1, address pair, uint); function createPair(address tokenA, address tokenB) external returns (address pair) { require(tokenA ! tokenB, DEX: IDENTICAL_ADDRESSES); (address token0, address token1) tokenA tokenB ? (tokenA, tokenB) : (tokenB, tokenA); require(token0 ! address(0), DEX: ZERO_ADDRESS); require(getPair[token0][token1] address(0), DEX: PAIR_EXISTS); bytes memory bytecode type(DEXPair).creationCode; bytes32 salt keccak256(abi.encodePacked(token0, token1)); assembly { pair : create2(0, add(bytecode, 32), mload(bytecode), salt) } DEXPair(pair).initialize(token0, token1); getPair[token0][token1] pair; getPair[token1][token0] pair; allPairs.push(pair); emit PairCreated(token0, token1, pair, allPairs.length); } }Factory负责的核心功能-创建Pair合约记录所有交易对通过getPair查询池子地址-2.3 外围合约RouterRouter是用户交互的入口提供了更友好的接口封装-contract DEXRouter { address public immutable factory; address public immutable WETH; constructor(address _factory, address _WETH) { factory _factory; WETH _WETH; } // 添加流动性ETH版本 function addLiquidityETH( address token, uint amountTokenDesired, uint amountTokenMin, uint amountETHMin, address to, uint deadline ) external payable returns (uint amountToken, uint amountETH, uint liquidity) { // 实现略 } // 精确输入兑换 function swapExactTokensForTokens( uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline ) external returns (uint[] memory amounts) { // 实现略 } // 获取兑换金额 function getAmountsOut(uint amountIn, address[] calldata path) external view returns (uint[] memory amounts) { // 实现略 } }Router支持的核心功能-精确输入/输出兑换滑点保护amountOutMin参数截止时间控制deadline参数多跳路由path[]数组-三、AMM交易模型详解3.1 价格计算与滑点.2 手续费机制DEX的手续费设计采用0.3%交易费100%归LP的模式交易者支付0.3%的手续费手续费留在池子中增加reservesLP通过持有LP代币按比例分享手续费收益-协议费用0.05%可通过feeTo地址收取--6手续费通过在swap中调整k值实现对amountIn扣手续费后计算实际进池数量-1。3.3 流动性提供LP机制LP通过添加流动性获得LP代币作为份额证明-四、前端开发与链上交互4.1 钱包连接ethers.js v6前端使用ethers.js v6的BrowserProvider连接MetaMask-import { BrowserProvider, ethers } from ethers; // 连接钱包 const connectWallet async () { if (!window.ethereum) { throw new Error(请安装MetaMask); } const provider new BrowserProvider(window.ethereum); await provider.send(eth_requestAccounts, []); const signer await provider.getSigner(); const address await signer.getAddress(); return { provider, signer, address }; };Web3前端与合约交互的本质流程-用ethers.js把用户操作编码成calldata让钱包签名通过RPC广播链上执行后读回events/storage刷新UI4.2 Swap交互实现// 执行Swap const executeSwap async ( router: ethers.Contract, path: string[], amountIn: bigint, amountOutMin: bigint, to: string, deadline: number ) { const tx await router.swapExactTokensForTokens( amountIn, amountOutMin, path, to, deadline ); const receipt await tx.wait(); return receipt; }; // 估算输出金额 const getAmountsOut async ( router: ethers.Contract, amountIn: bigint, path: string[] ): Promisebigint[] { return await router.getAmountsOut(amountIn, path); };前端关键注意点-27必须在发起swap之前先估算amountOut-27前端计算minAmountOut amountOut * (1 - slippage)并提交链上-27显示price impact、pool depth和expected fee-27正确处理ERC20的decimals-27使用estimateGas预估Gas防止交易失败-274.3 滑点保护实现滑点保护是DEX前端最重要的安全机制const swapWithSlippageProtection async ( router: ethers.Contract, amountIn: bigint, path: string[], slippageTolerance: number // 例如 0.005 0.5% ) { const amounts await router.getAmountsOut(amountIn, path); const amountOut amounts[amounts.length - 1]; const minAmountOut amountOut * BigInt(Math.floor((1 - slippageTolerance) * 1000)) / 1000n; // 执行swap传入minAmountOut作为滑点保护 return await router.swapExactTokensForTokens( amountIn, minAmountOut, path, await signer.getAddress(), Math.floor(Date.now() / 1000) 60 * 20 // 20分钟截止 ); };五、测试与部署5.1 Foundry测试Foundry是当前Solidity测试的最佳工具// test/DEX.t.sol contract DEXTest is Test { DEXFactory factory; DEXPair pair; TestToken tokenA; TestToken tokenB; function setUp() public { tokenA new TestToken(Token A, TKNA); tokenB new TestToken(Token B, TKNB); factory new DEXFactory(); address pairAddr factory.createPair(address(tokenA), address(tokenB)); pair DEXPair(pairAddr); } function testSwap() public { // 添加流动性 tokenA.approve(address(pair), 1000 ether); tokenB.approve(address(pair), 1000 ether); pair.mint(address(this)); // 执行Swap uint reserve0Before pair.reserve0(); uint reserve1Before pair.reserve1(); pair.swap(100 ether, 0, address(this)); // 验证... } }完整的测试应覆盖-27正常流程测试添加流动性、兑换、移除流动性异常场景测试滑点保护、截止时间Fuzz测试Foundry fuzz静态检查Slither模糊测试Echidna5.2 部署流程完整的部署流程-6部署Factory合约记录工厂合约地址和init code hash-6部署WETH合约记录WETH地址-6修改Router中的hash不要带0x前缀-6部署Router合约传入WETH和工厂合约地址-6部署前端配置合约地址和ABI5.3 安全审计要点DEX安全设计的核心关注点-27风险类型防护措施重入攻击CEI模式 nonReentrant修饰符-1闪电贷/价格操控使用TWAP或外部预言机-27滑点攻击前端强制滑点设置 显示price impact-27整数溢出Solidity ^0.8自带检查-27初始流动性攻击锁定最小流动性MINIMUM_LIQUIDITY-27ERC20兼容性使用SafeERC20-1六、Demo运行指南6.1 环境准备# 安装Foundry curl -L https://foundry.paradigm.xyz | bash foundryup # 克隆项目 git clone your-repo cd dex-demo # 安装依赖 forge install # 编译合约 forge build6.2 运行测试# 运行所有测试forge test# 查看Gas报告forge test --gas-report# 运行特定测试forge test --match-contract DEXTest --match-test testSwap6.3 启动前端cd frontend npm install npm run dev前端功能清单Swap - 代币兑换基于AMM算法Liquidity - 流动性添加/移除Pool - 流动性池管理钱包连接MetaMask滑点保护和价格影响提示七、总结与展望本文从零构建了一个完整的DEX DEMO涵盖Core-Periphery合约架构Factory管理Pair创建Pair实现AMM核心逻辑Router提供用户友好接口-AMM交易模型恒定乘积公式×x×yk驱动价格发现-11流动性池机制LP通过添加流动性获得LP代币并分享0.3%手续费-前端集成ethers.js v6实现钱包连接和链上交互-安全设计CEI模式、滑点保护、重入防护-1-27这个DEMO虽然是简化版但涵盖了DEX的核心技术要素可作为深入理解DeFi的起点。生产环境还需考虑TWAP预言机、多跳路由、协议费用治理、更完善的测试覆盖等进阶特性-27。未来的DEX将向更高性能Layer2扩容、更强可编程性Hooks机制和更深流动性聚合方向演进。而理解这个从0到1的构建过程正是掌握这些前沿技术的最佳起点。