公式
x * y = k
- x:代币A的数量
- y:代币B的数量
- k:恒定乘积(流动性池的总价值)
特点
- 代表协议:Uniswap V1/V2
- 滑点机制:交易量越大,价格偏离越多(非线性)。
- 流动性分布:全价格区间均匀分布,资本效率低。
2. 恒定和做市商(Constant Sum Market Maker, CSMM)
公式
x + y = k
特点
- 零滑点:价格固定为 1:1(如稳定币交易)。
- 缺点:流动性易耗尽(套利者会抽干某一代币)。
- 代表协议:早期稳定币兑换池(已淘汰)。
示例
- 池中有 100 DAI + 100 USDC,用户用 10 DAI 换 10 USDC,池变为 110 DAI + 90 USDC。
3. 恒定均值做市商(Constant Mean Market Maker, CMMM)
公式
x^a y^b z^c = k
- a,b,c:代币权重(可自定义)。
特点
- 支持多代币池(如Balancer的3种代币池)。
- 灵活权重:例如 80% ETH + 20% BTC 的指数基金池。
- 代表协议:Balancer。
4. 集中流动性AMM(Concentrated Liquidity)
代表协议:Uniswap V3
原理
- LP可自定义价格区间(如 ETH/USDC 在 $1800-$2200 提供流动性)。
公式分段处理:
- 在区间内:使用 CPMM(x * y = k)。
- 在区间外:流动性失效(变为单一资产)。
优势
- 资本效率提升:最高达4000倍(相比V2)。
- 支持限价单:通过流动性区间模拟。
5. 动态费用AMM(Dynamic Fees)
代表协议:Trader Joe(Liquidity Book)
特点
- 根据市场波动动态调整手续费(如高波动时提高费率)。
- 分档流动性:将价格划分为多个“档位”,每档独立计算流动性。
AMM算法的核心问题与解决方案
| 问题 | 解决方案 | 协议示例 |
|---|---|---|
| 资本效率低 | 集中流动性(Uniswap V3) | Uniswap V3 |
| 无常损失 | 动态费率或对冲策略 | Bancor V3 |
| 多代币池支持 | 恒定均值算法(Balancer) | Balancer |
总结
- CPMM(Uniswap):简单通用,适合大部分代币对。
- CSMM:仅适合稳定币,已淘汰。
- CMMM(Balancer):灵活支持多代币和自定义权重。
- 集中流动性(Uniswap V3):提升资本效率,适合专业做市。
{/collapse-item}
{collapse-item label="三、实现一个简单的swap" open}
SimpleSwap继承了 ERC20 代币标准,方便记录流动性提供者提供的流动性。在构造器中,我们指定一对代币地址token0和token1,交易所仅支持这对代币。reserve0和reserve1记录了合约中代币的储备量。
contract SimpleSwap is ERC20 {
// 代币合约
IERC20 public token0;
IERC20 public token1;
// 代币储备量
uint public reserve0;
uint public reserve1;
// 构造器,初始化代币地址
constructor(IERC20 _token0, IERC20 _token1) ERC20("SimpleSwap", "SS") {
token0 = _token0;
token1 = _token1;
}
}交易所主要有两类参与者:流动性提供者(Liquidity Provider,LP)和交易者(Trader)。下面我们分别实现这两部分的功能。
流动性提供
流动性提供者给市场提供流动性,让交易者获得更好的报价和流动性,并收取一定费用。
首先,我们需要实现添加流动性的功能。当用户向代币池添加流动性时,合约要记录添加的LP份额。这个LP份额主要是用户提供流动性的凭证,交易费的一定比例按 LP 份额分配给流动性提供者,同时也可以赎回代币,销毁 LP 代币,按LP比例取回 tokenA 和 tokenB,具体的tokenA和tokenB数量取决于他们现有数量然后乘LP比例。根据 Uniswap V2,LP份额如下计算:
Δx 和 Δy 分别表示一笔交易中token和美元的变化量
- 代币池被首次添加流动性时,LP份额 ΔL 由添加代币数量乘积的平方根决定:

- 非首次添加流动性时,LP份额由添加代币数量占池子代币储备量的比例决定(两个代币的比例取更小的那个):

