Why Tokens with Deflation Mechanism Are Often Attacked
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.