Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Uniswap V2 源码学习 (三). 手续费和交易池估值

Uniswap V2 源码学习 (三). 手续费和交易池估值

作者头像
Tiny熊
发布于 2022-05-25 08:00:39
发布于 2022-05-25 08:00:39
1.4K00
代码可运行
举报
运行总次数:0
代码可运行

本文作者:tonyh[1]

前面我们已经大致了解了 uniswap 的交易算法[2], 今天我们一起看看 Uniswap 手续费是怎么计算的

可能很多读者认为手续费计算并不重要, 因为手续费对于用户而言就是扣掉千分之三的 input 而已, 没什么难度. 但是我在阅读 pair 的 mintFee 函数时, 一开始有些看不懂, 琢磨了两三天才把它的逻辑搞明白, 所以今天就跟大家分享一下心得体会, 实际上平台的协议手续费收取算法是比较有意思的内容, 我们通过对手续费计算过程的学习, 可以窥探系统设计者背后的设计思路, 以及代码实现中用到的 gas 优化技巧

手续费的产生

假设 swap 前两个代币数量为 A1, B2, swap 后为 A2, B2

前面已经有介绍, 在不收手续费的情况下, swap 前后满足 A * B = K 不变.

但是在收取手续费的情况下, 实际的有效输入是 effectiveInput = amountIn _ 0.997, 这部分有效输入 effectiveInput 进行 swap 交易后满足交易后的 A2' _ B2' = A1 * B1

但是实际的输入量是 effectiveInput _ 1000 / 997, 这样池子里真实的 A2 _ B2 将会大于原有的 A * B, 池子的财富增加了 在 LP 代币总量不变的情况下, 每个 LP 持有人分享到的财富就会增加, 这个增量就是手续费

下面是 core/UniswapV2Pair.sol 中的 swap 的代码片段:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {
    ...
    { // 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');
    }
    ...
}

上面的代码中, 池子要求 扣掉 3/1000 的输入后, 仍然满足 A2'B2' >= A1B1 而真实的 A2, B2 中至少有一个满足 A2 = A2' + input _ 3/1000 或者 B2 = B2' + input _ 3/1000, 因此 A2B2 > A1B2, 池子的总财富得到增加

做市商(LP)的手续费 下面的讨论中, 用 lp 表示部分 pair 代币, lp_supply 表示 pair 代币总量 做市商的手续费不需要计算, 在池子财富增加的情况下, lp 代币总量不变, 那么每个做市商持有的 lp 将会分到更多的 tokenA 和 tokenB, 在 removeLiquidity 时, 根据 lp/lp_supply 这个比值获得更多的 tokenA 和 tokenB

平台收取的协议手续费 平台收取的手续费占手续费产生的财富增量的 1/6, 如果开启协议手续费, 那么做市商收取 2.5/1000, 平台收取 0.5/1000

平台手续费是通过定向增发 lp 给项目方的 feeTo 账户实现的

那么问题来了, 这个增发量应该是多少呢?

相信很多同学在看 _mintFee 这个函数的时候, 和我开始拿到代码一样没有看明白,

所以这是我们今天介绍的重点.

首先我们假设最简单的情况: 在一系列 swap 后, 池子的财富 tokenA 和 tokenB 是等比例增加的, 例如: A 和 B 都增加了 12%, 那么很简单, feeTo 账户将会得到两个代币增量的 1/6 , 即总量的 2/112, 因此发行的 lp = lp_supply * 2/110, 这样增发之后 , 平台方持有的 lp/新的 lp_supply 刚好等于 2/112

但是如果 A 和 B 不是等比例增加, 应该发多少 lp 给项目方呢? 例如 A 增加了 10%, B 增加了 20%, 那么应该分配多少的 tokenA 和 tokenB 给项目方? 15%, 还是 12%, or 18% ??? 注意 lp 增发的分配方式下, 项目方得到 tokenA 和 tokenB 的数量, 相对于库存 A, B 应该是相同的比例, 不可能 得到 10%/6 的 tokenA, 20% /6 的 tokenB, 只能是 x% 的 tokenA 和 tokenB

