在上周(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 转让了。然后,一个转移钩子读取了过时、预支付数据,并将其一部分反馈到全局分红池中,导致分红池在没有新的 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 的增加应该始终有真实的 Ether 作为后盾。如果 freakerIndex 在没有相应 ETH 涌入的情况下增长,Freaker 就可以兑换比合约实际持有的更多的 Ether。
漏洞分析
根本原因是 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:每次成功后,将代币 ID 591 转回给辅助地址,以便重复使用相同的对。
-
步骤 5:每次成功循环都会使
freakerIndex膨胀,超出系统实际保留的 Ether。 -
步骤 6:指数足够高后,逐批提取先前控制的 Freaker。代币 ID 496 至 520,每个都被提取了 0.278052246002402082 Ether。
-
步骤 7:将提取的 Ether 包装成
WETH,偿还 1,700WETH的闪电贷,并保留约 7.498WETH作为利润。
结论
根本原因是 attack() 的成功捕获流程:EtherFreakers 在目标代币的能量状态结算之前就支付了 targetCharge。然后 _transfer() 触发 _beforeTokenTransfer(),后者读取过时的、预支付的 energyOf(targetId) 并将其一部分消散到池中。这增加了 freakerIndex,而没有新的 Ether 作为后盾,因此相同的目标能量既被计为支付,也被计为池输入。这是一个业务逻辑膨胀 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和 2e17MT以增加流动性。由于与上面相同的原因,此转账绕过了费用收取逻辑。 -
步骤 4:攻击者从 Pair 购买了约 10,000,000e18
MT代币到 Router,从而绕过了买入限制和费用收取逻辑。 -
步骤 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,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:清算人闪电借入了约 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,524e18GAMMA。 -
步骤 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.sync() 来更新储备时执行销毁。此设计允许攻击者操纵池中的 AM 储备,扭曲链上价格,并通过套利获利。


攻击分析
以下分析基于交易 0xd0d1...f859。
-
步骤 1:攻击者闪电借入了约 27,265,119e18
USDC和约 361,710e18WBNB,然后将它们兑换为约 100,423,811e18USDT。 -
步骤 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 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 中销毁该金额。这允许攻击者将 Pair 的 AM 储备操纵到极端水平,并以人为抬高的价格出售 AM 来耗尽 USDT。
为了减少未来类似的风险:
- 限制每笔交易的最大销毁金额,并限制销毁触发的频率,防止攻击者在短时间内消耗池中大部分代币储备。
DBXen 事件
简要概述
2026 年 3 月 12 日,DBXen,一个在以太坊和 BNB Chain 上的烧毁盈利协议,被利用,总损失约为 149,000 美元。根本原因是 _msgSender() 和 msg.sender 之间的不一致。当通过 forwarder 调用 burnBatch() 时,烧毁的 XEN 金额记录在 _msgSender()(攻击者控制)下,但周期记录更新在 msg.sender(forwarder)上。这种分离允许攻击者利用过时的周期记录来申领奖励和费用,从而导致异常大的支出。
背景
DBXen 是一个烧毁盈利协议:用户烧毁 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,000e18XEN。 -
步骤 3:攻击者将 13,900,000,000e18
XEN转账到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,000 美元。根本原因是 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,884e18
EGG)从MasterChef收获奖励;359,561e18EGG被转移到StrategyGooseEgg并作为闲置/未计入的价值保留(R > 0)。 -
步骤 4:第二次存款使用已提取的资本(12,593,884e18
EGG)。份额在闲置价值结算之前定价,因此这是过度铸造的步骤。 -
步骤 5:第二次提款(12,826,027e18
EGG)实现了来自过度铸造份额的利润(即,比步骤 4 的存款输入多 232,143EGG)。 -
步骤 6:攻击者偿还了闪电贷,并保留了净价差。
结论
此漏洞源于 StrategyGooseEgg 中的份额定价顺序缺陷:deposit() 在 _farm() 更新 wantLockedTotal 之前铸造份额,而 withdraw() 可以从 MasterChef 收获暂时闲置且未计入的奖励。这使得存款可以基于过时的分母进行铸造,并在稍后基于更新的资产进行提款。
为了减少未来类似的风险:
-
在份额铸造和份额销毁计算之前,结算奖励并更新会计。
-
在确切的计算点,以单一的
totalAssets(质押 + 闲置)来定价份额。 -
在非零闲置奖励条件下,为
shares_minted <= D * S / (A + R)添加不变性测试。
关于 BlockSec
BlockSec 是一家提供全栈区块链安全和加密合规服务的提供商。我们构建产品和服务,帮助客户在协议和平台的整个生命周期内进行代码审计(包括智能合约、区块链和钱包)、实时拦截攻击、分析事件、追踪非法资金,并满足 AML/CFT 义务。
BlockSec 在享有盛誉的会议上发表了多篇区块链安全论文,报告了 DeFi 应用的多个零日攻击,阻止了多次黑客攻击以挽救超过 2000 万美元,并保护了数十亿美元的加密货币。
-
🔗 BlockSec 审计服务 : 提交请求
-
🔗 Phalcon 安全应用: 预约演示



