From a technical perspective: Why tokens with deflationary mechanisms are vulnerable to attacks

EoceneResearch
2023-03-08 23:45:10
Collection
This article will discuss and analyze the reasons why token tokens are attacked and provide corresponding defense solutions.

Author: Eocene Research

Overview

Tokens with deflationary mechanisms on the blockchain have recently been frequently attacked. This article will discuss and analyze the reasons why token tokens are attacked and provide corresponding defense solutions.

There are usually two ways to implement a deflationary mechanism in tokens: one is the burn mechanism, and the other is the reflection mechanism. Below, we will analyze the potential issues with these two implementation methods.

Burn Mechanism

Typically, tokens with a burn mechanism will implement the burning logic in their _transfer function. Sometimes, the sender may bear the transaction fee. In this case, the number of tokens received by the recipient does not change, but the sender needs to pay more tokens because they have to bear the transaction fee. Here is a simple example:

function _transfer(address sender, address recipient, uint256 amount) internal virtual returns (bool) {
    require(_balances[sender] >= amount, "ERC20: transfer amount exceeds balance");
    require(sender != address(0), "ERC20: transfer from the zero address");
    require(recipient != address(0), "ERC20: transfer to the zero address");
    burnFee = amount * burnFeeRate;
    _balances[sender] -= amount;
    _burn(sender, burnFee);
    _balances[recipient] += amount;
}

Then we discuss the potential risks in this case.

If we look solely at the token contract, we will find that this writing actually has no issues, but there are many complex situations in the blockchain that require us to consider many aspects.

Typically, to give the token a price, the project team will add liquidity for the token on decentralized exchanges such as Uniswap and Pancakeswap.

In Uniswap, there is a function called skim, which transfers the difference between the balances and reserves of the two tokens in the liquidity pool to the caller to balance the balances and reserves:

function skim(address to) external lock {
    address _token0 = token0; // gas savings
    address _token1 = token1; // gas savings
    _safeTransfer(_token0, to, IERC20(_token0).balanceOf(address(this)).sub(reserve0));
    _safeTransfer(_token1, to, IERC20(_token1).balanceOf(address(this)).sub(reserve1));
}

At this point, the sender becomes the liquidity pool, and when calling _transfer, some tokens in the liquidity pool will be partially burned, causing the token price to rise partially.

Attackers exploit this feature by directly transferring tokens into the liquidity pool, then calling the skim function to withdraw, and then repeating this operation multiple times, resulting in a large number of tokens being burned in the liquidity pool, and the price skyrocketing, ultimately selling the tokens for profit.

A real attack case is winner doge (WDOGE):

function _transfer(address sender, address recipient, uint256 amount) internal virtual returns (bool) {
    require(_balances[sender].amount >= amount, "ERC20: transfer amount exceeds balance");
    require(sender != address(0), "ERC20: transfer from the zero address");
    require(recipient != address(0), "ERC20: transfer to the zero address");
    if(block.timestamp >= openingTime && block.timestamp <= closingTime) {
        _balances[sender].amount -= amount;
        _balances[recipient].amount += amount;
        emit Transfer(sender, recipient, amount);
    } else {
        uint256 onePercent = findOnePercent(amount);
        uint256 tokensToBurn = onePercent * 4;
        uint256 tokensToRedistribute = onePercent * 4;
        uint256 toFeeWallet = onePercent * 1;
        uint256 todev = onePercent * 1;
        uint256 tokensToTransfer = amount - tokensToBurn - tokensToRedistribute - toFeeWallet - todev;

        _balances[sender].amount -= amount;
        _balances[recipient].amount += tokensToTransfer;
        _balances[feeWallet].amount += toFeeWallet;
        _balances[dev].amount += todev;

        if (!_balances[recipient].exists) {
            _balanceOwners.push(recipient);
            _balances[recipient].exists = true;
        }

        redistribute(sender, tokensToRedistribute);
        _burn(sender, tokensToBurn);
        emit Transfer(sender, recipient, tokensToTransfer);
    }
    return true;
}

In the _transfer function of the WDOGE contract, when block.timestamp > closingTime, it enters the else loop. In line 21 of the code, the transfer amount is deducted from the sender's balance, and in line 31 of the code, the sender is burned the amount of tokensToBurn. The attacker exploits this fee mechanism to steal all valuable tokens (WBNB) from the liquidity pool through the above attack method.

Reflection Mechanism

In the reflection mechanism, users are charged a fee for each transaction, which is used to reward users holding tokens, but it does not trigger a transfer; it simply modifies a coefficient.

