Resupply黑客事件分析
borrow:借出资产,在这里指 reUSD。是 resupply 协议的借出资产。colateral:抵押物,在这里指 wstUSR。Resupply Vault 金库合约中的存款凭证。underlying:底层资产,在这里指 crvUSD,属于 Curve 协议的稳定币。整套闭环的最底层资产依赖。LTV:loan to value 抵押率。例如70%则为抵押 100u 最多能借出 70u
我这里简单分析下 Resupply 协议的运作方式,如果有错误,请指正。
这是一个经典的 Defi 组合场景,流动性质押 + 金库质押 + 借贷协议 三重协议来达到一鱼多吃的效果。具体表现为:
流动性质押:用户在 Curve 这个 DEX 项目中提供流动性,赚取去中心化交易所的手续费收益和额外的奖励。在提供 crvUSD 这个稳定币作为流动性的同时,Curve 会给流动性提供者 mint 出 LP 代币,即 cvcrvUSD,作为将来如果流动性提供者想要换回原资产的凭证。
金库质押:由于 cvcrvUSD 也是一种资产(只要其他项目认可的情况下),所以 cvcrvUSD 又可以通过存款的方式放入 Resupply 的金库协议中,作为一种质押代币所存在。同时,在 Resupply Vault 的金库协议中,也会给存款者 mint 出相应份额的 wstUSR 作为凭证,同样是作为将来取回 cvcrvUSD 而存在的。在金库合约中,金库本身会用用户存款的 cvcrvUSD 去做投资,进而达到 wstUSR 升值或者给用户分发 CVX 等的一些代币作为回报。
借贷:由于 wstUSR 也是一种资产,所以 wstUSR 又可以通过抵押进 ResupplyPair 中,去通过借贷协议借出稳定币 reUSD。而借出的 reUSD 作为一种稳定币,又能被用户拿去其他地方做投资使用。
这里描述的是正常的 Defi 循环场景,下面我将来简单分析下这次 Resupply 被盗事件中的协议漏洞及黑客是如何盗取资金的。

在用户的借款中,每次借款都会去调用 isSolvent 函数修饰器,去检查这个用户有没有资格借款。这次的漏洞就出现在这个 isSolvent 函数修饰器中。

具体进到修饰器中看,这里主要是进行了一个判断:判断用户总借款金额是否超过上限。
例如:
总借款上限 maxLTV 假设为 85%,则每次用户调用借款都会去判断 LTV 是否超过 85%。图上主要依靠的计算公式为:((借款总额 * 汇率)) / 抵押物金额。这个公式为图上的简化。
由此可知,当用户每次借款时,借款总额都会累加,直到 LTV 超过 maxLTV。
那么,问题来了,假如在这个借款的过程中,_exchangeRate(汇率)是可以被操控的话,让 _exchangeRate 恒等于 0 或者极接近于 0。那么用户则会一直能借出 reUSD 且都是大额的借出。
此次黑客事件也是基于这个汇率做文章。

那么黑客是如何进行汇率的操控的呢?继续追踪下去,可以看到,在这个合约中,汇率的计算是通过 汇率 = 1e36/ 抵押品价格
如果要让这个 _exchangeRate 等于 0 (除法下溢可以做到)或者非常接近于 0,则这个抵押品(wstUSD)的价格要足够大(price 代表了一个 wtUSR 可以借出多少 reUSD)。
则此时黑客需要操控的 price (分母)变得很大很大。
而在合约协议中,这个 price 是通过预言机合约调用 getPrice 函数去获取的。那么我们继续追踪下去。

在预言机合约中,实际上并没有做什么事情,仅仅是做了一次金库合约的调用转发。
那么此时问题就来了,预言机对于价格的转发高度依赖 Vault 金库合约中的 _convertToAsset 函数,而不是真实市场上的价格(链下预言、喂价)。假如黑客有能力操控 Vault 金库的价格,那他就能操控借贷合约去实现 _exchangeRate 等于 0 或者接近于 0 的目标,进而攻击这个漏洞。
实际上,本次黑客攻击也的确是这样干的。具体是如何实现的,可以看下面金库合约

继续追踪到金库 Vault 合约中,我们可以看到在 _convertToAsset 函数中,实际是做了一次资产的换算,也就是计算在金库中的质押份额乘上总的金库资产。
那么这里就有一个问题,假如这个金库资产是空的金库(total_asset = 0, totalSupply = 0),或者资产的深度很小。那么黑客可以通过绕过 deposit 函数,而是直接打钱(捐赠)的方式让 total_asset 总资产变得很大,在 shares 不变(仍为 0,但黑客可以 deposit 极少量的 cvcrvUSD 进去,使得 shares 不为 0)的情况下,使得这个函数返回的值变得很大。进而达到攻击的目的。
就假如说,这个金库是一个接收 cvcrvUSD ,返回 wstUSR 的金库。那么根据这里的计算,黑客给这个空金库捐赠大额 cvcrvUSD。然后黑客 deposit 存入很少的 wstUSR 即可算出少量 wstUSR 可以换出大量 cvcrvUSD。
又因为这个金库实际上是个空的金库,只有黑客在操作,所以这个返回值变得很大了后,黑客可以可以让前面的汇率接近 0 或者等于 0 了。进而达到攻击的目的了。
Resupply 协议部署一个抵押 wstUSR 借出 reUSD 的新借贷合约。其中这个借贷合约依赖了 Resupply Vault 合约的 wstUSR 的汇率。Vault 金库中的打入大量 cvcrvUSD 资产。然后黑客通过正常 deposit 的方式存入很少量的 cvcrvUSD,获取极少的 wstUSR。又因为这个金库中实际上是个空金库,所以此时 wstUSR 对 cvcrvUSD 的汇率极高。仅一点点 wstUSR 可以换取大量 cvcrvUSD。(实际外部市场上汇率没这么高)Vault 市场的动作,使得借贷合约中 exchangeRate = 1e36 / price = 0(被整除下溢),所以导致借贷合约中_isSolvent() 被判定为安全,TVL 始终等于 0。isSolvent 一直返回 true,借贷合约可以被无限借出(不超限额的情况下)。则黑客此时可以用很小的 wstUSR 换取大量的 reUSD。直到达到上限。核心原因是 Resupply 这个借贷协议太傻逼了,高度依赖 Vault 金库合约中的局部市场价格汇率。要是有空池检测或者真实市场预言机就这种漏洞根本不会发生。这就是我觉得 ChainLink 的代币值得观察的原因,真实世界预言机对于区块链生态而言真的太重要了。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!