普通视图

发现新文章,点击刷新页面。
昨天以前一片瓜田

PancakeHunny 闪电贷 LP 池操控攻击分析

作者 冬瓜
2021年11月3日 16:00

背景

2021 年 10 月 20 日 UTC 时间上午 9 点,PancakeHunny 平台遭遇闪电贷智能合约攻击,攻击者通过操纵 PCS 上的 WBNB/TUSD 的流动性从而操纵了兑换比例,实现了 HUNNY 铸币合约的大量铸币,完成攻击。

最终攻击者获利 230 万美元(64.2 万是稳定币 + 435.31 ETH),并且大量铸造 HUNNY 代币,将 HUNNY 的价格从 0.3 抛售到 0.1 美元。 ​

这一操作的 TxHash 从 bscscan 上可以找到:0x1b698231965b72f64d55c561634600b087154f71bc73fc775622a45112a94a77。 ​ 下面我们来复盘一下整个攻击手法和流程。

代码中的根本原因

可以查看合约 VaultStrategyAlpacaRabbit,这个合约是可升级合约的原合约地址。目前该合约仍旧由线上 TUSD 单币池合约地址进行代理转发(也就是线上还没有进行更换),但是目前官方已经发现了问题,已经关闭了该池的铸币(那其实还不如直接存 Alpaca Finance)。 ​ 我们在 VaultStrategyAlpacaRabbit 合约中,可看到以下代码:

