20230827 - Balancer 攻击事件:价格操纵 + 精度丢失的经典组合拳

news/2025/11/14 21:28:41/文章来源:https://www.cnblogs.com/ACaiGarden/p/19223475

攻击背景介绍

2023.08.27(没错是 2023 不是 2025),Balancer V2 的稳定币池遭到了黑客攻击,导致多条链上价值约 368k 美元的资产被盗。黑客利用 rounding down(精度丢失)问题操纵 bb-a-USDC 的价格,从稳定币池中兑换出大量资产。

攻击交易之一:https://app.blocksec.com/explorer/tx/eth/0x2a027c8b915c3737942f512fc5d26fd15752d0332353b3059de771a35a606c2d

相关合约:

  1. Vault:https://etherscan.io/address/0xba12222222228d8ba445958a75a0704d566bf2c8
  2. bb-a-USDC:https://etherscan.io/address/0x9210F1204b5a24742Eba12f710636D76240dF3d0
  3. bb-a-USD:https://etherscan.io/address/0x7b50775383d3d6f0215a8f290f2c9e2eebbeceb2

项目背景介绍

Balancer 是一个基于以太坊的去中心化自动做市商 (AMM) 协议,能够集成任意数量的互换曲线和资金池类型。

本次攻击发生在 Balancer V2 ,涉及两种 Pool:

  1. Balancer Pool Token(BPT):https://docs.balancer.fi/concepts/core-concepts/balancer-pool-tokens.html
  2. Linear Pools:https://docs-v2.balancer.fi/concepts/pools/linear.html
  3. Stable Pools:https://docs-v2.balancer.fi/concepts/pools/composable-stable.html

BPT

BPT 代表 Balancer Pool 的代币份额。当用户向 Balancer 池存入代币增加流动性时,会收到流动性池中所占 share 的 BPT。

Linear Pools

Linear Pools 适用于某种代币及其已知(计算或查询得到的)汇率的收益代币之间的兑换。例如,Aave 的 DAI 和 aDAI 。为了激励用户维护池子保持一定比例的原生代币与收益代币,Linear Pools 设有目标范围,采用费用/奖励机制来激励套利者维持两种代币之间的理想比例(超出目标范围需支付费用,回到目标范围可获得奖励)。线性池的另一个关键特性是允许用户直接交易 BPT,无需加入或退出。

在本次攻击中的价格操纵环节涉及了一个由 USDC 和 Wrapped aUSDC 组成的 Linear Pool,这个池子的 BPT 为 bb-a-USDC。攻击者通过精度丢失问题操控并抬高 bb-a-USDC 的价格。

  • https://etherscan.io/address/0x9210F1204b5a24742Eba12f710636D76240dF3d0

Stable Pools

Stable Pools 专为接近等价或已知汇率的资产而设计,通过定制的 swap 逻辑,可以大幅提高同类资产互换和相关资产互换的资本效率。

以接近 1:1 的比例进行兑换的代币:例如两种相同货币的稳定币(例如:DAI、USDC、USDT),或合成资产(例如:renBTC、sBTC、WBTC)。

在本次攻击中涉及了一个由 [bb-a-USDT, bb-a-DAI, bb-a-USDC] 三种 BPT 组成的 Stable Pool,这个池子的 BPT 为 bb-a-USD。攻击者在抬高 bb-a-USDC 的价格后,利用 bb-a-USDC 兑换出超额的 bb-a-USDT 和 bb-a-DAI 完成获利。

  • https://etherscan.io/address/0x7b50775383d3d6f0215a8f290f2c9e2eebbeceb2

image

Trace 分析

攻击者通过 AAVE V3 闪电贷借出大量 USDC,然后在回调函数中通过 Balancer: Vault.batchSwap 进行攻击获利。最后归还闪电贷并转移资金。

重点分析 Balancer: Vault.batchSwap 中的操作,主要由 7 个 onSwap 操作和 repay 操作组成。

image

