时间 2022 年 10 月 11 日
涉案 约 $1.14 亿
结局 部分归还;⚠️ 攻击者后被起诉并定罪
类别 C4 外部数据假设失效 + C5 经济假设失效

一、背景:把浮盈当抵押品

Mango 是 Solana 上的一个保证金交易平台,同时提供现货、永续合约和借贷。它的抵押品计算有一个关键设计:

⭐ 账户的可借额度 = 存入的抵押品 + 【永续合约头寸的未实现盈利】

⟹ 也就是说:如果你的多头正在赚钱,
   那笔【还没平仓的浮盈】可以被当成抵押品去借别的资产。

这个设计在传统交易所也存在(保证金抵消),但传统交易所有两个 Mango 没有的东西:风控团队对标的流动性的限额

二、漏洞在哪:三层依赖叠在一起

可借额度
   ⟸ 依赖 未实现盈利
        ⟸ 依赖 MNGO 的标记价格
             ⟸ ⚠️ 依赖 外部现货市场的 MNGO 价格
                  ⟸ ⭐ 而 MNGO 的现货市场【极其薄】

每一层单独看都合理,但叠起来之后:

⭐ 借出协议全部资产所需要的,
   仅仅是把一个日均交易量很小的代币的价格【拉高几倍】。

⚠️ 而 MNGO 是【Mango 自己的治理代币】——
   于是形成一个循环:
      协议的偿付能力 ⟸ 自己代币的价格
      而自己代币的价格 ⟸ 协议是否还有偿付能力

三、攻击复盘

① 攻击者准备两个账户,各存入约 500 万美元的 USDC

② ⭐ 用账户 A 和账户 B 在 Mango 上【互相对敲】
      A 大量做多 MNGO 永续,B 大量做空
      ⟹ 两个账户合计的净敞口≈0,
         ⚠️ 但账户 A 持有一个巨大的【多头】头寸

③ ⭐ 去外部的现货市场买入 MNGO
      ⟹ MNGO 的现货市场日均量很小
      ⟹ 价格在几分钟内被拉高数倍

④ Mango 的标记价格跟随现货上涨
      ⟹ 账户 A 的永续多头产生【巨额未实现盈利】
      ⟹ ⭐ 这笔浮盈被计入可借额度

⑤ 用账户 A 借出 Mango 金库里几乎【所有资产】
      —— USDC、SOL、BTC、ETH 等,约 1.14 亿美元

⑥ 提走。价格随后回落。

⚠️ 为什么清算救不了

价格回落后,账户 A 的浮盈消失,它资不抵债。
⭐ 按设计,清算人应该来清算它并获得折扣。

⚠️ 但清算人清算它,拿到的是
   【一个巨额的、无法平仓的 MNGO 空头】——
   因为 MNGO 市场根本没有那么多流动性去接盘。

⟹ ⭐ 清算变成了一件【亏钱的事】,于是没有人来清算。
⟹ 坏账留在了协议里,由所有储户承担。

⭐⭐ 这一点值得单独记住:清算机制的有效性,取决于被清算资产的市场深度。 一个在纸面上"抵押充足"的系统,如果它的抵押品卖不掉,清算就只是一个无法执行的承诺。

四、荒诞的结局

攻击者随后在 Mango 的【治理系统】里发起提案:
   "我归还一部分资金,条件是协议不追究法律责任。"

⭐ 而他用来投票支持这个提案的,
   正是他刚刚从协议里取走的 MNGO 代币。

⟹ 提案以压倒性票数通过。

⚠️ 这暴露了两件事:

