去中心化交易所 DEX

去中心化交易所 (Decentralized Exchange, DEX) 是 DeFi 的核心基礎設施。與 Binance 或 Coinbase 不同,DEX 不需要註冊、不需要 KYC、你的資產永遠在你自己的錢包裡。

AMM:自動做市商

Uniswap 在 2018 年提出了革命性的 AMM (Automated Market Maker) 機制。它的核心是一條簡單的公式:

$$x \times y = k$$

  • $x$:池子中的 Token A 數量
  • $y$:池子中的 Token B 數量
  • $k$:一個常數(不變)

這條公式保證了:無論交易量多大,池子永遠有流動性。 當有人買入 Token A 時,$x$ 減少、$y$ 增加,價格會根據公式自動調整。

實作 AMM

// contracts/SimpleDEX.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract SimpleDEX is Ownable {
    IERC20 public tokenA;
    IERC20 public tokenB;
    
    uint256 public reserveA;
    uint256 public reserveB;
    
    uint256 public totalLiquidity;
    mapping(address => uint256) public liquidity;
    
    // 事件
    event LiquidityAdded(
        address indexed provider,
        uint256 amountA,
        uint256 amountB,
        uint256 shares
    );
    event LiquidityRemoved(
        address indexed provider,
        uint256 amountA,
        uint256 amountB,
        uint256 shares
    );
    event Swapped(
        address indexed trader,
        address indexed tokenIn,
        address indexed tokenOut,
        uint256 amountIn,
        uint256 amountOut
    );
    
    constructor(address _tokenA, address _tokenB) Ownable(msg.sender) {
        tokenA = IERC20(_tokenA);
        tokenB = IERC20(_tokenB);
    }
    
    // === 添加流動性 ===
    function addLiquidity(
        uint256 _amountA,
        uint256 _amountB
    ) external returns (uint256 shares) {
        require(_amountA > 0 && _amountB > 0, "金額必須大於 0");
        
        // 轉入代幣
        tokenA.transferFrom(msg.sender, address(this), _amountA);
        tokenB.transferFrom(msg.sender, address(this), _amountB);
        
        if (totalLiquidity == 0) {
            // 第一次添加流動性
            shares = _amountA;  // 簡化版:直接用 amountA 作為股份
        } else {
            // 按照比例計算股份
            shares = (_amountA * totalLiquidity) / reserveA;
        }
        
        reserveA += _amountA;
        reserveB += _amountB;
        
        liquidity[msg.sender] += shares;
        totalLiquidity += shares;
        
        emit LiquidityAdded(msg.sender, _amountA, _amountB, shares);
    }
    
    // === 移除流動性 ===
    function removeLiquidity(
        uint256 _shares
    ) external returns (uint256 amountA, uint256 amountB) {
        require(_shares > 0, "股份必須大於 0");
        require(liquidity[msg.sender] >= _shares, "股份不足");
        
        // 計算可以取回的代幣數量
        amountA = (reserveA * _shares) / totalLiquidity;
        amountB = (reserveB * _shares) / totalLiquidity;
        
        liquidity[msg.sender] -= _shares;
        totalLiquidity -= _shares;
        
        reserveA -= amountA;
        reserveB -= amountB;
        
        // 轉回代幣
        tokenA.transfer(msg.sender, amountA);
        tokenB.transfer(msg.sender, amountB);
        
        emit LiquidityRemoved(msg.sender, amountA, amountB, _shares);
    }
    
    // === 計算交換產出 ===
    function getAmountOut(
        uint256 _amountIn,
        uint256 _reserveIn,
        uint256 _reserveOut
    ) public pure returns (uint256) {
        require(_amountIn > 0, "輸入金額必須大於 0");
        require(_reserveIn > 0 && _reserveOut > 0, "流動性不足");
        
        // 0.3% 手續費
        uint256 amountInWithFee = _amountIn * 997;
        uint256 numerator = amountInWithFee * _reserveOut;
        uint256 denominator = (_reserveIn * 1000) + amountInWithFee;
        
        return numerator / denominator;
    }
    
    // === 交換代幣 ===
    function swapAForB(uint256 _amountIn) external returns (uint256 amountOut) {
        require(_amountIn > 0, "輸入金額必須大於 0");
        
        amountOut = getAmountOut(_amountIn, reserveA, reserveB);
        require(amountOut > 0, "產出為 0");
        
        tokenA.transferFrom(msg.sender, address(this), _amountIn);
        tokenB.transfer(msg.sender, amountOut);
        
        reserveA += _amountIn;
        reserveB -= amountOut;
        
        emit Swapped(msg.sender, address(tokenA), address(tokenB), _amountIn, amountOut);
    }
    
    function swapBForA(uint256 _amountIn) external returns (uint256 amountOut) {
        require(_amountIn > 0, "輸入金額必須大於 0");
        
        amountOut = getAmountOut(_amountIn, reserveB, reserveA);
        require(amountOut > 0, "產出為 0");
        
        tokenB.transferFrom(msg.sender, address(this), _amountIn);
        tokenA.transfer(msg.sender, amountOut);
        
        reserveB += _amountIn;
        reserveA -= amountOut;
        
        emit Swapped(msg.sender, address(tokenB), address(tokenA), _amountIn, amountOut);
    }
    
    // === 查看當前價格 ===
    function getPriceAInB() public view returns (uint256) {
        require(reserveA > 0 && reserveB > 0, "流動性不足");
        return (reserveB * 1e18) / reserveA;
    }
    
    function getPriceBInA() public view returns (uint256) {
        require(reserveA > 0 && reserveB > 0, "流動性不足");
        return (reserveA * 1e18) / reserveB;
    }
}

