1. 1. 事件总结与影响
  2. 2. 事件信息
  3. 3. 事件分析
    1. 3.1. 为了方便分析,我给涉及到的地址打上了对应的功能标签:
    2. 3.2. 1. 攻击者从 Pancake 的 WBNB/BUSD 交易对中借出大量 WBNB;
    3. 3.3. 2. 将第 1 步借出的全部 WBNB 中的一半通过 Panther 的 SHARK/WBNB 交易对兑换出大量的 SHARK,同时池中 WBNB 的数量增多;
    4. 3.4. 3. 将第 1 步和第 2 步的 WBNB 和 SHARK 打入到 SharkMinter 中,为后续攻击做准备;
    5. 3.5. 4. 调用 AutoShark 项目中的 WBNB/SHARK 策略池中的 getReward 函数,该函数会根据用户获利的资金从中抽出一部分手续费,作为贡献值给用户奖励 SHARK 代币,这部分操作在 SharkMinter 合约中进行操作;
    6. 3.6. 5. SharkMinter 合约在收到用户收益的 LP 手续费之后,会将 LP 重新拆成对应的 WBNB 和 SHARK,重新加入到 Panther 的 WBNB/SHARK 交易池中;
    7. 3.7. 6. 由于第 3 步攻击者已经事先将对应的代币打入到 SharkMinter 合约中,SharkMinter 合约在移除流动性后再添加流动性的时候,使用的是 SharkMinter 合约本身的 WBNB 和 SHARK 余额进行添加,这部分余额包含攻击者在第 3 步打入 SharkMinter 的余额,导致最后合约获取的添加流动性的余额是错误的,也就是说 SharkMinter 合约误以为攻击者打入了巨量的手续费到合约中;
    8. 3.8. 7. SharkMinter 合约在获取到手续费的数量后,会通过 tvlInWBNB 函数计算这部分手续费的价值,然后根据手续费的价值铸币 SHARK 代币给用户。但是在计算 LP 价值的时候,使用的是 Panther WBNB/SHARK 池的 WBNB 实时数量除以 LP 总量来计算 LP 能兑换多少 WBNB。但是由于在第 2 步中,Panther 池中 WBNB 的数量已经非常多,导致计算出来的 LP 的价值非常高;
    9. 3.9. 8. 在 LP 价值错误和手续费获取数量错误的情况下,SharkMinter 合约最后在计算攻击者的贡献的时候计算出了一个非常大的值,导致 SharkMinter 合约给攻击者铸出了大量的 SHARK 代币;
    10. 3.10. 9. 攻击者后续通过卖出 SHARK 代币来换出 WBNB,偿还闪电贷。然后获利离开。
  4. 4. 事件处理结果
    1. 4.1. AutoShark官方给的补偿计划:
    2. 4.2. PantherSwap关停所有$SHARK的LP池

AutoShark Finance被套利分析

事件总结与影响

  1. 此次事件“攻击者”通过闪电贷借出大量BNB再利用SHARK合约逻辑漏洞操纵BNB-SHARK池来获得大量SHARK,最后把获得的SHARK兑换成BNB获利
  2. 用户质押的SHARK数量没有损失
  3. 因为生成了大量的SHARK然后将其抛售导致SHARK价格下跌,官方给的数据是从1.2跌倒0.01,目前的价格如下图:

事件信息

AutoShark官方提供:AutoShark如何被经济利用

慢雾:AutoShark Finance 被黑分析

PeckShield分析

交易信息

pantherswap

autoshark.finance

pancakeswap

当前项目正在审计中,由WatchPug公司

事件分析

这里直接复用慢雾安全团队的分析步骤,我会结合token的流转和源码和具体的合约地址等再进一步解释。

为了方便分析,我给涉及到的地址打上了对应的功能标签:

