在过去一周(2026/03/09 - 2026/03/15),BlockSec 检测并分析了八起攻击事件,总估计损失约为 166 万美元。下表总结了这些事件,并在以下小节中提供了每起事件的详细分析。
| 日期 | 事件 | 类型 | 估计损失 |
|---|---|---|---|
| 2026/03/09 | EtherFreakers 事件 | 缺陷的业务逻辑 | ~$25K |
| 2026/03/10 | Alkemi 事件 | 缺陷的业务逻辑 | ~$89K |
| 2026/03/10 | MT 事件 | 缺陷的业务逻辑 | ~$242K |
| 2026/03/11 | AAVE 清算事件 | 配置错误 | ~$1.01M |
| 2026/03/11 | Planet Finance 事件 | 缺陷的业务逻辑 | ~$10K |
| 2026/03/12 | AM 事件 | 缺陷的业务逻辑 | ~$131K |
| 2026/03/12 | DBXen 事件 | 缺陷的业务逻辑 | ~$149K |
| 2026/03/15 | Goose Finance 事件 | 缺陷的业务逻辑 | ~$8K |
EtherFreakers 事件
简要概述
2026 年 3 月 9 日,以太坊上的 NFT 游戏 EtherFreakers 因错误的双重计数而被利用,导致损失约 25,000 美元。游戏中的每个 NFT 都拥有可提取的 ETH 余额(称为“能量”)。作为游戏机制,玩家可以使用 attack() 让一个 NFT 捕获另一个 NFT 并获得目标 NFT 的能量。然而,该合约在支付目标 NFT 的余额之前,会先转移该 NFT,然后再结算其记账。之后,一个转移钩子会读取旧的、支付前的账目数据,并将其一部分反馈到一个全局红利池中,导致该池在没有新的 ETH 支持的情况下膨胀。攻击者循环利用了这种捕获机制来推高全局指数,然后从一批 NFT 中提取了膨胀的余额。
背景
EtherFreakers 是一款链上 NFT 游戏,其中每个 NFT(称为“Freaker”)都拥有可提取的 ETH 余额,称为 energy。该系统的工作方式类似于红利池:当发生某些操作时,一部分 ETH 会按比例分配给所有 Freaker。每个 Freaker 可提取的 ETH 由全局累加器 freakerIndex 与每代币的份额权重 fortune 结合计算。
具体而言,记账公式为:energyOf = basic + (freakerIndex - index) * fortune。当 _dissipateEnergyIntoPool(amount) 运行时,freakerIndex 会增加,将 amount 的 80% 分配给所有 Freaker,20% 分配给创作者。通过 charge() 直接存款只会增加 basic,而不会影响 freakerIndex。因此,freakerIndex 的增加应该始终以进入系统的真实以太币为后盾。如果 freakerIndex 在没有相应以太币流入的情况下增长,Freaker 就能赎回比合约实际持有的以太币更多的金额。
漏洞分析
根本原因在于 EtherFreak 合约(0x3A27...c0f33)的执行顺序不正确。当捕获成功时,attack() 函数按顺序执行以下步骤:
- 第 237 行:将
targetCharge(目标 NFT 的全部能量)直接作为 ETH 转账支付给防御者。能量已被消耗。 - 第 240 行:调用
_transfer(defender, capturer, targetId)来移动 NFT。在内部,_transfer()会调用 ERC-721 钩子_beforeTokenTransfer(),该钩子会使用energyOf(targetId)的 0.1% 调用_dissipateEnergyIntoPool()。这是_dissipateEnergyIntoPool()的第一次调用,它读取的是一个过时的值,因为第 5 步尚未发生。 - 第 241 行:显式调用
_dissipateEnergyIntoPool(sourceSpent)。这是第二次调用,是正常游戏逻辑的一部分。 - 第 244-251 行:更新
sourceId和targetId的energyBalances。
bug 存在于第 2 步:由于 energyBalances[targetId] 尚未更新,钩子仍然看到支付前的余额,并将一部分已消耗的能量分配到红利池中。第 1 步中的直接 ETH 支付和第 2 步中的池输入都从相同的能量中扣除,导致 freakerIndex 在没有新的 ETH 支持的情况下膨胀。


