In-depth dismantling of Uniswap v2 contract code: Lao Liek takes you to understand the core architecture and key methods
An in-depth analysis of the Uniswap v2 smart contract architecture, from factory contracts to core transaction methods, will help you thoroughly understand the underlying logic of DeFi and avoid pitfalls.
Uniswap v2 合约代码深度拆解:老韭菜带你读懂核心架构与关键方法
上回咱们聊了 Uniswap v2 白皮书,今天直接上硬货——合约代码。别慌,咱不搞逐行翻译那套,重点抓架构和核心方法。想细抠代码的,建议去翻以太坊官方的 Uniswap v2 代码走读,那里更全。
合约架构:core 和 periphery 是啥关系?
Uniswap v2 的合约分两大类:core 合约和 periphery 合约。
- core 合约:就干最基础的交易活儿,代码精简到 200 行左右。为啥这么抠?因为用户的钱都搁这儿,必须稳如老狗,少一行代码就少一个 bug 风险。
- periphery 合约:这是给咱们用户用的封装层,比如支持直接用 ETH 交易(自动转成 WETH)、搞多路径交换(一口气完成 A→B→C)。你在 app.uniswap.org 上操作,背后调的都是 periphery 合约,它再底层去戳 core 合约。
下面咱掰开揉碎了说几个主要合约:
uniswap-v2-core
- UniswapV2Factory:工厂合约,专门生 Pair 合约(还能设协议手续费接收地址)。
- UniswapV2Pair:交易对合约,定义了
swap/mint/burn这些核心方法,还集成了价格预言机。它本身是个 ERC20 合约,继承了UniswapV2ERC20。 - UniswapV2ERC20:实现 ERC20 标准,基础操作都在这儿。
uniswap-v2-periphery
- UniswapV2Router02:最新版路由合约,比老版本
UniswapV2Router01多了对FeeOnTransfer代币的支持。添加/移除流动性、代币兑换、ETH 换币这些常用功能,都是它提供的接口。 - UniswapV1Router01:旧版路由,现在基本没人用了,因为不支持
FeeOnTransferTokens。
uniswap-v2-core 核心合约详解
UniswapV2Factory:工厂怎么造交易对?
工厂合约里最关键的 createPair 方法,咱们瞅一眼代码:
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
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);
}
先给 token0 和 token1 排个序,确保 token0 地址字面值更小。接着用 assembly + create2 创建合约——这玩意儿是 Solidity 里的底层操作,直接玩 EVM,够硬核。
上回白皮书里提过,create2 主要是为了生成确定性的交易对地址。你给俩代币地址,就能直接算出来 pair 地址,不用再去链上查。
CREATE2 来自 EIP-1014,核心是那个 salt 值。同一个交易对,salt 得一样,所以这里直接用排序后的代币地址当 salt。这样不管你先传 A 还是 B,都能算出同一个 pair(A,B)。
其实现在新版 EVM 已经支持给 new 方法传 salt 了,比如:
pair = new UniswapV2Pair{salt: salt}();
但 Uniswap v2 开发那会儿还没这功能,所以用了 assembly create2。
根据 Yul 规范,create2 的参数这么理解:
v=0:往新合约里打多少 ETH(单位 wei),这儿是 0。p=add(bytecode, 32):合约字节码从哪儿开始。因为bytecode是bytes类型,ABI 编码时前 32 字节存长度,所以真正内容得从bytecode+32开始。n=mload(bytecode):字节码总长度。mload读前 32 字节,刚好就是长度。s=salt:自定义的salt,就是token0和token1拼一起。
UniswapV2ERC20:permit 玩转离线签名
这合约主要实现 ERC20 标准,代码简单。重点说说 permit 方法:
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);
}
permit 干的就是白皮书里说的“元交易”功能。EIP-712 定了离线签名的规矩,用户签个名,授权某个合约在截止时间前能动用一定数量的流动性代币。应用拿着签名和生成的 v, r, s,调用 permit 就能拿到授权。ecrecover 还原签名地址,验证通过就批了。
UniswapV2Pair:mint、burn、swap 三大核心
Pair 合约的核心就三个方法:mint(加流动性)、burn(撤流动性)、swap(换币)。
mint:加流动性怎么算?
// this low-level function should be called from a contract which performs important safety checks
function mint(address to) external lock returns (uint liquidity) {
(uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
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; // gas savings, must be defined here since totalSupply can update in _mintFee
if (_totalSupply == 0) {
liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);
_mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens
} 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); // reserve0 and reserve1 are up-to-date
emit Mint(msg.sender, amount0, amount1);
}
先拿缓存余额(防预言机攻击用),再算实际转入的代币量。_mintFee 处理协议手续费——白皮书里有公式,这儿不展开了。
如果是第一个来提供流动性的,按根号 xy 发代币,但得永久锁住 MINIMUM_LIQUIDITY(1000 wei),防归零。不是第一次的话,就按价值比例铸币。
burn:撤流动性咋分币?
// this low-level function should be called from a contract which performs important safety checks
function burn(address to) external lock returns (uint amount0, uint amount1) {
(uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
address _token0 = token0; // gas savings
address _token1 = token1; // gas savings
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; // gas savings, must be defined here since totalSupply can update in _mintFee
amount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution
amount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distribution
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); // reserve0 and reserve1 are up-to-date
emit Burn(msg.sender, amount0, amount1, to);
}
跟 mint 类似,先算手续费。撤流动性时,按你销毁的流动性代币占比,分对应的两种代币。简单说,你占多少份,就拿多少币。
swap:换币和闪电贷的秘密
// this low-level function should be called from a contract which performs important safety checks
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(); // gas savings
require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');
uint balance0;
uint balance1;
{ // scope for _token{0,1}, avoids stack too deep errors
address _token0 = token0;
address _token1 = token1;
require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');
if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens
if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens
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');
{ // scope for reserve{0,1}Adjusted, avoids stack too deep errors
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);
}
为了兼容闪电贷和不同代币的 transfer,swap 方法压根儿没 amountIn 参数,而是靠比较当前余额和缓存余额来算你转入了多少币。
因为最后会检查 k 值(扣掉 0.3% 手续费后),所以合约可以先把你要的币转出去。如果你没提前转币进来,那就相当于借币——这就是闪电贷的玩法。搞闪电贷的话,你得在 uniswapV2Call 里把借的币还上。
最后更新价格预言机的累计价格,再把缓存余额设为当前余额,一次交易就闭环了。
唠点实在的:理解这些底层代码,能帮你少踩很多坑。但如果你是刚入圈的小白,建议先从主流 CEX 玩起,比如用咱们的邀请码注册币安、OKX,手续费打折还安全。别一上来就硬刚合约,容易懵。
想省手续费?戳这里:币安永久折扣注册 | OKX 最高返佣入口。老韭菜的经验:工具用对了,赚钱才不累。
Related Articles
- Daily quick overview of the crypto market: All the latest ETF trends in financing and liquidation, Lao Liek teaches you how to avoid pitfalls and make money
- Money-saving tips from an old leek in the currency circle: teach you how to use regular exchange permanent discount codes, and the handling fees will be directly broken!
- Geographical conflict + macro data double critical hit! Old Leek teaches you how to trade in stormy seas without overturning