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