每当调用 _dissipateEnergyIntoPool() 时,freakerIndex 都会增加:

攻击分析
以下分析基于交易 0x89e24d...9abd2942。
-
步骤 1:借入 1,700 WETH。
-
步骤 2:在攻击者控制的地址下铸造两个新的 Freaker,代币 ID 为 590 和 591。
-
步骤 3:反复调用游戏的
attack(590, 591)函数,并保持执行在成功的捕获分支。 -
步骤 4:每次成功后,将代币 591 转回给帮助者,以便重复使用同一对。
-
步骤 5:每次成功循环都会使
freakerIndex膨胀,超过系统实际保留的以太币。 -
步骤 6:一旦指数足够高,就赎回一批先前由攻击者控制的 Freaker。代币 ID 496 至 520,每个赎回 0.278052246002402082 以太币。
-
步骤 7:将消耗的以太币包装成 WETH,偿还 1,700 WETH 的闪电贷,并保留约 7.498 WETH 作为利润。
结论
根本原因在于 attack() 函数中的成功捕获流程:EtherFreakers 在目标代币的能量状态结算之前就支付了 targetCharge。然后 _transfer() 触发 _beforeTokenTransfer(),后者读取过时的、支付前的 energyOf(targetId) 并将其一部分分配到池中。这导致 freakerIndex 在没有新的以太币支持的情况下增加,因此相同的目标能量既被计为支付,也被计为池输入。这是一个业务逻辑膨胀 bug,而不是重入 bug。
为减少未来发生类似风险:
-
避免在交易仍在结算过程中,在转移钩子中从可变状态重新计算经济价值。
-
如果转移钩子读取状态变量,请确保执行顺序不会影响结果(例如,在钩子运行之前结算状态,而不是之后)。
Alkemi 事件
简要概述
2026 年 3 月 10 日,Alkemi 协议在以太坊上被利用,导致损失约 89,000 美元。根本原因是记账错误和业务逻辑缺陷。有缺陷的清算逻辑允许任何人通过同一笔交易清算自己的头寸并从中获利。此外,记账错误导致攻击者在清算过程中扣除自己抵押品的操作被覆盖,从而允许攻击者在不承担预期成本的情况下获得清算奖励。
背景
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 处获得了 51e18 WETH 的闪电贷。
-
步骤 2:攻击者将 51e18 WETH 兑换成 ETH,并将其存入 Alkemi 协议。
-
步骤 3:攻击者从 Alkemi 借入 39.5e18 ETH。
-
步骤 4:攻击者使用 39.5395e18 ETH 清算自己的头寸。
-
步骤 5:攻击者从 Alkemi 提取 93.5e18 ETH。
-
步骤 6:攻击者偿还了闪电贷,并获得了 43.4e18 ETH 的利润。