详细分析 batchSwap 中的每一步 onSwap 都做了什么:

  1. Swap 106520.941720152868211419 bb-a-USDC for 107796.952916 USDC in the bb-a-USDC Pool (Remove liquidity)
  2. Swap 0.000000775114420171 bb-a-USDC for 0 USDC in the bb-a-USDC Pool (Rounding down, rate manipulation)
  3. Swap 1 bb-a-USDC for 1.000339378515783699 bb-a-DAI in the bb-a-USD Stable Pool (Update the rate in the cache)
  4. Swap 7300 bb-a-USDC for 139430 bb-a-DAI in the bb-a-USD Stable Pool (Take profit)
  5. Swap 20000 bb-a-USDC for 248868 bb-a-USDT in the bb-a-USD Stable Pool (Take profit)
  6. Swap 0.00000002 bb-a-USDC for 0 USDC in the bb-a-USDC Pool (Empty bptSupply for reset the initial price)
  7. Swap 150000 USDC for 149450 bb-a-USDC in the bb-a-USDC Pool (Repay bb-a-USDC at the initial price)

batchSwap

batchSwap 调用每个 onSwap 的流程

  • batchSwap → _swapWithPools → _swapWithPool → _processGeneralPoolSwapRequest → pool.onSwap
  • https://vscode.blockscan.com/ethereum/0xba12222222228d8ba445958a75a0704d566bf2c8

在 batchSwap 中,用户可以在先不提供 Token 的情况下执行批量操作,在批量操作结束后统一结算所需要的代币。

onSwap1

Swap 106520.941720152868211419 bb-a-USDC for 107796.952916 USDC in the bb-a-USDC Pool (Remove liquidity)

黑客在第一步中用大量的 bb-a-USDC 换出 USDC,这一操作利用了 batchSwap 的特性“凭空创造”了大量的 bb-a-USDC 进行流动性的撤出。此时 Pool 中流动性大幅减少,但是 bb-a-USDC 的价格依旧正常。

batchSwap → _swapWithPools → _swapWithPool → _processGeneralPoolSwapRequest

在调用 onSwap 时,会传入 currentBalances ,Pool 根据改值对兑换的数量进行计算。

在调用 onSwap 后,Vault 合约会对 poolBalances 进行更新。

image

在进行 onSwap 操作前 Pool 中的 balances:

  1. bb-a-USDC:5,192,296,858,428,306,686,809,548,346,588,505
  2. USDC:108,376,836,940

在进行 onSwap 操作后 Pool 中的 balances:

  1. bb-a-USDC:5,192,296,858,534,827,628,529,701,214,799,924
  2. USDC:579,884,024

image

onSwap2

Swap 0.000000775114420171 bb-a-USDC for 0 USDC in the bb-a-USDC Pool (Rounding down, rate manipulation)

在 onSwap2 中,黑客利用计算 amountOut 时向下取整的特征,通过向 Pool 中转入少量 bb-a-USDC ,但没有转出 USDC 的操作,提高 bb-a-USDC 的价格(rate)。

bb-a-USDC 的 decimals 为 18,而 USDC 的 decimals 为 6

首先通过 _calcMainOutPerBptIn() 计算 amountIn 数量的 bb-a-USDC 可以换出多少 USDC

onSwap → _swapGivenBptIn → _calcMainOutPerBptIn

image

随后调用 _downscaleDown() 计算换出的 USDC 数量,由于是换出的数量,为了保护协议的利益不受损害,在设计上采取的是 rounded down 的设计。

onSwap → _downscaleDown

image

divDown() 中,会先将分子乘上 ONE = 1e18 再进行除法计算。

image

由于 bb-a-USDCUSDC 的 decimals 差为 18 - 6scalingFactor = 1e18 * 1e^(18 - 6) = 1e30 ,所以 784399492780 * 1e18 / 1e30 = 0.784399492780 ,经过rounded down 后结果为 0。

image

为什么往 Pool 中转入 bb-a-USDC 能够抬高其价格?

黑客往 bb-a-USDC 合约中发送了 775114420171 bb-a-USDC 代币(0.0000007),获得 0 USDC。

