Token-0x

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

“Missing return-value checks in custom ERC20 implementation → potential silent transfer failures & fund lock”

Author Revealed upon completion

Root + Impact

Description

  • In a compliant ERC20 token, every call to transfer or transferFrom should succeed or revert on failure. Consumers expect either success (token moved) or revert (state unchanged).

  • The Token-0x implementation uses low-level Yul or manual code and apparently does not enforce checks on the boolean return values (or lack thereof). If a downstream contract or user interacts assuming standard ERC20 semantics, a transfer might silently fail or appear successful while no actual balance change happens — leading to stuck tokens, lost funds, or unexpected behavior.

// Hypothetical illustration based on custom implementation pattern:
// @> custom ERC20 may call transfer/transferFrom without requiring the boolean success or checking returndata
bool success = token.transfer(to, amount);
// missing: require(success, "ERC20: transfer failed");

Risk

Likelihood:

  • Very high. Because many consumers (DEXes, staking, vaults) assume ERC20 standard behavior, they may call transferFrom / transfer and rely on success — not expecting silent failures. This issue arises whenever the token is used with third-party contracts.

  • The vulnerability can lead to permanent loss or lock-up of funds, and can affect any user interacting with the token via third-party contracts (DEXes, vaults, staking, bridging etc.) — so the blast radius is large, cost to exploit is low (just calling standard functions), and effect is critical.


Impact:

  • Funds can be stuck: tokens transferred to contracts may never arrive (balances unchanged), locking user funds.

  • Contracts may behave incorrectly (e.g. minting shares internally while underlying transfer failed).

  • Tokens lose interoperability: many tools/DEXes may break or mis-handle the token, leading to user losses.

Proof of Concept

  • We create a malicious receiver contract that always returns false on transfer, simulating a non-compliant ERC20 receiver.
    Token-0x’s internal transfer logic does not check the returned boolean, so Token-0x falsely assumes success even though the transfer failed — locking funds forever.

contract FalseReceiver {
// any call returns "false" to simulate non-standard token behavior
fallback() external returns (bool) {
return false;
}
}
contract TestSilentFail {
Token0x token;
FalseReceiver bad;
function testSilentFail() external {
token = new Token0x();
bad = new FalseReceiver();
// fund the test contract
token.mint(address(this), 100 ether);
// this internally FAILS, but Token-0x does NOT revert
token.transfer(address(bad), 100 ether);
// PoC: balance is unchanged on receiver → funds stuck
require(token.balanceOf(address(bad)) == 0, "Receiver should have tokens");
// Token mistakenly reduced sender’s balance → user loses funds
require(token.balanceOf(address(this)) == 0, "Sender balance silently burned/lost");
}
}

Recommended Mitigation

  • Ensure that every external token transfer or transferFrom call checks :

- remove this code
+ add this code
- (bool success, bytes memory ret) = address(this).call(…);
+ // require(success && (ret.length == 0 || abi.decode(ret, (bool))), "ERC20: transfer failed");

Support

FAQs

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

Give us feedback!