In this mechanism, users have two types of token amounts: tAmount and rAmount. tAmount is the actual token amount, and rAmount is the reflected token amount, with a ratio of tTotal / rTotal. A typical code implementation is as follows:

function balanceOf(address account) public view override returns (uint256) {
    if (_isExcluded[account]) return _tOwned[account];
    return tokenFromReflection(_rOwned[account]);
}

function tokenFromReflection(uint256 rAmount) public view returns(uint256) {
    require(rAmount <= _rTotal, "Amount must be less than total reflections");
    uint256 currentRate = _getRate();
    return rAmount.div(currentRate);
}

function _getRate() private view returns(uint256) {
    (uint256 rSupply, uint256 tSupply) = _getCurrentSupply();
    return rSupply.div(tSupply);
}

Tokens with a reflection mechanism generally have a function called deliver, which will burn the caller's tokens, reducing the value of rTotal, thus increasing the ratio, and the reflected token amounts for other users will also increase:

function deliver(uint256 tAmount) public {
    address sender = _msgSender();
    require(!_isExcluded[sender], "Excluded addresses cannot call this function");
    (uint256 rAmount,,,,,) = _getValues(tAmount);
    _rOwned[sender] = _rOwned[sender].sub(rAmount);
    _rTotal = _rTotal.sub(rAmount);
    _tFeeTotal = _tFeeTotal.add(tAmount);
}

The attacker notices this function and uses it to attack the corresponding Uniswap liquidity pool.

So how can they exploit it? Again, starting from the Uniswap skim function:

function skim(address to) external lock {
    address _token0 = token0; // gas savings
    address _token1 = token1; // gas savings
    _safeTransfer(_token0, to, IERC20(_token0).balanceOf(address(this)).sub(reserve0));
    _safeTransfer(_token1, to, IERC20(_token1).balanceOf(address(this)).sub(reserve1));
}

In Uniswap, reserve is the reserve fund, which is different from token.balanceOf(address(this)).

The attacker first calls the deliver function to burn their tokens, causing the value of rTotal to decrease, which in turn increases the ratio, so the value of the reflected tokens will also increase, and token.balanceOf(address(this)) will also correspondingly increase, creating a discrepancy with the value of reserve.

Therefore, the attacker can profit by calling the skim function to withdraw tokens equal to the difference between the two.

Attacker: token.deliver

rtotal: decrease

rate: increase

tokenFromReflection: increase

balanceOf: increase -> token.balanceOf(address(this)) > reserve

Attacker: pair.skim

token.balanceOf(address(this)) > reserve

token.transfer

A real attack case is BEVO NFT Art Token (BEVO):

When there is a burn function in the token contract, there exists another similar attack method:

function burn(uint256 _value) public {
    _burn(msg.sender, _value);
}

function _burn(address _who, uint256 _value) internal {
    require(_value <= _rOwned[_who]);
    _rOwned[_who] = _rOwned[_who].sub(_value);
    _tTotal = _tTotal.sub(_value);
    emit Transfer(_who, address(0), _value);
}

When a user calls the burn function, their tokens will be burned, and the value of tTotal will decrease, so the ratio will decrease, and the corresponding reflected token amount will also decrease, causing the number of tokens in the liquidity pool to decrease, thus increasing the token price.

Attackers exploit this feature by repeatedly calling the burn function to reduce the value of tTotal, then calling the liquidity pool's sync function to synchronize reserves and balances. Finally, the number of tokens in the liquidity pool decreases significantly, and the price skyrockets. The attacker then sells the tokens for profit.

Attacker: token.burn

tTotal: decrease

rate: decrease

tokenFromReflection: decrease

balanceOf: decrease

Attacker: pair.sync

token.balanceOf(address(this)) > reserve

token.transfer

A real attack case is Sheep Token (SHEEP):

Defense Solutions

By interpreting the attack methods against tokens with burn and reflection mechanisms, it is not difficult to find that the core point of the attackers' attacks is to manipulate the price of the liquidity pool. Therefore, adding the liquidity pool address to the whitelist, not involving the destruction of tokens, and not participating in the token reflection mechanism can avoid such attacks.

Conclusion

This article analyzes the two implementation mechanisms of deflationary mechanism tokens and the attack methods against these two mechanisms, and finally provides corresponding solutions. When writing contracts, project teams must consider the integration of tokens with decentralized exchanges to avoid such attacks.

ChainCatcher reminds readers to view blockchain rationally, enhance risk awareness, and be cautious of various virtual token issuances and speculations. All content on this site is solely market information or related party opinions, and does not constitute any form of investment advice. If you find sensitive information in the content, please click "Report", and we will handle it promptly.
ChainCatcher Building the Web3 world with innovators