Expects Optional ERC-20 Functionality
Overview
- Severity: Low
- Confidence: Medium
- Affected Versions: All
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.