Uniswap V2 —— Core 解析

简介

Uniswap v2 是去中心化交易所(DEX)发展的关键里程碑,它基于恒定乘积做市商(x × y = k)模型,实现了任意 ERC-20 代币间的自动兑换,无需中心化撮合或订单簿。

Uniswap V2 是去中心化交易所(DEX)的经典实现,其核心合约包含三个主要部分:

  • UniswapV2Factory: 工厂合约,用于创建和管理交易对
  • UniswapV2ERC20: LP Token 的基础实现
  • UniswapV2Pair: 交易对合约,实现 AMM 核心逻辑

UniswapV2Factory 合约解析

UniswapV2Factory工厂合约,主要功能是创建新的交易对、管理交易对地址映射以及设置协议手续费接收地址。

1
2
3
4
5
6
7
8
9
10
// 手续费接收地址
address public feeTo;
address public feeToSetter;

// 交易对映射
mapping(address => mapping(address => address)) public getPair;
address[] public allPairs;

// 交易对创建事件
event PairCreated(address indexed token0, address indexed token1, address pair, uint);

Uniswap 工厂合约的核心函数是 createPair(),用于为两种 ERC-20 代币创建新的交易对。该函数通过 CREATE2 指令部署 UniswapV2Pair 合约,使交易对地址在部署前即可根据固定字节码和代币组合的哈希(salt)精确计算出来,从而保证同一对代币始终对应唯一的流动性池地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function createPair(address tokenA, address tokenB) external returns (address pair) {

// 确保两种代币不是同一个地址,否则无法构成交易对
require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');

// 交易对按地址从小到大排序
(address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);

// 防止使用空地址并确保这对代币交易池还没被创建过
require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');
require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // single check is sufficient

// 使用 CREATE2 部署新合约
bytes memory bytecode = type(UniswapV2Pair).creationCode;
bytes32 salt = keccak256(abi.encodePacked(token0, token1));
assembly {
pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
}

// 初始化交易对
IUniswapV2Pair(pair).initialize(token0, token1);

// 更新映射
getPair[token0][token1] = pair;
getPair[token1][token0] = pair; // populate mapping in the reverse direction
allPairs.push(pair);

emit PairCreated(token0, token1, pair, allPairs.length);
}

UniswapV2ERC20 合约解析

UniswapV2ERC20流动性代币合约,也称为 LP Token,继承标准 ERC20 接口,并添加了一个 permit() 函数。

代币实际名称为 Uniswap V2,简称为 UNI-V2

1
2
3
4
// 基础代币信息
string public constant name = 'Uniswap V2';
string public constant symbol = 'UNI-V2';
uint8 public constant decimals = 18;

代币的总量 totalSupply 最初为 0,可通过调用 _mint() 函数铸造出来,还可通过调用 _burn() 进行销毁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 状态变量
uint public totalSupply;
mapping(address => uint) public balanceOf;
mapping(address => mapping(address => uint)) public allowance;

// 铸造代币
function _mint(address to, uint value) internal {
totalSupply = totalSupply.add(value);
balanceOf[to] = balanceOf[to].add(value);
emit Transfer(address(0), to, value);
}

// 销毁代币
function _burn(address from, uint value) internal {
balanceOf[from] = balanceOf[from].sub(value);
totalSupply = totalSupply.sub(value);
emit Transfer(from, address(0), value);
}

UniswapV2ERC20 还提供了一个 permit() 函数, 函数通过链下签名实现代币授权,让用户无需发送交易就能完成 approve,从而节省 gas 并简化交互。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 用来区分不同合约和链,防止跨合约复用签名(replay attack)
bytes32 public DOMAIN_SEPARATOR;
// keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
bytes32 public constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;
// 防止同一签名被重复使用。每次成功调用 permit,都会自增
mapping(address => uint) public nonces;

function permit(
address owner,
address spender,
uint value,
uint deadline,
uint8 v,
bytes32 r,
bytes32 s
) external {
require(deadline >= block.timestamp, 'UniswapV2: EXPIRED');
bytes32 digest = keccak256(
abi.encodePacked(
'\x19\x01',
DOMAIN_SEPARATOR,
keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline))
)
);
address recoveredAddress = ecrecover(digest, v, r, s);
require(recoveredAddress != address(0) && recoveredAddress == owner, 'UniswapV2: INVALID_SIGNATURE');
_approve(owner, spender, value);
}

UniswapV2Pair 合约解析

UniswapV2Pair 是 AMM 的核心,管理交易对的流动性和交易逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 代币地址
address public token0;
address public token1;

// 储备量
uint112 private reserve0;
uint112 private reserve1;
uint32 private blockTimestampLast;

// 价格累积器(用于 TWAP)
uint public price0CumulativeLast;
uint public price1CumulativeLast;

// 常量 k 的最后值
uint public kLast;

// 最小流动性(永久锁定)
uint public constant MINIMUM_LIQUIDITY = 10**3;

