ERC-20 Interface
Overview
- Severity: High
- Confidence: Medium
- Affected Versions: All
What is the ERC-20 Interface vulnerability?
In the context of Ethereum, the ERC-20
standard represents an interface for fungible tokens. The standardization of these tokens means that smart contracts can be developed that use the specified interface; however, if a fungible token contract fails to properly implement the specification, smart contracts interfacing with that token contract may experience subtle or drastic failures.
Further reading: ERC-20: Token Standard
Technical example of vulnerable code
// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;
contract NonStandardERC20Token {
string public name = "NonStandardERC20Token";
string public symbol = "NST";
uint8 public decimals = 18;
uint256 public totalSupply = 1000000 * (10 ** uint256(decimals));
mapping(address => uint256) public balanceOf;
event Transfer(address indexed _from, address indexed _to, uint256 _value);
event Approval(address indexed _owner, address indexed _spender, uint256 _value);
constructor() {
balanceOf[msg.sender] = totalSupply; // Assign all tokens to the creator for simplicity
}
// Notice the absence of a return value, deviating from the ERC-20 standard
function transfer(address _to, uint256 _value) public {
if(balanceOf[msg.sender] >= _value) {
balanceOf[msg.sender] -= _value;
balanceOf[_to] += _value;
}
// No return statement here
}
function transferFrom(address _from, address _to, uint256 _value) public returns (bool success) {
// Omitted for brevity
return success;
}
function approve(address _spender, uint256 _value) public returns (bool success)
{
// Omitted for brevity
return success;
}
function allowance(address _owner, address _spender) public view returns (uint256 remaining) {
// Omitted for brevity
return remaining;
}
}
interface IERC20 {
function transfer(address _to, uint256 _value) external returns (bool);
}
contract TokenSwapper {
event Swap(address indexed user, address token, uint256 amount);
// Attempt to swap tokens by transferring from the user to this contract
function swapToken(IERC20 token, uint256 amount) public {
require(token.transfer(msg.sender, amount), "Token transfer failed");
emit Swap(msg.sender, address(token), amount);
}
// Additional logic for swapping tokens, managing balances, etc., omitted for brevity
}
In the example above, contract TokenSwapper
illustrates a simplified program for swapping different ERC-20 tokens for some other digital asset. The function swapToken()
uses the transfer()
function on the NonStandardERC20Token
contract, which per the ERC-20 token standard should return a Boolean value indicating success or failure of the transfer. However, this function does not return anything, so the require()
condition in swapToken()
will never fail, even if the underlying transfer fails, counter to intended logic.
Technical example of how to fix the vulnerability
// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;
contract StandardERC20Token {
string public name = "NonStandardERC20Token";
string public symbol = "NST";
uint8 public decimals = 18;
uint256 public totalSupply = 1000000 * (10 ** uint256(decimals));
mapping(address => uint256) public balanceOf;
event Transfer(address indexed _from, address indexed _to, uint256 _value);
event Approval(address indexed _owner, address indexed _spender, uint256 _value);
constructor() {
balanceOf[msg.sender] = totalSupply; // Assign all tokens to the creator for simplicity
}
function transfer(address _to, uint256 _value) public returns (bool success) {
if(balanceOf[msg.sender] >= _value) {
balanceOf[msg.sender] -= _value;
balanceOf[_to] += _value;
success = true;
} else {
success = false;
}
return success;
}
function transferFrom(address _from, address _to, uint256 _value) public returns (bool success) {
// Omitted for brevity
return success;
}
function approve(address _spender, uint256 _value) public returns (bool success)
{
// Omitted for brevity
return success;
}
function allowance(address _owner, address _spender) public view returns (uint256 remaining) {
// Omitted for brevity
return remaining;
}
}
interface IERC20 {
function transfer(address _to, uint256 _value) external returns (bool);
}
contract TokenSwapper {
event Swap(address indexed user, address token, uint256 amount);
// Attempt to swap tokens by transferring from the user to this contract
function swapToken(IERC20 token, uint256 amount) public {
require(token.transfer(msg.sender, amount), "Token transfer failed");
emit Swap(msg.sender, address(token), amount);
}
// Additional logic for swapping tokens, managing balances, etc., omitted for brevity
}
In the revised example, contract StandardERC20Token
has had its transfer()
function updated to properly return the Boolean indicator for success or failure of the transaction, so the require()
condition in the TokenSwapper
contract's swapToken()
function can properly halt execution if the underlying transfer fails.