• Returning “FALSE” VS “Reverting” on failure. To standardize the tokens that give false values on failing the txn and the tokens that simply revert, we use safeTransfer which simply makes the false returning token revert.

  • Reentrant Calls: Some tokens allow reentrant calls on transfer (e.g. ERC777 tokens). This has been exploited many times(ex- 1, 2). Questions to ask while dealing with ERC-777s

  • Is this an arbitrary token?

  • Could it have callbacks?

  • Is the sender trusted?

  • Is the receiver trusted?

  • Could they do something before the balance of the token gets updated like catching the system in an invalid state?

  • Revert on zero address or zero amount.

  • Many tokens transferring to or from the zero address- txn gets reverted.

  • Many tokens(ex-lend)on transferring zero amount- txn gets reverted

    <aside> 💡 Always ask: How does this ERC20 token perform during the above 2 conditions?

    </aside>

  • Revert on Zero Value Approvals.

  • Some tokens (e.g. BNB) revert when approving a zero value amount (i.e. a call to approve(address, 0)).

  • Missing Return Values - What if the interface that you are using expects to have a bool return value but in your implementation, you don't give any bool value in return hence txn gets reverted

  • Here is the list of tokens that don't return a bool value.

  • There are some tokens (eg: Tether Gold) that declare a bool return but then return false even when the transfer was successful

  • Supporting tether gold tokens is a very troublesome thing. A good safe transfer abstraction (example) can help somewhat, but note that the existence of tether gold makes it impossible to correctly handle return values for all tokens.

  • Revert on Approval To Zero Address - Some tokens (e.g. OpenZeppelin) will revert if trying to approve the zero address to spend tokens (i.e. a call to approve(address(0), amt)).

  • Fee on Transfer - Tokens that change the balance on the transfer.

    //Here is a pseudo-code example to understand-
    WRONG PRACTICE ❌
    we don't want to assume the received amount by receiver-like
    function(amount) external{
       token.transfer(msg.sender, this, amount); 
    ... this amount should have been received by the receiver so
    let's use this assumed amount in our accounting
    }
    
    RIGHT PRACTICE ✅
    function(amount) external{
    	 uint256 balBefore = token.balanceOf(this);
       token.transfer(msg.sender, this, amount);
       uint256 balReceived = token.balanceOf(this)-balBefore;
       ... use balReceived amount in accounting. 
    
  • Upgradeable Tokens

  • Tokens like USDT, and USDC are upgradeable allowing the token owners to make arbitrary modifications to the logic of the token at any point in time.

  • To mitigate this Protocols can use something like TUSD_adapter used by MakerDAO

  • Balance Modifications Outside of Transfers (rebasing/airdrops)

  • Some tokens may make changes in the balance outside of transfers(e.g. Ampleforth style rebasing tokens)

  • In uni-swap-v2 contracts they cache the token balance data and arbitrary modifications to underlying balances can mean that the contract is operating with outdated information. -In the case of Ampleforth, some Balancer and Uniswap pools are special-cased to ensure that the pool's cached balances are atomically updated as part of the rebase procedure.

  • Flash mintable tokens - Some tokens, like DAI, can be flash-minted (500 million is the limit) and paid by the end of the transaction. Hence, no swap is involved.

  • This leads to the possibility of manipulating the accounting of the protocol.

  • **Tokens with Blacklists -**Many tokens like USDT, and USDC have a blacklisted Feature in which txn gets reverted. Now in some cases, this could be used to exploit the protocol EX- If a protocol needs a txn to go through using this feature you can cause it to fail. In liquidation cases, you need to close the position(Txn needs to go through) otherwise protocol might take a bad debt.

  • Pausable Tokens

  • Some tokens can be paused by an admin (e.g. BNB, ZIL)

  • Subsequent non-zero approvals are not allowed aka race protection. - In USDT, KNC you are NOT allowed to subsequent approvals and there is a very good reason for that-

    Untitled

  • Multiple Token Addresses - You should never assume that a token interaction (balances, approvals, etc.) could only be done through its address because there are many tokens like TUSD(TrueUSD) that have the same features on different addresses. If you think on a fundamental level, this could be done with any ERC20 token by writing a wrapper contract for it. Here is an example : This contract assumes a single address per token, which would allow the owner to steal all funds in the pool.

    mapping isPoolToken(address => bool);
    constructor(address tokenA, address tokenB) public {
    isPoolToken[tokenA] = true;
    isPoolToken[tokenB] = true;
    }
    function rescueFunds(address token, uint amount) external nonReentrant onlyOwner {
    require(!isPoolToken[token], "access denied");
    token.transfer(msg.sender, amount);
    }
    
  • Token with different amounts of Decimals - Tokens like YAMV2 which uses 24 decimals

  • Tokens like USDC(6 decimals) & Gemini-USD (2 decimals) This could lead to lead to a precision loss.

  • Transfer of less than amount - Some tokens (e.g., cUSDCv3) contain a special case for amount == type(uint256).max in their transfer functions that result in only the user's balance being transferred.

  • transferFrom with src == msg.sender

  • Some token implementations (e.g. DSToken) will not attempt to decrease the caller's allowance if the sender is the same as the caller. This gives transferFrom the same semantics as transfer in this case. Other implementations (e.g. OpenZeppelin, Uniswap-v2) will attempt to decrease the caller's allowance from the sender in transferFrom even if the caller and the sender are the same address, giving transfer(dst, amt) and transferFrom(address(this), dst, amt) a different semantics in this case.

  • Non string metadata - Some tokens (e.g. MKR) have metadata fields (name / symbol) encoded as bytes32 instead of the string prescribed by the ERC20 specification. This may cause issues when trying to consume metadata from these tokens.

  • Unusual Permit Function - Some tokens (DAI, RAI, GLM, STAKE, CHAI, HAKKA, USDFL, HNY) have a permit() implementation that does not follow EIP2612. Tokens that do not support permits may not revert, which could lead to the execution of later lines of code in unexpected scenarios. Uniswap's Permit2 may provide a more compatible alternative.

  • Revert on large approvals or transfer Ex- If the amt is more than uin96 in case of UNI approval txn gets reverted tho in case of COMP both txn and approval-txns get reverted