那么这个 x% 应该是多少?

也就是说, 当池子中 A 和 B 不是等比例增加的时候, 应该认为池子的财富增加了多少 ? 假设在状态 1, 池子中的代币余额为 A1, B1, 此时规定池子的财富是 w1 经过一系列交换后, 到达状态 2, 此时代币余额为 A2, B2, (假设 A2/A1 != B2/B1, 两个代币不是等比例增加) 那么我们认为池子的财富 w2 是多少?

这个问题对于手续费计算很重要, 因为只有定义了池子中财富度量的标准, 我们才能计算出财富增加的比例, 从而 mint 正确的 lp 代币 作为协议手续费

交易池的财富度量: rootK

下面的讨论中, 我们用 A, B 表示交易池的两个代币数量, w 表示交易池的财富值

现在我们需要制定一个度量标准 f, 并规定交易池的财富值 w = f(A,B)

只有制定了这样的度量标准, 我们才能计算出任意时刻交易池的财富值 w, 进而计算出需要 mint 给项目方的 lp : lp = lp*supply * (w2 - w1) _ (1/6) / w1

可能阅读过 Uniswap 源代码的同学可能已经知道了, f 的定义方法, 就是取两个代币数量的几何平均值 sqrt(A*B) 这个值我们将他命名为 rootK

但是为什么是 rootK? 为什么是几何平均值, 而不是代数平均值 (A+B)/2, 或者为什么不直接取 A*B ?

接下来我们将会证明, 为什么 rootK 可以作为, 而且必须是 rootK(或者 rootK * 常系数)作为交易池的财富度量.

首先我们规定两条公理:

下面是推导过程:

问题: 假设交易池在初始状态 A1, B1 的时候, 规定财富是 w1, 经过一系列 swap 交易后, 交易池的代币数量为 A2, B2, 且 A2/A1 != B2/B1 求此状态下财富值 w2 = ?

证明: 现在我们邀请一名交易者来做 swap, 让他用 A2/A1 和 B2/B1 中比值较小的币种, 换取比值较大的币种, 确保交易之后的 A3, B3 刚好满足 A3/A1 = B3/B1, 这笔交易不收手续费

根据前面的第二条假设前提 , 状态 2 到状态 3 财富不变. w3 = w2

同时又可以根据第一条等比例原则, 得出等式: w3 / w1 = A3 / A1

但是由于不知道 A3 的值, 还不能直接算出 w3 不过由于 A3/A1 = B3/B1, 所以可以得到 w3/w1 = sqrt[ (A3B3) / (A1B1) ]

将 w3 换成 w2, A3B3 换成 A2B2, 就可以计算出了 w2 的值: w2/w1 = sqrt(A2B2) / sqrt(A1B2)

可以看到, 财富值 w 的度量要满足前面两个前提条件, 只能是 sqrt(A*B)的常数倍, 为了简化, 这个常数就取为 1.

因此, Uniswap 定义的交易池财富值度量值 w = sqrt(A*B), 这个值在代码里的变量名为 rootK

  • 如果 A, B 是等比例增加, 那么财富值也按照相同的比例增加: 即 w2/w1 = A2/A1 , 同时 w2/w1 = B2/B1 这一点很好理解, 只有成比例增加, 才能确保 lp 持有人分到的财富是公平的, 否则先撤销流动性和后撤销流动性得到的代币不相等.

kLast 变量 和协议手续费的算法

平台的协议手续费, 在每次 addLiquidity 和 removeLiquidity 之前征收.

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 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;
    }
}

