本文详细分析了Abracadabra平台的GMX V2 CauldronV4漏洞,攻击者利用内部分数值未正确更新的设计缺陷,导致平台损失超过1300万美元。通过复杂的交易结构,攻击者能够在未实际提供足够抵押的情况下提取资金。文章探讨了这次攻击的机制、影响及预防建议。
Abracadabra.money 是一个平台,允许用户 存入带息代币 (ibTKNs) 作为抵押来借贷 Magic Internet Money (MIM),这是一种与美元Hook的稳定币。通过允许这些 ibTKNs(例如 yVault 代币)作为抵押,用户可以 解锁先前存入产生收益的头寸中的额外资本。
在其底层,Abracadabra 是由模块化智能合约和 BentoBox 技术构成的。这种架构提供了基本功能,比如 非托管的保管(通过 DegenBox)、隔离的借贷市场(Cauldrons - 直意为“锅”)和 收益策略,可以对存储的抵押品产生利息。当用户在特定Cauldrons中存入抵押品时,他们可以基于此借贷 MIM。如果他们的抵押品价值低于某个阈值,清算人可以偿还用户的债务以换取夺取抵押品。
这些合约共同允许 Abracadabra 创建多个隔离的借贷市场,每个市场具有自定义参数,同时确保 MIM 稳定币始终由抵押品 支持。
为了支持 GMX V2(一个链上的永久性去中心化交易所),Abracadabra 引入了一个专门的 GmxV2 CauldronV4。GMX V2 具有 非原子的 存款和提款:用户下达订单,最终由保管人或外部回调来执行。以下是使 Abracadabra 能够处理这一异步流程的关键调整:
afterDepositExecution()
或 afterWithdrawalCancellation()
。_isSolvent()
,以计算用户在 RouterOrder 中的“待处理”抵押品。RouterOrder 合约中的 orderValueInCollateral()
函数返回用户在“运输中”实际上拥有多少代币。cook()
函数可以将存款抵押、借款 MIM、交换等操作捆绑在一起。ACTION_CREATE_ORDER
、ACTION_WITHDRAW_FROM_ORDER
、ACTION_CANCEL_ORDER
)来管理 GMX 存款/提款订单。cancelOrder()
,强行从 RouterOrder 提取 USDC 或 GMX 代币。closeOrder()
函数将用户存储的 RouterOrder 地址设为零,最终化被强行关闭的位置。liquidate()
,可能会取消任何未完成的订单(归还剩余的 USDC)或扣押刚到账的 GM 代币。在实践中,这种方法 桥接了 GMX V2 的异步设计 与 Abracadabra 的标准Cauldrons架构。然而,正如在漏洞中所见,如果系统 不 在部分清算或提款后仔细更新“待处理抵押品”,攻击者可能在纸面上看似偿债,而实际上却在耗尽协议。
gmCauldronV2
合约于 2023年11月14日 由 Guardian Audits 进行了审查(审计 PDF)。审计发现了大量问题,包括 4 个关键/高 严重性发现和 10 个中等 严重性发现——这表明代码库需要进一步完善。
重要的是,这是 在合约部署之前进行的唯一审计。虽然 Guardian 专业地完成了他们的审计范围,但在重大架构更改后没有进行 后续审计。
发现如此多的漏洞,第二次审计不仅仅是建议 - 而是绝对必要的。
随后的漏洞并不是因为审计疏忽引起的,而是由于 协议团队未能在集成更改后寻求更深入的验证。
Abracadabra 的 GmxV2 CauldronV4 被设计用于处理 GMX V2 的非原子存款模型,用户创建的存款订单会在稍后执行。当存款成功时,GM 代币被记入用户的抵押品。然而,当存款失败时——例如,由于无法达到 minOut
——GMX 合约会 返回原始代币(例如 USDC)到 RouterOrder。在这种情况下,Cauldrons仍然将这些返回的代币视为有效抵押品。
攻击者利用此点,故意强制存款失败,在 RouterOrder 合约中创建“幽灵抵押品”。orderValueInCollateral()
函数继续报告失败存款的全额价值,而Cauldrons在提取真实资金后并未将订单标记为已关闭。
以下顺序概述了攻击者在执行攻击之前的设置。
0xf29120acd274a0c60a181a37b1ae9119fe0f1c9c
cook()
交易。cook()
包括故意失败的 GMX 存款,随后是清算和重新借款的序列。到活动结束时,攻击者在大约100分钟内通过56笔交易抽走了 约1340万美元。
完整活动日志 可通过官方的 事后分析 Google 表格 查阅。
从根本上看,该漏洞源于 GmxV2CauldronV4
及其相关 GmxV2CauldronRouterOrder
合约中的 两个关键设计缺陷:
inputAmount
、minOut
或 minOutLong
。这一遗漏导致Cauldrons认为仍然存在相同数量的“潜在抵押品”,即使部分已被提取。function sendValueInCollateral(address recipient, uint256 shareMarketToken) public onlyCauldron {
(uint256 shortExchangeRate, uint256 marketExchangeRate) = getExchangeRates();
uint256 amountShortToken = (degenBox.toAmount(IERC20(market), shareMarketToken, true) * oracleDecimalScale) /
(shortExchangeRate * marketExchangeRate);
shortToken.safeTransfer(address(degenBox), amountShortToken);
degenBox.deposit(IERC20(shortToken), address(degenBox), recipient, amountShortToken, 0);
}
orderValueInCollateral() 函数继续使用那些未更改的内部字段计算用户的待处理抵押品。由于这些值在部分清算或失败存款发生时从未减少,因此用户似乎持有比实际更多的抵押品——这使得欺诈性借贷成为可能。
function orderValueInCollateral() public view returns (uint256 result) {
(uint256 shortExchangeRate, uint256 marketExchangeRate) = getExchangeRates();
if (depositType) {
uint256 marketTokenFromValue = (inputAmount * shortExchangeRate * marketExchangeRate) / oracleDecimalScale;
result = minOut < marketTokenFromValue ? minOut : marketTokenFromValue;
} else {
uint256 marketTokenFromValue = ((minOut + minOutLong) * shortExchangeRate * marketExchangeRate) / oracleDecimalScale;
result = inputAmount < marketTokenFromValue ? inputAmount : marketTokenFromValue;
}
}
攻击者在通过 GMX 存入抵押品时,以 不切实际的 minOut
值发起 cook()
调用。这导致 GMX 拒绝存款并将输入代币(例如,USDC)返回到 RouterOrder 合约。尽管收回了代币,但Cauldrons仍将该失败存款视为已成功,由于未更改的会计字段。
攻击者使用 cook()
借取少量 MIM,用于资助将在漏洞中循环使用的抵押品。
确切的 cook()
步骤 第二个 cook()
调用包含多个操作:
get_before_liquidate_amount
)计算在清算期间需要拉取多少抵押品以实现最大提取。sendValueInCollateral()
从攻击者的 RouterOrder 中提取额外代币。这些代币是来自于失败 GMX 存款返回的真实 USDC。get_after_liquidate_amount
)以计算现在可以借多少 MIM 基于未更改的幽灵抵押品。orderValueInCollateral()
报告的抵押品支持。
在 cook()
批次结束时,Cauldrons调用 _isSolvent()
确保用户在所有操作后仍然保持偿债能力。然而:结果是,协议仍然根据过时的内部状态视用户为偿债能力,交易不会回滚。攻击者带着 借来的 MIM 和 在清算期间被扣押的真实抵押品 走了,而仍然看似完全抵押。_isSolvent()
在 cook()
批次结束时运行并错误通过,因为它使用了未更改的 orderValueInCollateral()
。
_isSolvent()
依赖于来自 RouterOrder 的 orderValueInCollateral()
,inputAmount
、minOut
或 minOutLong
报告 原始、虚高的抵押价值,因此,协议仍然根据过时的内部状态视用户为偿债能力,交易不会回滚。
攻击者带着 借来的 MIM 和 在清算期间被扣押的真实抵押品 走了,而仍然看似完全抵押。
_isSolvent()
在 cook()
批次结束时运行并错误通过,因为它使用了未更改的 orderValueInCollateral()
。
重复并抽干 攻击者可以在不同的Cauldrons和钱包中重复此模式,每次都在基于无效内部会计通过偿债检查时抽走真实资产。
攻击者通过在一次大 cook()
调用中重复执行漏洞步骤,净赚了约932 ETH。
以下是一个 diff,说明如何修补 sendValueInCollateral()
以减少用户的“纸”抵押字段。
注意:此代码片段仅为说明。在实际生产场景中,正确修复漏洞需要 对 RouterOrder 的内部会计逻辑有深入的了解,包括
inputAmount
、minOut
和minOutLong
如何在所有流程(例如存款、清算和取消)中使用。全面的修复还需要更新orderValueInCollateral()
并正确跟踪关闭或消耗的订单。
function sendValueInCollateral(address recipient, uint256 shareMarketToken) external onlyCauldron {
(uint256 shortExchangeRate, uint256 marketExchangeRate) = getExchangeRates();
uint256 amountShortToken =
(degenBox.toAmount(IERC20(market), shareMarketToken, true) * oracleDecimalScale)
/ (shortExchangeRate * marketExchangeRate);
// 示例方法:减少 "inputAmount" 或 "minOut"
// 以使 orderValueInCollateral() 不会继续高估用户的抵押品
if (depositType) {
inputAmount = (inputAmount >= someEquivalentShort) ? (inputAmount - someEquivalentShort) : 0;
if (minOut > someEquivalentShort) minOut -= someEquivalentShort;
+ } else {
+ // 类似逻辑调整 (minOut + minOutLong) 或 inputAmount
+ }
shortToken.safeTransfer(address(degenBox), amountShortToken);
degenBox.deposit(IERC20(shortToken), address(degenBox), recipient, amountShortToken, 0);
}
额外的最佳实践:
cook()
函数在“批量”执行多个操作,可以在每个主要步骤后,而不仅仅是在最后调用 _isSolvent()
。发现漏洞后,Abracadabra 实施了紧急对策并启动恢复工作:
orderAgent
地址设置为 0x000…000
,以防止继续创建 GMX 存款。注意:这并不是 Abracadabra 架构第一次被利用。在 2024年2月,其 CauldronV4 债务会计机制 的早期漏洞被利用提取了超过 $640 万。
我们的 Three Sigma 团队发布了对此事件的详细分析,涵盖了如何通过股份通货膨胀操纵内部借贷逻辑。
0x625Fe79547828b1B54467E5Ed822a9A8a074bD61
0xe9A4034E89608Df1731835A3Fd997fd3a82F2f39
(通过 Tornado Cash 在以太坊上资金,参与 GMX + Cauldrons)0xaf9e33aa03caaa613c3ba4221f7ea3ee2ac38649
(通过 Tornado Cash 融资,跨 Stargate 桥接 ETH,主网洗钱资金)Abracadabra GMX 漏洞指的是对协议的 GmxV2 CauldronV4 的 1300 万美元攻击,其中攻击者操纵内部会计逻辑,使其在自我清算中看似偿还,同时提取真实抵押品。
大约损失了价值约 1300 万美元的加密资产(≈6262 ETH)通过一系列精心构建的 cook()
交易被从协议中盗取。
没有。该漏洞并未涉及预言机操纵。偿债能力计算中使用的价格是准确的;问题在于 RouterOrder 合约中的过时抵押会计。
GmxV2 CauldronV4 是 Abracadabra 上的专用借贷市场合约,支持 GMX V2 的异步存款和提款系统,允许用户根据 GMX LP 头寸借贷 MIM。
攻击者利用 orderValueInCollateral()
在部分清算后从未更新的事实。他们借取 MIM,自我清算,提取真实资金,且在 cook()
调用结束时仍然通过了 _isSolvent()
。
cook()
函数是什么?cook()
是一个批处理函数,允许用户在一笔交易中执行多个操作(例如,存款、借款、提款、清算)。偿债能力只在最后进行检查,这在攻击中发挥了关键作用。
sendValueInCollateral()
函数从 RouterOrder 中移除了真实代币,但未减少 inputAmount
和 minOut
等内部值。这导致 orderValueInCollateral()
返回虚高的数字。
Abracadabra 停止了受影响市场的借贷,与 Chainalysis 和其他合作伙伴协作追踪资金,并为安全归还被盗资产提供了 20% 的悬赏(约 258 万美元)。
DeFi 协议应该严格测试涉及异步资产流的边缘情况,确保内部会计与实际代币流动相符,并在偿债能力检查时避免仅依赖乐观的“纸抵押品”。
- 原文链接: threesigma.xyz/blog/abra...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!