简介
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); }
|