Abracadabra 平台的 GmxV2 CauldronV4 合约由于不完善的抵押品计算方式遭受了 1300 万美元的攻击。攻击者通过操纵失败的订单,绕过了偿付能力检查,从而提取了资金。文章详细分析了攻击的原理、流程,以及如何通过修改合约代码来防止此类攻击,并总结了此次攻击事件的影响。
由于有缺陷的抵押品计算,一起 1300 万美元的漏洞攻击了 Abracadabra 的 GmxV2 CauldronV4。以下是攻击者如何使用失败的订单绕过偿付能力检查的。
深入剖析 Abracadabra GMX V2 的 1300 万美元漏洞
Abracadabra.money 是一个允许用户存入计息代币 (ibTKNs) 作为抵押品来借入 Magic Internet Money (MIM)(一种与美元Hook的稳定币)的平台。通过允许这些 ibTKNs(例如,yVault 代币)作为抵押品,用户可以解锁额外的资金,这些资金以前锁定在产生收益的位置中。
在底层,Abracadabra 由基于 BentoBox 技术的模块化智能合约组成。这种架构提供了基本功能,例如非托管金库(通过 DegenBox)、隔离的借贷市场 (Cauldrons) 和收益策略,这些策略可以从存储的抵押品中赚取利息。当用户将抵押品存入特定的 Cauldron 时,他们可以借入 MIM。如果他们的抵押品价值低于某个阈值,清算人可以偿还用户的债务,以换取没收抵押品。
这些合约共同使 Abracadabra 能够创建多个具有自定义参数的隔离借贷市场,同时确保 MIM 稳定币始终有抵押品支持。
为了支持 GMX V2(一种链上永久 DEX),Abracadabra 引入了一种专门的 GmxV2 CauldronV4。GMX V2 具有非原子性的存款和取款:用户下订单,然后由 keeper 或外部回调最终执行它。以下是让 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 的标准 Cauldron 架构之间的差距。但是,正如漏洞攻击中所看到的,如果系统在部分清算或取款后没有仔细更新“待处理的抵押品”,攻击者可能会在表面上显得有偿付能力,同时在现实中耗尽协议。
gmCauldronV2
合约已于 2023 年 11 月 14 日 由 Guardian Audits 在一份报告中进行了审查(审计 PDF)。审计发现大量问题,包括 4 个严重/高严重性发现和 10 个中等严重性发现——这表明代码库需要进一步完善。
重要的是,这是合约部署之前进行的唯一审计。虽然 Guardian 专业地完成了他们的范围,但在架构发生重大变化后,没有进行后续审计
发现了如此多的漏洞,不仅建议进行第二次审计,而且绝对有必要。
随后的漏洞攻击并非由审计疏忽引起,而是由于协议团队未能寻求集成更改后的更深入验证。
Abracadabra 的 GmxV2 CauldronV4 旨在处理 GMX V2 的非原子存款模型,用户可以在其中创建稍后执行的存款订单。当存款成功时,GM 代币会记入用户的抵押品。但是,当存款失败时——例如,由于无法访问的 minOut
——GMX 合约会将原始代币(例如,USDC)返回给 RouterOrder。在这种情况下,Cauldron 仍然认为这些返回的代币是有效的抵押品。
攻击者通过故意强制存款失败来利用这一点,从而在 RouterOrder 合约中创建“幻影抵押品”。orderValueInCollateral()
函数继续报告失败存款的全部价值,即使在从 RouterOrder 中提取真实的资金后,Cauldron 也没有将该订单标记为已关闭。
以下序列概述了攻击者在执行漏洞利用之前的设置。
0xf29120acd274a0c60a181a37b1ae9119fe0f1c9c
cook()
交易。cook()
都包含故意失败的 GMX 存款,然后是清算和重新借款序列。到活动结束时,攻击者通过大约 100 分钟内的 56 笔交易,从五个 GM Cauldrons 中耗尽了 ~1340 万美元。
完整的活动日志可通过死后分析中的官方 Google Sheet 查看。
该漏洞的核心在于 GmxV2CauldronV4
及其关联的 GmxV2CauldronRouterOrder
合约中的两个关键设计缺陷:
sendValueInCollateral](<https://github.com/Abracadabra-money/abracadabra-money-contracts/blob/dff69a19a219bbff90ab7b752c9f9c0ab5e8fe6f/src/periphery/GmxV2CauldronOrderAgent.sol#L241>)()
函数在清算期间从路由中删除真实的代币,但不更新内部状态变量,例如 inputAmount
、minOut
或 minOutLong
。这种遗漏意味着 Cauldron 认为仍然存在相同数量的“潜在抵押品”,即使在其中一些抵押品被提取之后。1function sendValueInCollateral(address recipient, uint256 shareMarketToken) public onlyCauldron {
2 (uint256 shortExchangeRate, uint256 marketExchangeRate) = getExchangeRates();
3
4 uint256 amountShortToken = (degenBox.toAmount(IERC20(market), shareMarketToken, true) * oracleDecimalScale) /
5 (shortExchangeRate * marketExchangeRate);
6
7 shortToken.safeTransfer(address(degenBox), amountShortToken);
8 degenBox.deposit(IERC20(shortToken), address(degenBox), recipient, amountShortToken, 0);
9 }
orderValueInCollateral() 函数继续使用那些未更改的内部字段来计算用户的待处理抵押品。由于这些值在发生部分清算或存款失败时从未减少,因此用户看起来持有的抵押品比实际持有的要多——从而实现了欺诈性借贷。
1 function orderValueInCollateral() public view returns (uint256 result) {
2 (uint256 shortExchangeRate, uint256 marketExchangeRate) = getExchangeRates();
3
4 if (depositType) {
5 uint256 marketTokenFromValue = (inputAmount * shortExchangeRate * marketExchangeRate) / oracleDecimalScale;
6 result = minOut < marketTokenFromValue ? minOut : marketTokenFromValue;
7 } else {
8 uint256 marketTokenFromValue = ((minOut + minOutLong) * shortExchangeRate * marketExchangeRate) / oracleDecimalScale;
9 result = inputAmount < marketTokenFromValue ? inputAmount : marketTokenFromValue;
10 }
11 }
攻击者在通过 GMX 存入抵押品时,以不切实际的 minOut
值发起 cook()
调用。这会导致 GMX 拒绝存款并将输入代币(例如,USDC)返回给 RouterOrder 合约。尽管收回了代币,但由于未更改的会计字段,Cauldron 仍然认为此失败的存款已成功。
攻击者使用 cook()
来借入少量 MIM,用于资助将在漏洞利用中稍后回收的抵押品。
精确的 cook()
步骤 第二个 cook()
调用包含多个操作:
get_before_liquidate_amount
) 来计算在清算期间应提取多少抵押品以实现最大提取。sendValueInCollateral()
从攻击者的 RouterOrder 中提取额外的代币。这些代币是真正的 USDC,已从失败的 GMX 存款中退回。get_after_liquidate_amount
) 来计算现在可以根据未更改的幻影抵押品借入多少 MIM。orderValueInCollateral()
报告的抵押品支持。
在 cook()
批处理结束时,Cauldron 调用 _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()
并正确跟踪已关闭或已使用的订单。
1 function sendValueInCollateral(address recipient, uint256 shareMarketToken) external onlyCauldron {
2 (uint256 shortExchangeRate, uint256 marketExchangeRate) = getExchangeRates();
3
4 uint256 amountShortToken =
5 (degenBox.toAmount(IERC20(market), shareMarketToken, true) * oracleDecimalScale)
6 / (shortExchangeRate * marketExchangeRate);
7
8+ // Example approach: Decrement the "inputAmount" or "minOut"
9+ // so that orderValueInCollateral() won't keep overstating the user's collateral
10+ if (depositType) {
11+ inputAmount = (inputAmount >= someEquivalentShort) ? (inputAmount - someEquivalentShort) : 0;
12+ if (minOut > someEquivalentShort) minOut -= someEquivalentShort;
13+ } else {
14+ // Similar logic adjusting (minOut + minOutLong) or inputAmount
15+ }
16
17 shortToken.safeTransfer(address(degenBox), amountShortToken);
18 degenBox.deposit(IERC20(shortToken), address(degenBox), recipient, amountShortToken, 0);
19 }
20
其他最佳实践:
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 万美元的攻击,其中攻击者操纵内部会计逻辑,使其在通过自我清算提取实际抵押品的同时显得有偿付能力。
通过一系列精心构建的 cook()
交易,从该协议中提取了大约 1300 万美元的加密资产(≈6,262 ETH)。
否。该漏洞利用不涉及预言机操纵。偿付能力计算中使用的价格是准确的;问题在于 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/expl...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!