Price manipulation attacks in Defi

Price manipulation attacks in Defi

Direct price manipulation attacks:

Some Defi applications have interfaces for trading Tokens in AMM. However, if these interfaces are not properly protected, attackers can abuse these interfaces to trade Tokens on behalf of the compromised Defi application, which will affect the exchange rate of Token pairs and then The attacker can use his own token to conduct another transaction to gain profit.

The price of Token is directly manipulated by trading Token pairs in AMM, which is called a direct price manipulation attack. '

https://pic1.zhimg.com/80/v2-6a170a2bb1213a4bdfd3ad9f98b0bcc8_720w.webp

The image above shows an example. Assume that the pool has the same initial reserve (1,000) as token pairs X and Y. In normal trading, according to formula (1), the user can get 9.9Y with 10X. An attacker can use the following three steps to perform a direct price manipulation attack.

Step One: Price Manipulation In the first stage, the attacker uses 900

Step 2: In the second stage of price manipulation, the attacker calls the public interface of the vulnerable DeFi application to sell 10X. However, DeFi applications can only earn 2.75Y after consuming 10X. This is because the previous step lowered the price of TokenX. In addition, the transaction further increased the price of XX. The price of TokenY in the pool.

Step 3: Cost redemption and profit. The attacker sells 473 Y through a reverse transaction and obtains 905X. That's because the price of TokenY has been increased in the second step. In this way, the attacker can obtain 5 times the profit.

Specifically, the first step is to increase the price of TokenY and decrease the price of TokenX in the pool. Root equation (1), this is the expected behavior. However, the second step enables the vulnerable DeFi application to sell its TokenX and further increase the price of TokenY. This is achieved by leveraging the exposed interfaces of vulnerable DeFi applications. As a result, the attacker can reverse the exchange by selling TokenY and obtain more X (5 in this example). Compared to normal transactions (10 X and 9.9 Y), the victim DeFi application lost 7.15 Y (i.e. 7.15 = 9.9 – 2.75).

Indirect price manipulation attacks

Some Defi applications need to use Token prices for commercial purposes. For example, a borrowing application is needed to calculate the price of collateral to determine how many Tokens a borrower is eligible to borrow. If the price mechanism of the borrowing application is manipulable, the borrower may borrow more tokens than the outstanding principal balance of the collateral (i.e. undercollateralization)

https://pic1.zhimg.com/80/v2-9ae2145434d17f36a6c867d59541a16c_720w.webp

In the above example, the borrowing application uses the real-time exchange rate of the token pair obtained from the AMM (by calling the API exposed by the AMM's smart contract) to determine the value of the collateral. Assume that the initial exchange rate between TokenX and Y is 1:1. Under normal borrowing conditions, since the collateral ratio of the borrowing application is 150%, the user deposits 1.5 TokenX into the lending application as collateral and borrows 1 TokenY. The attacker launches an indirect price manipulation attack through the following steps:

Step 1: Price manipulation. The attacker exchanges a large amount of TokenY for TokenX, depleting a large part of the TokenX in the pool, thereby generating an inflated price for TokenX. Since the price mechanism of the borrowing application depends on the real-time quotation of the AMM, the price of TokenX will also be inflated in the borrowing application.

Step 2: Profit. After manipulating the price of TokenX, the attacker only needs to use TokenX as collateral to borrow TokenY. In particular, he or she can borrow 2 Y instead of 1 Y with the same collateral as in the normal borrowing situation (1.5 X).

Step 3: Cost compensation, the attacker only needs to redeem the cost of price manipulation through reverse exchange in the AMM pool. The root cause of this attack is that the vulnerable lending application uses the AMM's real-time quotes to determine the price of the collateral. As a result, an attacker can conduct transactions in the AMM's transaction pool to influence the token price (step I), and then borrow undercollateralized funds from the borrowing application (step II). Afterwards, the attacker performs a reverse transaction to redeem the cost (Step III)

1. HEALTH -20221020

incorrect calculation

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

An attacker can perform price manipulation by transferring HEALTH tokens multiple times to reduce the number of HEALTH tokens in the Uniswap pair.

Attack process:

  1. The attack contract first obtains a large amount of WBNB through flash loans, and then exchanges HEALTH through the PancakeRouter exchange.
  2. Looking at the attack process, we found that HEALTH.transfer was repeatedly called, destroying a large number of Health tokens in the liquidity pool.
  3. When the contract calls _transfer, the verification conditions are too loose and the Health tokens in the liquidity pool are destroyed, causing the price of Health to be exchanged for WBNB to increase.

2. ATK-20221012

Incorrect price calculation caused by balanceOf function

Unsafe use of the balanceOf function makes it easy to be affected by flash loan price manipulation

Using the getPrice() function in the AST token contract

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

Incorrect reward calculation

thisAToB() function, burn RES tokens to increase the exchange rate

The attacker conducted multiple exchanges to obtain reward ALL tokens, and burned RES tokens to increase the exchange rate

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

Incorrect reward calculation

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);
    }
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. Chart -20220928

Incorrect reward calculation

Incorrectly using the getReserves() function to get the balance in the pool and getAmountOut to calculate the bonus

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

Guess you like

Origin blog.csdn.net/m0_53689197/article/details/130127667