"有些事,产生在别人身上是故事,产生在自己身上就成了事端"

蝴蝶效应:一文了解 Compound 代码更新事故

代码的升级是一件苦楚且软弱的事。尤其是在本就十分复杂的代码大厦上,任何细小的改动,都或许因为某些边界条件的疏忽而形成坍塌,Compound最近就遇到了这事

蝴蝶效应:一文了解 Compound 代码更新事故

布景介绍

Compound是一个老牌的去中心化假贷渠道

Q1: 为什么要有假贷这回事儿?

在区块链上,一切的财物都是代币化的。咱们看好一个项目,可是却没有这个项目发行的代币。这时最简略的办法便是去交易所用手里有的币来换。可是,假如咱们又不舍得自己手里的币,该怎么办呢。这时能够去借,典当品便是咱们具有的币。假如这个项目涨了,取得收益的一同,还能够赎回从前典当的币。

交易会产生一切权的搬运,而假贷不产生一切权的搬运,仅仅被暂时锁在合约中

一同,因为假贷的引进,咱们能够控制更大的资金敞口,实现杠杆交易...

Q2:Compound的运作流程?

和AMM相似,在区块链上实现自动化的假贷,首先要做的便是招引资金(流动性),而用户所以能将钱存在一个渠道,必然是受到利益的唆使。AMM经过交易费来鼓励用户增加流动性,而假贷渠道的手段便是借款利息

因为存在假贷这一需求,总会有人乐意付出利息来借币。而有了利息的鼓励,也有人乐意将闲钱拿来供应流动性。此外,假贷渠道经过利率模型参数的动态调整,能够维持整个系统的供应平衡与危险

Q3: 怎么与Compound交互?

用户与Compound的交互接口主要是CToken/CEther合约(这些合约本身便是一种代币), CToken 相当于 Compound这一渠道的"入场券"。经过向不同CToken合约质押其底层代币(underlying token)便能够取得相应的CToken

这一操作,表现在代码层面便是ctoken.mint(amount),比方说:我手里有1000个ETH,便能够调用cEth.mint(1000)来向cEth池中 "注入流动性"

要注意的是,cToken和底层代币并不是1:1的兑换联系,当蛋糕越做越大时,cToken所能换出的底层代币也就越多。这和LP token的相似,利息便是以这种方法来发放的

蝴蝶效应:一文了解 Compound 代码更新事故

那有了cToken今后,咱们能够做什么呢?