address对应的tag 解释
AS-Exploiter AutoShark-Exploiter的地址
Pancake-WBNB-BUSD-LP Pancake的WBNB-BUSD流动池,是本次事件的入口,在这个LP闪电贷的BNB
AS-Exploiter-Contract AutoShark-Exploiter创建的合约地址
Panther-SHARK-BNB-LP Panther的SHARK-BNB流动池

攻击步骤如下:

1. 攻击者从 Pancake 的 WBNB/BUSD 交易对中借出大量 WBNB;

对应的交易中的token转移如下:

此处是通过调用Pancake-WBNB-BUSD-LP合约的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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46


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

本地事件的第一步就是调用swap,入参如下:

1
2
3
4
5
6
7
8
9
Function: swap(uint256 amount0Out, uint256 amount1Out, address to, bytes data) ***

MethodID: 0x022c0d9f
[0]: 00000000000000000000000000000000000000000000152d02c7e14af6800000
[1]: 0000000000000000000000000000000000000000000000000000000000000000
[2]: 00000000000000000000000022de6648685e4e47fd944e68b264e0e0667e2e00
[3]: 0000000000000000000000000000000000000000000000000000000000000080
[4]: 0000000000000000000000000000000000000000000000000000000000000001
[5]: 0100000000000000000000000000000000000000000000000000000000000000

前面的三个参数是在描述“兑换100000个BNB和0个BUSD给AS-Exploiter-Contract”,然后因为data参数不为空,所以会调用AS-Exploiter-Contract合于的pancakeCall方法。

2. 将第 1 步借出的全部 WBNB 中的一半通过 Panther 的 SHARK/WBNB 交易对兑换出大量的 SHARK,同时池中 WBNB 的数量增多;

对应的交易中的token转移如下:

此处是一个兑换动作,调用合约的话其实有多种方式实现,下面是一种实现方式,调用PantherPair.solswapExactTokensForTokens方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function swapExactTokensForTokens(
uint amountIn,
uint amountOutMin,
address[] calldata path,
address to,
uint deadline
) external virtual override ensure(deadline) returns (uint[] memory amounts) {
amounts = PantherLibrary.getAmountsOut(factory, amountIn, path);
require(amounts[amounts.length - 1] >= amountOutMin, 'PantherRouter: INSUFFICIENT_OUTPUT_AMOUNT');
TransferHelper.safeTransferFrom(
path[0], msg.sender, PantherLibrary.pairFor(factory, path[0], path[1]), amounts[0]
);
_swap(amounts, path, to);
}

3. 将第 1 步和第 2 步的 WBNB 和 SHARK 打入到 SharkMinter 中,为后续攻击做准备;

4. 调用 AutoShark 项目中的 WBNB/SHARK 策略池中的 getReward 函数,该函数会根据用户获利的资金从中抽出一部分手续费,作为贡献值给用户奖励 SHARK 代币,这部分操作在 SharkMinter 合约中进行操作;

5. SharkMinter 合约在收到用户收益的 LP 手续费之后,会将 LP 重新拆成对应的 WBNB 和 SHARK,重新加入到 Panther 的 WBNB/SHARK 交易池中;

6. 由于第 3 步攻击者已经事先将对应的代币打入到 SharkMinter 合约中,SharkMinter 合约在移除流动性后再添加流动性的时候,使用的是 SharkMinter 合约本身的 WBNB 和 SHARK 余额进行添加,这部分余额包含攻击者在第 3 步打入 SharkMinter 的余额,导致最后合约获取的添加流动性的余额是错误的,也就是说 SharkMinter 合约误以为攻击者打入了巨量的手续费到合约中;

7. SharkMinter 合约在获取到手续费的数量后,会通过 tvlInWBNB 函数计算这部分手续费的价值,然后根据手续费的价值铸币 SHARK 代币给用户。但是在计算 LP 价值的时候,使用的是 Panther WBNB/SHARK 池的 WBNB 实时数量除以 LP 总量来计算 LP 能兑换多少 WBNB。但是由于在第 2 步中,Panther 池中 WBNB 的数量已经非常多,导致计算出来的 LP 的价值非常高;