读者看到这里的时候可能会感觉到困惑,Pool 中的 bb-a-USDC 数量增加,而 USDC 数量不变,此时 bb-a-USDC 的价格不是应该变低才对吗?为什么还会抬高其价格?

这是因为 bb-a-USDC 合约中的资产价格计算方式和常规的 pool (如 Uniswap)是不一样的,不是直接通过 Pool 中的代币余额来进行计算的,需要做一些小小的转换。

首先,Balancer Pool 在创建时候就会把总的流动性代币 bb-a-USDC 的数量设置为 _INITIAL_BPT_SUPPLY = 2**(112) - 1

其次,合约通过 _getApproximateVirtualSupply() 计算 BPT 代币的虚拟供应(Virtual Supply),也就是“流通中”的 BPT,即用户持有的部分。

image

  • _INITIAL_BPT_SUPPLY :预先铸造的 bb-a-USDC 总流通量。
  • bptBalance:目前合约持有的 bb-a-USDC 代币数量。

Virtual Supply = _INITIAL_BPT_SUPPLY - bptBalance

所以当黑客往 Pool 合约发送 bb-a-USDC 代币时,bptBalance 的值会增大,而 _getApproximateVirtualSupply() 的返回值会变小。

接着通过公式 totalBalance.divUp(_getApproximateVirtualSupply(balances[_bptIndex])) 计算 bb-a-USDC 的 rate。

  • totalBalance:代表合约中 USDC 和 Wrapped aUSDC 的数量和
  • _getApproximateVirtualSupply():在 rounding down 操作后,该值偏小

image

至此,通过 getRate() 计算得到 bb-a-USDC 的 rate 的值已经被攻击者操纵变大。

onSwap3

Swap 1 bb-a-USDC for 1.000339378515783699 bb-a-DAI in the bb-a-USD Stable Pool (Update the rate in the cache)

黑客在 onSwap3 中用 1 bb-a-USDC 兑换 bb-a-DAI,目的是将上一步操纵后的 bb-a-USDC rate 更新到 cache 中。

通过 _updateTokenRateCache() 将被操纵后的 rate 更新到 cache 中,该值在后续 Stable Pool 进行 swap 的时候会从 cache 中被读取。

BaseGeneralPool.onSwap → _swapGivenIn → StablePhantomPool._onSwapGivenIn → _cacheTokenRatesIfNecessary → _cacheTokenRateIfNecessary → _updateTokenRateCache

image

从 Trace 中可以看到,此时 bb-a-USDC rate 的值被操控到了 40240000000000000000 ,约为 bb-a-USDT 和 bb-a-DAI 的 40 倍。

image

onSwap4

Swap 7300 bb-a-USDC for 139430 bb-a-DAI in the bb-a-USD Stable Pool (Take profit)

BaseGeneralPool.onSwap 函数中,首先会调用 _scalingFactors() 获取代币的缩放因子列表 scalingFactors,然后传到 swap 函数中进行计算。

BaseGeneralPool.onSwap →

  1. _scalingFactors (Read the manipulated rate)
  2. _swapGivenIn → StablePhantomPool._onSwapGivenIn → StableMath._calcOutGivenIn → _getTokenBalanceGivenInvariantAndAllOtherBalances

image

scalingFactors 的计算是由 super._scalingFactors() 获取的缩放因子乘上 rate 得到的。由于 bb-a-USDC 的 rate 在前面已经被攻击者操纵变大,所以在本次操作中,攻击者能够用 7300 bb-a-USDC 兑换 139430 bb-a-DAI,完成获利。

image

onSwap5

Swap 20000 bb-a-USDC for 248868 bb-a-USDT in the bb-a-USD Stable Pool (Take profit)

onSwap5 与 onSwap4 同理,黑客用 bb-a-USDC 换出了大量 bb-a-USDT 完成获利。

onSwap6

Swap 0.00000002 bb-a-USDC for 0 USDC in the bb-a-USDC Pool (Empty bptSupply for reset the initial price)

黑客在 onSwap6 中将剩余的 bptSupply 全部发送到 Pool 中,目的是使得 bptBalance = _INITIAL_BPT_SUPPLY,从而将 bptSupply 置零,为下一步平账操作做准备。

