BurgerSwap被攻击分析

事件影响

  1. 本次攻击主要是因为burgerswap的智能合约分层时的验证不足,导致被重入攻击成功

  2. BURGER-BNB流动池中的BNB和BURGER都被大量拿走,导致流动性不足

  3. 因为黑客手里有大量BURGER,后续就算官方补充流动性也会因为黑客抛售BURGER而导致BURGER的价格变低

事件信息

此项目由成都链安进行审计

慢雾安全团队BurgerSwap 被黑分析

地址对应的tag与解释:

TAG 解释
Pancake-BUSDT-BNB-LP Pancake的BUSDT-BNB流动池
BS-A-Contract BuegerSwap攻击者使用的合约地址
BS-Attacker BuegerSwap攻击者的地址
BS-A-Fake-Token BuegerSwap攻击者的“假币”
BURGER BURGER Token
Demax-BURGER-BNB-LP Demax的BURGER-BNB流动池
BLP-Fake-BURGER Burger的假币-BURGER流动池
Demax-Fake-BURGER-LP Demax的假币-BURGER流动池

攻击交易

https://bscscan.com/tx/0xac8a739c1f668b13d065d56a03c37a686e0aa1c9339e79fcbc5a2d0a6311e333

事件分析

本次攻击共分成六个步骤:

  1. 攻击者在Pancake的BUSDT-BNB流动池使用闪电贷贷出6,047个BNB
  2. 攻击者用兑换出的6,028个BNB在burgerswap的BURGER-BNB池兑换出92,677(直到目前为止没有任何问题)
  3. 攻击者使用兑换出的近一半(45,452)的BURGER和自己的token创建一个交易对
  4. 攻击者使用100个自己的token兑换BNB,兑换路径为FakeToken->BURGER->BNB
    1. 在兑换之前,会调用_getAmountsOut先计算出来路径上的金额是多少
    2. 在兑换时会调用"FakeToken"的transfer方法,此时发起重入攻击,使用剩余的45,316个BURGER兑换4,478个BNB
    3. 在攻击完成之后继续兑换路径上的其他transfer方法,原本因为4.2步已经使用45,316个BURGER兑换了4,478个BNB,无法再使用45,452个BURGER兑换出4,478个BNB(因为此时的BNB的价格已经升高,应该兑换的BNB更少才对);但是因为此处依然使用4.1步骤计算好的兑换金额,并且最底层的兑换逻辑并没有根据x*y=k的公式进行校验,因此导致又以相同的价格兑换了BNB,即兑换出的BNB变多了
  5. 在完成第四步的攻击之后,BURGER-BNB池中的BNB数量变少,BURGER的价格变低,因此攻击者又使用491个的BNB兑换出了108,791个BURGER。(注意第一步使用6,028个BNB才换了92,677个BURGER!)
  6. 归还闪电贷6,062个BNB(闪电贷归还的金额必须大于借出的BNB)

事件涉及的源码

步骤4的调用DemaxPlatform合约的swapExactTokensForTokens方法如下:

从合约代码中可以看出,在这里已经计算好了所有兑换路径的所有的兑换出的金额是多少,然后调用_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

function swapExactTokensForTokens(
uint256 amountIn,
uint256 amountOutMin,
address[] calldata path,
address to,
uint256 deadline
) external ensure(deadline) returns (uint256[] memory amounts) {

uint256 percent = _getSwapFeePercent();
// 计算路径上的token的out
amounts = _getAmountsOut(amountIn, path, percent);
// 最后一个目标token的数量必须大于等于期待的最小兑换数量
require(amounts[amounts.length - 1] >= amountOutMin, 'DEMAX PLATFORM : INSUFFICIENT_OUTPUT_AMOUNT');
address pair = DemaxSwapLibrary.pairFor(FACTORY, path[0], path[1]);
// 先收取交易费
_innerTransferFrom(
path[0],
msg.sender,
pair,
SafeMath.mul(amountIn, SafeMath.sub(PERCENT_DENOMINATOR, percent)) / PERCENT_DENOMINATOR
);
// 兑换已经计算好的金额和path
_swap(amounts, path, to);
// 把收取交易费后的金额转给pair
_innerTransferFrom(path[0], msg.sender, pair, SafeMath.mul(amounts[0], percent) / PERCENT_DENOMINATOR);
_swapFee(amounts, path, percent);
}

步骤4中调用的DemaxPlatform合约的_swap方法如下:

下面的代码展示了在计算好金额之后,如果满足兑换之前的校验就按照计算好的金额在指定的路径进行兑换;最终兑换的逻辑会调用IDemaxPairswap方法

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

function _swap(
uint256[] memory amounts,
address[] memory path,
address _to
) internal {
require(!isPause, "DEMAX PAUSED");
// 做一些swap之前的校验
require(swapPrecondition(path[path.length - 1]), 'DEMAX PLATFORM : CHECK DGAS/TOKEN TO VALUE FAIL');
for (uint256 i; i < path.length - 1; i++) {
(address input, address output) = (path[i], path[i + 1]);
require(swapPrecondition(input), 'DEMAX PLATFORM : CHECK DGAS/TOKEN VALUE FROM FAIL');
require(IDemaxConfig(CONFIG).checkPair(input, output), 'DEMAX PLATFORM : SWAP PAIR CONFIG CHECK FAIL');
(address token0, address token1) = DemaxSwapLibrary.sortTokens(input, output);
uint256 amountOut = amounts[i + 1];
(uint256 amount0Out, uint256 amount1Out) = input == token0
? (uint256(0), amountOut)
: (amountOut, uint256(0));
address to = i < path.length - 2 ? DemaxSwapLibrary.pairFor(FACTORY, output, path[i + 2]) : _to;
// 调用DemaxPair的swap方法,这里直接传递了amount0Out和amount1Out两个金额
IDemaxPair(DemaxSwapLibrary.pairFor(FACTORY, input, output)).swap(amount0Out, amount1Out, to, new bytes(0));
if (amount0Out > 0)
_transferNotify(DemaxSwapLibrary.pairFor(FACTORY, input, output), to, token0, amount0Out);
if (amount1Out > 0)
_transferNotify(DemaxSwapLibrary.pairFor(FACTORY, input, output), to, token1, amount1Out);
}
emit SwapToken(_to, path[0], path[path.length - 1], amounts[0], amounts[path.length - 1]);
}

