CoinRebate
Back to Guides
Uniswap

تفكيك متعمق لرمز عقد Uniswap v2: يأخذك Lao Liek إلى فهم البنية الأساسية والأساليب الرئيسية

سيساعدك التحليل المتعمق لبنية العقود الذكية Uniswap v2، بدءًا من عقود المصانع وحتى طرق المعاملات الأساسية، على فهم المنطق الأساسي لـ DeFi تمامًا وتجنب المخاطر.

تفكيك متعمق لرمز عقد Uniswap v2: يأخذك Lao Liek إلى فهم البنية الأساسية والأساليب الرئيسية

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)