TokenA
和 TokenB
,储备量分别为 x
和 y
x * y = k
k
不变我们实现一个简化的 ETH ↔ ERC20 Token 交易对:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
// IERC20: ERC20 代币标准接口
interface IERC20 {
// 转账代币
function transfer(address to, uint amount) external returns (bool);
// 从授权地址转账代币
function transferFrom(address from, address to, uint amount) external returns (bool);
// 查询代币余额
function balanceOf(address account) external view returns (uint);
}
// SimpleDEX: 简单的去中心化交易所合约
contract SimpleDEX {
IERC20 public token; // 交易对中的 ERC20 代币合约
uint public totalLiquidity; // 总流动性(LP 总量)
mapping(address => uint) public liquidity; // 用户地址到其 LP 份额的映射
// 事件定义
event Init(address indexed provider, uint ethAmount, uint tokenAmount); // 池子初始化事件
event Deposit(address indexed provider, uint ethAmount, uint tokenAmount, uint liquidityMinted); // 流动性添加事件
event Withdraw(address indexed provider, uint ethAmount, uint tokenAmount, uint liquidityBurned); // 流动性提取事件
event Swap(address indexed trader, string direction, uint inputAmount, uint outputAmount); // 交易事件
// 构造函数
// @param tokenAddr: ERC20 代币合约地址
constructor(address tokenAddr) {
token = IERC20(tokenAddr);
}
// 初始化流动性池
// @param tokenAmount: 初始代币数量
// @return uint: 初始流动性数量
function init(uint tokenAmount) public payable returns (uint) {
require(totalLiquidity == 0, "already initialized"); // 确保池子未初始化
totalLiquidity = address(this).balance; // 初始流动性为合约中的 ETH 余额
liquidity[msg.sender] = totalLiquidity; // 记录用户的 LP 份额
require(token.transferFrom(msg.sender, address(this), tokenAmount), "token transfer failed"); // 转入代币
emit Init(msg.sender, msg.value, tokenAmount); // 触发初始化事件
return totalLiquidity;
}
// 提供流动性(添加 ETH 和代币到池子)
// @return uint: 新铸造的 LP 份额数量
function deposit() public payable returns (uint) {
uint ethReserve = address(this).balance - msg.value; // 计算 ETH 储备(扣除当前转账)
uint tokenReserve = token.balanceOf(address(this)); // 获取代币储备
// 计算需要转入的代币数量(按比例)
uint tokenAmount = (msg.value * tokenReserve) / ethReserve;
// 计算新铸造的 LP 份额
uint liquidityMinted = (msg.value * totalLiquidity) / ethReserve;
liquidity[msg.sender] += liquidityMinted; // 更新用户 LP 份额
totalLiquidity += liquidityMinted; // 更新总流动性
require(token.transferFrom(msg.sender, address(this), tokenAmount), "token transfer failed"); // 转入代币
emit Deposit(msg.sender, msg.value, tokenAmount, liquidityMinted); // 触发流动性添加事件
return liquidityMinted;
}
// 提取流动性(赎回 LP 份额)
// @param amount: 要提取的 LP 份额数量
// @return (uint, uint): 返回提取的 ETH 数量和代币数量
function withdraw(uint amount) public returns (uint, uint) {
require(liquidity[msg.sender] >= amount, "not enough liquidity"); // 检查用户 LP 余额
// 按比例计算可提取的 ETH 和代币数量
uint ethAmount = (amount * address(this).balance) / totalLiquidity;
uint tokenAmount = (amount * token.balanceOf(address(this))) / totalLiquidity;
liquidity[msg.sender] -= amount; // 扣除用户 LP 份额
totalLiquidity -= amount; // 减少总流动性
payable(msg.sender).transfer(ethAmount); // 转出 ETH
require(token.transfer(msg.sender, tokenAmount), "token transfer failed"); // 转出代币
emit Withdraw(msg.sender, ethAmount, tokenAmount, amount); // 触发流动性提取事件
return (ethAmount, tokenAmount);
}
// 用 ETH 兑换代币
// @return uint: 兑换获得的代币数量
function ethToToken() public payable returns (uint) {
uint ethReserve = address(this).balance - msg.value; // 计算 ETH 储备(扣除当前转账)
uint tokenReserve = token.balanceOf(address(this)); // 获取代币储备
// 计算兑换的代币数量(含手续费)
uint tokenAmount = getOutputAmount(msg.value, ethReserve, tokenReserve);
require(token.transfer(msg.sender, tokenAmount), "token transfer failed"); // 转出代币
emit Swap(msg.sender, "ETH_TO_TOKEN", msg.value, tokenAmount); // 触发交易事件
return tokenAmount;
}
// 用代币兑换 ETH
// @param tokenAmount: 要兑换的代币数量
// @return uint: 兑换获得的 ETH 数量
function tokenToEth(uint tokenAmount) public returns (uint) {
uint tokenReserve = token.balanceOf(address(this)); // 获取代币储备
uint ethReserve = address(this).balance; // 获取 ETH 储备
// 计算兑换的 ETH 数量(含手续费)
uint ethAmount = getOutputAmount(tokenAmount, tokenReserve, ethReserve);
require(token.transferFrom(msg.sender, address(this), tokenAmount), "token transfer failed"); // 转入代币
payable(msg.sender).transfer(ethAmount); // 转出 ETH
emit Swap(msg.sender, "TOKEN_TO_ETH", tokenAmount, ethAmount); // 触发交易事件
return ethAmount;
}
// AMM 定价公式(含 0.3% 手续费)
// @param inputAmount: 输入数量
// @param inputReserve: 输入资产储备
// @param outputReserve: 输出资产储备
// @return uint: 输出数量
function getOutputAmount(uint inputAmount, uint inputReserve, uint outputReserve) internal pure returns (uint) {
uint inputAmountWithFee = inputAmount * 997; // 扣除 0.3% 手续费
uint numerator = inputAmountWithFee * outputReserve; // 分子
uint denominator = (inputReserve * 1000) + inputAmountWithFee; // 分母
return numerator / denominator; // 计算结果
}
// 查询当前价格(1 代币 = ? ETH, 1 ETH = ? 代币)
// @return (uint, uint): ETH 价格和代币价格(乘以 1e18 避免浮点数)
function getPrice() external view returns (uint ethPerToken, uint tokenPerEth) {
uint ethReserve = address(this).balance; // ETH 储备
uint tokenReserve = token.balanceOf(address(this)); // 代币储备
return (
ethReserve * 1e18 / tokenReserve, // 1 代币 = ? ETH
tokenReserve * 1e18 / ethReserve // 1 ETH = ? 代币
);
}
}
init(1000 tokens)
并附带 1 ETHx=1 ETH, y=1000 tokens
我们还是使用 Foundry 来测试合约:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../src/SimpleDEX.sol";
// MockERC20: 模拟 ERC20 代币合约,用于测试 DEX 功能
contract MockERC20 {
string public name = "MockToken"; // 代币名称
string public symbol = "MTK"; // 代币符号
uint8 public decimals = 18; // 代币精度
mapping(address => uint) public balanceOf; // 地址到余额的映射
mapping(address => mapping(address => uint)) public allowance; // 授权额度映射
event Transfer(address indexed from, address indexed to, uint value); // 转账事件
event Approval(address indexed owner, address indexed spender, uint value); // 授权事件
// 铸造代币
// @param to: 接收地址
// @param amount: 铸造数量
function mint(address to, uint amount) external {
balanceOf[to] += amount;
emit Transfer(address(0), to, amount);
}
// 转账代币
// @param to: 接收地址
// @param amount: 转账数量
// @return bool: 是否成功
function transfer(address to, uint amount) external returns (bool) {
require(balanceOf[msg.sender] >= amount, "balance too low");
balanceOf[msg.sender] -= amount;
balanceOf[to] += amount;
emit Transfer(msg.sender, to, amount);
return true;
}
// 授权额度
// @param spender: 被授权地址
// @param amount: 授权数量
// @return bool: 是否成功
function approve(address spender, uint amount) external returns (bool) {
allowance[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
// 从授权地址转账
// @param from: 转出地址
// @param to: 接收地址
// @param amount: 转账数量
// @return bool: 是否成功
function transferFrom(
address from,
address to,
uint amount
) external returns (bool) {
require(balanceOf[from] >= amount, "balance too low");
require(allowance[from][msg.sender] >= amount, "allowance too low");
balanceOf[from] -= amount;
allowance[from][msg.sender] -= amount;
balanceOf[to] += amount;
emit Transfer(from, to, amount);
return true;
}
}
// SimpleDEXTest: 测试 SimpleDEX 合约的功能
contract SimpleDEXTest is Test {
MockERC20 token; // 模拟 ERC20 代币
SimpleDEX dex; // 待测试的 DEX 合约
address alice = address(0x123); // 测试账户 Alice
address bob = address(0x234); // 测试账户 Bob
// 初始化测试环境
function setUp() public {
token = new MockERC20();
dex = new SimpleDEX(address(token));
// 给 Alice 和 Bob 分配初始 ETH
vm.deal(alice, 100 ether);
vm.deal(bob, 100 ether);
// 给 Alice 铸造代币并授权 DEX
token.mint(alice, 10000 ether);
vm.startPrank(alice);
token.approve(address(dex), type(uint).max);
vm.stopPrank();
}
// 测试初始化和流动性提供功能
function testInitAndDeposit() public {
vm.startPrank(alice);
dex.init{value: 1 ether}(1000 ether); // 初始化 DEX
vm.stopPrank();
// 验证价格计算是否正确
(uint ethPrice, uint tokenPrice) = dex.getPrice();
assertGt(ethPrice, 0);
assertGt(tokenPrice, 0);
// Bob 提供流动性
vm.startPrank(bob);
token.mint(bob, 5000 ether);
token.approve(address(dex), type(uint).max);
dex.deposit{value: 0.5 ether}();
vm.stopPrank();
}
// 测试 ETH 兑换代币功能
function testSwapEthToToken() public {
vm.startPrank(alice);
dex.init{value: 1 ether}(1000 ether);
uint tokenOut = dex.ethToToken{value: 0.1 ether}(); // 兑换代币
assertGt(tokenOut, 0); // 验证兑换数量大于零
vm.stopPrank();
}
// 测试代币兑换 ETH 功能
function testSwapTokenToEth() public {
vm.startPrank(alice);
dex.init{value: 1 ether}(1000 ether);
vm.stopPrank();
vm.startPrank(bob);
token.mint(bob, 100 ether);
token.approve(address(dex), type(uint).max);
uint ethOut = dex.tokenToEth(50 ether); // 兑换 ETH
assertGt(ethOut, 0); // 验证兑换数量大于零
vm.stopPrank();
}
// 测试提取流动性功能
function testWithdrawLiquidity() public {
vm.startPrank(alice);
dex.init{value: 1 ether}(1000 ether);
(uint ethOut, uint tokenOut) = dex.withdraw(0.5 ether); // 提取流动性
assertGt(ethOut, 0); // 验证提取的 ETH 数量大于零
assertGt(tokenOut, 0); // 验证提取的代币数量大于零
vm.stopPrank();
}
}
运行测试:
➜ counter git:(main) ✗ forge test --match-path test/SimpleDEX.t.sol -vvv
[⠊] Compiling...
[⠢] Compiling 1 files with Solc 0.8.29
[⠆] Solc 0.8.29 finished in 1.13s
Compiler run successful!
Ran 4 tests for test/SimpleDEX.t.sol:SimpleDEXTest
[PASS] testInitAndDeposit() (gas: 211169)
[PASS] testSwapEthToToken() (gas: 126392)
[PASS] testSwapTokenToEth() (gas: 187912)
[PASS] testWithdrawLiquidity() (gas: 128136)
Suite result: ok. 4 passed; 0 failed; 0 skipped; finished in 8.74ms (5.84ms CPU time)
Ran 1 test suite in 346.43ms (8.74ms CPU time): 4 tests passed, 0 failed, 0 skipped (4 total tests)
SimpleDEX
中补充 getPrice()
方法,返回当前 ETH/Token 价格。原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。