多个 V2 Boost 池的严重漏洞报告Balancer漏洞分析

    Balancer官方发布公告表示收到影响多个V2Boost池的严重漏洞报告,只有1.4%的TVL受影响,多个池子已暂停,并通知用户尽快提取流动性LP。慢雾MistEye系统发现疑似Balancer漏洞被利用的攻击交易。由于池子无法暂停,一部分资金仍然受到攻击的影响,Balancer官方再次提醒用户将受影响池子中的LP取回。随后,Balancer官方于Medium发布了8月披露的漏洞细节,慢雾安全团队对其进行复盘,详情如下:Balancer漏洞分析Balancer官方在其披露中简单指出此次的问题在于,线性池的向下舍入以及可组合池的虚拟供应量导致bptSupply为0。首先让我们来简单了解一下与这次漏洞相关的Balancer协议中的内容。BalancerV2VaultBalancerV2[6]协议是一种基于以太坊的去中心化自动做市商(AMM)协议,它代表了可编程流动性的灵活构建块。其核心组件是Vault合约,该合约维护着所有池子的记录,并管理代币的记账和转移,甚至包括原生ETH的包装和解包。也就是说,Vault的实现是将代币记账和管理与池子逻辑分开。Vault中有四个接口,分别是joinPool,exitPool,swap和batchSwap(加入、退出和交换是分开的调用,不存在单次调用时的组合)。其中一个突出的特点是batchSwap,它能实现多个池子之间多次原子交换,将一个池子交换的输出与另一个池子的输入相连(GiveIn和GiveOut)。该系统还引入了闪电交换[7],类似于一个内部的闪电贷。LinearPools线性池Balancer为了提高LP的资本效率及warp和unwarp高额开销的问题,在V2中推出线性池作为解决方案,由此引入了BPT(ERC20BalancerPoolToken)代币。

    线性池[8]包含maintoken(底层资产),warppedtoken(包装代币)和BPT代币,通过已知汇率交换资产及其包装的、具有收益的对应物。包装代币的比例越高,收益率和资金池的资本效率就越高。在warp的过程中,通常都会通过缩放因子来确保不同代币以相同的精度进行计算。ComposablePools可组合池所有的Balancer池都是可组合池,池子包含其他代币,池子本身也有自己的代币。其中BPT币指的是ERC20平衡池代币,是所有池的基础。用户可以在其他池内使用BPT代币自由组合进行兑换。兑换总是涉及一个池和两个代币:GiveIn和GiveOut。In代表送入成分代币并接收BPT,而Out意味着送入BPT并接收成分代币。如果BPT本身就是成分代币,它就可以像其他代币一样进行交换。这样的实现构成了外部池中的基础资产和代币之间的一个简单batchSwap路径,用户可以用BPT交换到线性池的底层资产,这也是BalancerBoostedPool[9]的基础。通过以上的组合,Balancer的可组合池就形成了。一个bb-a-USD可组合稳定池由三个线性池组成,同时向外部协议(Aave)发送闲置流动性。

    例如,bb-a-DAI是一个包含DAI和waDAI(包装的aDAI)的线性池。当用户需要进行batchSwap时(如要将USDT换成DAI),交换路径举例如下:简单了解过前置知识后,我们进入漏洞分析环节。USDT线性池:将USDT兑换bb-a-USDT(进入USDT线性池)。bb-a-USD:bb-a-USDT兑换bb-a-DAI(线性BPT之间的交换)。DAI线性池:bb-a-DAI兑换为DAI(退出DAI线性池)。Balancer漏洞分析在8月27号时,慢雾安全团队收到MistEye系统识别,一笔疑似Balancer漏洞的在野利用发生。交易如下:攻击者首先从AAVE通过闪电贷借出300,000枚USDC。接着调用Vault的batchSwap操作,通过可组合稳定池bb-a-USD池进行BPT代币的兑换计算,最终将94,508枚USDC兑换为59,964枚bb-a-USDC,68,201枚bb-a-DAI和74,280枚bb-a-USDT。最后将获得的BPT代币通过Vault合约的exitPool退出池子换取底层资产,偿还闪电贷,并获利约108,843.7美元离场。由此可见,这次攻击的关键在batchSwap里,而batchSwap中具体发生了什么呢?我们深入了解一下。攻击者在整个batchSwap过程中,先在bb-a-USDC池中兑换出USDC,接着进行BPT代币间的兑换,将bb-a-USDC兑换为bb-a-DAI,bb-a-USDT和USDC。

    最后再将底层的main代币USDC兑换为bb-a-USDT。也就是说,bb-a-USDC作为关键的BPT代币充当GiveOut和GiveIn的成分代币。攻击者在第一步以固定的缩放因子在bb-a-USDC线性池中用BPT代币兑换出USDCmain代币,其增加的数量记录在池子中的bptBalance中。但是在第二次onSwap的兑换后,我们发现,同样的兑换过程,兑换出USDC的amountOut值却是0。这是为什么呢?深入onSwap函数,我们发现在这个过程中会先做一次精度处理nominal化并计算出对应代币的缩放因子。而在接下来调用_downscaleDown函数时,amountOut存在向下舍入的情况。如果amountOut和scalingFactors[indexOut]之间的值相差很大,计算出的_downscaleDown值就为零。也就是说当我们使用BPT代币来兑换main代币时,如果amountOut过小,返回值将向下舍入为零,且这个值就是小于由scalingFactors所计算来的1e12。但amountIn进来的bb-a-USDC数量仍然会加入到bptBalance虚拟数量当中,而此操作会增加bb-a-USDC池子中的余额,可以将其看作为单边添加bb-a-USDC流动性。接着利用可组合稳定池的特性,通过BPT代币之间的相互转换,首先将bb-a-USDC兑换为其他BPT代币。跟进这个兑换过程,可组合稳定池的以下调用路径bb-a-DAIonSwap->_swapGivenIn->_onSwapGivenIn先将bb-a-USDC依次换成bb-a-DAI和bb-a-USDT。

    与在线性池中不同的是,可组合稳定池在进行onSwap操作之前需要进行汇率的缓存更新。从代码中我们可以看到,在组合池中,onSwap会先判断是否需要更新缓存的token兑换率。经过之前的兑换,bb-a-USDC的数量发生了改变,并通过_toNominal名义化后的真实总量为totalBalance994,010,000,000,虚拟供应的BPT代币为20,000,000,000。可以计算出,更新后的汇率几乎是之前线性池原始缓存兑换率1,100,443,876,587,504,549的45倍,即49,700,500,000,000,000,000。随后,在线性池中将bb-a-USDC兑换为USDC。然而这一次的兑换同第二次的兑换一样,再一次造成amountOut向下舍入为0的情况,兑换路径和之前相同。而接下来的这一次兑换则是反向将USDC兑换成bb-a-USDC,兑换路径为onSwap->onSwapGivenIn->_swapGivenMainIn。在这个过程中,我们发现,在计算需要兑换的amountOut的时候,其中对于虚拟供应量的计算,是基于兑换后的BPT代币totalsupply与池中剩余量之间的差值,该差值为0。这是因为bptSupply为0,在计算BPTOut时直接通过调用_toNominal函数,而此路径的调用使得USDC兑bb-a-USDC的兑换比例接近1:1。严重漏洞报告总结batchSwap通过多个池子之间多次原子交换,将一个池子交换的输出与另一个池子的输入相连(tokenIn和tokenOut),将USDC兑换为BPT代币。在这个batchSwap中并不会发生实际代币转移,而是通过记录转入和转出的数量来确认最后的兑换数量。又因为线性池是通过底层资产代币进行兑换的,兑换方式是通过一个虚拟供应量且是固定的算法计算出Rate。因此,batchSwap中存在两个安全漏洞,可以发现,漏洞一为兑换增加了兑换率,而反向兑换时漏洞二再反向降低兑换率,攻击者利用了双重buff获利离场。一是线性池的向下舍入问题,攻击者通过舍入为池子单边添加main代币提高缓存代币的比率,从而操纵相应可组合池中的代币兑换率;二是由于可组合池的虚拟供应量特性,虚拟供应量通过BPT代币减去池子中的余额来计算,在兑换的时候如果GiveIn是BPT代币,那么之后的供应量就会扣掉这部分,攻击者只需要将BPT作为GiveIn来进行兑换,并将其供应量先操纵为0,之后进行反向swap,即BPT再作为GiveOut一方,这时候由于供应量是0,算法会按照接近1:1的比例低于线性池的兑换比例来进行实际兑换,使得GiveOut的BPT代币数量间接被操控。