可能很多朋友有疑问, 上面的代码中, mint 的数量 为什么是 (rootK - rootKLast)/(5 倍 rootK + 1 倍 rootKLast) 而不是 (rootK - rootKLast)/6 倍 rootKLast 呢? 别急, 我们通过下图, 看看要增发多少 lp, 使得 lp 分到的财富刚好是增量的 1/6:

如上图所示, 为了得到新增财富的 1/6, 需要增发的 lp 应该满足: lp/lp_supply = (∆/6) / [(∆5/6) + rootKLast ], 这里 ∆ = rootK - rootKLast 解出 lp = lp_supply * ∆ / (5rootK + rootKLast), 与源代码的计算方法一致, 证实了 Uniswap 收取的协议手续费就是总手续费的 1/6.

手续费的记录和结算:

为了记录手续费, UniswapV2Pair 使用了一个变量 kLast, 用来记录最后一次结算后的 K 值 (reserve0 * reserve1)

我们记录手续费真正需要的是 rootK - rootKLast 但是为什么记录的是 kLast 而不是 rootKLast 呢? 为了节省 gas,

只有当程序检查到 当前 K > kLast 时 , 才会执行开方运算, 计算出 rootK - rootKLast 如果当前 K 和之前的 kLast 一致, 那就没必要开方.

所以记录手续费虽然真正关注的是 rootK 的差值, 但是保存的变量是 kLast.

需要注意的一点是, K 值不仅包含了手续费产生的财富增量, 他还会受到 addLiquidity 和 removeLiquidity 的影响, 如果上次记录 kLast 后,发生了添加/撤销流动性事件, 那么交易池的财富增量包含了添加流动性的增量,手续费产生的增量, 同时还会受到撤销流动性的抵消, 这样就无法正确的追踪 "因手续费而产生的财富增量了", 那要怎么解决呢.

办法就是在任何添加/撤销流动性之前把迄今为止的手续费结清, 在流动性操作结束后重新开始记录 K 值, 这样我们每次结算时 rootK - rootKLat 就不存在加减流动性产生的变化了, 全部都是手续费产生的增量.

因此, 可以看到在 Pair 合约的 mint() 和 burn()中, 每次添加/撤销流动性之前, 都会调用 _mintFee() 结算协议手续费, 而在函数的最后都有一条语句重新记录 kLast:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// this low-level function should be called from a contract which performs important safety checks
function mint(address to) external lock returns (uint liquidity) {
    ...
    /****************************************************************************
     *  流动性操作之前, 结清迄今为止产生的手续费
     ****************************************************************************/
    bool feeOn = _mintFee(_reserve0, _reserve1);

    ...
    //按照输入的 balance-reserve, mint 代币给 to(即项目方钱包)
    ...

    _update(balance0, balance1, _reserve0, _reserve1);

    /****************************************************************************
     *  流动性操作结束后, 重新记录 kLast, 使得rootK - rootKLast 始终不受增减流动性的影响
     ****************************************************************************/
    if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date
    emit Mint(msg.sender, amount0, amount1);
}

那么为什么不使用更直观的方式记录手续费呢? 比如下面我们用一个变量记录 "迄今为止未结算手续费"

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
uint256 public cumulatedFee;

function swap(xxx, xxx, xxx ...){

    uint256 rootKBefore = sqrt(reserve0 * reserve1);

    ...
    //执行swap交易
    ...

    _update(balance0, balance1, _reserve0, _reserve1);

    cumulatedFee += ( sqrt(reserve0 * reserve1) - rootKBefore );

}

虽然上面的代码可以更直观的方式实现手续费结算, 但是, 每次 swap 需要读取和存储一次 cumulatedFee, 计算两次开平方, 通常 swap 执行的次数要远大于添加/撤销流动性的次数, 从 gas 经济性考虑, 使用前面的 kLast 方法更好.

好的, 今天我们一起分析了 Uniswap 的手续费计算方法和代码实现细节, 相信大家应该对 Uniswap 的手续费算法有了更加深入的理解, 我们下期再见!