測試 AMM 功能

// scripts/test_dex.js
async function main() {
  const [deployer, user1, user2] = await hre.ethers.getSigners();
  
  // 部署代幣
  const VibeToken = await hre.ethers.getContractFactory("VibeToken");
  const tokenA = await VibeToken.deploy(deployer.address);
  await tokenA.waitForDeployment();
  
  const tokenB = await VibeToken.deploy(deployer.address);
  await tokenB.waitForDeployment();
  
  // 部署 DEX
  const SimpleDEX = await hre.ethers.getContractFactory("SimpleDEX");
  const dex = await SimpleDEX.deploy(
    await tokenA.getAddress(),
    await tokenB.getAddress()
  );
  await dex.waitForDeployment();
  
  console.log("DEX 部署地址:", await dex.getAddress());
  
  // 準備流動性
  const amountA = hre.ethers.parseEther("1000");
  const amountB = hre.ethers.parseEther("1000");
  
  await tokenA.approve(await dex.getAddress(), amountA);
  await tokenB.approve(await dex.getAddress(), amountB);
  
  // 添加流動性
  await dex.addLiquidity(amountA, amountB);
  console.log("流動性已添加: 1000 A + 1000 B");
  
  // 查看價格
  const priceA = await dex.getPriceAInB();
  console.log("1 A = ", hre.ethers.formatEther(priceA), "B");
  
  // 用 A 換 B
  const swapAmount = hre.ethers.parseEther("100");
  await tokenA.connect(user1).approve(await dex.getAddress(), swapAmount);
  
  // 先給 user1 一些 A 代幣
  await tokenA.transfer(user1.address, swapAmount);
  
  const result = await dex.connect(user1).swapAForB(swapAmount);
  console.log(`用 100 A 換到了 B 代幣`);
  
  // 查看新價格(滑價影響)
  const newPriceA = await dex.getPriceAInB();
  console.log("交換後 1 A = ", hre.ethers.formatEther(newPriceA), "B");
  console.log("價格滑價了!流動性池需要更深的流動性來減少滑價。");
}

main().catch(console.error);

使用 Vibe Coding 建 DEX

🔥 【DEX 詠唱範例】 「請幫我實作一個去中心化交易所: 1. 支援 ETH/Token 交易對(不只是 Token/Token)。 2. 使用 x * y = k AMM 公式。 3. 每筆交易收取 0.3% 手續費,自動加入流動性池。 4. 流動性提供者可以隨時添加或移除流動性。 5. 寫一個測試腳本:部署 → 加流動性 → 交易 → 移除流動性。 6. 計算滑價 (Slippage) 並顯示。」

本日總結

在本章中,你學到了:

  1. AMM 自動做市商:x × y = k 的核心公式
  2. 流動性池:提供流動性賺取手續費
  3. 交換機制:getAmountOut 計算產出
  4. 滑價 (Slippage):大額交易對價格的影響
  5. 完整測試:部署 → 加流動性 → 交易 → 驗證

下一章,我們將實作去中心化借貸協議!

解鎖完整教學內容

本章為付費內容。加入專案即可解鎖超過 5000 字的深度解析,包含 10 個以上神級 Prompt 與真實 Source Code 範例!