添加流动性(mint)

假设当前流动性池子是新的还没有添加过流动性。此时 Tim 成为第一个 LP 并且往池子里存入:

1
2
3
4
token0 = ETH
token1 = USDC
amount0 = 10 ETH
amount1 = 20,000 USDC

此时,_totalSupply == 0,所以这是 首次添加流动性

1
2
liquidity = sqrt(amount0 * amount1) - MINIMUM_LIQUIDITY
liquidity = sqrt(10 * 20000) = sqrt(200000) ≈ 447 LP Token

接下来会给 Tim 铸造约 447 个 LP Token,锁定一份极小的 MINIMUM_LIQUIDITY(比如 1000 wei),防止池子被完全抽空,然后更新池子储备,并触发事件 Mint(Tim, 10 ETH, 20000 USDC)

1
2
reserve0 = 10 ETH
reserve1 = 20000 USDC

此时池子的恒等式:

1
x * y = 10 * 20000 = 200000
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function mint(address to) external lock 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.sub(_reserve0);
uint amount1 = balance1.sub(_reserve1);

bool feeOn = _mintFee(_reserve0, _reserve1);
uint _totalSupply = totalSupply;
// 首次添加流动性
if (_totalSupply == 0) {
liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);
_mint(address(0), MINIMUM_LIQUIDITY); // 永久锁定最小流动性
} else {
// 后续添加流动性
liquidity = Math.min(
amount0.mul(_totalSupply) / _reserve0,
amount1.mul(_totalSupply) / _reserve1
);
}
require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');
_mint(to, liquidity);

_update(balance0, balance1, _reserve0, _reserve1);
if (feeOn) kLast = uint(reserve0).mul(reserve1);
emit Mint(msg.sender, amount0, amount1);
}

移除流动性(burn)

不久后,Tim 想退出 50% 几天后,Alice 想退出 50% 的流动性(223.5 LP Token)

1
2
amount0 = liquidity * balance0 / totalSupply
amount1 = liquidity * balance1 / totalSupply

代入数据(假设储备未变化):

1
2
amount0 = 223.5 * 10 / 447 = 5 ETH
amount1 = 223.5 * 20000 / 447 = 10000 USDC

合约执行 _burn() 销毁她的 LP Token,把 5 ETH 和 10,000 USDC 发送回给 Alice 并更新池子储备,最后触发事件 Burn(Tim, 5 ETH, 10000 USDC, Tim)

1
2
reserve0 = 5 ETH
reserve1 = 10000 USDC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function burn(address to) external lock returns (uint amount0, uint amount1) {
(uint112 _reserve0, uint112 _reserve1,) = getReserves();
address _token0 = token0;
address _token1 = token1;
uint balance0 = IERC20(_token0).balanceOf(address(this));
uint balance1 = IERC20(_token1).balanceOf(address(this));
uint liquidity = balanceOf[address(this)];

bool feeOn = _mintFee(_reserve0, _reserve1);
uint _totalSupply = totalSupply;
// 按比例计算应得代币
amount0 = liquidity.mul(balance0) / _totalSupply;
amount1 = liquidity.mul(balance1) / _totalSupply;
require(amount0 > 0 && amount1 > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED');

_burn(address(this), liquidity);
_safeTransfer(_token0, to, amount0);
_safeTransfer(_token1, to, amount1);

balance0 = IERC20(_token0).balanceOf(address(this));
balance1 = IERC20(_token1).balanceOf(address(this));

_update(balance0, balance1, _reserve0, _reserve1);
if (feeOn) kLast = uint(reserve0).mul(reserve1);
emit Burn(msg.sender, amount0, amount1, to);
}

交换(swap)

现在 Mia 想用 1 ETH 兑换 USDC

1
swap(amount0Out = 0, amount1Out = ?, to = Mia)

池子储备当前为:

1
2
reserve0 = 5 ETH
reserve1 = 10000 USDC

恒等式要求交易后仍然满足:

1
2
3
4
(x + Δx) * (y - Δy) = k

# 代入数值
(5 + 1) * (10000 - Δy) = 50000 → Δy = 1666.67 USDC

扣除 0.3% 手续费后,Mia 实际拿到约 1661 USDC。此时更新池子储备并触发事件 Swap(Mia, 1 ETH in, 1661 USDC out)

1
2
reserve0 = 6 ETH
reserve1 = 8339 USDC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {
require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');
(uint112 _reserve0, uint112 _reserve1,) = getReserves();
require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');

uint balance0;
uint balance1;
{
address _token0 = token0;
address _token1 = token1;
require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');
if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out);
if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out);
if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);
balance0 = IERC20(_token0).balanceOf(address(this));
balance1 = IERC20(_token1).balanceOf(address(this));
}

// 计算输入金额
uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;
require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT');

// 验证 K 值(扣除 0.3% 手续费)
{
uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));
uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));
require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K');
}

_update(balance0, balance1, _reserve0, _reserve1);
emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
}