去中心化交易所 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) 並顯示。」
本日總結
在本章中,你學到了:
- ✅ AMM 自動做市商:x × y = k 的核心公式
- ✅ 流動性池:提供流動性賺取手續費
- ✅ 交換機制:getAmountOut 計算產出
- ✅ 滑價 (Slippage):大額交易對價格的影響
- ✅ 完整測試:部署 → 加流動性 → 交易 → 驗證
下一章,我們將實作去中心化借貸協議!