TomInu代币通缩漏洞分析

说明

代币通缩的漏洞目前是分析了两种情况。一种情况是通过burn的方式减少pair中的代币数量从而提升代币的价格,代表的漏洞案例就是Sheep代币漏洞。还是有一种就是通过deliver的方式是的Pair中的代币数量变多,从而可以直接兑换出多余的钱,代表的漏洞案例就是FDP代币漏洞。

本篇文章分析的TINU的漏洞就是和FDP的漏洞案例一样,都是因为deliver导致的漏洞。

基本信息

漏洞函数

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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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 tteam) = _getTValues(tAmount, _taxFee, _teamFee);
uint256 currentRate = _getRate();
(uint256 rAmount, uint256 rTransferAmount, uint256 rFee) = _getRValues(tAmount, tFee, currentRate);
return (rAmount, rTransferAmount, rFee, tTransferAmount, tFee, tteam);
}

function _getRate() private view returns(uint256) {
(uint256 rSupply, uint256 tSupply) = _getCurrentSupply();
return rSupply.div(tSupply);
}


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 _getRValues(uint256 tAmount, uint256 tFee, uint256 currentRate) private pure returns (uint256, uint256, uint256) {
uint256 rAmount = tAmount.mul(currentRate);
uint256 rFee = tFee.mul(currentRate);
uint256 rTransferAmount = rAmount.sub(rFee);
return (rAmount, rTransferAmount, rFee);
}

通过对于deliver()的调用分析,当调用了deliver()之后,

  • _rOwned[sender] = _rOwned[sender].sub(rAmount),用户实际的_rOwned[sender]会减少;
  • _rTotal = _rTotal.sub(rAmount),全局变量_rTotal 也会减少;

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
function balanceOf(address account) public view override returns (uint256) {
if (_isExcluded[account]) return _tOwned[account];
return tokenFromReflection(_rOwned[account]);
}


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 _getRate() private view returns(uint256) {
(uint256 rSupply, uint256 tSupply) = _getCurrentSupply();
return rSupply.div(tSupply);
}

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

分析balanceof的计算公式:
$$
balance = \frac{_rOwned[account]}{currentRate} = \frac{_rOwned[account]}{\frac{rSupply}{tSupply}} = \frac{_rOwned[account]}{\frac{_rTotal}{_tTotal}}
= \frac{_rOwned[account] \times _tTotal}{_rTotal}
$$
通过公式变换,最终发现balance的计算和_tTotal正相关,和_rTotal负相关。

结合deliver()函数,调用了deliver()之后,_rTotal减小,意味着balance就会增加。

攻击分析

由于整个攻击就是不断调用deliver()skim()函数,从而使得TINU.balanceOf(Pair)变大。

闪电贷

通过闪电贷获得139039729301264180297254 ETH

WETHTOTINU

调用swap方法,将ETH兑换成为TINU。

通过swapExactTokensForTokensSupportingFeeOnTransferTokens将数量为104850000000000000000的ETH兑换得到1465904852700232013011个TINU代币。

deliver

循环两次调用deliverskim方法。至于为什么要调用skim()方法,可以详细看FDP的分析。

经过两次的deliver()函数的调用,最终在UNI-V2中的TINU的代币数量就会发生变化(增多),WETH的数量保持不变。相当于向UNI-V2注入了多的TINU代币。

计算分为两个阶段,未执行deliver()和执行deliver()之后。

未执行deliver()之前:

  • TINU316871513264115731249
  • WETH126994561461014981232

执行deliver()之后:

通过使用tenderly的动态调试功能,可以计算的到如下的值:

  • token0(TINU): 11191855315120216048899805
  • token1(WETH)126994561461014981232

可以看到在Pair中的WETH没有变化,但是token0变多了,数量是

1
11191855315120216048899805 - 316871513264115731249 = 11191538443606951933168556

swap获利

首先分析swap的函数实现:

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
// 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 {
require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');
(uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');

uint balance0;
uint balance1;
{ // scope for _token{0,1}, avoids stack too deep errors
address _token0 = token0;
address _token1 = token1;
require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');
if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens
if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens
if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);
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;
require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT');
{ // scope for reserve{0,1}Adjusted, avoids stack too deep errors
uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));
uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));
require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K');
}

_update(balance0, balance1, _reserve0, _reserve1);
emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
}

修改计算公式,就可以计算得到对应的balance1

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
    uint256 _reserve0 = 316871513264115731249;
uint256 _reserve1 = 126994561461014981232;
uint256 balance0 = 11191855315120216048899805;
uint256 amount0In = 11191538443606951933168556;
uint256 amount1In = 0;
uint256 balance1Adjusted;
uint256 balance1;

uint balance0Adjusted = (balance0.mul(1000).sub(amount0In.mul(3))); //

console2.log("balance0Adjusted %s",balance0Adjusted); // 11158280699789395193100299332


balance1Adjusted = uint(_reserve0).mul(_reserve1).mul(1000**2).div(balance0Adjusted); // 3606376282255033184

console2.log("balance1Adjusted %s",balance1Adjusted);

balance1 = balance1Adjusted.add(amount1In.mul(3)).div(1000); // 3606376282255033

console2.log("balance1 %s",balance1);

uint256 out = 126994561461014981232 - 3606376282255033;

console2.log("out %s",out); // out 126990955084732726199
}

所以按照实际的swap的公式推算,最终得到的amount1Out的值是126990955084732726199,攻击者实际调用取的是126990751624171150782。两者的相差不大。

攻击者通过两次deliver()操作,最终swap得到的数量是126990751624171150782

获利

攻击者通过swap最终获利的数量是126990751624171150782,除去前面通过闪电贷借款的139039729301264180297254

实际攻击者获利的数量是:

1
2
126990751624171150782 - 104850000000000000000
= 22140751624171150782

所以攻击者实际获得到的ETH数量是22140751624171150782/10**18 = 22

通过实际的资金流向图,也印证了我们的计算结果。

参考

https://twitter.com/libevm/status/1618731761894309889

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