结论
根本原因是存在缺陷的清算逻辑,允许攻击者通过在同一笔交易中借款然后清算自己的头寸来获利,而错误的记账逻辑进一步放大了攻击者的收益。
为减少未来发生类似风险:
- 对于借款人和清算人的余额更新,协议应直接操作存储变量,而不是将余额复制到临时内存变量中进行单独计算和写回。
MT 事件
简要概述
2026 年 3 月 10 日,BNB Chain 上的通缩代币 MT Token 被利用,导致损失约 242,000 美元。根本原因是交易限制逻辑存在缺陷,并且对特殊转账条件的处理不一致。在通缩阶段,当池子储备量超过固定阈值时,合约会限制买入操作。然而,合约将精确金额的转账(例如 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,681e18 WBNB。
-
步骤 2:攻击者购买了 2e17 MT,从而绕过了买入限制。
-
步骤 3:攻击者向 Pair 提供了 4e12 WBNB 和 2e17 MT 来添加流动性。由于与上述原因相同,此转账绕过了费用收取逻辑。
-
步骤 4:攻击者通过 Pair 到 Router 购买了约 10,000,000e18 MT 代币,从而绕过了买入限制和费用收取逻辑。
-
步骤 5:攻击者移除了其一半的流动性头寸,在此过程中提取了 Router 持有的所有 MT 代币,然后出售了回收的 MT 以换取 WBNB。在此步骤中,
pendingBurnAmount被操纵到约 9,000,000e18。
-
步骤 6:攻击者再次购买了约 10,000,000e18 MT 代币,将池子的 MT 储备量降低到约 6,756,516e18,低于
pendingBurnAmount。
-
步骤 7:攻击者移除了剩余的一半流动性头寸,提取了购买的 MT 代币,然后调用
distributeDailyRewards()来销毁池中的 MT。结果,MT 储备量减少到 21,000e18。
-
步骤 8:攻击者将所有 MT 兑换回约 1,198e18 WBNB,偿还了闪电贷,并完成了利润。
结论
此攻击是由不正确的交易限制造成的,允许攻击者通过购买精确的 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(),即 1 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:清算人通过闪电贷借入约 6304e18 WETH 并清算了借款人。
-
步骤 2:清算人偿还了闪电贷,完成了清算。
结论
此清算是由错误的预言机价格配置引起的,该配置将本应保持健康的借款人错误地推入不健康状态,从而触发了对其头寸的清算。
为减少未来发生类似风险:
-
在每次更新前确保关键参数的正确性得到验证。
-
在实现中添加验证检查,以拒绝不正确的参数,并防止不正确的配置被成功应用。
Planet Finance 事件
简要概述
2026 年 3 月 11 日,Planet Finance 在 BNB Chain 上被利用,估计损失约 10,000 美元。根本原因是协议错误地将借款人存储借款余额的增加视为已计息利息,允许攻击者反复借款并触发折扣结算以低估其记录的债务。
背景
Planet Finance 是一个借贷协议,允许借款人以折扣价还款。折扣是分级的,由用户质押的 GAMMA 与其质押在其他资产中的价值的比例决定:该比例越高,还款折扣越大。折扣时间表包括三个级别,范围从 0%(最低)到 50%(最高)。
漏洞分析
根本原因是,在 changeUserBorrowDiscount() 中结算借款人折扣时,协议(0x4c9E...F467)错误地将借款人存储借款余额的增加视为新产生的利息。结果是,原本只适用于已计息利息的折扣被错误地应用于新借入的本金,不当降低了借款人记录的债务。攻击者可以反复执行 borrow 然后 changeUserBorrowDiscount 循环来累积过多的折扣,导致链上记录的负债持续低于实际借款金额,并最终从中获利。


攻击分析
以下分析基于交易 0x5f45...5ec9。
-
步骤 1:攻击者获得了 200,000e18 USDT 的闪电贷。
-
步骤 2:攻击者使用 5,000e18 USDT 购买了 WBNB,然后使用获得的 WBNB 购买了约 8,726,524e18 GAMMA。
-
步骤 3:攻击者首先将所有获得的 GAMMA 质押到 gGAMMA 市场,然后将剩余的 USDT 作为抵押品存入,这将其还款折扣提高到 5%,并使其能够进行后续借款。
-
步骤 4:攻击者反复调用
borrow然后updateUserDiscount来持续降低其记录的债务。
-
步骤 5:攻击者最终偿还了债务,赎回了抵押品,并实现了利润。
结论
此事件是由于 Planet Finance 在 changeUserBorrowDiscount() 中存在有缺陷的折扣结算逻辑,它错误地将借款人存储借款余额的增加视为新产生的利息,并将利息折扣应用于该差值。攻击者可以反复调用 borrow 然后 updateUserDiscount 来低估其记录的债务,并最终偿还少于实际负债的金额以提取利润。
为减少未来发生类似风险:
- 在借贷协议中区分利息和新借款。
AM 事件
简要概述
2026 年 3 月 12 日,BNB Chain 上的通缩代币 AM Token 被利用,估计损失约 131,000 美元。AM Token 实施了一项通缩机制,其中每次卖出都会触发对流动性池的额外销毁,永久移除代币以减少总供应量。然而,销毁不是立即执行的——相反,完整的卖出金额被记录为 toBurnAmount,实际销毁推迟到下一次卖出。此延迟会在记录和执行之间创造一个窗口,在此期间攻击者可以回购 AM 以将池子的 AM 储备量缩小到 toBurnAmount。当下一次卖出触发延迟销毁时,整个 AM 储备量被清空,导致价格飙升到极端水平,并允许攻击者出售 AM 以获利。
背景
AM Token 是 BNB Chain 上的一个通缩代币。每次卖出时,合约会将交易涉及的 AM 金额记录为 toBurnAmount,并在下一次卖出时从流动性池中销毁该记录的金额。实际上,卖出会触发一个延迟销毁,从而减少池子的 AM 储备量。此外,在执行销毁之前,协议会将累积的 totalTokenFee 兑换成 USDT 并根据其费用分配逻辑进行分配。
漏洞分析
根本原因是该代币(0x27f9...213f)的卖出逻辑将所有交易的 AM 金额累积为 toBurnAmount,并且仅在下一次卖出时通过从 AM/USDT Pair 中移除代币并调用 pair.sync() 更新储备量来执行销毁。此设计允许攻击者操纵池子的 AM 储备量,扭曲链上价格,并通过套利获利。


攻击分析
以下分析基于交易 0xd0d1...f859。
-
步骤 1:攻击者闪电贷了约 27,265,119e18 USDC 和约 361,710e18 WBNB,然后将其兑换为约 100,423,811e18 USDT。
-
步骤 2:攻击者卖出了约 5,062e18 AM 代币换取 USDT,这导致合约记录的
toBurnAmount被操纵为约 4,303e18。
-
步骤 3:攻击者用 USDT 购买 AM 代币,将池子的 AM 储备量降低到约 4,303e18。

-
步骤 4:攻击者将 6 wei AM 转账到池子,触发了卖出路径的销毁逻辑。结果,合约销毁了池子中所有的 AM 余额,将 AM 储备量降至 0。注意:协议在销毁之前会尝试将累积的费用兑换成 USDT。此费用转换路径也触发了卖出路径的销毁逻辑。在销毁执行并 AM 储备量达到 0 后,费用兑换失败。由于它被包含在 try/catch 中,失败不会回滚交易。相反,执行继续,费用累加器被重置为 0。

-
步骤 5:攻击者调用
pool.sync()并将剩余的 USDT 和 1 wei AM 转入池子。由于两个代币同时转入,合约将其视为添加流动性,因此未累积toBurnAmount。AM 储备量更新为 7。

-
步骤 6:攻击者卖出了剩余的 AM 代币换取 USDT。在此次兑换期间,将 AM 转入 Pair 触发了卖出路径的销毁逻辑,将 AM 储备量减少到 1。此外,由于
totalFeeAmount在步骤 4 中已重置为 0,因此不再执行费用到 USDT 的兑换,允许攻击者以人为抬高的价格出售 AM。
-
步骤 7:攻击者偿还了闪电贷,并实现了剩余的利润。
结论
此事件是由 AM Token 有缺陷的销毁机制造成的,该机制将每次卖出的交易涉及的 AM 累积为 toBurnAmount,然后将该金额在下一次卖出时从 AM/USDT Pair 中销毁,同时调用 pool.sync()。这允许攻击者将 Pair 的 AM 储备量操纵到极端水平,并以人为抬高的价格出售 AM 来消耗 USDT。
为减少未来发生类似风险:
- 限制每次交易的最大销毁金额,并限制销毁触发的频率,以防止攻击者在短时间内消耗池子中大部分代币储备。
DBXen 事件
简要概述
2026 年 3 月 12 日,DBXen,一个在以太坊和 BNB Chain 上的 burn-to-earn(燃烧以赚取)协议,被利用,总损失约 149,000 美元。根本原因是 _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() 直接燃烧 forwarder 持有的 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.14e18 ETH 兑换成 13,900,000,000e18 XEN。
-
步骤 3:攻击者将 13,900,000,000e18 XEN 转账到
Forwarder合约。 -
步骤 4:攻击者使用
Forwarder.execute()批准 DBXen 消耗Forwarder持有的 13,900,000,000e18 XEN。 -
步骤 5:攻击者使用
Forwarder.execute()调用DBXen.burnBatch()并燃烧了 13,900,000,000e18 XEN。燃烧金额记录在地址 0x425D3eC2DCeBE2c04bA1687504D43AFC6be7328d 下,而燃烧执行期间,XEN 通过onTokenBurned()回调 DBXen,更新了Forwarder上的相关周期记录。
-
步骤 6:攻击者使用
Forwarder.execute()调用DBXen.claimFees()并获得了 65.36e18 ETH。 -
步骤 7:攻击者使用
Forwarder.execute()调用DBXen.claimRewards()并铸造了 2,305.4e18 DXN。
结论
此事件的根本原因是 DBXen 协议在 _msgSender() 和 msg.sender 的使用上存在不一致。由于这两个值可能不同,协议的内部记账变得不一致,这使得攻击者能够利用这种差异获利。
为减少未来发生类似风险:
- 在所有逻辑路径中一致地使用
_msgSender(),或确保依赖msg.sender的操作和依赖_msgSender()的记账始终引用同一地址。
Goose Finance 事件
简要概述
2026 年 3 月 15 日,BNB Chain 上的收益农场协议 Goose Finance 被利用,损失约 8,000 美元。根本原因是 StrategyGooseEgg 中存在份额定价顺序缺陷:deposit() 在将收获的奖励结算到记账之前就铸造份额,因此用于份额定价的总资产分母不包括这些奖励,并且低于真实价值。这意味着存款人收到的份额比应有的要多。当调用 withdraw() 时,它会触发奖励收获,从而增加总资产,使得每个份额的价值更高。通过在同一笔交易中循环进行存款和提款,攻击者反复铸造了定价过高的份额,并以修正后的(更高的)价值赎回它们,从而提取差额作为利润。
背景
Goose Finance 是一个 BNB Chain 收益农场协议,用户资金从金库流向策略,策略将资产质押到 MasterChef 以赚取 EGG 奖励。
与此事件相关的组件是:
-
VaultChef(0x3f64...):跟踪用户头寸并将资金转发给StrategyGooseEgg。 -
StrategyGooseEgg(0x0980...):维护策略级别的记账,包括sharesTotal和wantLockedTotal。 -
MasterChef(0xe70e...):接收质押的资产并支付 EGG 奖励。 -
WrappedEgg(0xb815...):将 EGG 按 1: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,000e18 EGG)。 -
步骤 3:首次提款(12,593,884e18 EGG)从
MasterChef收获奖励;359,561e18 EGG 被转账到StrategyGooseEgg并作为闲置/未记账资产(R > 0)保留。 -
步骤 4:第二次存款(12,593,884e18 EGG)重新使用了提款的资本。份额在闲置资产结算之前被定价,因此这是过度铸造的步骤。
-
步骤 5:第二次提款(12,826,027e18 EGG)实现了过度铸造份额的利润(即,比第 4 步存款输入多 232,143 EGG)。
-
步骤 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



