• YouTube Channel
  • System Status
  • VS Code Extension
  • Expects Optional ERC-20 Functionality

    Overview

    What is the Expects Optional ERC-20 Functionality vulnerability?

    In the context of Ethereum, the ERC-20 token standard defines an interface for fungible tokens. While many of the properties of these tokens are required by the standard, a few properties are optional. If developing an application (e.g. a decentralized finance or DeFi application) that interacts with ERC-20 tokens, developers should take care not to rely on optional ERC-20 functionality as it may not always be present.

    Further reading: ERC-20 Token Standard

    Technical example of vulnerable code

      // SPDX-License-Identifier: Unlicense
      pragma solidity ^0.8.0;
    
      interface IERC20 {
          function name() external returns (string memory);
          function symbol() external returns (string memory);
          function decimals() external returns (uint8);
          function totalSupply() external returns (uint256);
          function balanceOf(address _owner) external returns (uint256 balance);
          function transfer(address _to, uint256 _value) external returns (bool success);
          function transferFrom(address _from, address _to, uint256 _value) external returns (bool success);
          function approve(address _spender, uint256 _value) external returns (bool success);
          function allowance(address _owner, address _spender) external returns (uint256 remaining);
    
          event Transfer(address indexed _from, address indexed _to, uint256 _value);
          event Approval(address indexed _owner, address indexed _spender, uint256 _value);
      }
    
      contract TokenSwapper {
          IERC20 public tokenA;
          IERC20 public tokenB;
    
          constructor(address _tokenA, address _tokenB) {
              tokenA = IERC20(_tokenA);
              tokenB = IERC20(_tokenB);
          }
    
          // Function to swap `amount` of tokenA for tokenB based on a provided `exchangeRate`
          // The `exchangeRate` is the amount of tokenB equivalent to 1 unit of tokenA
          function swap(uint256 amount, uint256 exchangeRate) public {
              uint8 decimalsA = tokenA.decimals();
              uint8 decimalsB = tokenB.decimals();
    
              // Calculate the amount of tokenB to send based on the exchange rate and decimals
              uint256 amountToSend = (amount * exchangeRate * (10 ** uint256(decimalsB))) / (10 ** uint256(decimalsA));
    
              // Transfer `amount` of tokenA from the caller to this contract
              require(tokenA.transferFrom(msg.sender, address(this), amount), "Transfer of tokenA failed");
    
              // Transfer `amountToSend` of tokenB from this contract to the caller
              require(tokenB.transfer(msg.sender, amountToSend), "Transfer of tokenB failed");
          }
    
          // Other functionalities like handling received tokens are omitted for brevity
      }
    

    In the example above, contract TokenSwapper represents a portion of a DeFi system that is designed to swap two tokens at a particular exchange rate, using the decimals() function for its calculations. However, this function is not a required part of the ERC-20 standard, so if one or both of the token contracts do not support it, this function will revert.

    Technical example of how to fix the vulnerability

      // SPDX-License-Identifier: Unlicense
      pragma solidity ^0.8.0;
    
      interface IERC20 {
          function name() external returns (string memory);
          function symbol() external returns (string memory);
          function decimals() external returns (uint8);
          function totalSupply() external returns (uint256);
          function balanceOf(address _owner) external returns (uint256 balance);
          function transfer(address _to, uint256 _value) external returns (bool success);
          function transferFrom(address _from, address _to, uint256 _value) external returns (bool success);
          function approve(address _spender, uint256 _value) external returns (bool success);
          function allowance(address _owner, address _spender) external returns (uint256 remaining);
    
          event Transfer(address indexed _from, address indexed _to, uint256 _value);
          event Approval(address indexed _owner, address indexed _spender, uint256 _value);
      }
    
      contract TokenSwapperUpdated {
          IERC20 public tokenA;
          IERC20 public tokenB;
    
          constructor(address _tokenA, address _tokenB) {
              tokenA = IERC20(_tokenA);
              tokenB = IERC20(_tokenB);
          }
    
          // Function to swap `amount` of tokenA for tokenB based on a provided `exchangeRate`
          // The `exchangeRate` is the amount of tokenB equivalent to 1 unit of tokenA
          function swap(uint256 amount, uint256 exchangeRate) public {
              require(tokenA.decimals() >= 0, "Decimals not supported for Token A");
              require(tokenB.decimals() >= 0, "Decimals not supported for Token B");
    
              uint8 decimalsA = tokenA.decimals();
              uint8 decimalsB = tokenB.decimals();
    
              // Calculate the amount of tokenB to send based on the exchange rate and decimals
              uint256 amountToSend = (amount * exchangeRate * (10 ** uint256(decimalsB))) / (10 ** uint256(decimalsA));
    
              // Transfer `amount` of tokenA from the caller to this contract
              require(tokenA.transferFrom(msg.sender, address(this), amount), "Transfer of tokenA failed");
    
              // Transfer `amountToSend` of tokenB from this contract to the caller
              require(tokenB.transfer(msg.sender, amountToSend), "Transfer of tokenB failed");
          }
    
          // Other functionalities like handling received tokens are omitted for brevity
      }
    
    

    In the revised example above, contract TokenSwapperUpdated now performs explicit checks in the swap() function to ensure that decimals are present before attempting to use them. A more robust implementation could use custom error handling depending on the logic of the broader application in the event that an optional ERC-20 function were unsupported.