Weird Token List
ERC20 tokens are widely used in the Ethereum ecosystem, but not all tokens adhere strictly to the ERC20 standard. In this section, we will cover various unusual and unexpected behaviors of ERC20 tokens, illustrating real-world cases where these behaviors have been exploited. The goal is to help developers and auditors understand potential pitfalls when integrating or interacting with ERC20 tokens in smart contracts.
Reentrant Calls
Some tokens, such as ERC777 tokens, allow reentrant calls during transfers. This means that during a token transfer, an external call can be made back into the smart contract before the original transfer completes, which can lead to vulnerabilities. This behavior has been exploited, for instance, in the imBTC Uniswap pool, where reentrant calls led to drained funds.
Example: Reentrant.sol
Missing Return Values
Certain tokens do not return a boolean (bool
) value from ERC20 methods as expected. Tokens like USDT, BNB, and OMG are examples of this. Some tokens, such as BNB, may return a boolean for some methods but fail to do so for others, which caused stuck tokens in Uniswap v1. In extreme cases, such as Tether Gold, tokens may declare a bool
return type but always return false
even when transfers are successful.
To handle this issue, a robust transfer abstraction (example provided) can help, though some tokens are impossible to handle consistently due to their broken implementations.
Example Tokens:
MissingReturns.sol
: Does not return a boolean for any ERC20 operation.ReturnsFalse.sol
: Always returnsfalse
for all ERC20 operations.
Fee on Transfer
Some tokens charge a fee on transfers, such as STA and PAXG. Even tokens that don't currently charge fees, like USDT or USDC, may introduce them in the future. These transfer fees can disrupt systems, such as when $500k was drained from Balancer pools due to the STA transfer fee.
Example: TransferFee.sol
Balance Modifications Outside of Transfers (Rebasing / Airdrops)
Certain tokens modify user balances without initiating a transfer. These include rebasing tokens like Ampleforth and airdrop models like Compound’s governance tokens. Such arbitrary balance changes can break systems that cache balances, like Uniswap V2 or Balancer. To prevent this, some systems ensure their pools are updated atomically during rebasing.
Example: A rebasing token implementation.
Upgradable Tokens
Tokens like USDC and USDT can be upgraded, allowing the contract owner to change the token's behavior at any time. This poses a risk to smart contracts that depend on specific behaviors from tokens. To mitigate this, developers can introduce logic that freezes interactions with an upgradable token if an upgrade is detected, as MakerDAO did with the TUSD adapter.
Example: Upgradable.sol
Flash Mintable Tokens
Tokens like DAI support "flash minting," allowing tokens to be minted for the duration of a single transaction, provided they are returned by the end of the transaction. This is similar to flash loans but without requiring pre-existing tokens. Such tokens could technically mint up to max uint256
tokens within a single transaction.
Documentation for the MakerDAO flash mint module can be found here.
Tokens with Blocklists
Tokens like USDC and USDT implement admin-controlled blocklists that prevent transfers to or from certain addresses. This could be used to trap funds, for example, by adding a contract’s address to the blocklist. This risk may arise due to regulatory action or even malicious intent.
Example: BlockList.sol
Pausable Tokens
Tokens such as BNB and ZIL allow an admin to pause the token, preventing all transfers. This can expose users to risk if the admin is compromised or acts maliciously.
Example: Pausable.sol
Approval Race Protections
Some tokens, like USDT and KNC, do not allow increasing an approved amount (M > 0
) if an existing amount (N > 0
) is already approved. This is a protection against an ERC20 attack vector described here.
Example: Approval.sol
Revert on Approval to Zero Address
Tokens like those in the OpenZeppelin framework will revert if an attempt is made to approve the zero address to spend tokens. Developers may need to implement special logic to handle this behavior.
Example: ApprovalToZeroAddress.sol
Revert on Zero Value Approvals
Some tokens, such as BNB, revert when approving a zero-value amount. Integrators must account for this by adding special cases in their contract logic.
Example: ApprovalWithZeroValue.sol
Revert on Zero Value Transfers
Some tokens revert when a zero-value transfer is initiated.
Example: RevertZero.sol
Multiple Token Addresses
Some proxied tokens, particularly those with multiple addresses, pose unique risks. For example, a rescueFunds
function could allow a contract owner to steal all tokens in a pool if the logic assumes a single token address per contract.
Example: Proxied.sol
Low Decimals
Tokens with fewer than 18 decimals, like USDC (which has 6), may introduce precision loss in calculations. Even more extreme, tokens like Gemini USD only have 2 decimals.
Example: LowDecimals.sol
High Decimals
Some tokens, such as YAM-V2, have more than 18 decimals (YAM-V2 has 24). This can cause overflows and introduce risks of unexpected reverts in smart contracts.
Example: HighDecimals.sol
TransferFrom with src == msg.sender
Some tokens, like DSToken, do not attempt to decrease the caller's allowance if the caller is also the sender, making transferFrom
behave like transfer
. In contrast, other tokens, like those using OpenZeppelin, always decrease the allowance.
Examples:
ERC20.sol
: Does not decrease allowance.TransferFromSelf.sol
: Always decreases allowance.
Non-String Metadata
Tokens like MKR encode their metadata (e.g., name, symbol) as bytes32
rather than string
. This can lead to issues when attempting to read metadata.
Example: Bytes32Metadata.sol
Revert on Transfer to the Zero Address
Tokens, such as those based on OpenZeppelin’s implementation, revert when transferring to address(0)
. This could disrupt systems relying on address(0)
for burning tokens.
Example: RevertToZero.sol
No Revert on Failure
Some tokens, such as ZRX and EURS, return false
rather than reverting on failure. This behavior, while technically compliant with ERC20, deviates from typical Solidity coding practices and may be overlooked by developers who forget to handle non-revert failures.
Example: NoRevert.sol
Revert on Large Approvals & Transfers
Tokens like UNI and COMP revert if the value passed to approve
or transfer
exceeds uint96
. They also implement special logic for approve
that sets allowances to type(uint96).max
if the approval amount equals uint256(-1)
.
Example: Uint96.sol
Code Injection via Token Name
Some malicious tokens include JavaScript code in their name
attribute, allowing attackers to steal private keys from users interacting with the token via vulnerable frontends. This tactic has been used in the wild, notably to exploit EtherDelta users.
Unusual Permit Function
Certain tokens, such as DAI, RAI, GLM, and others, have a permit()
function that does not follow EIP2612. Tokens without a proper permit()
implementation may not revert, leading to unexpected execution of subsequent lines of code. Uniswap’s Permit2
is a more compatible alternative for handling such tokens.
Example: DaiPermit.sol
Transfer of Less Than Amount
Tokens like cUSDCv3 contain special handling for transfers when amount == type(uint256).max
, transferring only the user’s balance. Systems that transfer user-supplied amounts without verifying the actual transferred value may encounter issues with these tokens.
This section provides examples and insights into some of the common pitfalls smart contract developers face when interacting with ERC20 tokens. These behaviors can break smart contracts or lead to vulnerabilities if not accounted for properly. The provided examples illustrate these scenarios, encouraging caution and defensive programming practices.
Last updated