Most token buyers check the price chart, the liquidity pool, and maybe the holder count. Developers building on top of a token contract — integrations, DEX aggregators, wallets — need to go deeper. A token that looks fine on the surface can hide transfer restrictions, infinite mint authority, or upgradeable logic that silently changes all the rules after you’ve integrated.

This guide breaks down the ERC-20 standard from a security auditor’s perspective: what each function does, how it can be weaponized, and what to look for when reading verified source code.

BLUF: Six areas to audit in any ERC-20 contract — (1) transfer/transferFrom logic for hidden restrictions, (2) approve/allowance for phising-style approval drains, (3) mint/burn authority and who holds it, (4) proxy upgradeability that can replace all logic, (5) event emission consistency, and (6) interface compliance. Run automated checks with the token risk API alongside manual review.

1. Transfer and TransferFrom: Where Honeypots Hide

The core of any ERC-20 token is the _transfer internal function. In a standard implementation, it moves tokens between addresses with no conditions beyond balance checks. Any deviation from this pattern is where scams live.

Safe Pattern

function _transfer(address from, address to, uint256 amount) internal {
    require(balanceOf[from] >= amount, "Insufficient balance");
    balanceOf[from] -= amount;
    balanceOf[to] += amount;
    emit Transfer(from, to, amount);
}

Dangerous Patterns

Honeypot — sells blocked:

function _transfer(address from, address to, uint256 amount) internal {
    require(balanceOf[from] >= amount);
    if (to == PAIR_ADDRESS && from != OWNER) {
        revert(); // Sells to DEX blocked for everyone except owner
    }
    balanceOf[from] -= amount;
    balanceOf[to] += amount;
}

Selective blocking — blacklist trap:

function _transfer(address from, address to, uint256 amount) internal {
    require(!isBlacklisted[from], "Blocked");
    // Owner can add anyone to blacklist at any time
    balanceOf[from] -= amount;
    balanceOf[to] += amount;
}

Cooldown or max-sell restrictions:

function _transfer(address from, address to, uint256 amount) internal {
    require(block.timestamp >= lastSell[from] + 1 days, "Cooldown");
    require(amount <= maxSellAmount, "Exceeds max sell");
    // Legitimate in some community tokens, weaponizable by adjusting parameters
}

What to Look For

PatternRiskNotes
Clean transfer, no conditionsSafeStandard ERC-20
Condition checking to == DEX_PAIRHoneypot riskSell blocking
isBlacklisted or isExcluded mappingMedium-highCan be weaponized retroactively
maxSellAmount or cooldownMediumCheck if adjustable by owner
if (from != owner) conditionsHighOwner-only exemptions = privileged escape hatch

2. Approve and Allowance: The Approval Drain

The approve/allowance mechanism lets a spender move tokens on your behalf. This is how DEX routers, staking contracts, and DEX aggregators work. It’s also one of the most exploited mechanisms in crypto — phishing sites trick users into approving unlimited spending caps on malicious contracts.

Contract-Side Risk

A token contract itself rarely has approval bugs if it follows the standard. The risk is in contracts that the token interacts with. But check for:

  • approve with non-zero value when already non-zero: The ERC-20 standard requires setting to zero first, then to the new value. Some contracts skip this (race condition risk, though rarely exploited in practice).
  • increaseAllowance/decreaseAllowance: These are OpenZeppelin extensions, not part of the original ERC-20 spec. They’re safe but indicate the contract uses extensions — check which library version.
  • permit (EIP-2612): Gasless approvals via signature. Safe in standard implementations, but verify the signature verification logic — a flawed ecrecover can let anyone spend anyone’s tokens.

User-Side Risk

The bigger danger is at the user level. When a user approves a malicious smart contract with unlimited allowance, the contract can drain the entire balance. Tools like Revoke.cash and Etherscan’s token approval checker help users monitor active approvals.

3. Mint and Burn: Supply Control

Who can create or destroy tokens? This is the difference between a fixed-supply store of value and a printable inflation machine.

Safe Configuration