对应的SharkMinter方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

function mintFor(address flip, uint _withdrawalFee, uint _performanceFee, address to, uint, uint boostRate) override external onlyMinter returns(uint mintAmount) {
uint feeSum = _performanceFee.add(_withdrawalFee);
uint tax = 0;
if (flip == address(PANTHER)) {
tax = feeSum.mul(PANTHER.transferTaxRate()).div(10000);
}
IBEP20(flip).safeTransferFrom(msg.sender, address(this), feeSum.sub(tax).sub(1));

uint sharkBNBAmount = tokenToSharkBNB(flip, IBEP20(flip).balanceOf(address(this)));
address flipToken = sharkBNBFlipToken();
IBEP20(flipToken).safeTransfer(sharkPool, sharkBNBAmount);
IStakingRewards(sharkPool).notifyRewardAmount(sharkBNBAmount);
// 这里计算的贡献变大了
uint contribution = helper.tvlInBNB(flipToken, sharkBNBAmount).mul(_performanceFee).div(feeSum);
// 所以计算出的要铸造的shark的数量也变大了
uint mintShark = amountSharkToMint(contribution).mul(boostRate).div(10000);
mint(mintShark, to);
mintAmount = mintShark;
}

合约StrategyHelperV1tvlInBNB方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

function tvlInBNB(address _flip, uint amount) override public view returns (uint) {
if (_flip == address(PANTHER)) {
return pantherPriceInBNB().mul(amount).div(1e18);
}
address _token0 = IPantherPair(_flip).token0();
address _token1 = IPantherPair(_flip).token1();
if (_token0 == address(WBNB) || _token1 == address(WBNB)) {
// _flip是lp,因为此交易对的bnb很多,所以在得到的bnb就很大
uint bnb = WBNB.balanceOf(address(_flip)).mul(amount).div(IBEP20(_flip).totalSupply());
return bnb.mul(2);
}
uint balanceToken0 = IBEP20(_token0).balanceOf(_flip);
// 因为之前使用大量的bnb兑换token,导致price变大了
uint price = tokenPriceInBNB(_token0);
// (((balanceToken0 * price) / 1e18) * 2 * amount) / totalSupply
return balanceToken0.mul(price).div(1e18).mul(2).mul(amount).div(IBEP20(_flip).totalSupply());
}

8. 在 LP 价值错误和手续费获取数量错误的情况下,SharkMinter 合约最后在计算攻击者的贡献的时候计算出了一个非常大的值,导致 SharkMinter 合约给攻击者铸出了大量的 SHARK 代币;

对应的交易中的token转移如下:图中从0地址到SharkMinter就是在铸造大量的SHARK。

9. 攻击者后续通过卖出 SHARK 代币来换出 WBNB,偿还闪电贷。然后获利离开。

对应的交易中的token转移如下:

此步骤的操作逻辑比较简单,主要是调用Panther-SHARK-BNB-LP合约的swapTokensForExactTokens方法;然后再把WBNB转给Pancake 的 WBNB/BUSD 交易对(调用WBNBtransfer方法)来偿还闪电贷。

事件处理结果

AutoShark官方给的补偿计划

  1. 使用新的代币$JAW代替原来的$SHARK
  2. 被攻击前一个区块作为快照将$JAW分配给此时的$SHARK持有人
  3. 在180天里将所有金库的盈利的30%给$JAW的持有者
  4. 之前每铸造100个$SHARK,然后再额外铸造15个$SHARK给研发人员;接下来的60天里每铸造100个$JAW将其中的15个$JAW给$JAW的持有者。
  5. 将在PantherSwap平台发起IPO所得的部分BNB分配给$JAW持有者(金额没说)
  6. 将自己平台持有的所有$SHARK全部销毁
  7. 集成ChainLink的资产报价

PantherSwap关停所有$SHARK的LP池