Token-0x

First Flight #54
Beginner FriendlyDeFi
100 EXP
Submission Details
Impact: high
Likelihood: high

Non-Compliance with IERC20 Interface - Missing Interface Implementation

Author Revealed upon completion

Description

Normal Behavior

ERC20 compliant tokens should explicitly declare that they implement the IERC20 interface. This allows smart contracts expecting IERC20 tokens to properly type-check token interactions. External systems can verify ERC20 compliance through interface detection.

Specific Issue

The ERC20 contract does not inherit from the IERC20 interface, even though the implementation provides all required functions. This breaks type compatibility with ERC20-expecting systems, wallets, and DeFi protocols. While the implementation is functionally correct, the lack of interface inheritance prevents proper integration and standard compliance.


Root Cause in Codebase

File: src/ERC20.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {ERC20Internals} from "./helpers/ERC20Internals.sol";
import {IERC20Errors} from "./helpers/IERC20Errors.sol";
//@> contract ERC20 is IERC20Errors, ERC20Internals {
//@> // ❌ Does NOT inherit from IERC20 interface
//@> // ❌ No import of IERC20
//@> }

File: src/IERC20.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// ✅ IERC20 interface exists
interface IERC20 {
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 value) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
function approve(address spender, uint256 value) external returns (bool);
function transferFrom(address from, address to, uint256 value) external returns (bool);
}
// ❌ But ERC20.sol doesn't inherit from this!

Issue: The IERC20 interface is defined but ERC20.sol does not explicitly inherit from it.


Risk Analysis

Likelihood

Reason 1: Type checking is performed at compile time

  • Any code expecting IERC20 type will reject ERC20 type

  • This affects every integration attempt

Reason 2: DeFi protocols universally use IERC20 interface

  • DEXs, lending protocols, bridges all expect IERC20 tokens

  • Token without interface inheritance cannot be used in these systems

Reason 3: Wallet and explorer tools check for IERC20 compliance

  • Block explorers use interface detection to identify ERC20 tokens

  • Wallets may not display tokens if they don't implement interface


Impact

Impact 1: Type incompatibility with external contracts

  • Functions expecting IERC20 parameter cannot accept this ERC20 token

  • Requires ugly casting: IERC20(address(token)) instead of direct usage

  • Breaks type safety and contract interaction patterns

Impact 2: Integration failures with DeFi protocols

  • DEX liquidity pools won't accept the token directly

  • Lending protocols can't use token as collateral without workarounds

  • Bridge protocols may reject non-standard token implementations

Impact 3: Wallet and tool recognition issues

  • MetaMask and other wallets may not properly display token balance

  • Etherscan may not mark token as ERC20 compliant

  • Third-party tools may not track token movements correctly

Impact 4: Standards compliance failure

  • Token fails formal ERC20 compliance checks

  • Cannot be listed on exchanges without modifications

  • Regulatory/audit concerns about non-standard implementation


Proof of Concept

Scenario: Integration with a simple DEX contract

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {IERC20} from "./IERC20.sol";
import {Token} from "./test/Token.sol";
contract SimpleSwap {
// Function expects IERC20 interface
function swapTokens(IERC20 token, uint256 amount) external {
require(token.transferFrom(msg.sender, address(this), amount), "Transfer failed");
// ... swap logic ...
}
}
contract TestIntegration {
function test_integrationFails() public {
Token token = new Token();
SimpleSwap dex = new SimpleSwap();
// ❌ This fails at compile time!
// dex.swapTokens(token, 100 ether);
// Error: Type "Token" is not implicitly convertible to expected type "IERC20"
// ✅ Workaround required (ugly cast)
dex.swapTokens(IERC20(address(token)), 100 ether);
}
}

Compilation Error:

Error: Type "Token" is not implicitly convertible to expected type "IERC20"
--> contracts/test.sol:25:30:
|
25 | dex.swapTokens(token, 100 ether);
| ^^^^

Recommended Mitigation

Solution 1: Add IERC20 Interface Inheritance (Recommended)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
+ import {IERC20} from "./IERC20.sol";
import {ERC20Internals} from "./helpers/ERC20Internals.sol";
import {IERC20Errors} from "./helpers/IERC20Errors.sol";
- contract ERC20 is IERC20Errors, ERC20Internals {
+ contract ERC20 is IERC20, IERC20Errors, ERC20Internals {
constructor(string memory name_, string memory symbol_) {
_name = name_;
_symbol = symbol_;
}
function name() public view virtual returns (string memory) {
return _name;
}
function symbol() public view virtual returns (string memory) {
return _symbol;
}
function decimals() public view virtual returns (uint8) {
return 18;
}
function totalSupply() public view virtual returns (uint256) {
return totalSupply_();
}
function balanceOf(address owner) public view virtual returns (uint256) {
return _balanceOf(owner);
}
function transfer(address to, uint256 value) public virtual returns (bool success) {
success = _transfer(msg.sender, to, value);
}
function transferFrom(address from, address to, uint256 value) public virtual returns (bool success) {
address spender = msg.sender;
_spendAllowance(from, spender, value);
success = _transfer(from, to, value);
}
function approve(address spender, uint256 value) public virtual returns (bool success) {
address owner = msg.sender;
success = _approve(owner, spender, value);
}
function allowance(address owner, address spender) public view virtual returns (uint256) {
return _allowance(owner, spender);
}
}

Solution 2: Add Events to IERC20 (Optional Enhancement)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface IERC20 {
+ event Transfer(address indexed from, address indexed to, uint256 value);
+ event Approval(address indexed owner, address indexed spender, uint256 value);
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 value) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
function approve(address spender, uint256 value) external returns (bool);
function transferFrom(address from, address to, uint256 value) external returns (bool);
}

Verification Tests

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test} from "forge-std/Test.sol";
import {Token} from "./Token.sol";
import {IERC20} from "./IERC20.sol";
contract InterfaceComplianceTest is Test {
Token token;
function setUp() public {
token = new Token();
}
function test_tokenImplementsIERC20() public {
// ✅ After fix: This should compile and work
IERC20 erc20Token = IERC20(address(token));
// Can call IERC20 interface methods
uint256 supply = erc20Token.totalSupply();
assert(supply == 0); // No mint yet
}
function test_directTypeConversion() public {
// ✅ After fix: Direct type conversion works (no cast needed)
IERC20 erc20Token = token;
token.mint(address(this), 100 ether);
assertEq(erc20Token.balanceOf(address(this)), 100 ether);
}
function test_dexIntegration() public {
// ✅ After fix: Can pass token directly to IERC20-expecting functions
token.mint(address(this), 100 ether);
// This function expects IERC20 - should work directly now
processDEXToken(token);
}
function processDEXToken(IERC20 token) internal {
uint256 balance = token.balanceOf(address(this));
assert(balance > 0);
}
}

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!