"有些事,产生在他人身上是故事,产生在自己身上就成了事端"
代码的晋级是一件痛苦且软弱的事。尤其是在本就十分复杂的代码大厦上,任何微小的改动,都或许因为某些边界条件的疏忽而形成崩塌,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的相似,利息便是以这种方法来发放的
那有了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
这儿咱们能够看出 Bug 和 CompSpeed 有关,CompSpeed 这个变量代表是每个区块能够挖出的 COMP 数量
这儿以 mint 为例简略介绍Bug的原理:
ctoken 的 mint 函数的调用链为:mint → mintInternal → mintFresh
能够看到,在mintFresh中,会先调用 Comptroller 的mintAllowed函数,再更新用户 ctoken 的余额
而 mintAllowed 中,会先调用 updateCompSupplyIndex,再调用 distributeSupplierComp
前者会更新假贷池的奖赏状况,主要是 compSupplyState
这一结构体中,block字段记录了更新时的区块号,index字段记录的是更新时的奖赏指数
**什么是奖赏指数(index)呢?**这是一个随时刻不断累加的值,其公式为
表明的是一个假贷池,随着时刻的推移,向每个cToken分发的COMP数量。因而,其差值能够简略理解为,这段时刻内一个cToken能够获得的COMP数量
接下来咱们看另一个函数:distributeSupplierComp。这个函数的作用,便是将用户能够获得的COMP数记录到compAccrued[supplier]中:
每次有用户来和 Compound 交互,都会触发大局的奖赏指数 compSupplyState 更新
与此一起,在上面的函数中,咱们能够看到,用户会先从 compSupplierIndex 中获得前次的 compSupplyState 保存在暂时变量 supplierIndex 中,接下来更新 compSupplyState
这儿要区分好 supplyIndex 和 supplierIndex,前者表明当前的奖赏指数,后者表明用户前次交互时的奖赏指数
而两个时刻点大局奖赏指数的差 * 用户具有的 cToken 数量,便是这段时刻奖赏给该用户的 COMP 数量
现在看起来都是一起正常,岁月静好,直到...
有一天Compound调用了setCompSpeed:
因为一个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:
这儿咱们能够回答一下问题1:未初始化的商场和暂停的商场是不一样的,暂停的商场尽管index = 0,但是block会一向更新!
因而,当咱们为一个暂停的商场从头设置compSpeed时:index不会被初始化!
【注】Compound假定奖赏指数初始值为CompInitialIndex = 1e36
这会有什么影响呢?
咱们再来看下奖赏分发函数distributeSupplierComp:
看出来了吗?用户自己的奖赏指数(supplierIndex)会被初始化为compInitialIndex (1e36),而商场的奖赏指数(supplyIndex)因为上面的问题为0,这就导致:Double memory deltaIndex = sub_(supplyIndex=0, supplierIndex=1e36)出现下溢!
事端2:修正后引入的Bug
事端2代码地址:0x374abb8ce19a73f2c4efad642bda76c797f19233
Compound方面对事端1的修正如下:
Compound很明显意识到了问题出在setCompSpeed函数只考虑了"未初始化商场",而没有考虑"暂停的商场"
因而,新代码中,增加了函数:_initializeMarket这个函数会在增加新商场时调用。也便是说,只要增加新商场,就会初始化其奖赏指数为compInitialIndex
但是已然商场奖赏指数初始化为了compInitialIndex,那用户的奖赏指数呢?这是咱们来看新的distributeSupplierComp函数:
因为很多商场的 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
其Claim大量的COMP代币,经过调试咱们发现,在调用distribute时:
因为事端2,获得的deltaIndex = 1e36,而恰恰该用户之前有cToken
然后能够薅到大量的COMP:
结尾
毕竟,工作的处理方法也很简略
在接下来一条提案中(Proposal 63),暂停COMP奖赏,但是毕竟被撤销掉了
最新的一条提案,更新了Comptroller合约,该提案目前仍在排队中:
最新的合约里,distributeSupplierComp函数中初始化用户奖赏指数的判别条件修改如下:
总结与反思
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
原文出处:BlockSec_Team,不代表云顶量化立场,如有侵权,请联系删除
云顶量化是币圈专业量化策略团队,团队成员均有3年以上金融量化实战经验,主要针对比特币量化(BTC),以太坊量化(ETH),狗狗币(DOGE),SHIB等各种虚拟数字货币推出资金费率套利策略和合约趋势型量化机器人策略。经过长时间实盘测试,策略的稳定型,实用型,可行性都是顶尖水平。