bptSupply 被置零以后,会进入 if 分支,按照初次添加流动性来计算比例。

onSwap → _onSwapGivenIn → _swapGivenMainIn → LinearMath._calcBptOutPerMainIn

image

由于前面通过 rounding down 把 bb-a-USDC 的价格抬高了,所以直接用 USDC 回购 bb-a-USDC 进行平账的话会导致亏损。所以黑客通过将 bb-a-USDCbptSupply 操控为 0,回到 initial 的状态,此时可以按照大约 1:1 的价格(考虑 fee)将 USDC 兑换成 bb-a-USDC

onSwap7

Swap 150000 USDC for 149450 bb-a-USDC in the bb-a-USDC Pool (Repay bb-a-USDC at the initial price)

前面提到,在 batchSwap 操作中,用户可以在先不提供 Token 的情况下执行批量操作,在批量操作结束后统一结算所需要的代币。

这一步操作就是为了抵消前面所使用的大量 bb-a-USDC,由于攻击者没有足够数量的 bb-a-USDC 满足结算要求,所以只能通过 USDC 换取大量的 bb-a-USDC 来抵消前面操作的欠款(如果还是不好理解的话,可以假设这笔交易是 batchSwap 的第一笔操作)。

在扣除了 0.55k 的 fee 之后,黑客用 150k USDC 兑换得到了 149.45k bb-a-USDC

image

Funds settlement

执行完所有 onSwap 操作后,batchSwap 会进行资金结算。攻击者支付了 42203 USDC,获取了 15628 bb-a-USDC,139431 bb-a-DAI,248888bb-a-USDC

image

Repay and Transfer

攻击者将获得的 bb-a-USDC,bb-a-USDT,bb-a-DAI 兑换成对应的 Token

image

归还闪电贷后,转移所有获利资产。

困惑

在分析 onSwap2 的过程中发现了一个很别扭的地方,一般来说协议在计算 amountIn/amountOut 的兑换数量时,为了尽量减少由于精度丢失造成的误差,会在计算前乘上 1e18 来提高精度,得到结果后再除 1e18 恢复精度。

但是在计算 request.amount 时,通过 _upscale() 进行缩放。

image

在缩放的过程中先乘以 scalingFactor 提高精度,然后紧接着又除 1e18 给降回去了,好像并没有起到提高精度的作用,没太搞明白这样操作的含义是什么。

image

后记

好了你可能从开头就已经疑惑到现在了,为什么明明最近 2025 年 Balancer 发生了攻击事件我却分析了 2023 年的...其实是前几天攻击发生的时候很快就看到有人分享攻击分析的文档了,我是立马就跟进对攻击进行分析啊。没想到分享的是 2023 年的文档,原因是 2025 年的攻击事件和 23 年的类似。所以,就这么阴差阳错地就投入到了攻击事件的分析当中去了,接近收尾的时候才发现这是 23 年的 tx。唉,真的是难顶啊。不过看错了 tx 归看错了,攻击分析的内容还是有很认真的去做的。分析过程被打断得有点多,如果存在什么纰漏的话欢迎指出或讨论。感谢你的阅读。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/965717.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

我的标题2

我的内容2222222

破解cocos creator 2.3.2, 让它支持M芯片

cocos creator 貌似低于2.4.3的版本,都无法在mac m芯片上运行, 之所以不能运行,还是因为electron版本太低导致 对它进行逆向,破解app.asar, 然后升级electron相关的东西,并且升级vue ui组建 开源地址:https://gi…

Kotlin Coroutines

https://kotlinlang.org/docs/coroutines-overview.html 协程是作为三方库进行提供的,类似 javax <properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><kotlin.co…

我的标题

我的内容111111

深入解析:软考中级-系统集成项目管理工程师**的超详细知识点笔记。

深入解析:软考中级-系统集成项目管理工程师**的超详细知识点笔记。2025-11-14 21:14 tlnshuju 阅读(0) 评论(0) 收藏 举报pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !…

GeoScene Pro试用申请

