说明
前面分析了两个漏洞,都是获取奖励的漏洞。这些漏洞最大的问题仅仅只是通过balanceOf计算账户的余额,然后根据余额的大小以此发送代币奖励。本篇文章讲解的DaoSwap的漏洞基本上前面两者的漏洞一致,不同的地方是在于,DaoSwap是根据swap的金额的大小,从而发送奖励。
合约分析
DaoRouter的设计实现基本上和UniswapV2保持一致,唯一的区别就是所有的swap token增加了一个inviter。当用户完成swap操作之后就会向inviter发送奖励,这个功能就是造成漏洞的原因。
在分析具体攻击交易之前,先分析合约代码,确定漏洞成因。
DaoRouter
DAORouter.sol
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| function swapTokensForExactTokens( uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline, address inviter ) external virtual override ensure(deadline) returns (uint[] memory amounts) { amounts = DAOLibrary.getAmountsIn(factory, amountOut, path); require(amounts[0] <= amountInMax, 'DAORouter: EXCESSIVE_INPUT_AMOUNT'); TransferHelper.safeTransferFrom( path[0], msg.sender, DAOLibrary.pairFor(factory, path[0], path[1]), amounts[0] ); _swap(amounts, path, to, inviter); }
|
整个代码逻辑和UniswapV2的代码逻辑一致,只是额外多了一个inviter参数。
继续跟踪进入到_swap()中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| function _swap(uint[] memory amounts, address[] memory path, address _to, address _inviter) internal virtual { for (uint i; i < path.length - 1; i++) { (address input, address output) = (path[i], path[i + 1]); (address token0,) = DAOLibrary.sortTokens(input, output); uint amountOut = amounts[i + 1]; (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0)); address to = i < path.length - 2 ? DAOLibrary.pairFor(factory, output, path[i + 2]) : _to;
IDAOPair(DAOLibrary.pairFor(factory, input, output)).swap( amount0Out, amount1Out, to );
swap2earnRouter.swapCall(ISwapCallee.Params(msg.sender, DAOLibrary.pairFor(factory, input, output), _inviter, input, output, amounts[i], amountOut)); } }
|
_swap()中的整个交换逻辑和UniswapV2中的_swap()实现完全一致,除了最后一行代码。
1
| swap2earnRouter.swapCall(ISwapCallee.Params(msg.sender, DAOLibrary.pairFor(factory, input, output), _inviter, input, output, amounts[i], amountOut));
|
这行代码就是用来计算给_inviter发送奖励的代码。继续分析swap2earnRouter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| // 为了方便说明,对 DAORouter 合约代码进行了精简,仅仅现实和swap2earnRouter有关的代码 interface ISwapCallee { struct Params { address user; address pair; address inviter; address input; address output; uint256 amountIn; uint256 amountOut; }
function swapCall(Params calldata p) external; }
ISwapCallee public immutable swap2earnRouter;
constructor(address _factory, address _WETH, address _swap2earnRouter) { swap2earnRouter = ISwapCallee(_swap2earnRouter); }
|
对应于
1
| swap2earnRouter.swapCall(ISwapCallee.Params(msg.sender, DAOLibrary.pairFor(factory, input, output), inviter, input, output, amounts[i], amountOut));
|
user,当前调用者 msg.sender
pair, 配对合约 DAOLibrary.pairFor(factory, input, output)
inviter,邀请人inviter
input,token0 的地址
output,token1的地址
amountIn,token0的输入数额
amountOut,token1的输出金额
后面就需要分析swap2earnRouter中的实现了
SwaptoEarn
顾名思义,通过swap就可以获得奖励。在swapCall()中存在两个逻辑。不仅调用swap本身的合约可以获取到swap的奖励。如果swap中的一个token是Dao代币,inviter还可以获取Dao代币奖励。
user 获取奖励
下面就是user因为swap 之后,根据swap的数量或者是金额获取奖励。查看swapCall函数的基本调用如下:
1 2 3 4 5 6 7 8 9 10 11 12
| function swapCall(Params memory p) external { address user = p.user; address pair = p.pair; address inviter = p.inviter; address input = p.input; address output = p.output; uint256 amountIn = p.amountIn; uint256 amountOut = p.amountOut;
require(msg.sender == swapRouter, "not swap router"); .... }
|
swapCall仅仅只是限制了msg.sender需要是swapRouter,对于user和inviter没有任何的限制,所以我们在通过swap获取奖励时,可以将user和inviter同时设置为我们的地址,这样,我们就可以获得更多的奖励。
下面分析user和inviter的奖励算法。
user奖励
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
| // output token has swap rewards if (tokenRewards[output] > 0) { // 获得 output 对应token的精度 uint256 d = uint256(IERC20Metadata(output).decimals()); // 计算 efficiency 的份额 uint256 staked = daoStakedInfo[user].staked / (10 ** d); //efficiency based 10000 uint256 efficiency = 10; if (staked < 200) { efficiency = 10; } else if (staked >= 200 && staked < 500) { efficiency = 20; } else if (staked >= 500 && staked < 1000) { efficiency = 30; } else if (staked >= 1000 && staked < 5000) { efficiency = 40; } else if (staked >= 5000) { efficiency = 60; } // 获得需要奖励的代币数量 uint256 amount = amountOut.mul(efficiency).div(10000); if (amount > tokenRewards[output]) { amount = tokenRewards[output]; } // 更新代币数量 tokenRewards[output] = tokenRewards[output].sub(amount); swapRewards[user][output] = swapRewards[user][output].add(amount); IERC20(output).safeTransfer(user, amount); }
|
inviter奖励
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| //inviter if (inviter != address(0)) { // based 10000 uint256 reward = 10; uint256 amount = 0; // 要求swap中的任意一个token是DAO,最高是0.1%的奖励 if (input == address(DAO)) { amount = amountIn.mul(reward).div(10000); } else if (output == address(DAO)) { amount = amountOut.mul(reward).div(10000); } if (amount > DAO.balanceOf(address(this))) { amount = DAO.balanceOf(address(this)); } if (amount > 0) { totalInviteRewards = totalInviteRewards.add(amount); // 发放奖励 DAO.safeTransfer(inviter, amount); inviteRewards[inviter] = inviteRewards[inviter].add(amount); } }
|
漏洞分析
以一个实际的攻击例子为例来进行说明,0x8eb87423f2d021e3acbe35c07875d1d1b30ab6dff14574a3f71f138c432a40ef
交易分析
首先,还是分析Token间的转移关系。
第一步,从不同的pair中贷出大量的BUSD
调用swap兑换,获得大量的DAO代币
前面已经说过,每一次的swap操作都可以获得DAO代币。
通过分析,发现攻击合约获得了两次奖励。两次奖励分别是作为user获得的奖励以及作为inviter获得的奖励。
后面就是循环swap,不断的获得奖励。基本上都是一样的,就不做过多的分析了。
调用栈分析
上面只是通过Tokens Transferred大致猜测,下面通过调用栈进行更加详细的分析。
0x414462f2aa63f371fbcf3c8df46b9a64ab64085ac0ab48900f675acd63931f23
在调用栈的开始,是大量的闪电贷的调用,导致占用了很大的篇幅。如下所示:
直接从第一个swap函数,swapExactTokensForTokens开始分析:
奖励详情
作为user和inviter获得的奖励分别是:
- 作为user获得的奖励,1865169287842972103590
- 作为inviter获得的奖励,310861547973828683931
可以看到,通过交易的Tokens Transferred和调用栈分析,两者的分析数据完全一致,这也验证了我们的想法。通过swap同时获得了作为user和inviter的奖励。
接下来就是循环调用swap,获得大量的奖励,就不做过多的分析了。
总结
DaoSwap的漏洞基本上和奖励漏洞原因类似,但是漏洞却更加严重。因为swap的时候没有限制合约也没有限制每个用户可以获得奖励的上限,导致攻击者利用闪电贷循环swap获得了大量的奖励。
通过近期几个奖励漏洞的分析,发现这些合约都存在一些共性:
- 漏洞合约都是闭源的
- 漏洞合约都没有限制合约调用,同时也没有考虑到闪电贷的情况
- 部分逻辑判断条件过于简单,导致被黑客利用,包括对用户代币数量的判断,用户奖励的获取计算
闪电贷的出现让之前直接根据余额的方式计算奖励的合约都将面临巨大的风险,闭源合约也不例外,只不过是时间问题而已。
对于合约开发来说,涉及到奖励方法的判断,需要额外小心,要考虑到多种情况。鉴于合约一旦上链便无法修改,所以合约开发者在开发合约时需要额外谨慎。
参考
https://mp.weixin.qq.com/s/3u0EvSyumkT-tGE25bJHOw