在过去一周(2026/03/09 - 2026/03/15),BlockSec 检测并分析了八起攻击事件,估计总损失约为 166 万美元。下表总结了这些事件,详细分析将在以下各节中提供。
| 日期 | 事件 | 类型 | 估计损失 |
|---|---|---|---|
| 2026/03/09 | EtherFreakers 事件 | 业务逻辑缺陷 | ~$2.5万 |
| 2026/03/10 | Alkemi 事件 | 业务逻辑缺陷 | ~$8.9万 |
| 2026/03/10 | MT 事件 | 业务逻辑缺陷 | ~$24.2万 |
| 2026/03/11 | AAVE 清算事件 | 配置错误 | ~$101万 |
| 2026/03/11 | Planet Finance 事件 | 业务逻辑缺陷 | ~$1万 |
| 2026/03/12 | AM 事件 | 业务逻辑缺陷 | ~$13.1万 |
| 2026/03/12 | DBXen 事件 | 业务逻辑缺陷 | ~$14.9万 |
| 2026/03/15 | Goose Finance 事件 | 业务逻辑缺陷 | ~$8千 |
EtherFreakers 事件
简要概述
2026 年 3 月 9 日,以太坊上的 NFT 游戏 EtherFreakers 因双重计费错误被攻击,造成约 2.5 万美元的损失。游戏中的每个 NFT 都持有一个可提取的 ETH 余额(称为“能量”)。作为游戏机制,玩家可以使用 attack() 使一个 NFT 捕获另一个 NFT 并获取目标 NFT 的能量。然而,该合约在结算其会计之前,就支付了目标的余额并转移了 NFT。然后,一个转移钩子读取过时、预支付数据,并将其一部分反馈到一个全局分红池中,从而在没有新的 ETH 支持的情况下膨胀了该池。攻击者循环使用此捕获机制来推高全局指数,然后从一批 NFT 中提取了膨胀的余额。
背景
EtherFreakers 是一款链上 NFT 游戏,其中每个 NFT(称为“Freaker”)都持有一个称为 energy 的可提取 ETH 余额。该系统的工作原理类似于分红池:当发生某些操作时,一部分 ETH 会按比例分配给所有 Freaker。每个 Freaker 的可提取 ETH 由全局累加器 freakerIndex 和每个代币的份额权重 fortune 共同跟踪。
具体来说,会计公式为:energyOf = basic + (freakerIndex - index) * fortune。当 _dissipateEnergyIntoPool(amount) 运行时,freakerIndex 会增加,将 amount 的 80% 分配给所有 Freaker,20% 分配给创作者。通过 charge() 直接存款只会增加 basic 而不会影响 freakerIndex。因此,freakerIndex 的增加应该总是由进入系统的真实以太支持。如果 freakerIndex 在没有相应 ETH 流入的情况下增长,Freaker 可以赎回比合约实际持有的更多的以太。
漏洞分析
根本原因是以太坊上的 EtherFreak 合约(0x3A27...c0f33)中执行顺序不正确。当捕获成功时,attack() 函数按以下顺序执行这些步骤:
- 第 237 行:将
targetCharge(目标 NFT 的全部能量)直接以 ETH 转账方式支付给防御者。能量现已消耗。 - 第 240 行:调用
_transfer(defender, capturer, targetId)来移动 NFT。内部,_transfer()会调用 ERC-721 钩子_beforeTokenTransfer(),该钩子会调用_dissipateEnergyIntoPool(),传入energyOf(targetId)的 0.1%。这是_dissipateEnergyIntoPool()的第一次调用,它读取了一个过时的值,因为第 5 步尚未发生。 - 第 241 行:显式调用
_dissipateEnergyIntoPool(sourceSpent)。这是第二次调用,是正常游戏逻辑的一部分。 - 第 244-251 行:更新
sourceId和targetId的energyBalances。
bug 存在于第 2 步:因为 energyBalances[targetId] 尚未更新,钩子仍然看到预支付的余额,并将已消耗的部分能量分散到分红池中。第 1 步的直接 ETH 支付和第 2 步的池输入都来自相同的能量,导致 freakerIndex 在没有新 ETH 支持的情况下膨胀。
freakerIndex 在调用 _dissipateEnergyIntoPool() 时会增长:
攻击分析
以下分析基于交易 0x89e24d...9abd2942。
-
步骤 1:借入
1,700WETH。 -
步骤 2:在攻击者控制的地址下铸造两个新的 Freaker,代币 ID 为 590 和 591。
-
步骤 3:反复调用游戏的
attack(590, 591)函数,并保持执行在成功的捕获分支。 -
步骤 4:每次成功后,将代币 591 转移回助手地址,以便重复使用同一对。
-
步骤 5:每次成功的循环都会使
freakerIndex膨胀,超出系统实际保留的以太。 -
步骤 6:在指数足够高后,赎回一批先前控制的 Freaker。代币 ID 496 到 520 每个都被赎回了
0.278052246002402082以太。 -
步骤 7:将消耗的以太包装成
WETH,偿还1,700WETH的闪电贷,并保留约7.498WETH作为利润。
结论
根本原因存在于 attack() 的成功捕获流程中:EtherFreakers 在目标代币的能量状态结算之前就支付了 targetCharge。然后 _transfer() 触发 _beforeTokenTransfer(),该函数读取了过时的预支付 energyOf(targetId) 并将其一部分分散到池中。这导致 freakerIndex 增加而没有新的以太支持,因此相同的目标能量既被计为支付,也被计为池输入。这是一个业务逻辑膨胀 bug,而不是重入 bug。
为了减少未来发生类似风险:
-
避免在同一交易仍在结算时,在转移钩子中从可变状态重新计算经济价值。
-
如果转移钩子读取状态变量,请确保执行顺序不会影响结果(例如,在钩子运行之前结算状态,而不是之后)。
Alkemi 事件
简要概述
2026 年 3 月 10 日,Alkemi 协议在以太坊上被攻击,造成约 8.9 万美元的损失。根本原因是会计错误和业务逻辑缺陷。有缺陷的清算逻辑允许任何人通过同一笔交易清算自己的头寸并从中获利。此外,会计错误导致攻击者在清算期间自身的抵押品扣除被覆盖,使攻击者能够在不承担预期成本的情况下获得清算奖励。
背景
Alkemi 是一个借贷协议。当借款人的头寸变得抵押不足时,任何人都可以调用 liquidateBorrow() 来偿还部分债务并以折扣价获取抵押品。为防止过度清算,协议将每笔交易的偿还金额限制在以下三个值中的最小值:
- 借款人的当前借款余额(
currentBorrowBalance_TargetUnderwaterAsset)。 - 借款人抵押品在应用清算折扣后可覆盖的最大还款额(
calculateDiscountedBorrowDenominatedCollateral())。 - 将账户恢复到清算边界所需的还款额(
calculateDiscountedRepayToEvenAmount()),仅在市场isSupported时检查。
漏洞分析
根本原因是 Alkemi 协议(0x4822...a888)中的业务逻辑缺陷和会计错误。由于只要借款人有未偿债务,currentBorrowBalance_TargetUnderwaterAsset 必然大于 0,并且只要借款人有抵押品,calculateDiscountedBorrowDenominatedCollateral() 返回的值也必然大于 0,因此 AlkemiEarnPublic 协议有效地依赖于 calculateDiscountedRepayToEvenAmount() 来确定给定的贷款是否可以被清算。在此函数中,需要清算的债务金额应根据称为 accountShortfall_TargetUser 的变量计算。
然而,在实际实现中,该函数使用了全局变量 closeFactorMantissa 来计算允许偿还的债务金额的上限,并返回此值。结果是,攻击者能够借款并在同一笔交易中立即清算自己的头寸。
此外,在 liquidateBorrow() 函数中,当清算人和借款人是同一地址时,变量 supplyBalance_TargetCollateralAsset 和 supplyBalance_LiquidatorCollateralAsset 指向相同的存储槽。然后,该函数分别基于相同的初始余额计算“缩减余额”和“奖励余额”,随后按顺序将它们写回同一个存储槽。由于先写入缩减余额,后写入奖励余额,因此缩减效果被覆盖并丢失,只留下奖励结果。这使得攻击者可以进一步放大其利润。
攻击分析
以下分析基于交易 0xa170...6d9d。
-
步骤 1:攻击者从 Balancer 处获得了闪电贷
51e18WETH。 -
步骤 2:攻击者将
51e18WETH解包为ETH并将其提供给 Alkemi 协议。 -
步骤 3:攻击者从 Alkemi 借入
39.5e18ETH。 -
步骤 4:攻击者使用
39.5395e18ETH清算了自己的头寸。 -
步骤 5:攻击者从 Alkemi 提取了
93.5e18ETH。 -
步骤 6:攻击者偿还了闪电贷,并获得了
43.4e18ETH的利润。
结论
根本原因是清算逻辑缺陷允许攻击者通过在同一笔交易中借款然后清算自己的头寸来获利,而错误的会计逻辑进一步放大了攻击者的收益。
为了减少未来发生类似风险:
- 对于借款人和清算人的余额更新,协议应直接操作存储变量,而不是将余额复制到临时内存变量中进行单独计算和写回。
MT 事件
简要概述
2026 年 3 月 10 日,BNB Chain 上的通缩代币 MT Token 被攻击,造成约 24.2 万美元的损失。根本原因是交易限制逻辑缺陷与特殊转账条件处理不一致。在通缩阶段,当池中储备金超过固定阈值时,合约会限制买入操作。然而,合约将确切金额的转账(例如 2e17 MT)视为推荐绑定操作,允许攻击者绕过买入限制并获取初始代币。此外,限制逻辑依赖于不完整的路径检测 (isBuy),未能涵盖间接的兑换路线,例如 Pair 到 Router,而白名单检查进一步绕过了关键验证。攻击者在不触发限制或费用的情况下累积了 MT 代币,通过受控的流动性操作和交易操纵了 pendingBurnAmount,并将池推入一种代币价格被人为抬高的异常状态。
背景
MT Token 是 BNB Chain 上的一种通缩代币,具有内置的交易限制。在通缩阶段,当池中 MT 储备金超过 21,000e18 时,合约会阻止买入操作。一旦储备金降至该阈值以下,通缩阶段结束,买入将被重新启用。MT Token 还包含一个推荐机制:转账恰好 2e17 MT 或 1e17 MT 被视为推荐绑定操作,而非正常交易。
漏洞分析
根本原因是 MT 合约(0x037E...b449)中存在缺陷的买入限制设计。在正常情况下,攻击者在受限阶段不应能够获取 MT 作为种子资金。然而,该合约将恰好 2e17 MT 的转账视为推荐绑定操作而非买入,这允许攻击者购买 2e17 MT,同时绕过买入限制。
此外,交易限制依赖于 isBuy 分支来阻止买入,但它不包括“Pair 到 Router”路径。由于 Router 和 Pair 都在白名单地址中,此类转账在白名单检查处被绕过,永远无法到达买入限制逻辑,从而允许攻击者通过将买入路由到 Router 并随后通过移除流动性提取代币来获取 MT。
攻击分析
以下分析基于交易 0xfb57...fca6。
-
步骤 1:攻击者闪电贷了约
358,681e18WBNB。 -
步骤 2:攻击者购买了
2e17MT,从而绕过了买入限制。 -
步骤 3:攻击者向 Pair 提供了
4e12WBNB和2e17MT以增加流动性。由于相同的原因,此次转账绕过了费用收取逻辑。 -
步骤 4:攻击者从 Pair 买入了约
10,000,000e18MT代币到 Router,从而绕过了买入限制和费用收取逻辑。 -
步骤 5:攻击者移除了其一半的流动性头寸,在此过程中提取了 Router 持有的所有
MT代币,然后将收回的MT出售以换取WBNB。在此步骤中,pendingBurnAmount被操纵至约9,000,000e18。 -
步骤 6:攻击者再次购买了约
10,000,000e18MT代币,将池中的MT储备金降低至约6,756,516e18,低于pendingBurnAmount。 -
步骤 7:攻击者移除了其剩余一半的流动性头寸,提取了购买的
MT代币,然后调用distributeDailyRewards()从池中销毁MT。结果,MT储备金减少到21,000e18。 -
步骤 8:攻击者将所有
MT兑换回约1,198e18WBNB,偿还了闪电贷,并最终获利。
结论
此次攻击是由于错误的交易限制引起的,该限制允许攻击者通过购买恰好 BINDING_AMOUNT 的 MT 代币来绕过买入禁令。在获取 MT 代币后,攻击者通过首先增加流动性,然后将 MT 买入 Router,最后移除流动性来收回代币,从而进一步绕过了费用收取逻辑和买入限制。攻击者随后通过卖出操作累积了 pendingBurnAmount 并执行了销毁,将池储备金推入异常状态,使其能够以被高估的价格出售 MT 并获利。
为了减少未来发生类似风险:
- 强制执行转账语义和交易逻辑之间的严格分离。
AAVE 清算事件
简要概述
2026 年 3 月 11 日,AAVE 在以太坊上发生了 2100 万美元的不正确清算,造成约 101 万美元的损失。根本原因是 wstETH 的预言机价格错误,导致原本健康的头寸变得抵押不足。因此,用户的头寸被清算,导致了财务损失。
背景
AAVE 使用预言机适配器为 wstETH 等包装资产定价。适配器 CAPO(Capped Price Oracle)通过将基础 ETH/USD 价格乘以转换比率(getRatio(),即一个 wstETH 等于多少 ETH)来推导 wstETH 的价格。为防止比率操纵,CAPO 应用基于快照的增长上限:
maxRatio = snapshotRatio + maxGrowthPerSecond x (currentTime - snapshotTimestamp)
并在定价期间限制 getRatio() 的输出(如果 currentRatio > maxRatio,则使用 maxRatio)。此机制有效地限制了比率及其导致的价格的最大上行漂移。
漏洞分析
根本原因是 CAPO 预言机锚定配置(0xe1D9...61Ef)中的时间和比率不匹配:设置了快照时间戳和快照比率,但快照比率被配置低于真实的 wstETH/ETH 比率。因此,适配器计算的 maxRatio 低于实时比率,并向下限制了 getRatio(),系统性地低估了 wstETH/USD 预言机的价格。这种压低的抵押品估值降低了使用 wstETH 作为抵押品的头寸的健康因子,导致本应健康的账户被错误地归类为不健康并被清算。
攻击分析
以下分析基于交易 0x9064...8a9c。
-
步骤 1:清算人闪电贷了约
6304e18WETH并清算了借款人。 -
步骤 2:清算人偿还了闪电贷,完成了清算。
结论
此次清算是由不正确的预言机价格配置引起的,该配置错误地将本应健康的借款人推入不健康状态,从而触发了其头寸的清算。
为了减少未来发生类似风险:
-
在每次更新之前,确保关键参数的正确性得到验证。
-
在实现中添加验证检查,以拒绝不正确的参数,并防止不正确的配置成功应用。
Planet Finance 事件
简要概述
2026 年 3 月 11 日,Planet Finance 在 BNB Chain 上被攻击,估计损失约为 1 万美元。根本原因是协议错误地将借款人存储的借款余额的增加视为应计利息,允许攻击者反复借款并触发折扣结算以低估其记录的债务。
背景
Planet Finance 是一个借贷协议,允许借款人享受利息折扣还款。折扣是分级的,由用户质押的 GAMMA 与其在其他资产中的质押价值之比决定:此比率越高,还款折扣越大。折扣计划包括三个级别,范围从 0%(最低)到 50%(最高)。
漏洞分析
根本原因是,在 changeUserBorrowDiscount() 中结算借款人的折扣时,协议(0x4c9E...F467)错误地将借款人存储借款余额的增加视为新产生的利息。因此,原本只适用于应计利息的折扣被错误地应用于新借入的本金,不当降低了借款人记录的债务。攻击者可以反复执行 borrow 然后 changeUserBorrowDiscount 循环,积累过多的折扣,导致链上记录的负债持续低于实际借款金额,并最终从差异中获利。
攻击分析
以下分析基于交易 0x5f45...5ec9。
-
步骤 1:攻击者进行了
200,000e18USDT的闪电贷。 -
步骤 2:攻击者使用
5,000e18USDT购买了WBNB,然后使用获得的WBNB购买了约8,726,524e18GAMMA。 -
步骤 3:攻击者首先将所有获得的
GAMMA质押到 gGAMMA 市场,然后提供了剩余的USDT作为抵押品,这将其还款折扣提高到 5%,并启用了后续借款。 -
步骤 4:攻击者反复调用
borrow然后updateUserDiscount以持续减少其记录的债务。 -
步骤 5:攻击者最终偿还了债务,赎回了抵押品,并实现了利润。
结论
此事件是由 Planet Finance 在 changeUserBorrowDiscount() 中存在的折扣结算逻辑缺陷引起的,该逻辑错误地将借款人存储借款余额的增加视为新产生的利息,并将利息折扣应用于该差值。攻击者可以通过反复调用 borrow 后跟 updateUserDiscount 来低估其记录的债务,并最终偿还少于实际负债的金额来提取利润。
为了减少未来发生类似风险:
- 在借贷协议中区分利息和新借款。
AM 事件
简要概述
2026 年 3 月 12 日,BNB Chain 上的通缩代币 AM Token 被攻击,估计损失约为 13.1 万美元。AM Token 实施了一种通缩机制,其中每次卖出都会触发从流动性池中额外销毁,永久性地减少代币供应量。然而,销毁并非立即执行——相反,全部卖出金额被记录为 toBurnAmount,实际销毁推迟到下一次卖出。这种延迟在记录和执行之间创建了一个窗口,在此期间攻击者可以回购 AM 以将池中的 AM 储备金减少到 toBurnAmount。当下一次卖出触发延迟销毁时,整个 AM 储备金被清空,导致价格飙升至极端水平,使攻击者能够以高价卖出 AM 获利。
背景
AM Token 是 BNB Chain 上的一种通缩代币。每次卖出时,合约会将交易涉及的 AM 金额记录为 toBurnAmount,并在下一次卖出时从流动性池中销毁记录的金额。实际上,卖出会触发一个延迟销毁,该销毁会缩小池中的 AM 储备金。此外,在执行销毁之前,协议会将累积的 totalTokenFee 兑换成 USDT 并根据其费用分配逻辑进行分配。
漏洞分析
根本原因是该代币(0x27f9...213f)的卖出逻辑将全部交易的 AM 金额累积为 toBurnAmount,并在下一次卖出时通过从 AM/USDT 对中移除代币并调用 pair.sync() 来更新储备金。这种设计允许攻击者操纵池中的 AM 储备金,扭曲链上价格,并通过套利获利。
攻击分析
以下分析基于交易 0xd0d1...f859。
-
步骤 1:攻击者闪电贷了约
27,265,119e18USDC和约361,710e18WBNB,然后将它们兑换成约100,423,811e18USDT。 -
步骤 2:攻击者将约
5,062e18AM代币兑换成USDT,这操纵了合约记录的toBurnAmount至约4,303e18。 -
步骤 3:攻击者将
USDT兑换成 AM Token,将池中的AM储备金降低至约4,303e18。 -
步骤 4:攻击者将
6 weiAM转入池中,触发了卖出路径销毁逻辑。结果,合约销毁了池中所有的AM余额,将AM储备金降至 0。注意:协议首先尝试将累积的费用兑换成USDT,然后再进行销毁。此费用转换路径也会触发卖出分支销毁逻辑。在销毁执行且AM储备金降至 0 后,费用兑换失败。由于它被包含在 try/catch 中,失败不会回滚交易。相反,执行继续,并且费用累加器重置为 0。 -
步骤 5:攻击者调用
pool.sync()并将剩余的USDT和1 weiAM转入池中。由于两个代币同时转入,合约将其视为 addLiquidity,因此toBurnAmount未被累积。AM储备金更新为 7。 -
步骤 6:攻击者兑换了剩余的
AM代币以获取USDT。在此次兑换过程中,将AM转入 Pair 触发了卖出路径销毁逻辑,将AM储备金降至 1。此外,由于totalFeeAmount在步骤 4 中已重置为 0,费用到USDT的转换不再执行,允许攻击者以人为抬高的价格出售AM。 -
步骤 7:攻击者偿还了闪电贷,并实现了剩余的利润。
结论
此事件是由 AM Token 有缺陷的销毁机制引起的,该机制将每次卖出交易中涉及的 AM 累积为 toBurnAmount,然后在下一次卖出时通过调用 pool.sync() 从 AM/USDT 对中销毁该金额。这使得攻击者能够操纵 Pair 的 AM 储备金到一个极端水平,并以人为抬高的价格出售 AM 来耗尽 USDT。
为了减少未来发生类似风险:
- 限制每次交易的最大销毁金额,并限制销毁触发的频率,防止攻击者在短时间内消耗池中大部分代币储备。
DBXen 事件
简要概述
2026 年 3 月 12 日,DBXen,一个在以太坊和 BNB Chain 上的 burn-to-earn(销毁即赚)协议,被攻击,总损失约为 14.9 万美元。根本原因是 _msgSender() 和 msg.sender 之间的不一致。当通过 forwarder 调用 burnBatch() 时,销毁的 XEN 金额记录在 _msgSender()(攻击者控制)下,但周期记录在 msg.sender(forwarder)上更新。这种分离使得攻击者能够根据过时的周期记录索取奖励和费用,导致异常大的支付。
背景
DBXen 是一个 burn-to-earn 协议:用户销毁 XEN 代币以换取 DXN 奖励和累积协议费用的份额。关键机制以周期为单位工作。当用户调用 burnBatch() 时,会发生两件事:(1)销毁的 XEN 金额在调用者的地址(由 _msgSender() 标识)下记录,并且(2)XEN 合约通过 onTokenBurned() 回调到 DBXen,将调用者的周期记录(销毁周期和 lastFeeUpdateCycle)更新到当前周期。
奖励和费用通过 updateStats() 结算。奖励与用户在其销毁周期中销毁的总 XEN 的份额成正比。费用基于用户上次记录周期以来累积的协议费用。这两种计算都依赖于用户周期记录的及时更新。
漏洞分析
根本原因是 DBXen 协议(0xf5c8...2abd)中的业务逻辑缺陷。_msgSender() 函数检查 msg.sender 是否为 forwarder。如果是,它会返回 calldata 的最后 20 个字节,而此返回值可以在 forwarder 上下文中任意控制。然而,burnBatch() 直接销毁 msg.sender 持有的 XEN。结果是,攻击者可以通过 forwarder 调用 burnBatch(),导致协议销毁 forwarder 持有的 XEN 并将 forwarder 的销毁周期记录和费用更新周期更新到当前周期。同时,协议会将销毁的 XEN 金额记录在对应于 _msgSender() 的地址下。
之后,攻击者调用 claimFees(),该函数调用 updateStats()。由于 _msgSender() 地址的周期记录从未更新(销毁周期和 lastFeeUpdateCycle 都保留为 0),updateStats() 会在当前周期计算奖励,并计算自周期 0 以来的累积费用——涵盖协议的全部费用历史。然后,攻击者通过调用 claimFees() 和 claimRewards() 来获利。
攻击分析
以下分析基于交易 0x914a5a...b808bc37。
-
步骤 1:攻击者首先调用
Forwarder合约的registerDomainSeparator()函数,从而能够后续调用Forwarder.execute()。 -
步骤 2:攻击者在 Uniswap V2 池中将
0.14e18ETH兑换成13,900,000,000e18XEN。 -
步骤 3:攻击者将
13,900,000,000e18XEN转入Forwarder合约。 -
步骤 4:攻击者使用
Forwarder.execute()授权 DBXen 使用Forwarder持有的13,900,000,000e18XEN。 -
步骤 5:攻击者使用
Forwarder.execute()调用DBXen.burnBatch()并销毁了13,900,000,000e18XEN。销毁金额记录在地址0x425D3eC2DCeBE2c04bA1687504D43AFC6be7328d下,而在销毁执行期间,XEN通过onTokenBurned()回调到DBXen,更新了Forwarder上的相关周期记录。 -
步骤 6:攻击者使用
Forwarder.execute()调用DBXen.claimFees()并获得了65.36e18ETH。 -
步骤 7:攻击者使用
Forwarder.execute()调用DBXen.claimRewards()并铸造了2,305.4e18DXN。
结论
此事件的根本原因是 DBXen 协议在 _msgSender() 和 msg.sender 的使用上不一致。由于这两个值可能不同,协议的内部会计变得不一致,这使得攻击者能够利用这种差异获利。
为了减少未来发生类似风险:
- 在所有逻辑路径中始终如一地使用
_msgSender(),或者确保依赖msg.sender的操作和依赖_msgSender()的会计始终引用相同的地址。
Goose Finance 事件
简要概述
2026 年 3 月 15 日,BNB Chain 上的收益农场协议 Goose Finance 被攻击,损失约 8 千美元。根本原因是 StrategyGooseEgg 中的份额定价顺序错误:deposit() 在将收获的奖励结算到会计之前铸造份额,因此用于份额定价的总资产分母不包括这些奖励,且低于真实价值。这意味着存款人获得的份额多于他们应得的。当调用 withdraw() 时,它会触发奖励收获,从而增加总资产,使得每个份额的价值更高。通过在单笔交易中循环进行存款和提款,攻击者反复铸造了定价过高的份额,并以修正后的(更高)价值赎回它们,提取差额作为利润。
背景
Goose Finance 是一个 BNB Chain 收益农场协议,用户资金从金库流向策略,策略将资产质押到 MasterChef 以赚取 EGG 奖励。
与此事件相关的组件是:
-
VaultChef(0x3f64...):跟踪用户头寸并将资本转交给StrategyGooseEgg。 -
StrategyGooseEgg(0x0980...):在策略级别维护会计,具有sharesTotal和wantLockedTotal。 -
MasterChef(0xe70e...):接收质押的资产并支付EGG奖励。 -
WrappedEgg(0xb815...):将EGG1:1 包装成WEGG进行质押。
在操作上,存款从 VaultChef 路由到 StrategyGooseEgg,然后质押到 MasterChef。提款从 VaultChef 发起并由策略执行。
份额定价的一个关键会计预期是,份额定价应反映定价时策略的总资产(质押本金加上策略已持有的闲置奖励)。然而,在 StrategyGooseEgg 中,deposit() 在 _farm() 将闲置资产结算到 wantLockedTotal 之前铸造份额,而 withdraw() 可以触发 MasterChef 的奖励收获。此顺序是下文分析漏洞的基础。
漏洞分析
根本原因是 StrategyGooseEgg(0x0980...b26b)中奖励收获和份额定价之间的会计不同步。
在 StrategyGooseEgg 中,份额定价使用 wantLockedTotal 作为分母:shares = deposit * sharesTotal / wantLockedTotal。为了公平起见,wantLockedTotal 必须反映策略实际持有的所有资产,包括任何闲置的 EGG 奖励。然而,deposit() 在 _farm() 将闲置奖励结算到 wantLockedTotal 之前就铸造了份额。这意味着分母不包括未记账的奖励,且低于真实的资产总额,导致存款人获得的份额多于他们应得的。
此外,withdraw() 调用 MasterChef.withdraw(),该函数将质押本金和待处理的 EGG 奖励返回给策略。策略的会计只从 wantLockedTotal 中减去请求的 _wantAmt,因此收获的奖励保留在策略的余额中,而未在 wantLockedTotal 中反映。这扩大了实际持有资产与记录的 wantLockedTotal 之间的差距,使得任何后续的 deposit() 份额定价更加不准确。
攻击分析
以下分析基于交易 0x86efdf...ce316223。
-
步骤 1:攻击者从两个 Pancake 对闪电借入了
EGG。 -
步骤 2:首次存款到
VaultChef/StrategyGooseEgg(10,170,000e18EGG)。 -
步骤 3:首次提款(
12,593,884e18EGG)从MasterChef收获了奖励;359,561e18EGG被转入StrategyGooseEgg并作为闲置/未记账价值保留(R > 0)。 -
步骤 4:第二次存款(
12,593,884e18EGG)使用了提款的资本。在闲置价值结算之前对份额进行定价,这是过度铸造步骤。 -
步骤 5:第二次提款(
12,826,027e18EGG)实现了过度铸造份额的利润(即,比步骤 4 的存款输入高出232,143EGG)。 -
步骤 6:攻击者偿还了闪电贷,并保留了净差额。
结论
此攻击源于 StrategyGooseEgg 中的份额定价顺序缺陷:deposit() 在 _farm() 更新 wantLockedTotal 之前铸造份额,而 withdraw() 可以从 MasterChef 收获奖励,这些奖励暂时保持闲置且未记账。这使得存款可以在过时的分母下铸造,并在之后在更新的资产下进行提款。
为了减少未来发生类似风险:
-
在计算份额铸造和份额销毁之前,先结算奖励并更新会计。
-
在计算点上,使用单一的
totalAssets(质押 + 闲置)来为份额定价。 -
在非零闲置奖励条件下,为
shares_minted <= D * S / (A + R)添加不变性测试。
关于 BlockSec
BlockSec 是一家提供全栈区块链安全和加密合规服务的提供商。我们构建的产品和服务可以帮助客户在协议和平台的整个生命周期中进行代码审计(包括智能合约、区块链和钱包)、实时拦截攻击、分析事件、追踪非法资金,并满足 AML/CFT 要求。
BlockSec 在顶尖会议上发表了多篇区块链安全论文,报告了多起 DeFi 应用的零日攻击,阻止了多起黑客攻击并挽救了超过 2000 万美元,并保障了数十亿美元的加密货币。
-
官方 Twitter 账号:https://twitter.com/BlockSecTeam
-
🔗 BlockSec 审计服务 : 提交请求



