New to blockchain software development? Read my beginners guide here

Weird things to know about some ERC20 tokens

Created on November 2022 • Tags: ethereumsolidityauditing

A list of some quirks that some ERC20 tokens have, that you should be aware of if ever writing smart contracts that interact with erc20 tokens


Table of Contents for Weird things to know about some ERC20 tokens


ERC-20 tokens are extremely popular. A quick search on Google shows that there are some ERC20 tokens in the top 10 crypto currencies (USDC, USDT).

Here is a list of ‘gotchas’ to be aware of if you are working with ERC-20 tokens.

Calling transfer() or transferFrom() may transfer a different amount of the token

Some tokens, such as USDT have support to add a fee to transfers. This is not currently enabled, but it could be in the future.

Here is the USDT implementation of the transfer function. You can see the logic to add fees.

function transfer(address _to, uint _value) public onlyPayloadSize(2 * 32) {
uint fee = (_value.mul(basisPointsRate)).div(10000);
if (fee > maximumFee) {
fee = maximumFee;
}
uint sendAmount = _value.sub(fee);
balances[msg.sender] = balances[msg.sender].sub(_value);
balances[_to] = balances[_to].add(sendAmount);
if (fee > 0) {
balances[owner] = balances[owner].add(fee);
Transfer(msg.sender, owner, fee);
}
Transfer(msg.sender, _to, sendAmount);
}

This is known as fee on transfer. Not accounting for this feature can easily lead to incorrect balances being listed. There was a famous hack that exploited this (with the STA token, which uses fee on transfer).

The name() and symbol() for some tokens doesn’t return a string

The specs define them as:

function symbol() public view returns (string)
function name() public view returns (string)

But some tokens define them and return something else (such as bytes)

Some commonly used functions are optional in the specs

Apart from the fact that some tokens won’t implement functions that are required in the specs, you also have to remember that some commonly used functions are optional in the specs.

Here is a list of all optional functions in the erc20 specs :

function name() public view returns (string)
function symbol() public view returns (string)
function decimals() public view returns (uint8)

Not all tokens use 18 decimals of precision

We sometimes assume that all tokens use 18 decimals of precision.

It is often the case, but it isn’t rare to have something else. For example USDC has 6 decimals. Some tokens have num of decimals higher than 18 too.

It is important to bear this in mind if you’re ever hard coding the decimals or make assumptions about the number of decimals.

Some tokens require approving 0 first

  • Some ERC20 tokens require setting the amount of approved to 0
  • This is to avoid a security issue.
  • If Alice allows Bob to transfer 200 tokens, she could call approve(bobsAddress, 200). Then Alice decides to lower it to 100. Bob could see this change in the mempool, and could therefore spend 200 just before she sets it to 100, then straight after that could spend 100 (total 300, when at most Alice set it as 200).
  • The best workaround/fix for this is to ensure when changing approved amount to always set it back to 0 first.
  • full write up
  • Because of this issue, some tokens such as USDT implement this logic, so you have to ensure you approve 0 tokens first

They don’t all return a boolean on success, like the ERC-20 specs says they should

The ERC-20 spec says that some functions (such as the transfer function) should return a boolean to indicate if it was successful or not.

However, not all ERC-20 tokens implement that feature or do not correctly implement it.

For example this is the transfer() function for USDT (link). Notice how it has no return type.

function transfer(address _to, uint _value) public onlyPayloadSize(2 * 32) {
// ...
}

Some tokens (such as BNB) return booleans (like the spec says) on some functions, but not all.

Some tokens (such as XAUt) will return false even if the function was successful.

The best way to know if a transfer was successful is to call balanceOf() before and after, and check it changed as expected.

Tokens can be blocked

Some wallets have an extremely high balance of certain tokens, but they are unable to call transfer() or transferFrom() as the token contract has added it to a list of blocked/banned wallets.

Here is the logic from USDT:

function transfer(address _to, uint _value) public whenNotPaused {
require(!isBlackListed[msg.sender]);
if (deprecated) {
return UpgradedStandardToken(upgradedAddress).transferByLegacy(msg.sender, _to, _value);
} else {
return super.transfer(_to, _value);
}
}

Tokens cab be paused

This is a very common thing to see in contracts, but still something to think about. While testing things out, everything might work fine, but you may need to add in support for when tokens have paused things!

OpenZeppelin have a well known ‘pausable’ contract (link). You can check out its implementation to understand it.

Some tokens will revert if you try and transfer 0 tokens

While you might think it is a valid error, we might also sometimes code things and expect that a transfer of anything < some balance is valid… and of course 0 seems valid sometimes.

But some contracts will revert if you try and transfer 0.

You should check out the EIP-20 spec

Spotted a typo or have a suggestion to make this crypto dev article better? Please let me know!

See all posts (70+ more)

See all posts (70+ more)

Was this post helpful? 📧

If you liked this content and want to receive emails about future posts like this, enter your email. I'll never spam you.

Or follow me on @CryptoGuide_Dev on twitter

By using this site, you agree that you have read and understand its Privacy Policy and Terms of Use.
Use any information on this site at your own risk, I take no responsibility for the accuracy of safety of the information on this site.