因为 SimpleSwap 合约继承了 ERC20 代币标准,在计算好LP份额后,可以将份额以代币形式铸造LP代币给用户。
下面的 addLiquidity() 函数实现了添加流动性的功能,主要步骤如下:
- 将用户添加的代币转入合约,需要用户事先给合约授权。
- 根据公式计算添加的流动性份额,并检查铸造的LP数量。
- 更新合约的代币储备量。
- 给流动性提供者铸造LP代币。
- 释放
Mint事件。
event Mint(address indexed sender, uint amount0, uint amount1);
// 添加流动性,转进代币,铸造LP
// @param amount0Desired 添加的token0数量
// @param amount1Desired 添加的token1数量
function addLiquidity(uint amount0Desired, uint amount1Desired) public returns(uint liquidity){
// 将添加的流动性转入Swap合约,需事先给Swap合约授权
token0.transferFrom(msg.sender, address(this), amount0Desired);
token1.transferFrom(msg.sender, address(this), amount1Desired);
// 计算添加的流动性
uint _totalSupply = totalSupply();
if (_totalSupply == 0) {
// 如果是第一次添加流动性,铸造 L = sqrt(x * y) 单位的LP(流动性提供者)代币
liquidity = sqrt(amount0Desired * amount1Desired);
} else {
// 如果不是第一次添加流动性,按添加代币的数量比例铸造LP,取两个代币更小的那个比例
liquidity = min(amount0Desired * _totalSupply / reserve0, amount1Desired * _totalSupply /reserve1);
}
// 检查铸造的LP数量
require(liquidity > 0, 'INSUFFICIENT_LIQUIDITY_MINTED');
// 更新储备量
reserve0 = token0.balanceOf(address(this));
reserve1 = token1.balanceOf(address(this));
// 给流动性提供者铸造liquidity个LP代币,代表他们提供的流动性
_mint(msg.sender, liquidity);
emit Mint(msg.sender, amount0Desired, amount1Desired);
}接下来,我们需要实现移除流动性的功能。当用户从池子中移除流动性 ΔL 时,合约要销毁LP份额代币,并按比例将代币返还给用户。返还代币的计算公式如下:

