Углубленный разбор кода контракта Uniswap v2: Лао Лиек поможет вам понять основную архитектуру и ключевые методы
Углубленный анализ архитектуры смарт-контрактов Uniswap v2, от заводских контрактов до основных методов транзакций, поможет вам полностью понять основную логику DeFi и избежать ошибок.
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 最高返佣入口。老韭菜的经验:工具用对了,赚钱才不累。
Похожие статьи
- Ежедневный краткий обзор рынка криптовалют: все последние тенденции ETF в финансировании и ликвидации. Лао Лиек учит вас, как избежать ловушек и заработать деньги.
- Советы по экономии денег от старого лука-порея в валютном кругу: научите вас, как использовать постоянные коды скидок при регулярном обмене, и комиссия за обработку будет напрямую снижена!
- Географический конфликт + двойной критический удар по макроданным! Старый Лик учит, как торговать в штормовом море, не переворачиваясь.