Defi中的价格操纵攻击
直接价格操纵攻击:
某些Defi应用程序具有AMM中交易Token的接口,但是,这些接口如果没有得到适当的保护,则攻击者可以滥用这些接口来代表受攻击的Defi应用程序交易Token,会影响Token对的汇率,然后攻击者可以利用自己的Token进行另一笔交易以获取利益。
Token的价格是通过在AMM中交易Token对来直接操纵的,称为直接价格操纵攻击。‘
上图显示了一个示例。假设池具有与Token对X和Y相同的初始储备金(1,000)。在正常交易中,根据公式(1),用户可以用10 X获得9.9Y。攻击者可以使用以下三个步骤来执行直接价格操纵攻击。
步骤一:价格操纵第一阶段,攻击者使用900 X(占池的大部分)来交换TokenY,这破坏了Token对的余额并提高了TokenY在池中的价格。
步骤二:价格操纵第二阶段,攻击者调用易受攻击的DeFi应用程序的公共界面来出售10X。但是,DeFi应用程序在消耗10 X后只能获得2.75Y。这是因为上一步降低了TokenX的价格。此外,该交易进一步提升了X X的价格。池中TokenY的价格。
步骤三:成本赎回和获利,攻击者通过反向交易出售473 Y,获得905X。那是因为TokenY的价格已在第二步中提高了。这样,攻击者可以获得5倍的利润。
具体来说,第一步是增加TokenY的价格,并降低TokenX在池中的价格。根公式(1),这是预期的行为。但是,第二步使易受攻击的DeFi应用程序出售其TokenX,并进一步提高TokenY的价格。这是通过利用易受攻击的DeFi应用程序的公开接口来实现的。结果,攻击者可以通过出售TokenY进行反向交换,并获得更多的X(本示例中为5)。与正常交易(10 X和9.9 Y)相比,受害者DeFi应用损失了7.15 Y(即7.15 = 9.9 – 2.75)。
间接价格操纵攻击
一些Defi应用需要出于商业目的使用Token价格,例如,需要一个借款应用程序来计算抵押物的价格,以决定借款人有资格借多少枚Token。如果借款应用程序的价格机制是可操纵的,则借款人所借的Token可能会比抵押品的未偿本金余额更多(即抵押不足)
上面的例子中,借款应用程序使用从AMM中获取的Token对的实时汇率(通过调用AMM的智能合约公开的API)来确定抵押物的价值。假设TokenX和Y之间的初始汇率为1:1。在正常借款情况下,由于借款应用程序的抵押物比率为150%,因此用户将1.5个TokenX存入贷方应用程序作为抵押,并借入1个TokenY。攻击者通过以下步骤发起间接价格操纵攻击:
步骤一:价格操纵,攻击者用大量的TokenY来交换TokenX,耗尽了池中很大一部分的TokenX,从而为TokenX产生了虚高的价格。由于借用应用程序的价格机制取决于AMM的实时报价,TokenX的价格也会在借款应用中被夸大。
步骤二:获利,在操纵了TokenX的价格之后,攻击者只需使用TokenX作为抵押品就可以借入TokenY。特别是,他或她可以以与正常借款情况相同的抵押物(1.5 X)借入2 Y而不是1Y。
步骤三:成本补偿,攻击者只需要通过在AMM池中进行反向交换来赎回价格操纵的成本。这种攻击的根本原因是,脆弱的借款应用利用AMM的实时报价来决定抵押物的价格。结果,攻击者可以在AMM的交易池中进行交易以影响Token价格(步骤I),然后从借款应用借入抵押不足的借款(步骤II)。之后,攻击者进行反向交易以赎回成本(步骤III)
1. HEALTH -20221020
不正确的计算
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
| function _transfer(address from, address to, uint256 value) private { require(value <= _balances[from]); require(to != address(0)); uint256 contractTokenBalance = balanceOf(address(this));
bool overMinTokenBalance = contractTokenBalance >= numTokensSellToAddToLiquidity; if ( overMinTokenBalance && !inSwapAndLiquify && to == uniswapV2Pair && swapAndLiquifyEnabled ) { contractTokenBalance = numTokensSellToAddToLiquidity; //add liquidity swapAndLiquify(contractTokenBalance); } if (block.timestamp >= pairStartTime.add(jgTime) && pairStartTime != 0) { if (from != uniswapV2Pair) { uint256 burnValue = _balances[uniswapV2Pair].mul(burnFee).div(1000); //vulnerable point _balances[uniswapV2Pair] = _balances[uniswapV2Pair].sub(burnValue); //vulnerable point _balances[_burnAddress] = _balances[_burnAddress].add(burnValue); //vulnerable point if (block.timestamp >= pairStartTime.add(jgTime)) { pairStartTime += jgTime; } emit Transfer(uniswapV2Pair,_burnAddress, burnValue); IPancakePair(uniswapV2Pair).sync(); }
|
攻击者可以通过多次转移HEALTH代币,以减少Uniswap对中的HEALTH代币,来执行价格操纵。
攻击过程:
- 攻击合约首先通过闪电贷获得大量WBNB,然后通过PancakeRouter交易所兑换HEALTH。
- 查看攻击过程,发现反复调用HEALTH.transfer,销毁了流动池大量的Health代币
- 合约调用_transfer时,校验条件太松,销毁流动池中的Health代币,导致Health兑换WBNB价格升高。
2. ATK-20221012
balanceOf函数导致的不正确的价格计算
不安全地使用balanceOf函数,导致很容易收到闪电贷价格操纵的影响
在AST代币合约中使用getPrice()函数
1 2 3 4 5 6
| function getPrice() public view returns(uint256){ uint256 UDPrice; uint256 UDAmount = balanceOf(_uniswapV2Pair); //vulnerable point uint256 USDTAmount = USDT.balanceOf(_uniswapV2Pair); //vulnerable point UDPrice = UDAmount.mul(10**18).div(USDTAmount); return UDPrice;
|
3. RES Token-20221006
不正确的奖励计算
thisAToB()函数,burn RES代币以提高兑换率
攻击者进行了多次交换以获得奖励ALL代币,并且burn RES代币以提高兑换率
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
| function _transfer(address sender, address recipient, uint256 amount) internal { require(!_blacklist[tx.origin], "blacklist!"); require(!isContract(recipient) || _whiteContract[recipient] || sender == owner() || recipient == owner(), "no white contract"); require(sender != address(0), "BEP20: transfer from the zero address"); require(recipient != address(0), "BEP20: transfer to the zero address"); require(recipient != address(this), "transfer fail"); require(_allToken != address(0), "no set allToken"); if(sender != owner() && recipient != owner() && IPancakePair(_swapV2Pair).totalSupply() == 0) { require(recipient != _swapV2Pair,"no start"); } _balances[sender] = _balances[sender].sub(amount, "BEP20: transfer amount exceeds balance"); bool skip = _isSkip(sender, recipient); TransferType transferType = _transferType(sender, recipient); uint256 amountRecipient = amount; if (!_lockSwapFee && !skip && transferType != TransferType.TRANSFER){ if (transferType == TransferType.SWAP_BUY){ if (_isBuySwap(amount)){ amountRecipient = amount.mul(uint256(100).sub(_buyFee)).div(100); _distBuyFee(recipient, amount.mul(_buyFee).div(100)); //Get ALLtoken reward } }else if(transferType == TransferType.SWAP_SELL){ if (_isSellSwap(amount)){ amountRecipient = amount.mul(uint256(100).sub(_sellFee)).div(100); _distSellFee(sender, amount.mul(_sellFee).div(100)); } } } if (transferType == TransferType.TRANSFER){ _thisAToB(); //vulnerable point - burn RES }
function _thisAToB() internal{ if (_balances[address(this)] > _minAToB){ uint256 burnNumber = _balances[address(this)]; _approve(address(this),_pancakeRouterToken, _balances[address(this)]); IPancakeRouter(_pancakeRouterToken).swapExactTokensForTokensSupportingFeeOnTransferTokens( _balances[address(this)], 0, _pathAToB, address(this), block.timestamp); _burn(_swapV2Pair, burnNumber); //vulnerable point IPancakePair(_swapV2Pair).sync(); } }
|
4. RL Token-20221001
不正确的奖励计算
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
| function transferFrom( address from, address to, uint256 amount ) public virtual override returns (bool) { if (from != address(pancakeSwapV2Pair) && from != address(pancakeSwapV2Router)) { incentive.distributeAirdrop(from); } if (to != address(pancakeSwapV2Pair) && to != address(pancakeSwapV2Router)) { incentive.distributeAirdrop(to); //trace function } if (msg.sender != address(pancakeSwapV2Pair) && msg.sender != address(pancakeSwapV2Router)) { incentive.distributeAirdrop(msg.sender); //trace function } require(allowance(from, msg.sender) >= amount, "insufficient allowance"); if (govIDO != address(0)) { if (IKBKGovIDO(govIDO).isPriSaler(from)) { IKBKGovIDO(govIDO).releasePriSale(from); } if (IKBKGovIDO(govIDO).isPriSaler(to)) { IKBKGovIDO(govIDO).releasePriSale(to); } } //sell if (to == address(pancakeSwapV2Pair) && msg.sender == address(pancakeSwapV2Router)) { if (!isCommunityAddress[from]) { uint burnAmt = amount / 100; _burn(from, burnAmt); uint slideAmt = amount * 2 / 100; _transfer(from, slideReceiver, slideAmt); amount -= (burnAmt + slideAmt); } } else { if (!isCommunityAddress[from] && !isCommunityAddress[to]) { uint burnAmt = amount / 100; amount -= burnAmt; _burn(from, burnAmt); } } return super.transferFrom(from, to, amount); }
|
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
| function distributeAirdrop(address user) public override { if (block.timestamp < airdropStartTime) { return; } updateIndex(); uint256 rewards = getUserUnclaimedRewards(user); //vulnerable point usersIndex[user] = globalAirdropInfo.index; if (rewards > 0) { uint256 bal = rewardToken.balanceOf(address(this)); if (bal >= rewards) { rewardToken.transfer(user, rewards); userUnclaimedRewards[user] = 0; } } } function getUserUnclaimedRewards(address user) public view returns (uint256) { if (block.timestamp < airdropStartTime) { return 0; } (uint256 newIndex,) = getNewIndex(); uint256 userIndex = usersIndex[user]; if (userIndex >= newIndex || userIndex == 0) { return userUnclaimedRewards[user]; } else { //vulnerable point, Incorrect Reward calculation. only check balanceof of user without any requirement. return userUnclaimedRewards[user] + (newIndex - userIndex) * lpToken.balanceOf(user) / PRECISION; } }
|
5. BXH -20220928
不正确的奖励计算
错误地使用getReserves()函数来获取池中的余额,并通过getAmountOut来计算bonus
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
| function getITokenBonusAmount( uint256 _pid, uint256 _amountInToken ) public view returns (uint256){ PoolInfo storage pool = poolInfo[_pid];
(uint112 _reserve0, uint112 _reserve1, ) = IUniswapV2Pair(pool.swapPairAddress).getReserves(); //vulnerable point uint256 amountTokenOut = 0; uint256 _fee = 0; if(IUniswapV2Pair(pool.swapPairAddress).token0() == address(iToken)){ amountTokenOut = getAmountOut( _amountInToken , _reserve0, _reserve1, _fee); //vulnerable point } else { amountTokenOut = getAmountOut( _amountInToken , _reserve1, _reserve0, _fee); //vulnerable point } return amountTokenOut; }
function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut, uint256 feeFactor) private pure returns (uint ) { require(amountIn > 0, 'UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT'); require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
uint256 feeBase = 10000;
uint amountInWithFee = amountIn.mul(feeBase.sub(feeFactor)); uint numerator = amountInWithFee.mul(reserveOut); uint denominator = reserveIn.mul(feeBase).add(amountInWithFee); uint amountOut = numerator / denominator; return amountOut; }
|