Root + Impact
Description
Normal expected behavior (ERC20 standard):
The ERC20 specification requires both balanceOf(address) and allowance(owner, spender) to always return a uint256 value and must not revert, even when called with invalid or zero addresses. The correct result in these cases is simply 0.
Actual behavior:
In this implementation, calling balanceOf(0x0), allowance(0x0, spender), or allowance(owner, 0x0) triggers a revert with a custom error. This introduces breaking behavior for wallets, DeFi protocols, indexers, off-chain tooling, explorers, DEX routers, and portfolio trackers that universally rely on ERC20 view functions being non-reverting.
function balanceOf(address account) public view returns (uint256) {
assembly ("memory-safe") {
@> if iszero(account) {
@>
mstore(0x00, shl(224, 0xf4844814))
revert(0x00, 0x04)
}
...
}
}
function allowance(address owner, address spender) public view returns (uint256) {
assembly ("memory-safe") {
@> if or(iszero(owner), iszero(spender)) {
@>
mstore(0x00, shl(224, 0xf4844814))
revert(0x00, 0x04)
}
...
}
}
Risk
Likelihood:
Reason 1 — Frequent Calls in All ERC20 Integrations
Wallets, explorers, and indexers regularly query balances and allowances, including edge-case addresses such as 0x0. These calls will revert immediately.
Reason 2 — Common DeFi Router Behavior
DEX routers and protocol adapters often query allowance and balance as part of swap estimation. A revert breaks swap paths, causing tokens to be marked as incompatible.
Impact:
-
Impact 1 — Non-Standard ERC20 Behavior Breaks Integrations
Any application expecting ERC20-compliant behavior (including major wallets and DEXes) will fail when interacting with this token
-
Impact 2 — Ecosystem-Wide Incompatibility
This behavior makes the token incompatible with infrastructure such as block explorers, AMMs, liquidity mining contracts, and portfolio analytics.
PoC :
add this test
pragma solidity ^0.8.24;
import "forge-std/Test.sol";
import "./TestToken.sol";
contract Issue4_ViewRevertsZeroAddressTest is Test {
TestToken token;
function setUp() public {
token = new TestToken();
}
function test_balanceOf_zeroAddress_reverts() public {
vm.expectRevert();
token.balanceOf(address(0));
}
function test_allowance_zeroOwner_reverts() public {
vm.expectRevert();
token.allowance(address(0), address(1));
}
function test_allowance_zeroSpender_reverts() public {
vm.expectRevert();
token.allowance(address(1), address(0));
}
}
Foundry Test log Output
Ran 3 tests for test/Issue4_ViewRevertsZeroAddress.t.sol:Issue4_ViewRevertsZeroAddressTest
[PASS] test_allowance_zeroOwner_reverts() (gas: 9314)
Traces:
[9314] Issue4_ViewRevertsZeroAddressTest::test_allowance_zeroOwner_reverts()
├─ [0] VM::expectRevert(custom error 0xf4844814)
│ └─ ← [Return]
├─ [780] TestToken::allowance(0x0000000000000000000000000000000000000000, ECRecover: [0x0000000000000000000000000000000000000001]) [staticcall]
│ └─ ← [Revert] EvmError: Revert
└─ ← [Stop]
[PASS] test_allowance_zeroSpender_reverts() (gas: 9380)
Traces:
[9380] Issue4_ViewRevertsZeroAddressTest::test_allowance_zeroSpender_reverts()
├─ [0] VM::expectRevert(custom error 0xf4844814)
│ └─ ← [Return]
├─ [780] TestToken::allowance(ECRecover: [0x0000000000000000000000000000000000000001], 0x0000000000000000000000000000000000000000) [staticcall]
│ └─ ← [Revert] EvmError: Revert
└─ ← [Stop]
[PASS] test_balanceOf_zeroAddress_reverts() (gas: 8910)
Traces:
[8910] Issue4_ViewRevertsZeroAddressTest::test_balanceOf_zeroAddress_reverts()
├─ [0] VM::expectRevert(custom error 0xf4844814)
│ └─ ← [Return]
├─ [503] TestToken::balanceOf(0x0000000000000000000000000000000000000000) [staticcall]
│ └─ ← [Revert] EvmError: Revert
└─ ← [Stop]
Summary of PoC Behavior:
balanceOf(0x000...000) → Reverts
allowance(0x000...000, spender) → Reverts
allowance(owner, 0x000...000) → Reverts
This strictly violates the ERC20 standard.
Recommended Mitigation
Replace zero-address reverts in view functions with simple return 0;.
- if (account == address(0)) revert InvalidAddress();
+ if (account == address(0)) return 0;
- if (owner == address(0) || spender == address(0)) revert InvalidAddress();
+ if (owner == address(0) || spender == address(0)) return 0;