CoinRebate
Back to Guides
Uniswap

Углубленный разбор кода контракта Uniswap v2: Лао Лиек поможет вам понять основную архитектуру и ключевые методы

Углубленный анализ архитектуры смарт-контрактов Uniswap v2, от заводских контрактов до основных методов транзакций, поможет вам полностью понять основную логику DeFi и избежать ошибок.

Углубленный разбор кода контракта Uniswap v2: Лао Лиек поможет вам понять основную архитектуру и ключевые методы

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

先给 token0token1 排个序,确保 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):合约字节码从哪儿开始。因为 bytecodebytes 类型,ABI 编码时前 32 字节存长度,所以真正内容得从 bytecode+32 开始。
  • n=mload(bytecode):字节码总长度。mload 读前 32 字节,刚好就是长度。
  • s=salt:自定义的 salt,就是 token0token1 拼一起。

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

为了兼容闪电贷和不同代币的 transferswap 方法压根儿没 amountIn 参数,而是靠比较当前余额和缓存余额来算你转入了多少币。

因为最后会检查 k 值(扣掉 0.3% 手续费后),所以合约可以先把你要的币转出去。如果你没提前转币进来,那就相当于借币——这就是闪电贷的玩法。搞闪电贷的话,你得在 uniswapV2Call 里把借的币还上。

最后更新价格预言机的累计价格,再把缓存余额设为当前余额,一次交易就闭环了。


唠点实在的:理解这些底层代码,能帮你少踩很多坑。但如果你是刚入圈的小白,建议先从主流 CEX 玩起,比如用咱们的邀请码注册币安、OKX,手续费打折还安全。别一上来就硬刚合约,容易懵。

想省手续费?戳这里:币安永久折扣注册 | OKX 最高返佣入口。老韭菜的经验:工具用对了,赚钱才不累。

Похожие статьи

Share:𝕏✈️R

Comments (0)