在上述代码中,黄色高亮的一行就是此次攻击的根本原因。原因就是因为这个 swap 的 Path 最终选用的是 [ALPACA, WBNB, TUSD]然而 TUSD/WBNB 的 LP Token 其流动性仅有 2 美元(这是目前的情况,可以查看 PCS 的流动性数据,于是攻击者就通过闪电贷放大资金量,从而控制这组 LP Token 的兑换汇率,从而进行攻击。 ​

接下来我们来分步骤解析这个过程: ​

  • 攻击者利用闪电贷,借出 270 万 TUSD,并且全部通过 [TUSD, WBNB] 的 Path 兑换成了 WBNB。根据 AMM 的恒定乘积公式 \(x \times y = k\) ,由于大量的 TUSD 进入到了 TUSD/WBNB Lp 池中,所以通过十分少量的 WBNB 沿着相反的 Path 就能兑换出大量的 TUSD
  • 第二步,攻击者会将一笔可观的 TUSD 数额放入 TUSD 单币池中,让其占据了该池 99% 的收益。此时因为步骤一种操控了 Lp 池,大量的 TUSD 会被兑换出来。
  • 第三步,攻击者会调用 getReward() 方法,这个方法会调用 _withdrawStakingToken() 方法,其中会返回 withdrawAmount 这个变量。
withdrawAmount = _stakingToken.balanceOf(address(this)).sub(stakingTokenBefore);

它会通过 _stakingToken 也就是我们的 TUSD 总量来计算。而 withdrawAmount 就是用来传入到 minter 中,其价值的 30% 为总量负责铸造 HUNNY 代币的数量控制变量,从而造成大量的 HUNNY 被铸造。

  • 攻击者抛售大量的 HUNNY 完成此次经济攻击。

复盘

攻击者完成本次攻击,是与以往的 BUNNY 攻击有所区别的,BUNNY 中的错误实在是太低级了,使用了账户余额的代币数量来铸造 BUNNY 。虽然 HUNNY 通过使用增量变量的方式避免了 BUNNY 的漏洞,但对于 LP Token 市值太低容易操纵这一环节没有戒备心,从而导致了经济漏洞。 ​

反思:在制作机枪池的时候,如果有 Minter 进行铸造操纵,一定要慎之又慎,来验证每一步用到的数量关系,再进行代码编写。

LP Token 价格计算推导及安全性

作者 冬瓜
2021年9月6日 16:00

背景

在实现 CakeBot 的 USDT/USDC 池时,需要计算 LP Token 的代币价值,从而方便的给用户提示 LP Token 当前准确的价格,来计算收益率。所以对 LP Token 的价值计算做了一点深入的研究,并且还翻阅到 Alpha Finance 团队的关于安全获取 LP 价格的方法。 本位将这些学习笔记分享给大家。 ​

一般 LP Token 价格的获取方法

我们知道对于一般 Token 的价格,在 Cex 中其实是市场上交易撮合的成交价。在 Dex 中,由于 AMM 做市商模型通过一组 LP 来构建价格的锚定。所以如果我们想获取到一个 Token 的价格,都是通过对于稳定币 USDT、USDC 或者 BUSD 的币对关系,从而反映现实世界的价格。 ​

我们知道 LP Token 是不具有流动性池的,如果有那就是套娃了。那么我们应该如何去计算价格呢?其实我们只需要用总增发量和货币价格反推即可。 ​

\[Cap_x = P_x \times T_x\]

任意一个 Token X 的总市值是 $Cap_x$,是用当前的价格 $P_x$ 和当前总铸造数量 $T_x$相乘可得。对于 LP Token,我们可以用这个公式来反推币价。因为在 LP Token 中,总市值是可以通过两种币的数量和对应价格求得,并且总的制造数量也是已知的。 ​

所以我们可以如此计算 LP Token 总价格: ​

\[P_{LP} = \frac{Cap_{LP}}{T_{LP}} = \frac{r_0 \times price_0 + r_1 \times price_1}{totalSupply}\]

其中,$r_0$和 $r_1$就是 LP Token 合约中两种代币的存量,$price_0$和 $price_1$分别代表 $r_0$和 $r_1$ 对应 Token 的价格。市面上无论 BSC、ETH 还是 Polygon 还是 Heco 链等,其 LP 代币基本都是 fork Uniswap 的,所以 $r_0$和 $r_1$、$price_0$和 $price_1$ 都是能拿到的。 ​

上面的公式我们其实可以看出,是通过市值反推价格,也没有什么巨大的逻辑问题。当我们需要访问其币价的时候已经可以满足需求。在 Web3.js 前端中,我们就可以照此拿到结果。

export const getLpTokenPrice = async (
  lpAddress: string,
  lib: any,
  price0: BigNumber,
  price1: BigNumber
) => {
  const lpToken = getPancakeLp(lib, lpAddress);
  let [r0, r1] = (await lpToken.getReserves()).map((n) => bignumberToBN(n));
  let totalSupply = bignumberToBN(await lpToken.totalSupply());
  return r0
    .multipliedBy(price0)
    .plus(r1.multipliedBy(price1))
    .dividedBy(totalSupply);
};

至此,我的需求完成。

延时喂价漏洞

对于上文公式: ​

\[price_{lp}= \frac{r_0 \times price_0 + r_1 \times price_1}{totalSupply}\]

其实乍一看是不存在问题的。但是如果我们所做的需求,不仅仅是一个价格展示,而是一个借贷系统,用这种方式来获取清算系数,就会存在被闪电贷的风险。虽然 $price_0$和 $price_1$不能被操控,但是 $r_0$和 $r_1$是可以的。黑客可以通过操作 $r_0$ 和 $r_1$,从而对价格实现控制。

之前漫雾团队写过一篇「Warp Finance 被黑详解」的分析,采用了如下攻击流程:

  1. 通过 dydx 与 Uniswap 闪电贷借出 DAI 和 WETH;
  2. 用小部分 DAI 和 WETH 在 Uniswap 的 WETH-DAI LP 中添加流动性,获得 LP Token;
  3. 将 LP Token 抵押到 Wrap Finance 中;
  4. 用巨量的 WETH 兑换成 DAI,因为 WETH 迅速进入了 WETH-DAI 流动池,总数量大增。但是由于价格使用的是 Uniswap 的预言机,访问的是 Uniswap 的 LP 池,所以 WETH 的价格并未发生变化。从而导致 Wrap Finance 中的 WETH-DAI LP Token 价格迅速提高;
  5. 由于 LP Token 单价变高,导致黑客抵押的 LP Token 可以借出更多的稳定币来获息。

这里,我们发现漏洞的关键地方,其实是 $price$ 计算对于借贷项目中,使用的是他人的 LP 合约,还未等套利者来平衡价格,从而终究留出了时间差。 ​

为了解决这个问题,如果我们可以找到一种方式,从而规避价格查询,就能大概率防止上述漏洞。这里,Alpha Finance 给出了另外一个推导公式。

获取公平 LP 价格方法

首先我们在一个 LP 池中,我们能保证的是恒定乘积 $K$ 值的大小,我们定义价格比值是 $P$,那么我们会有以下关系式: ​

\[\begin{cases} K=r_0 \times r_1 \\ P = \frac{r_1}{r_0} \end{cases}\]

因为 $r_0$ 和 $r_1$ 在旧方法中是可以被操纵的,所以我们用 $K$ 和 $P$ 来反解真实的 $r’_{0}$ 和 $r’_1$ : ​

\[\begin{cases} r'_0 = \sqrt{K / P} \\ r'_1 = \sqrt{K \times P} \end{cases}\]

如此,我们在带入一开始计算 $price_{lp}$的公式中: ​

\[\begin{align} price_{lp} &= \frac{r'_0 \times price_0 + r'_1 \times price_1}{totalSupply} \\ & = \frac{\sqrt{K/P}·price_0 + \sqrt{K·P}·price_1}{totalSupply} \\ & = \frac{\sqrt{K · \frac{price_1}{price_0}·price_0^2} + \sqrt{K·\frac{price_0}{price_1}·price_1^2}}{totalSupply} \\ & = \frac{2\sqrt{K·price_0·price_1}}{totalSupply} \\ & = 2 \frac{\sqrt{r_0·r_1}·\sqrt{price_0 · price_1}}{totalSupply} \end{align}\]

我们可以发现,最终 Alpha Finance 给我们的推导式中,不会存在独立的 $r_0$ 和 $r_1$ ,取而代之的是它们的恒定乘积 $K$。

攻击可能性分析

使用以上公式,我们可以真正的避免攻击吗?

  1. ​$price_0$和 $price_1$ 首先是可信源获取的正确价格,无法操纵;
  2. $totalSupply$ 只是改变了质押数量,其变化与质押的两个代币数量有关系;
  3. 对于 $r_0$ 和 $r_1$ ,在 Alpha Finance 的博客中提供了两种思路:
    1. 直接进行代币兑换(类似于上述攻击手段),由于 $r_0 \times r_1$ 是定值 $K$,所以无论如何变化都不会影响计算结果;
    2. 直接将 Token 打入 LP Token 合约地址中,由于 $r_0$ 和 $r_1$ 都是在二次根式下,所以付出 $x$ 倍的成果,最终只能获得 $\sqrt{x}$ 倍的收益,这显然是亏本的;

综上,在已知情况下,是可以有效避免攻击的。

总结

通过这次对 LP Token 价格计算的研究,并且对延时喂价漏洞的探求,了解了 LP 抵押使用一般方式计算带来的风险。计算价格的需求,一定要根据所做业务的类型,谨慎选择。

参考链接


欢迎大家使用我个人研发的 Cakebot Finance 来体验去中心化 DAPP 挖矿。

Liquidity Providers 的数学原理

作者 冬瓜
2021年4月25日 16:00

最近半年多一直在币圈玩 DeFi,这半年来增加了好多十分神奇的玩法,这都要得益于 Uniswap 的创新。

看了这么多 Up 主的视频,讲的都云里雾里的,其实就是简单的几个公式,这篇文章我们来从头推导一下无偿损失的公式,来计算一下为什么当单币种价格有强烈浮动的时候,为什么会有无偿损失

Liquidity Providers 代币是什么

流动性提供者代币(Liquidity Providers Token),也就是我们经常说的 LP 代币,这是一个什么东西呢?

可以举一个最简单的例子,加入我们现在使用 BUSD 来购买 BNB,我们经常使用交易所的朋友们知道,交易所的挣钱模式是用户在交易时从交易金额中进行抽成来赚取手续费

其实很多中心化交易所(CEX)看到了盈利模式中的用户激励机制,那就是你如果邀请别人试用我们的产品,那么交易所在进行手续费抽成的过程中,也会将其再次抽成,作为那些 KOL 的奖励

而 Uniswap 这种去中心化交易所(DEX)就无法利用这种机制进行用户增长,因为所有的 DEX 没有用户注册机制,只需要连接用户的数字钱包即可完成交易。

于是,Uniswap 就设计了另外一种激励模式,AMM(Automated Market Maker) - 自动化做市商模型。关于自动化做市商这里我们不详细的去讲,我们只要了解以下几个动作代表了什么意思就可以(这里使用 BNB 和 BUSD 交易以及 PancakeSwap 平台为例):

  1. 用 BNB 和 BUSD 换取 BNB-BUSD LP 代币:相当于你为 BNB 和 BUSD 流动池增加了代币。并且你获得了 BNB ⇄ BUSD 这个交易中的对应代币份额的抽成奖励占比;
    1. 这里就体现了 DEX 通过换取流动性证明,从而完成了一种激励模式;
    2. PancakeSwap 在文档中提到,其 0.2% 的交易手续费当中,有 0.17% 的比例是提供给 LP 持有者的抽成奖励的。剩下的 0.03% 是平台的利润。具体规则可以查看官方文档
  2. BNB-BUSD 代币挖矿:当我们拿到 BNB-BUSD LP Token 之后,我们可以继续放在 PancakeSwap 的 Farms 中进行挖矿(其年化 APR 如以下截图所示)。

所以,看到这里你应该也明白这个 LP Token 是个什么东西了。他其实就是你提供的流动性证明代币,持有这个代币你就可以获得对应币对在交易过程中手续费的抽成

并且平台为了让你去填充代币池,通过质押 LP 代币挖矿的形式来吸引你去兑换,这样你也获得了收益,平台也获得了交易币对的深度池

无偿损失是如何来的?

有很多朋友应该是被 LP 代币挖矿的高收益吸引过来的。在一顿操作之后开始了挖矿,但是在 Remove LP 代币的时候,发现自己损失了好多 U。本来是来挖矿赚收益的,最后矿没挖多少,自己却亏了好多。这是怎么一回事呢?

这种情况就是我们所说的** IL 无偿损失(Impermanet Loss),用来指在流动性代币价值与持有两种现货资产相比产生负收益的结果**。简单来说就是亏钱了!

为什么会产生无偿损失,我们以为 BNB 和 BUSD 提供流动性为例,来具体的描述一个场景。在例子之前我们需要了解以下变量:

常数 K(Constant Product)

常数 K 因子是用来为交易定价的一种方式,用这个常数来保证币对池子的价值平衡。 \(K = A \times B\) 这里面 \(A\)  和 \(B\)  是两个 Token 的数量。这里面我们用 BNB 和 BUSD 来举例子:

  • A - BNB 的数量
  • B - BUSD 的数量

A 和 B 带入到我们的例子中,后面用 \(C_{BNB}\) 和 \(C_{BUSD}\) 代替 \(K_{BNB-BUSD} = C_{BNB} \times C_{BUSD}\)

定价 P(Token Price)

这个是 Uniswap 中对于 LP 比对的一个条件,就是要保证当前配对时,币对中两个币的 U 本位价值相同。在这个例子中,我们引入 BNB 此时的价格 \(P_{BNB}\)  以及 BUSD 的价格 \(P_{BUSD}\)。此时我们可以得到等式: \(P_{BNB} \times C_{BNB} = P_{BUSD} \times C_{BUSD}\) 由于我们的 BUSD 其实是和 $ 锚定的,那么其实 \(P_{BUSD} = 1\),所以有以下式子: \(P_{BNB} \times C_{BNB} = C_{BUSD}\) \(P_{BNB} = \frac{C_{BUSD}}{C_{BNB}}\)

数量公式

我们通过上述公式来推出两个代币的定价 P 的公式:

\[\begin{cases} C_{BNB} = \sqrt{\frac{K_{BNB-BUSD}}{P_{BNB}}} \\ C_{BUSD} = \sqrt{K_{BNB-BUSD} \times P_{BNB}} \end{cases}\]

如此,我们就可以通过常数 K 以及当前的币价来推导我们代币的数量了。接下来我们来看一个实际的场景,来直接感受一下无偿损失。

实际场景

假如,我们在 1 BNB = 500 BUSD 的时候,组了一组 LP 。我们拿出了 20 个 BNB 和 10000 个 BUSD 进行 LP 流动性提供代币兑换。此时我们得到了这几个变量:

\[\begin{cases} P_{BNB} = 500 \\ P_{BUSD} = 1 \\ K = C_{BNB} \times C_{BUSD} = 2 \times 10^5 \end{cases}\]

并且,我们保证此时的 K 也是后续所有情况下的常数 K,即组完 LP 代币后即时生效的常数。

过了 10 天,BNB 涨价了,当前价格为 1 BNB = 550 BUSD 了。随之我们的价格也变成了如下关系:

\[\begin{cases} P_{BNB} = 550 \\ P_{BUSD} = 1 \end{cases}\]

我们带入到之前数量公式来计算此时 LP Token 等值的代币个数:

\[\begin{split} C'_{BNB} &= \sqrt{\frac{K_{BNB-BUSD}}{P_{BNB}}} \\ & = \sqrt{\frac{2 \times 10^5}{550}} \\ & \approx 19.069 \ BNB\\ C'_{BUSD} &= \sqrt{K_{BNB-BUSD} \times P_{BNB}} \\ & = \sqrt{2 \times 10^5 \times 550} \\ & \approx 10488.09\ BUSD \end{split}\]

在 BNB 涨价到 550 BUSD 数量到时候,我们发现等值的 LP Token 兑换只能换回 19.069 个 BNB 和 10488.09 个 BUSD。如果我们来换算成 BUSD 作为单位来对比一下前后收益:

  • 情况一:就是上述情况,我们持有来一定数量的 LP 代币,接触流动性后全部折算成 BUSD 可以得到以下结果:
\[\begin{split} V_1 & = 19.069 \times 550 + 10488.09 = 20976.04 \ BUSD \end{split}\]
  • 情况二:如果我们持续持有 20 个 BNB 和 10000 个 BUSD,那么此时我们折算成 BUSD 可以得到以下结果:

\(V_2 = 20 \times 550 + 10000 = 21000\ BUSD\) 经过以上分析,我们是可以看到如果我们持有 LP 代币不进行任何理财操作,其实我们组了 LP Token 后是会亏 \(21000 - 20976.04 = 23.96 \ BUSD\)的。 **

相关补充

LP Token 代币数量计算

其实 LP Token 也是一种代币,它也拥有自己的合约地址。比如 Pancakeswap 上的 BNB-BUSD LP 代币合约地址 0x58F876857a02D6762E0101bb5C46A8c1ED44Dc16 。是代币肯定就有一个方式来计算数量,这里 Pancakeswap 和 Uniswap 的计算方式一样,采用以下公式:

\[C_{BNB-BUSD\ LP} = \sqrt{C_{BNB}\times C_{BUSD}}\]

所以,当我们用 20 个 BNB 和 10000 个 BUSD 兑换 LP 代币的时候,我们会获得大约 447.21 个 LP Token。

简易的无偿损失表

这个我下一篇文会具体的推演一下如何决策以及如何与收益来共同计算。这里先引用 「Uniswap: A Good Deal for Liquidity Providers?」这篇文章中给出的结论:

  • a 1.25x price change results in a 0.6% loss relative to HODL
  • a 1.50x price change results in a 2.0% loss relative to HODL
  • a 1.75x price change results in a 3.8% loss relative to HODL
  • a 2x price change results in a 5.7% loss relative to HODL
  • a 3x price change results in a 13.4% loss relative to HODL
  • a 4x price change results in a 20.0% loss relative to HODL
  • a 5x price change results in a 25.5% loss relative to HODL

Losses/Price Change Function

写在最后

DeFi 是一个金融游戏,如果你什么也不做研究并且什么也不去动手计算,那么永远都是韭菜。所以希望大家对于有趣的项目,先做好研究以及风险评估,在去玩耍。

欢迎大家使用我个人研发的 Cakebot Finance 来体验去中心化 DAPP 挖矿。

❌
❌