Why Tokens with Deflation Mechanism Are Often Attacked

Eocene | Security
6 min readMar 7, 2023

--

Overview

Tokens with deflation mechanism on the blockchain are often attacked recently. This article discusses and analyzes why these tokens are attacked, and gives corresponding defense solutions.

There are generally two ways to implement the deflation mechanism in tokens, one is the burn mechanism, and the other is the reflection mechanism. Below we will analyze the possible problems of these two methods.

Burn Mechanism

Usually tokens with a burn mechanism will implement the logic of burn at its _transfer function. There will be a situation where the sender bears the handling fee. In this case, the amount of tokens received by the receiver will not change, but the sender needs to pay more for the handling fee. A simple example is as follows:

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 risks in this situation.

If we look at the token contract alone, we will find that there is actually no problem with this way of writing, but there are many complicated situations in the blockchain, and we need to consider many aspects.

Usually, in order to make the tokens have a price, the project party will add liquidity to the tokens in decentralized exchanges such as Uniswap and Pancakeswap.

Among them, in Uniswap, there is a function skim, which will transfer the difference between the balance and reserve of the two tokens in the liquidity pool to the caller to balance the balance and reserve:

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 time, the sender becomes a liquidity pool, and when _transfer is called, the tokens in the liquidity pool will be burned, causing the price of tokens to rise.

Attackers take advantage of this feature to transfer tokens to the liquidity pool, and then call the skim function to transfer them out, repeating this operation many times, causing a large amount of tokens in the liquidity pool to burn, the price soars, and finally sell the tokens Make a profit.

A real attack case, 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 WDOGE, when block.timestamp > closingTime, it enters the else loop. In #21, the transfer amount is deducted from the sender's balance, and in #31, the sender is burned tokensToBurn amount of tokens. The attacker uses this mechanism to steal all the value coins in the liquidity pool through the above attack method.

Reflection Mechanism

In reflection mechanism, users will be charged a fee every time they trade, which is used to reward users who hold tokens, but it will not trigger a transfer, but only modify a coefficient.

In this mechanism, the user has two types of token amounts, tAmount and rAmount. tAmount is the actual amount of tokens, and rAmount is the amount of tokens after reflection, and the rate is tTotal divided by rTotal:

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

There is generally a function called deliver in the token of the reflection mechanism, which will destroy the caller's token and reduce the value of rTotal, so the rate will increase, and the tokens reflected by other users will 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);
}

Attacker notices this function and uses it to attack the liquidity pool of Uniswap.

Then what should he do? Also start with Uniswap’s 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));
}

The value of reserve in Uniswap is the reserve fund, which is different from token.balanceOf(address(this)).

The attacker first calls the deliver function to destroy his own tokens, reducing the value of rTotal, and the rate increases accordingly, so the value of tokens after reflection will also increase, and token.balanceOf(address(this)) also becomes larger, and there is a difference with reserve.

Therefore, the attacker can make profits by calling the skim function.

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, BEVO NFT Art Token(BEVO):

Here is another attack method, when there is a burn function in the token contract:

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 the user calls burn function, his own tokens will be burned, and the value of tTotal will be reduced at the same time, so the rate will decrease, and the corresponding reflected tokens will also decrease, so the number of tokens in the liquidity pool will also decrease at this time , the price will rise.

Then the attacker reduces the value of tTotal by calling the burn function multiple times, and then calls the sync function of the liquidity pool to reserve and balances synchronously. Finally, the tokens in the liquidity pool are greatly reduced and the price soars. Then the attacker sells the token for a 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, Sheep Token(SHEEP):

Solution

After interpreting the attack techniques against the tokens of burn mechanism and reflection mechanism, it is not difficult to find that the core point of the attacker’s attack is the price manipulation of the liquidity pool, so adding the address of the liquidity pool to the white list, not involving the burning of tokens and not participating in the reflection mechanism in each operation can avoid such attacks.

Summary

This article analyzes the two implementation mechanisms of deflationary tokens and the attack methods against the two mechanisms, and finally gives the corresponding solutions. When writing contracts, the project party must consider the combination of tokens and decentralized exchanges to avoid such attacks.

About Eocene Research

At Eocene Research, we provide the insights of intentions and security behind everything you know or don’t know of blockchain, and empower every individual and organization to answer complex questions we hadn’t even dreamed of back then.

Learn more: Website | Medium | Twitter

--

--

Eocene | Security

Smart contract audit, attack analysis, web3 security research, on-chain monitor and alert. Powered by Eceone Research.