Token-0x

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

Custom ERC20 Implementation in src/ERC20.sol Breaks ERC20 Standard and Causes Invalid Test Passes / Missed Reverts

Author Revealed upon completion

Root + Impact

Description

  • ERC20 should follow the OpenZeppelin specification:

    • Zero-address sends revert with "ERC20: transfer to the zero address"

    • approve(address(0)) should be allowed (believe it or not, OZ allows this!)

    • mint and burn must be controlled by access control

    • Allowance changes must follow OZ patterns

  • Your custom ERC20 introduces:

    • Unknown revert selectors (IERC20Errors.ERC20InvalidReceiver)

    • Disallowed approve(address(0)) even though OZ allows it

    • Anyone can mint/burn tokens

    • Reverts mismatch OZ semantics

    • Tests encode these incorrect semantics as “correct”

// Token.sol inherits custom ERC20 instead of OpenZeppelin ERC20
contract Token is ERC20 {
constructor() ERC20("Token", "TKN") {}
// @> No access control — ANYONE can mint tokens
function mint(address account, uint256 value) public {
_mint(account, value); // @> Unsafe mint
}
// @> Anyone can burn other accounts' funds
function burn(address account, uint256 value) public {
_burn(account, value); // @> Unsafe burn
}
}
//Additional root causes (in custom ERC20):
// @> Non-standard revert errors used instead of OpenZeppelin revert strings
revert IERC20Errors.ERC20InvalidReceiver(receiver);
// @> approve(address(0)) incorrectly forbidden
revert IERC20Errors.ERC20InvalidSpender(spender);
// @> zero address mints/burns revert with custom errors instead of standard OZ errors

Risk

Likelihood:

  • This will happen whenever the token integrates with a DEX, wallet, or bridge, because revert selectors do NOT match ERC20 standard expectations.

Impact:

  • ERC20 integrations (DEXes, bridges, wallets, explorers) will break because revert selectors are nonstandard.

  • Tests encode incorrect behavior as “correct”, making developers believe this ERC20 is valid.

Proof of Concept

  • 1. Anyone can mint unlimited tokens

  • 2. Anyone can burn a victim’s balance

  • approve(address(0)) incorrectly reverts, breaking standard ERC20 integrations

  • test_approveRevert() expects revert on approve(address(0)) → this is wrong behavior per OZ.

contract Exploit {
Token token;
function pwn() external {
token.mint(msg.sender, 1_000_000 ether);
}
}
function burnVictim(address victim) external {
token.burn(victim, token.balanceOf(victim));
}
token.approve(address(0), 100); // Reverts on custom ERC20 but not on OZ ERC20

Recommended Mitigation

  • Follow OZ revert patterns.

  • Write tests using OZ revert messages / revert selectors.

- remove this code
+ add this code
//Fix 1 — Add access control
- function mint(address account, uint256 value) public {
+ function mint(address account, uint256 value) public onlyOwner {
_mint(account, value);
}
//Fix 2 — Use OpenZeppelin’s ERC20 implementation
Replace custom ERC20:
- import {ERC20} from "../src/ERC20.sol";
+ import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";

Support

FAQs

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

Give us feedback!