下面的 removeLiquidity() 函数实现移除流动性的功能,主要步骤如下:
- 获取合约中的代币余额。
- 按LP的比例计算要转出的代币数量。
- 检查代币数量。
- 销毁LP份额。
- 将相应的代币转账给用户。
- 更新储备量。
- 释放
Burn事件。
// 移除流动性,销毁LP,转出代币
// 转出数量 = (liquidity / totalSupply_LP) * reserve
// @param liquidity 移除的流动性数量
function removeLiquidity(uint liquidity) external returns (uint amount0, uint amount1) {
// 获取余额
uint balance0 = token0.balanceOf(address(this));
uint balance1 = token1.balanceOf(address(this));
// 按LP的比例计算要转出的代币数量
uint _totalSupply = totalSupply();
amount0 = liquidity * balance0 / _totalSupply;
amount1 = liquidity * balance1 / _totalSupply;
// 检查代币数量
require(amount0 > 0 && amount1 > 0, 'INSUFFICIENT_LIQUIDITY_BURNED');
// 销毁LP
_burn(msg.sender, liquidity);
// 转出代币
token0.transfer(msg.sender, amount0);
token1.transfer(msg.sender, amount1);
// 更新储备量
reserve0 = token0.balanceOf(address(this));
reserve1 = token1.balanceOf(address(this));
emit Burn(msg.sender, amount0, amount1);
}至此,合约中与流动性提供者相关的功能完成了,接下来是交易的部分。
交易
在Swap合约中,用户可以使用一种代币交易另一种。那么我用 Δx单位的 token0,可以交换多少单位的 token1 呢?下面我们来简单推导一下。
根据恒定乘积公式,交易前:
交易后,有:
交易前后 k 值不变,联立上面等式,可以得到:
因此,可以交换到的代币数量 Δy 由 Δx,x,和 y 决定。注意 Δx 和 Δy 的符号相反,因为转入会增加代币储备量,而转出会减少。
下面的 getAmountOut() 实现了给定一个资产的数量和代币对的储备,计算交换另一个代币的数量。
// 给定一个资产的数量和代币对的储备,计算交换另一个代币的数量
function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) public pure returns (uint amountOut) {
require(amountIn > 0, 'INSUFFICIENT_AMOUNT');
require(reserveIn > 0 && reserveOut > 0, 'INSUFFICIENT_LIQUIDITY');
amountOut = amountIn * reserveOut / (reserveIn + amountIn);
}有了这一核心公式后,我们可以着手实现交易功能了。下面的 swap() 函数实现了交易代币的功能,主要步骤如下:
- 用户在调用函数时指定用于交换的代币数量,交换的代币地址,以及换出另一种代币的最低数量。
- 判断是 token0 交换 token1,还是 token1 交换 token0。
- 利用上面的公式,计算交换出代币的数量。
- 判断交换出的代币是否达到了用户指定的最低数量,这里类似于交易的滑点。
- 将用户的代币转入合约。
- 将交换的代币从合约转给用户。
- 更新合约的代币储备量。
- 释放
Swap事件。
// swap代币
// @param amountIn 用于交换的代币数量
// @param tokenIn 用于交换的代币合约地址
// @param amountOutMin 交换出另一种代币的最低数量
function swap(uint amountIn, IERC20 tokenIn, uint amountOutMin) external returns (uint amountOut, IERC20 tokenOut){
require(amountIn > 0, 'INSUFFICIENT_OUTPUT_AMOUNT');
require(tokenIn == token0 || tokenIn == token1, 'INVALID_TOKEN');
uint balance0 = token0.balanceOf(address(this));
uint balance1 = token1.balanceOf(address(this));
if(tokenIn == token0){
// 如果是token0交换token1
tokenOut = token1;
// 计算能交换出的token1数量
amountOut = getAmountOut(amountIn, balance0, balance1);
require(amountOut > amountOutMin, 'INSUFFICIENT_OUTPUT_AMOUNT');
// 进行交换
tokenIn.transferFrom(msg.sender, address(this), amountIn);
tokenOut.transfer(msg.sender, amountOut);
}else{
// 如果是token1交换token0
tokenOut = token0;
// 计算能交换出的token1数量
amountOut = getAmountOut(amountIn, balance1, balance0);
require(amountOut > amountOutMin, 'INSUFFICIENT_OUTPUT_AMOUNT');
// 进行交换
tokenIn.transferFrom(msg.sender, address(this), amountIn);
tokenOut.transfer(msg.sender, amountOut);
}
// 更新储备量
reserve0 = token0.balanceOf(address(this));
reserve1 = token1.balanceOf(address(this));
emit Swap(msg.sender, amountIn, address(tokenIn), amountOut, address(tokenOut));
}Swap 合约
SimpleSwap 的完整代码如下:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract SimpleSwap is ERC20 {
// 代币合约
IERC20 public token0;
IERC20 public token1;
// 代币储备量
uint public reserve0;
uint public reserve1;
// 事件
event Mint(address indexed sender, uint amount0, uint amount1);
event Burn(address indexed sender, uint amount0, uint amount1);
event Swap(
address indexed sender,
uint amountIn,
address tokenIn,
uint amountOut,
address tokenOut
);
// 构造器,初始化代币地址
constructor(IERC20 _token0, IERC20 _token1) ERC20("SimpleSwap", "SS") {
token0 = _token0;
token1 = _token1;
}
// 取两个数的最小值
function min(uint x, uint y) internal pure returns (uint z) {
z = x < y ? x : y;
}
// 计算平方根 babylonian method (https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method)
function sqrt(uint y) internal pure returns (uint z) {
if (y > 3) {
z = y;
uint x = y / 2 + 1;
while (x < z) {
z = x;
x = (y / x + x) / 2;
}
} else if (y != 0) {
z = 1;
}
}
// 添加流动性,转进代币,铸造LP
// 如果首次添加,铸造的LP数量 = sqrt(amount0 * amount1)
// 如果非首次,铸造的LP数量 = min(amount0/reserve0, amount1/reserve1)* totalSupply_LP
// @param amount0Desired 添加的token0数量
// @param amount1Desired 添加的token1数量
function addLiquidity(uint amount0Desired, uint amount1Desired) public returns(uint liquidity){
// 将添加的流动性转入Swap合约,需事先给Swap合约授权
token0.transferFrom(msg.sender, address(this), amount0Desired);
token1.transferFrom(msg.sender, address(this), amount1Desired);
// 计算添加的流动性
uint _totalSupply = totalSupply();
if (_totalSupply == 0) {
// 如果是第一次添加流动性,铸造 L = sqrt(x * y) 单位的LP(流动性提供者)代币
liquidity = sqrt(amount0Desired * amount1Desired);
} else {
// 如果不是第一次添加流动性,按添加代币的数量比例铸造LP,取两个代币更小的那个比例
liquidity = min(amount0Desired * _totalSupply / reserve0, amount1Desired * _totalSupply /reserve1);
}
// 检查铸造的LP数量
require(liquidity > 0, 'INSUFFICIENT_LIQUIDITY_MINTED');
// 更新储备量
reserve0 = token0.balanceOf(address(this));
reserve1 = token1.balanceOf(address(this));
// 给流动性提供者铸造LP代币,代表他们提供的流动性
_mint(msg.sender, liquidity);
emit Mint(msg.sender, amount0Desired, amount1Desired);
}
// 移除流动性,销毁LP,转出代币
// 转出数量 = (liquidity / totalSupply_LP) * reserve
// @param liquidity 移除的流动性数量
function removeLiquidity(uint liquidity) external returns (uint amount0, uint amount1) {
// 获取余额
uint balance0 = token0.balanceOf(address(this));
uint balance1 = token1.balanceOf(address(this));
// 按LP的比例计算要转出的代币数量
uint _totalSupply = totalSupply();
amount0 = liquidity * balance0 / _totalSupply;
amount1 = liquidity * balance1 / _totalSupply;
// 检查代币数量
require(amount0 > 0 && amount1 > 0, 'INSUFFICIENT_LIQUIDITY_BURNED');
// 销毁LP
_burn(msg.sender, liquidity);
// 转出代币
token0.transfer(msg.sender, amount0);
token1.transfer(msg.sender, amount1);
// 更新储备量
reserve0 = token0.balanceOf(address(this));
reserve1 = token1.balanceOf(address(this));
emit Burn(msg.sender, amount0, amount1);
}
// 给定一个资产的数量和代币对的储备,计算交换另一个代币的数量
// 由于乘积恒定
// 交换前: k = x * y
// 交换后: k = (x + delta_x) * (y + delta_y)
// 可得 delta_y = - delta_x * y / (x + delta_x)
// 正/负号代表转入/转出
function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) public pure returns (uint amountOut) {
require(amountIn > 0, 'INSUFFICIENT_AMOUNT');
require(reserveIn > 0 && reserveOut > 0, 'INSUFFICIENT_LIQUIDITY');
amountOut = amountIn * reserveOut / (reserveIn + amountIn);
}
// swap代币
// @param amountIn 用于交换的代币数量
// @param tokenIn 用于交换的代币合约地址
// @param amountOutMin 交换出另一种代币的最低数量
function swap(uint amountIn, IERC20 tokenIn, uint amountOutMin) external returns (uint amountOut, IERC20 tokenOut){
require(amountIn > 0, 'INSUFFICIENT_OUTPUT_AMOUNT');
require(tokenIn == token0 || tokenIn == token1, 'INVALID_TOKEN');
uint balance0 = token0.balanceOf(address(this));
uint balance1 = token1.balanceOf(address(this));
if(tokenIn == token0){
// 如果是token0交换token1
tokenOut = token1;
// 计算能交换出的token1数量
amountOut = getAmountOut(amountIn, balance0, balance1);
require(amountOut > amountOutMin, 'INSUFFICIENT_OUTPUT_AMOUNT');
// 进行交换
tokenIn.transferFrom(msg.sender, address(this), amountIn);
tokenOut.transfer(msg.sender, amountOut);
}else{
// 如果是token1交换token0
tokenOut = token0;
// 计算能交换出的token1数量
amountOut = getAmountOut(amountIn, balance1, balance0);
require(amountOut > amountOutMin, 'INSUFFICIENT_OUTPUT_AMOUNT');
// 进行交换
tokenIn.transferFrom(msg.sender, address(this), amountIn);
tokenOut.transfer(msg.sender, amountOut);
}
// 更新储备量
reserve0 = token0.balanceOf(address(this));
reserve1 = token1.balanceOf(address(this));
emit Swap(msg.sender, amountIn, address(tokenIn), amountOut, address(tokenOut));
}
}1.部署两个ERC20合约token0和token1

2.部署 SimpleSwap 合约,并将token0和token1的地址作为构造函数参数
3.调用两个ERC20代币的approve()函数,分别给 SimpleSwap 合约授权 1000 单位代币。

4.调用 SimpleSwap 合约的 addLiquidity() 函数给交易所添加流动性,token0 和 token1 分别添加 100 单位。
5.调用 SimpleSwap 合约的 balanceOf() 函数查看用户的LP份额,这里应该为 100。
6.调用 SimpleSwap 合约的 swap() 函数进行代币交易,用 100 单位的 token0
7.调用 SimpleSwap 合约的 reserve0 和 reserve1 函数查看合约中的代币储备粮,应为 200 和 50。上一步我们利用 100 单位的 token0 交换了 50 单位的 token 1
{/collapse-item}
评论 (0)