合约DemaxPairswap方法如下:

此合约直接根据传入的金额进行代币的_safeTransfer,最终只是校验了一下amount0In > 0 || amount1In > 0

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

// this low-level function should be called from a contract which performs important safety checks
function swap(
uint256 amount0Out,
uint256 amount1Out,
address to,
bytes calldata data
) external onlyPlatform lock {
require(amount0Out > 0 || amount1Out > 0, 'DEMAX PAIR : INSUFFICIENT_OUTPUT_AMOUNT');
(uint112 _reserve0, uint112 _reserve1, ) = getReserves(); // gas savings
require(amount0Out < _reserve0 && amount1Out < _reserve1, 'DEMAX PAIR : INSUFFICIENT_LIQUIDITY');
uint256 balance0;
uint256 balance1;
{
address _token0 = token0;
address _token1 = token1;
require(to != _token0 && to != _token1, 'DEMAX PAIR : INVALID_TO');
if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out);
if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out);
// 因为在demaxPlatform调用此方法时data传入的是空,因此这个地方不会被调用
if (data.length > 0) IDemaxCallee(to).demaxCall(msg.sender, amount0Out, amount1Out, data);
balance0 = _balanceOf(_token0, address(this));
balance1 = _balanceOf(_token1, address(this));
}
uint256 amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
uint256 amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;
uint256 _amount0Out = amount0Out;
uint256 _amount1Out = amount1Out;
require(amount0In > 0 || amount1In > 0, 'DEMAX PAIR : INSUFFICIENT_INPUT_AMOUNT');
_update(balance0, balance1, _reserve0, _reserve1);
emit Swap(msg.sender, amount0In, amount1In, _amount0Out, _amount1Out, to);
}

作为对比uniswapV2的UniswapV2Pairswap方法如下:

在最后验证了新的x’和y‘的乘积必须大于等于原来的x和y的乘积:x‘ * y‘ >= x * y

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

// 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));
// x‘ * y‘ = x * y
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);
}

作为对比pancakeswap的PancakePairswap方法代码如下:

在最后不仅有校验进入的金额大于出去的金额,也有乘积的校验。

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

// 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 {
// 至少有一个token兑换出的金额大于0
require(amount0Out > 0 || amount1Out > 0, 'Pancake: INSUFFICIENT_OUTPUT_AMOUNT');
// 获取当前lp池中的两个token的储备数量
(uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
// 兑换出的token数量必须小于当前池中的token数量
require(amount0Out < _reserve0 && amount1Out < _reserve1, 'Pancake: INSUFFICIENT_LIQUIDITY');

uint balance0;
uint balance1;
{ // scope for _token{0,1}, avoids stack too deep errors
address _token0 = token0;
address _token1 = token1;
// 接收兑换token的地址不能是当前lp池中的token中的任意一个
require(to != _token0 && to != _token1, 'Pancake: INVALID_TO');
// 如果token0兑换出的金额大于0,则把amount0Out转账给指定的地址to
if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens
if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens
// 如果参数data不为空,则调用合约to的pancakeCall方法;接下来就是合约to的表演了
if (data.length > 0) IPancakeCallee(to).pancakeCall(msg.sender, amount0Out, amount1Out, data);
// 当合约to表演完之后,分别获取当前lp在两个token中的余额
balance0 = IERC20(_token0).balanceOf(address(this));
balance1 = IERC20(_token1).balanceOf(address(this));
}
// 如果新的余额大于(当前存储量-兑换出的数量),则amount0In等于新的余额 - (当前存储量-兑换出的数量)
// amount0In和amount1In是在计算实际进来了多少
uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;
// 如果两个都是0,说明新的balance都不大于(存储量-兑换出的金额);这说明没有进来,那么就报错;
// 其实这个校验是在说打给当前lp池的token数量必须大于兑换出去的数量
require(amount0In > 0 || amount1In > 0, 'Pancake: INSUFFICIENT_INPUT_AMOUNT');
{ // scope for reserve{0,1}Adjusted, avoids stack too deep errors
// 计算调整后的balance,此处会收取千分之二的费用
uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(2));
uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(2));
// 调用此方法后的两个token的积必须大于等于原始储备的两个token的积;即还回来的token在收完手续费之后必须大于借出去的
require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'Pancake: K');
}

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

合约DemaxPair_safeTransfer方法如下:

此方法会调用token的transfer方法,而本次的攻击就是攻击者自己的token的transfer方法又调用了DemaxPlatform合约的swapExactTokensForTokens方法。

1
2
3
4
5
6
7
8
9
10
11

function _safeTransfer(
address token,
address to,
uint256 value
) private {
// 调用token的transfer方法
(bool success, bytes memory data) = token.call(abi.encodeWithSelector(SELECTOR, to, value));
require(success && (data.length == 0 || abi.decode(data, (bool))), 'DEMAX PAIR : TRANSFER_FAILED');
}

官方后续处理

  1. 创建新的**$cBURGER**并空头给满足条件的原$BURGER的持有者
  2. 使用burgerswap的收入以及原本要奖励给团队的burger建造一个价值700万美元的奖励池