cover

Uniswap 中的奇技淫巧

A

https://ssimg.frontenduse.top/article/2020/10/11/07e91ad926195278aa60444e27d5e69e.jpg

手续费

关于 Uniswap 的手续费,在之前的 Introduction to DeFi | Ch.2 Uniswap 有过介绍。简单说来就是第二类 可分润代币 模型,每次交易的时候,都扣除一部分,这样整个池子里的资金变多(K 单调递增),但 LP Token 却没有增加,从而完成了分红。

    // given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset
    function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) {
        require(amountIn > 0, 'UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT');
        require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
        uint amountInWithFee = amountIn.mul(997);
        uint numerator = amountInWithFee.mul(reserveOut);
        uint denominator = reserveIn.mul(1000).add(amountInWithFee);
        amountOut = numerator / denominator;
    }

    // given an output amount of an asset and pair reserves, returns a required input amount of the other asset
    function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) internal pure returns (uint amountIn) {
        require(amountOut > 0, 'UniswapV2Library: INSUFFICIENT_OUTPUT_AMOUNT');
        require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
        uint numerator = reserveIn.mul(amountOut).mul(1000);
        uint denominator = reserveOut.sub(amountOut).mul(997);
        amountIn = (numerator / denominator).add(1);
    }

不过,Uniswap 设计之初还有一类手续费 —— Protocol Fee,原本计划中,0.3% 的交易手续费中的 1/6,也就是 0.05% 将会作为协议的利润。。。

也就是这个利润,让 Sushiswap 什么的有了可乘之机。。然而目前无论是 Sushiswap 还是 Uniswap ,现在都没有开启这个开关。

    // if fee is on, mint liquidity equivalent to 1/6th of the growth in sqrt(k)
    function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) {
        address feeTo = IUniswapV2Factory(factory).feeTo();
        feeOn = feeTo != address(0);
        uint _kLast = kLast; // gas savings
        if (feeOn) {
            if (_kLast != 0) {
                uint rootK = Math.sqrt(uint(_reserve0).mul(_reserve1));
                uint rootKLast = Math.sqrt(_kLast);
                if (rootK > rootKLast) {
                    uint numerator = totalSupply.mul(rootK.sub(rootKLast));
                    uint denominator = rootK.mul(5).add(rootKLast);
                    uint liquidity = numerator / denominator;
                    if (liquidity > 0) _mint(feeTo, liquidity);
                }
            }
        } else if (_kLast != 0) {
            kLast = 0;
        }
    }

这段代码记录了两次铸币操作之间 sqrt(K) 的差值,以计算出用户 stake 期间所产生的利润,并从中拿出 1/6 给到 feeTo 账号,如果 feeTo 是合约则可以完成对 UNI token 的分红操作,不过面对这一把 LP Token 计算起来大概会烧不少 gas。。

具体上面代码里的 5 是怎么得来的,需要用纸笔算一下,白皮书里有提到。

https://ssimg.frontenduse.top/article/2020/10/11/e69a2c65b4b502f233d1ca761900d504.png

工厂合约

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

Uniswap 工厂合约最重要的功能就是创建交易对了。。。
不过这段代码初看起来并不是很好丽洁。。。。。

原因是里面用到了底层 Opcode Create2 。。。

https://docs.openzeppelin.com/cli/2.8/deploying-with-create2

https://solidity.readthedocs.io/en/v0.7.0/control-structures.html#creating-contracts-via-new

为什么我们不用更高层的 new() 方法把合约 new() 出来呢?

原因是 new() 的话创建出的合约地址是和 nouce 有关的,而用底层的 Create2 的话可以自定义 salt 去替换掉这里的 nouce。。。换句话说,每个交易对的地址,实际上不是存出来的,而是预先就可以计算出来的。。
 
https://github.com/Uniswap/uniswap-v2-periphery/blob/master/contracts/libraries/UniswapV2Library.sol

    // calculates the CREATE2 address for a pair without making any external calls
    function pairFor(address factory, address tokenA, address tokenB) internal pure returns (address pair) {
        (address token0, address token1) = sortTokens(tokenA, tokenB);
        pair = address(uint(keccak256(abi.encodePacked(
                hex'ff',
                factory,
                keccak256(abi.encodePacked(token0, token1)),
                hex'96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f' // init code hash
            ))));
    }

这样不仅在合约中,前端也可以直接使用这些地址了。

精度控制

Solidity 中不像隔壁 EOS 中的 cpp 支持浮点数,因为合约必须要逻辑可以被预测,因而一般使用大整数进行模拟,不可避免的会产生精度误差。
Uniswap 中的很多代码如果你读起来很奇怪的话,八成不是为了省 gas,就是在控制精度。。

其中精度最爆炸的,大概就是下面这一段牛顿迭代求 sqrt 了。

https://github.com/Uniswap/uniswap-v2-core/blob/1f563ce23b6dbe972d667c79058d0299e6f10ff6/contracts/libraries/Math.sol

    // babylonian method (https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method)
    function sqrt(uint y) internal pure returns (uint z) {
        if (y > 3) {
            z = y;
            uint x = y / 2 + 1;
            while (x < z) {
                z = x;
                x = (y / x + x) / 2;
            }
        } else if (y != 0) {
            z = 1;
        }
    }
}

这段代码的结果是求出最大的正整数 z,满足 z^2 <= y。当两边的 token 数量相差特别多时,比如两边的 decimal 本来就不一样,USDT/ETH 这种交易对时,这里的误差就会非常明显。。

本文发布于瞬matataki, 本文使用 知识共享 署名-非商业性使用-禁止演绎 4.0 协议 请遵守协议许可进行转载

免责声明:本文由用户「小岛美奈子」上传发布,内容为作者独立观点。不代表瞬Matataki立场,不构成投资建议,请谨慎对待。

Loading...
Price:
暂无价格
简 介:

Nothing

已持有:

0

喜欢就打赏Fan票吧~

avatar
0/500
评论0 打赏0