最简略的便是借钱,因为cToken代表用户质押在Compound的财物,因而能够经过"过典当"的方法来借出Compound具有的代币。Compound会先核算用户具有一切cToken的价值(或许来自于不同的池),依据典当率来核算用户的流动性(Liquidity

表现在代码层面便是ctoken.borrow(amount),比方说:我经过ceth.mint(1000)质押了 1000 个 ETH,假如我想借 Dai 的话,需求调用cDai.borrow(x)这儿的 x 最多价值750 ETH (典当率75%)

这些都是以美元核算的,再依据Oracle来换算成不同的Token数量

而Comptroller这一合约是一个中间层,它所做的工作,便是交互前的一些核算与验证工作,相似银行的审计员。比方说:张三借了多少钱,欠了多少钱,这小子又来借1000个ETH还能不能借给他

表现在代码层面便是:getHypotheticalAccountLiquidityInternal()、borrowAllowed()、mintAllowed()...

Q4: COMP代币与Compound的联系?

COMP代币是Compound发行的渠道代币,能够用于管理。因为Compound选用DAO的管理形式。对Compound一切的操作,都需求经过投票来决议,提案(proposals)经过后由一个特权合约来执行写在提案中的操作。经过COMP能够取得投票的权重

详情见:https://compound.finance/governance

当然只能用来投票显然仍是缺少些招引力的,COMP本质上便是Compound发行的股票,具有更多的COMP,能够享受更优的利率,跟着Compound的发展,COMP带来的价值也会越来越大,因而COMP值钱(目前 $300 左右)

一同,为了鼓励用户使用Compound,无论是向Compound供应流动性,仍是从Compound借出财物,都会取得一定的COMP奖赏,这些奖赏以区块为单位核算(划要点:这儿与本次事情相关)

修了东墙又补西墙事端1:Bug的原理

事端1代码地址:0x75442Ac771a7243433e033F3F8EaB2631e22938f

工作的起因是这样的:

2021年9月31日,Compound DAO呈现这样一条提案(Proposals 62: https://compound.finance/governance/proposals/62):

该提案提出更新 Comptroller 合约以修正一些 Bug

蝴蝶效应:一文了解 Compound 代码更新事故

这儿咱们能够看出 Bug 和 CompSpeed 有关,CompSpeed 这个变量代表是每个区块能够挖出的 COMP 数量

这儿以 mint 为例简略介绍Bug的原理:

ctoken 的 mint 函数的调用链为:mint → mintInternal → mintFresh

蝴蝶效应:一文了解 Compound 代码更新事故

能够看到,在mintFresh中,会先调用 Comptroller 的mintAllowed函数,再更新用户 ctoken 的余额

蝴蝶效应:一文了解 Compound 代码更新事故

而 mintAllowed 中,会先调用 updateCompSupplyIndex,再调用 distributeSupplierComp

蝴蝶效应:一文了解 Compound 代码更新事故

前者会更新假贷池的奖赏状况,主要是 compSupplyState

蝴蝶效应:一文了解 Compound 代码更新事故

这一结构体中,block字段记载了更新时的区块号,index字段记载的是更新时的奖赏指数

**什么是奖赏指数(index)呢?**这是一个随时刻不断累加的值,其公式为

蝴蝶效应:一文了解 Compound 代码更新事故

表明的是一个假贷池,跟着时刻的推移,向每个cToken分发的COMP数量。因而,其差值能够简略理解为,这段时刻内一个cToken能够取得的COMP数量

接下来咱们看另一个函数:distributeSupplierComp。这个函数的效果,便是将用户能够取得的COMP数记载到compAccrued[supplier]中:

蝴蝶效应:一文了解 Compound 代码更新事故

每次有用户来和 Compound 交互,都会触发大局的奖赏指数 compSupplyState 更新

与此一同,在上面的函数中,咱们能够看到,用户会先从 compSupplierIndex 中取得上次的 compSupplyState 保存在暂时变量 supplierIndex 中,接下来更新 compSupplyState

这儿要区分好 supplyIndex 和 supplierIndex,前者表明当时的奖赏指数,后者表明用户上次交互时的奖赏指数

而两个时刻点大局奖赏指数的差 * 用户具有的 cToken 数量,便是这段时刻奖赏给该用户的 COMP 数量

现在看起来都是一同正常,岁月静好,直到...

有一天Compound调用了setCompSpeed:

蝴蝶效应:一文了解 Compound 代码更新事故

因为一个Market的CompSpeed是能够设置为0(表明暂停发放COMP奖赏),所以存在这样一种情况:

咱们先把一个商场的CompSpeed设置为0

过了一段时刻后又想要从头敞开COMP奖赏,这时就会调用setCompSpeed设置compSpeed为一个非零值

这会产生什么呢?

很显然,合约会走到else if (compSpeed != 0)这个分支。咱们来看这个分支中有两个if判别(以第一个为例):if (compSupplyState[address(cToken)].index == 0 && compSupplyState[address(cToken)].block == 0)。其效果是:为一个未初始化的商场,初始化奖赏指数(index)和区块号(block)

问题1:这儿能够想想:未初始化的商场(index = 0 && block = 0)和被暂停的商场(index = 0)一样吗?

先别急,咱们从头来看updateCompSupplyIndex:

蝴蝶效应:一文了解 Compound 代码更新事故

这儿咱们能够答复一下问题1:未初始化的商场和暂停的商场是不一样的,暂停的商场尽管index = 0,可是block会一直更新!

因而,当咱们为一个暂停的商场从头设置compSpeed时:index不会被初始化!

【注】Compound假设奖赏指数初始值为CompInitialIndex = 1e36

这会有什么影响呢?

咱们再来看下奖赏分发函数distributeSupplierComp:

蝴蝶效应:一文了解 Compound 代码更新事故

看出来了吗?用户自己的奖赏指数(supplierIndex)会被初始化为compInitialIndex (1e36),而商场的奖赏指数(supplyIndex)因为上面的问题为0,这就导致:Double memory deltaIndex = sub_(supplyIndex=0, supplierIndex=1e36)呈现下溢!

蝴蝶效应:一文了解 Compound 代码更新事故

事端2:修正后引进的Bug

事端2代码地址:0x374abb8ce19a73f2c4efad642bda76c797f19233

Compound方面对事端1的修正如下:

Compound很显然认识到了问题出在setCompSpeed函数只考虑了"未初始化商场",而没有考虑"暂停的商场"

因而,新代码中,增加了函数:_initializeMarket这个函数会在增加新商场时调用。也便是说,只需增加新商场,就会初始化其奖赏指数为compInitialIndex

蝴蝶效应:一文了解 Compound 代码更新事故

可是已然商场奖赏指数初始化为了compInitialIndex,那用户的奖赏指数呢?这是咱们来看新的distributeSupplierComp函数:

蝴蝶效应:一文了解 Compound 代码更新事故

因为很多商场的 CompSpeed 为0,所以其奖赏指数会停留在 compInitialIndex(1e36) 这个值,此刻假如调用这个函数会产生什么?

很显然上图中的if被绕过了,这意味着没有初始化用户的奖赏指数(supplierIndex),而商场的奖赏指数(supplyIndex)是compInitialIndex

所以deltaIndex本应是(compInitialIndex - compInitialIndex = 0)就变成了 (compInitialIndex - 0 = 1e36)

哦豁,出大问题。可是,奖赏不仅仅依赖于这个deltaIndex,还需求用户有cToken(supplierTokens)

是否存在这一情况呢?显然是存在的,假如用户在合约更新之前就做了mint操作,其supplierIndex=0,可是手里是存在cToken的。当合约更新后,用户再次调用该函数,就能够取得 1e36 * ctoken.balanceOf(user) 数量的COMP奖赏Real World

经过compStateIndex = compInitialIndex,能够很容易的得到受到影响的商场有:

0xF5DCe57282A584D2746FaF1593d3121Fcac444dC: cSAI0x12392F67bdf24faE0AF363c24aC620a2f67DAd86: cTUSD0x95b4eF2869eBD94BEb4eEE400a99824BF5DC325b: cMKR0x4B0181102A0112A2ef11AbEE5563bb4a3176c9d7: cSUSHI0xe65cdB6479BaC1e22340E4E755fAE7E509EcD06c: cAAVE0x80a2AE356fc9ef4305676f7a3E2Ed04e12C33946: cYFI

咱们以一位涉事者为例:0xa7b95d2a2d10028cc4450e453151181cbcac74fc

咱们看到在这笔交易中:0x6416ed016c39ffa23694a70d8a386c613f005be18aa0048ded8094f6165e7308

蝴蝶效应:一文了解 Compound 代码更新事故

其Claim很多的COMP代币,经过调试咱们发现,在调用distribute时:

蝴蝶效应:一文了解 Compound 代码更新事故

因为事端2,取得的deltaIndex = 1e36,而恰恰该用户之前有cToken

蝴蝶效应:一文了解 Compound 代码更新事故

然后能够薅到很多的COMP:

蝴蝶效应:一文了解 Compound 代码更新事故

尾声

终究,工作的解决方法也很简略

在接下来一条提案中(Proposal 63),暂停COMP奖赏,可是终究被取消掉了

最新的一条提案,更新了Comptroller合约,该提案目前仍在排队中:

蝴蝶效应:一文了解 Compound 代码更新事故

最新的合约里,distributeSupplierComp函数中初始化用户奖赏指数的判别条件修正如下:

蝴蝶效应:一文了解 Compound 代码更新事故

总结与反思

Compound作为假贷渠道的老大哥,本次的事情有些唏嘘

尽管Compound软硬兼施,一方面许诺拿出10%的白帽奖赏给取得"意外之财"的用户,一方面又寻求法律的手段。可是,事端终究现已产生

当咱们不断探究区块链,不断追求更高的APY,追求项目快速落地。是否还有人记住,区块链最基本的一条准则便是:覆水难收!

启示如下:

代码布置上链前一定要做好足够的审计与测验工作

使用署理形式时,更新逻辑合约要保证一致性,注意是否会对本来的Storage产生影响

DAO形式尽管减少了中心化的危险,可是应对紧急情况时的反响迟缓问题

即使是大公司依然会有犯错误的或许,借鉴其他项目代码时要注意检查

参考

Comptroller: compSpeed bug:https://www.comp.xyz/t/comptroller-compspeed-bug/2111

github issue:https://github.com/compound-finance/compound-protocol/pull/144/commits/f6d717bb78bef0c9851ad672f7b9aa1d90b0f00a

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注