① 治理代币被攻击者持有 ⟹ 治理本身被接管
   (和[第 17 篇](https://blog.ifcalm.org/posts/security/blockchain/17-beanstalk/)的 Beanstalk 是同一个结构)

② ⭐ 协议层的"结果"最终由【链外因素】决定 ——
   这次是法律。攻击者后来被起诉并定罪,
   他公开主张的"这是合法的交易策略"没有被法院接受。

对写代码的人来说,这里的教训不是法律,而是

不要把"事后有人会追究"当成安全设计的一部分。 你的协议必须在"攻击者永远不会被抓"的假设下依然安全。

五、防御

① 浮盈不能全额计入抵押品

⭐ 分档折算(haircut):

   已实现的稳定币余额        ⟹ 100% 计入
   蓝筹资产的市值            ⟹ 80%  计入
   未实现盈利                ⟹ ⚠️ 打大折,或【完全不计入】
   ⭐ 协议自身代币的浮盈      ⟹ 计入 0

② 按市场深度设置敞口上限

// ⭐ 单个资产的总敞口,不得超过它在市场上的可承受深度
require(totalExposure[asset] <= maxExposure[asset], "asset cap");

maxExposure 应该由"能在不造成 X% 滑点的前提下卖出多少"决定,而不是拍脑袋。这是一个需要定期重新评估的参数——市场深度会变。

③ 自己的代币不能做自己的抵押品

⭐⭐ 这是一条几乎没有例外的规则。

⚠️ 用协议自身代币做抵押,制造了一个死亡螺旋:

   协议出问题 ⟹ 代币下跌 ⟹ 抵押品贬值
             ⟹ 更多清算 ⟹ 抛压 ⟹ 代币继续下跌
             ⟹ ⭐ 正反馈,直到归零

⟹ 而在正常时期,这个风险完全不可见 ——
   它只在你最需要抵押品有价值的那一刻失效。

④ 清算可行性必须被验证

⭐ 对每一个抵押品,定期回答:

   □ 如果我要在【一小时内】清掉全部该资产的敞口,
     滑点是多少?
   □ ⚠️ 清算人拿到这些抵押品之后,
     他能不能【卖得掉】?
   □ 清算折扣是否足以覆盖他的平仓滑点?

⟹ 如果答案是"不能",那么这个抵押品的清算机制
   ⭐ 只是一个【无法执行的承诺】。

⑤ 预言机要考虑标的深度

⭐ 一个资产的预言机质量,上限由它的市场深度决定。

⟹ 上架前算一遍:
   "把这个价格拉高 2 倍需要多少钱?"
   ⚠️ 如果这个数小于协议里该资产的敞口 ——
      不要上架,无论用多好的预言机。

⟹ 这一条和[第 12 篇](https://blog.ifcalm.org/posts/security/blockchain/12-bzx/)的结论是同一条,
   ⭐ 但 Mango 的教训是:它同样适用于【外部 CEX 市场】,
      不只是链上池子。

六、⭐ 举一反三

核心命题一:抵押品的价值 ≠ 抵押品的可变现价值

⭐⭐ 清算是一个需要有人在另一头接盘的操作。 没有买家的抵押品,它在链上的价格是多少都无所谓。

同一个盲区的其它形态:

   · NFT 借贷:地板价 100 ETH,但挂单深度只有 2 个
   · 长尾代币抵押:链上有报价,实际卖 10 万就砸穿
   · ⭐ LP 代币抵押:赎回时的滑点没被计入估值
   · 流动性质押代币:脱锚时无法按 1:1 赎回
   · 极端行情:⚠️ 所有抵押品的相关性同时趋近 1,
     "分散化"的抵押品组合一起崩塌

核心命题二:多层依赖的复合脆弱性

⭐ 把你系统里的"决定资金去向"的量,画成依赖链:

   可借额度 ⟸ 浮盈 ⟸ 标记价 ⟸ 外部现货价 ⟸ 市场深度

⚠️ 链条越长,越容易出现"每一层单独看都合理"的情况。
   ⭐ 而攻击者只需要攻击【最弱的那一层】,
      收益却按【最上层】的规模计算。

⟹ 检查方法:
   对链条上每一环问"操纵它要多少钱",
   然后和【最上层能撬动的资金】比较。

Mango 的例子里:操纵最底层需要几百万,撬动的是一亿多。 这个比例本身就是警报。

核心命题三:循环依赖

⚠️ 凡是出现 "A 的安全性依赖 B,而 B 的价值依赖 A" 的地方,
   都是一个潜在的死亡螺旋:

   · 协议自身代币做抵押品(本篇)
   · 用自己发行的稳定币作为储备
   · 用自己的 LP 代币做抵押
   · ⭐ 保险基金里装的是自己的治理代币
   · 治理代币质押量决定安全性,而安全性决定代币价格

⭐ 检查方法:画出资产依赖图,找环。
   ⚠️ 任何环都是问题,无论看起来多小。

核心命题四:不要依赖事后追责

⭐ 攻击者被起诉定罪,这不是你的安全设计的一部分。

设计协议时的正确假设:
   □ 攻击者是匿名的
   □ 他在一个你无法触及的司法辖区
   □ ⚠️ 他不在乎名誉
   □ 他有无限的耐心和充足的资金(闪电贷)
   □ ⭐ 他永远不会被抓

⟹ 在这个假设下仍然安全的设计,才是安全的设计。

七、本案小结

  • 攻击者的每一步在协议规则内都合法:开多头、买现货、按规则借款。没有任何一行代码被绕过。
  • ⭐⭐ 三层依赖叠加:可借额度 ⟸ 未实现盈利 ⟸ MNGO 标记价 ⟸ 一个极薄的外部现货市场。每一层单独看都合理,叠起来之后借空协议只需要拉高一个小市值代币的价格。
  • ⚠️ MNGO 是 Mango 自己的治理代币,形成循环:协议的偿付能力依赖自己代币的价格,而代币价格又依赖协议是否有偿付能力。 -⭐ 清算救不了:清算人拿到的是一个无法平仓的巨额 MNGO 空头,因为市场没有那么多流动性接盘——清算变成了亏钱的事,于是没人来清算。坏账由储户承担。 -⭐ 清算机制的有效性取决于被清算资产的市场深度。 抵押品卖不掉时,清算只是一个无法执行的承诺
  • 攻击者用赃款在治理系统里投票决定是否归还,提案压倒性通过——治理代币被攻击者持有意味着治理本身被接管。 -⭐ 不要把"事后有人会追究"当成安全设计的一部分。 正确假设是:攻击者匿名、在你触及不到的辖区、不在乎名誉、有闪电贷、永远不会被抓。在这个假设下仍安全的设计,才是安全的设计。
  • 防御:浮盈打大折或不计入、按市场深度设敞口上限、自己的代币不能做自己的抵押品、定期验证"清算人能不能真的卖掉"。
  • 多层依赖的复合脆弱性:攻击最弱的一环(几百万),收益按最上层计算(一亿多)——这个比例本身就是警报
  • 凡是出现"A 的安全性依赖 B,而 B 的价值依赖 A"的地方都是死亡螺旋。 画出资产依赖图,找环。

思考题

  1. 为什么攻击者要用两个账户对敲?只用一个账户做多行不行?
  2. 精确说明"未实现盈利被计入抵押品"这个设计在什么条件下是安全的、什么条件下不是。
  3. 为什么清算人不愿意清算账户 A?请从清算人的收益计算说明。
  4. “清算是一个需要有人在另一头接盘的操作。“请再举两个"链上有价格但卖不掉"的抵押品例子。
  5. 设计一个抵押品折算表(haircut),为五类资产分别定折算率,并说明依据。
  6. maxExposure[asset] 应该怎么定?请给出一个可计算的定义,并说明多久要重新评估一次。
  7. 画出 Mango 的资产依赖图,指出其中的环。如果禁止 MNGO 做抵押品,这个环会消失吗?
  8. “操纵最底层需要几百万,撬动的是一亿多。“请对你熟悉的协议做一次同样的计算。
  9. 攻击者用赃款投票通过了对自己有利的提案。请设计一个机制防止这种情况(提示:投票权快照的时点)。
  10. 在"攻击者永远不会被抓"的假设下重新评估你的协议。有哪些设计其实隐含依赖了事后追责?