20250823 - Equilibria 攻击事件分析:奖励计算公式不要轻易采用 balanceOf 做参数呀!

背景信息

Equilibria Finance 是一个为 $PENDLE 代币持有者提高收益的 DeFi 协议。其主要功能包括将 PENDLE 代币转换为 ePENDLE(PENDLE --lock--> vePENDLE --mint--> ePENDLE),将 ePENDLE 进行 stake 后获得 stk-ePENDLE 等,目的是提升流动性提供者的收益。除此以外,Equilibria 还为 stk-ePENDLE 的持有者提供了 xEQB 和 vlEQB 代币激励。

本次攻击的原因是 stk-ePENDLE 相关的奖励计算逻辑错误,在 stk-ePENDLE 代币可以转移的前提下,根据用户持有的 stk-ePENDLE 代币余额来进行奖励计算。使得攻击者可以通过一笔 stk-ePENDLE 代币在多个账户间重复利用来领取超额的奖励。

Trace 分析

image

  1. 攻击合约通过一系列的代币转换获取价值 0.01 ETH 的 ePENDLE 代币
  2. 调用 stk-ePendle.harvest() 从 ePENDLE 奖励池中收集奖励代币,将收集到的 WETH 和 PENDLE 兑换成 ePendle 代币后进行 stake。

image

此时可以看到 stk-ePendle 合约中存在 13.6 ETH

image

  1. 随后通过闪电贷 Balancer.receiveFlashLoan() 发起攻击。

Balancer.receiveFlashLoan()

先通过闪电贷获取了大量的 ePENDLE,然后 deposit 获得 stk-PENDLE,所有准备工作已经完成。

image

执行攻击部分,

  1. 创建一个新的合约
  2. 将 stk-PENDLE 发送到该合约
  3. 调用 getReward() 函数获取 EQB,xEQB 和 ETH 等奖励代币
  4. 把 ETH 汇总到主攻击合约
  5. 把 stk-PENDLE 发送到主攻击合约
  6. 把 EQB,xEQB 返还给 stk-PENDLE 合约

image

随后将这个攻击流程重复进行了 20 次,每次获利约 0.664 ETH,总计获利 13.27 ETH。

攻击者通过漏洞获取到奖励代币后,只要 ETH,把 EQB,xEQB 返还给 stk-PENDLE 合约,是为了重复 20 次的奖励获取能够正常运行下去。因为合约中持有 EQB 的数量是 5538 个,而每次获取 EQB 383 个,20 次就是 7660 个。所以需要每次都将获得的 EQB 和 xEQB 返还给 stk-PENDLE 合约。

image

代码分析

在 Trace 分析中可以了解到,攻击者利用同一笔 stk-PENDLE 发送到不同的合约中进行重复的奖励领取,多半是计算奖励的逻辑中采用了 balanceOf 的方法来计算奖励权重。

stk-PENDLE 合约:https://etherscan.deth.net/address/0xd30d6fd662c0d92b49f3c3e478e125ba1d968059

getReward() 函数中利用 updateReward() 计算用户奖励。

image

而在 earned() 函数中,通过 balanceOf 获取目标账户 stk-PENDLE 的余额,余额越大,奖励越多。所以攻击者可以通过同一笔 stk-PENDLE 代币重复获取大量的奖励。

image

后记

漏洞修复

对于这个漏洞修复方案,看到社区也存在一些讨论,主要是围绕着以下几个方面

  1. 避免直接采用 balanceOf 来计算收益。
  2. 如果需要采用 balanceOf 来计算收益,需要限制 stk-PENDLE 代币不允许 transfer
  3. stk-PENDLE 代币可以 transfer 的场景下,则需要在代币转移前后,更新 senderrecipient 的奖励累计情况。

如果想要较为优雅地解决这个问题,推荐是选择第三个方法,在 _beforeTokenTransfer()_afterTokenTransfer() 中设计好奖励的更新逻辑。

stk-ePendle.harvest() 的作用

在攻击之前,攻击者专门调用了一次 stk-ePendle.harvest() ,其主要目的是为了通过 _queueNewRewards() 函数累加 rewardInfo.rewardPerTokenStored 的值,以谋求更多的收益。但是在实际执行的 Trace 中,由于 harvestAmount = 0 ,所以 continue 跳过了 _queueNewRewards() 函数的执行步骤。虽然从事后的角度来看这个操作并没有生效,但是漏洞利用的设计与执行上存在这个环节还是必要的。

image

posted @ 2025-08-25 18:27  ACai_sec  阅读(88)  评论(0)    收藏  举报