GeoScene Pro试用申请 按在线的文档配置 GeoScene Pro申请试用 - 易智瑞教育网 1)要求你输入许可的,如下图,这时候需要更换【许可类型】为“指定用户许可”(4.0版本软件这里写的是“授权用户许可”) 更换许可…

题解:P13573 [CCPC 2024 重庆站] Pico Park

P13573:区间 DP、组合数学VP 的时候没题可跟了,就开了这题切掉了,结果 VP 结束发现正赛就一个队伍过了??? 若 \(x\) 用缩小枪击中了 \(y\),则从 \(x\) 向 \(y\) 连一条有向边。注意到,任何一个时刻得到的图是若…

【AI智能体】Coze 提取对标账号短视频生成视频文案实战详解 - 指南

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

Java Benchmark使用

如何测量Java代码的性能 在 Java 中,可以使用多种方法来测量一段代码的执行性能。使用 System.currentTimeMillis()是最常见的方法 long startTime = System.currentTimeMillis();// 需要测量的代码块 for (int i = 0…

实用指南:12-机器学习与大模型开发数学教程-第1章1-4 导数与几何意义

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

基于Vue社区共享游泳馆预约高效的系统n897q36e (工具+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

docker登录容器镜像仓库

容器镜像仓库就是我们平时自己构建的镜像有一个存储的位置,方便自己平时进行拉取,测试用的我使用的是ucloud这容器仓库ucloud.cn登录容器仓库的操作docker logindocker login uhub.service.ucloud.cn# username 为登…

吴恩达深度学习课程二: 改善深层神经网络 第三周:超参数调整,批量标准化和编程框架(一)超参数调整

此分类用于记录吴恩达深度学习课程的学习笔记。 课程相关信息链接如下:原课程视频链接:[双语字幕]吴恩达深度学习deeplearning.ai github课程资料,含课件与笔记:吴恩达深度学习教学资料 课程配套练习(中英)与答案…

Go-秘籍-全-

Go 秘籍(全)原文:zh.annas-archive.org/md5/d17f8ead62b31a6ec2bbef4005dc3b6d 译者:飞龙 协议:CC BY-NC-SA 4.0第一章:错误处理技巧 1.0 引言 亚历山大蒲柏在他的论批评的散文中写道:“出错是人性的”。由于软…

Kotlin中的flow、stateflow、shareflow之间的区别和各自的功能 - 教程

Kotlin中的flow、stateflow、shareflow之间的区别和各自的功能 - 教程2025-11-14 20:36 tlnshuju 阅读(0) 评论(0) 收藏 举报pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto…

非离散网络流——P3347 [ZJOI2015] 醉熏熏的幻想乡

非离散网络流——P3347 [ZJOI2015] 醉熏熏的幻想乡 观察费用为 \(a_ix^2+b_ix\),如果是离散的,则可以套路的建边 \(a_i+b_i,3a_i+b_i,5a_i+b_i,\dots\),可本题 \(x\in R\)。 于是连续意义下我们应该求导得到 \(2a_i…

[note] 素数判定与分解质因数

在某些毒瘤的数论题中,可能出现对 \(10^{18}\) 的范围内的数质因数分解的情况。这时,可以使用 Fermat 和 Miller-Rabin 算法进行素性判定,Pollard-Rho 算法寻找非平凡因子,两者结合以快速质因数分解。 Fermat 素性…

不能识别adb/usb口记录 - 实践

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

恭喜自己,挑战成功! - Ghost

恭喜自己,挑战成功! 我终于,拿省一啦! 正文: 在2025年8月18日,本人开始了一项挑战 挑战三个月达省一 在三个月后,2025年11月14日,NOI官网发布了分数and分数线 本人以高出一等分数线10分的分数(250pts),成功…

如何在测试覆盖不足后补充验证

测试覆盖不足是项目质量的重大隐患,一旦发现(尤其是当它已导致线上问题时),团队必须立即采取系统性的补充验证措施。核心策略是停止盲目开发,转而执行一套以风险为导向的补救流程。 首先,必须立即对未覆盖的区域…