// No mint function at all — fixed supply forever
uint256 public constant MAX_SUPPLY = 1_000_000 * 10**18;

constructor() {
    _mint(msg.sender, MAX_SUPPLY);
}

Dangerous Configuration

// Owner can mint unlimited tokens at any time
function mint(address to, uint256 amount) external onlyOwner {
    _mint(to, amount);
}

Even if the owner promises “we will never mint,” the capability existing in code means it can be exercised at any moment. Projects that later rugs often have a mint function that was “never going to be used.”

What to Check

ConfigurationRisk
No mint function, supply set in constructorSafest
Mint capped at a hard maximumLow-medium
Mint with timelock (governance vote required)Medium — check timelock delay
Mint with onlyOwner, no capHigh — unlimited inflation risk
Mint callable by anyoneCritical — token is worthless

Also check for indirect minting: some contracts implement “rebasing” or “reflection” mechanisms that effectively change supply without a explicit mint call. Token rebalances can dilute holders invisibly.

4. Proxy and Upgradeability: The Logic Swap

A proxy pattern lets the contract’s logic be upgraded after deployment. This isn’t inherently malicious — it’s used by major protocols like Uniswap V3 and Compound. But in the hands of an anonymous token team, an upgradeable contract means they can replace the transfer logic with a honeypot after building trust.

How to Detect

  1. Look at the contract on the block explorer. If the “Contract” tab shows an “Implementation” address, it’s a proxy.
  2. Read the proxy admin contract. Who can trigger upgrades? Is there a timelock?
  3. Check if previous implementations have been swapped. How many times? Each upgrade is a point where logic changed.
// TransparentUpgradeableProxy pattern — standard, used by OpenZeppelin
// The admin can call upgradeTo(newImplementation)

// Dangerous: no timelock on upgrade
function upgradeTo(address newImpl) external onlyAdmin {
    // Can be called instantly — no warning, no delay
}

Risk Matrix

Proxy SetupRisk
No proxy (immutable logic)None — what you read is what runs
Proxy with multi-sig + long timelockLow
Proxy with single-owner admin, short timelockMedium-high
Proxy with single-owner admin, no timelockHigh — logic can change instantly
Hidden proxy (EIP-1967 with misleading naming)Critical

5. Event Emission: Audit Trail Integrity

ERC-20 tokens must emit Transfer and Approval events. These events are how block explorers, indexers, and analytics tools track token movement. Missing or inconsistent events create blind spots.

What to Check

Every _transfer call should emit a Transfer event. Every _approve should emit an Approval event. Missing events on some code paths can indicate:

  • Hidden transfers: Tokens moved internally without events won’t show up on explorers, making holder distribution analysis unreliable.
  • Tax skimming: Some tokens deduct a fee on transfer but emit a Transfer event for the full amount, hiding where the fee goes.
  • Double events: Emitting duplicate events to confuse indexers.

This is a subtle check that automated scanners often miss. If you’re building infrastructure on top of a token, verify event consistency by replaying a few transfers against the emitted logs.

6. Interface Compliance: Does It Actually Behave Like ERC-20?

A contract can claim to be ERC-20 but fail to implement the standard correctly. Common deviations:

  • decimals() returns a non-standard value (not 18). Exchanges and integrations that assume 18 decimals will calculate wrong amounts.
  • totalSupply() is dynamic (changes with rebasing or minting). Integration code that caches totalSupply will display incorrect percentages.
  • Transfer returns false instead of reverting on failure. The ERC-20 standard allows both behaviors, but most integrations expect reverts. A false return can silently fail.
  • name() and symbol() are empty or misleading. Not a security issue, but a sign of low-quality contracts.

Run the token address through our token risk API to automatically verify interface compliance alongside all the checks above — honeypot detection, tax analysis, holder concentration, and liquidity status. One call covers the full audit surface.

Putting It All Together

For a quick manual review, focus on three things: read _transfer for any conditions beyond balance checks, check if supply is truly fixed, and verify there’s no proxy. If all three are clean, the contract clears the basic security bar for integration. For a deeper audit, walk through each section above and cross-reference with automated on-chain analysis tools.