作者 mail:star4evar@gmail.com

参考资料

[1]

tonyh: https://learnblockchain.cn/people/8619

[2]

uniswap 的交易算法: https://learnblockchain.cn/article/3952

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-05-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 深入浅出区块链技术 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Uniswap V2 学习笔记2. 交易算法
大家好, 今天继续分享 Uniswap V2 的学习心得, 今天的内容是 Uniswap[2]的交易算法
Tiny熊
2022/05/25
1.9K0
UniswapV2协议解析
本篇文章主要对Uniswap V2协议的工作原理、项目构成、源码实现等部分进行详细解读。
Al1ex
2021/07/21
3.6K2
UniswapV2协议解析
剖析DeFi交易产品之Uniswap:V2上篇
在 DeFi 赛道中,DEX 无疑是最核心的一块,而 Uniswap 又是整个 DEX 领域中的龙头,如 SushiSwap、PancakeSwap 等都是 Fork 了 Uniswap 的。虽然网上关于 Uniswap 的文章已经挺多,但大多都只是从机制上进行介绍,很少谈及具体实现,也存在一些问题没能解答,比如:手续费分配是如何实现的?最优路径是如何得出的?TWAP 怎么用?注入流动性时返回多少 LP Token 是如何计算的?因此,我从代码层面去剖析 Uniswap,搞清楚这些问题,同时也对 Uniswap 从整体到细节都有所理解。
Keegan小钢
2021/08/20
1.7K0
pancakeSwapV2交易手续费实现原理
在 Uniswap V2 中,会对每次交易收取手续费。具体来说,Uniswap V2 对每笔交易收取 0.3% 的手续费。这些手续费的分配如下:
终有链响
2024/12/11
1590
IDO预售代币合约系统开发技术说明及源码分析
Core逻辑实现了单个交易对的逻辑。通过UniswapV2Factory可以创建一个个Pair(交易池)。每个具体实现逻辑在UniswapV2Pair中。
DD_MrsFu123
2022/08/02
7780
IDO代币预售合约系统开发技术详细
Uniswap智能合约代码由两个github项目组成。一个是core,一个是periphery。
DD_MrsFu123
2022/08/02
7760
剖析DeFi交易产品之Uniswap:V2下篇
上篇我们主要讲了 UniswapV2 整体分为了哪些项目,并重点讲解了 uniswap-v2-core 的核心代码实现;中篇主要对 uniswap-v2-periphery 的路由合约实现进行了剖析;现在剩下 V2 系列的最后一篇,我会介绍剩下的一些内容,主要包括:TWAP、FlashSwap、质押挖矿。
Keegan小钢
2021/10/20
1.7K0
剖析DeFi交易产品之Uniswap:V2下篇
SushiSwap协议分析
SushiSwap是一个分叉自Uniswap的去中心化交易协议,它在交易模式上延续了Uniswap的核心设计——AMM(自动做市商)模型,但与Uniswap不同之处在于SushiSwap增加了经济奖励模型,SushiSwap交易手续费为0.3%,其中0.25%直接分给发给流动性提供,0.05%买成SUSHI并分配给Sushi代币持有者(Uniswap是通过开关模式决定是否将0.05%的手续费给开发者团队),Sushi在每次分发时会预留10%给项目未来开发迭代及安全审计等。
Al1ex
2021/07/21
2.2K0
SushiSwap协议分析
LP流动性挖矿系统开发(详细讲解)丨LP流动性质押开发(逻辑分析)
core偏核心逻辑,单个swap的逻辑。periphery偏外围服务,一个个swap的基础上构建服务。单个swap,两种代币形成的交易对,俗称“池子”。每个交易对有一些基本属性:reserve0/reserve1以及total supply。reserve0/reserve1是交易对的两种代币的储存量。total supply是当前流动性代币的总量。每个交易对都对应一个流动性代币(LPT-liquidity provider token)。简单的说,LPT记录了所有流动性提供者的贡献。所有流动性代币的总和就是total supply。Uniswap协议的思想是reserve0*reserve1的乘积不变。
系统_I8O28578624
2022/12/09
1K0
剖析DeFi交易产品之Uniswap:V2中篇
上篇我们主要讲了 UniswapV2 整体分为了哪些项目,并重点讲解了 uniswap-v2-core 的核心代码实现。这篇我们来看看 uniswap-v2-periphery。
Keegan小钢
2021/10/08
2.7K0
UniSwap V3协议浅析(下)
NoDelegateCall合约的主要功能是提供一个修饰器来阻止对使用修饰器修饰过的函数进行delegatecall调用,合约代码如下:
Al1ex
2021/07/21
2.5K0
UniSwap V3协议浅析(下)
一文讲清-NFT市场新秀SudoSwap的AMM机制-创新挑战与局限
NFT交易市场的近期颓势频现,整个市场的流动性大幅降低,而此时8月异军突起的SudoSwap则凭借一超多强的增长数据,让基于AMM机制的交易市场映入大众视野。
十四君
2023/02/20
7550
一文讲清-NFT市场新秀SudoSwap的AMM机制-创新挑战与局限
这几天我写了一个DEX交易聚合器
目前,DeFi 赛道中,专门做 DEX 交易聚合的产品挺多的,以下是其中一些平台:
Keegan小钢
2021/07/23
1.7K0
这几天我写了一个DEX交易聚合器
使用带有存储证明的Uniswap V2 预言机
在本文中,我们将讨论“价格累积预言机”的工作原理和使用方法。并且我们将介绍一个可将预言机集成到你自己以太坊项目中的 Solidity 库。本文将假设你对 Uniswap 此类恒定乘积市场有深入的了解。如果你不清楚下面即将讨论的定价机制,请从这篇[优秀]的 Uniswap 文档[5]开始。
Tiny熊
2023/01/09
1.2K0
使用带有存储证明的Uniswap V2 预言机
剖析DeFi交易产品之UniswapV3:工厂合约
UniswapV3Factory 合约主要用来创建不同代币对的流动性池子合约,其代码实现并不复杂,以下就是代码实现:
Keegan小钢
2023/11/07
3560
剖析DeFi交易产品之UniswapV3:工厂合约
Uniswap V3 释疑: 集中流动性, 无常损失和滑点
Uniswap 协议是一组原生的 ETH 的智能合约,它可以实现 ERC20 代币与 ERC20 代币的交换, 以及 ERC20 代币与 ETH 之间的的交换。
Tiny熊
2023/01/09
2K0
使用Uniswap V2部署自己的去中心化交易所
Dapp链接:https://www.chainpip.com/dapp-view/6724
fingernft
2022/10/24
1.4K0
使用Uniswap V2部署自己的去中心化交易所
uniswap的工作原理(下)
市场价格=池子里DAI的数量/池子里ETH的数量(P市场=X/Y)。假设市场数量趋近于无穷大,兑换价格无限趋近于X/Y
用户7976544
2020/11/14
2.6K1
uniswap的工作原理(下)
如何把Uniswap v2作为预言机使用
Uniswap 是目前最流行的去中心化交易所,估计大家读已经了解它, 但我还是先把基础知识再过一遍。
Tiny熊
2021/02/25
1.8K0
如何把Uniswap v2作为预言机使用
uniswap的工作原理(上)
你吃过天上掉下的馅饼吗?只要你在2020年的9月1号之前在uniswap交易所进行过任何一笔操作,就可以获得400的uni币。这个消息刚出的时候uni价值3美元,后续最高峰涨到了8.7美元。也就是说只要你进行了一笔交易,就能获得8k~2w人民币不等的奖励。
用户7976544
2020/11/14
1.8K0
uniswap的工作原理(上)
相关推荐
Uniswap V2 学习笔记2. 交易算法
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验