How to Audit an ERC-20 Token Contract
Auditing an ERC-20 token contract is not a single check — it is a structured process that moves from interface compliance, through economic controls, to centralization risks and known vulnerability classes. This guide walks you through each layer in the order a reviewer should approach it, with concrete code examples and the tools best suited to each task.
Start With ERC-20 Standard Compliance
Before evaluating security, confirm that the contract actually implements ERC-20 correctly. The standard is defined in EIP-20 and requires nine functions and two events. Missing or misimplemented members break composability with DEXs, bridges, and wallets in ways that may also create exploitable edge cases.
totalSupply(),balanceOf(address),allowance(address,address)— pure view functions that must return accurate state.transfer(address,uint256)andtransferFrom(address,address,uint256)— must return abool, revert on failure rather than returningfalsesilently, and emit theTransferevent.approve(address,uint256)— must emit theApprovalevent and returnbool.TransferandApprovalevents — must be emitted with the correct indexed topics.
A common non-compliance pattern is a transfer that emits no event on zero-value transfers, or one that silently returns false instead of reverting. Integrating contracts that check the return value will mis-account balances. Slither's erc20-interface detector flags these deviations automatically.
Mint and Burn Controls
Unlimited or inadequately gated minting is the most direct path to value destruction in an ERC-20. Audit every code path that increases totalSupply.
Questions to answer
- Who can call
mint()? Is it a single EOA, a multisig, or a DAO timelock? - Is there a hard cap enforced on-chain, or only a soft convention?
- Can the owner transfer the minter role to an arbitrary address in a single transaction?
- Does
burn()correctly reduce both the sender's balance andtotalSupply?
Vulnerable pattern
// No cap, callable by any address that has been granted MINTER_ROLE
function mint(address to, uint256 amount) external onlyRole(MINTER_ROLE) {
_mint(to, amount);
}
// Role can be granted by DEFAULT_ADMIN_ROLE — a single EOA at deployment
function grantRole(bytes32 role, address account) public override onlyRole(getRoleAdmin(role)) {
super.grantRole(role, account);
}
Safer pattern
uint256 public constant MAX_SUPPLY = 1_000_000_000 * 10 ** 18;
function mint(address to, uint256 amount) external onlyRole(MINTER_ROLE) {
require(totalSupply() + amount <= MAX_SUPPLY, "Cap exceeded");
_mint(to, amount);
}
The cap must be enforced inside _mint or the calling function — not just documented. OpenZeppelin's ERC20Capped extension handles this correctly if you do not override _mint downstream.
Fee-on-Transfer Pitfalls
Fee-on-transfer tokens deduct a percentage from every transfer, meaning the recipient receives less than the amount argument passed to transfer. This is a deliberate design choice in many tokenomics models, but it introduces a class of integration bugs that can drain funds from AMM pools, lending protocols, and bridges.
What to check in the contract itself
- Is the fee rate immutable, or can the owner increase it after deployment? An owner-controlled fee that can be set to 100% is effectively a rug mechanism.
- Is there a fee exemption list (
mapping(address => bool) isExcluded)? Verify who can modify it and whether the deployer's address is pre-exempted. - Does the fee calculation round in favor of the contract rather than the user in ways that cause balances to drift?
// Dangerous: owner can change fee to any value at will
function setTransferFee(uint256 newFee) external onlyOwner {
transferFee = newFee; // no upper-bound check
}
// Safer: cap and timelock
function setTransferFee(uint256 newFee) external onlyOwner {
require(newFee <= 500, "Max 5%"); // 500 basis points
transferFee = newFee;
}
If you are auditing a protocol that integrates a fee-on-transfer token rather than the token itself, ensure the protocol accounts for the received amount rather than the sent amount. Failing to do so is a well-documented class of AMM and vault exploits.
Blacklist and Pause: Centralization Risks
Many ERC-20 tokens include address blacklisting (blocking specific addresses from sending or receiving) and pause functionality (halting all transfers). These features are legitimate compliance tools in some contexts — stablecoins like USDC use them — but they introduce significant centralization risk that should be disclosed and scoped tightly.
Red flags
- A single EOA holds the blacklist admin role with no multisig or timelock.
- The pause function has no time limit — the owner can pause transfers indefinitely.
- The blacklist is applied inside
_beforeTokenTransferbut not intransferFrom, creating an inconsistency. - The contract owner can blacklist the Uniswap pool address, preventing all secondary market sales (a common "honeypot" technique).
Slither's centralization-risk and Aderyn's privilege escalation detectors both surface these patterns. When you run an automated scan against a token contract, cross-referencing findings from multiple tools makes it far easier to distinguish intentional admin controls from accidental privilege overreach.
For a broader checklist of centralization and ownership issues before a token goes live, see the Is My Token Safe? A Pre-Launch Security Checklist.
The Approval Race Condition (ERC-20 Allowance Attack)
The original ERC-20 approve function has a well-known race condition. If Alice approves Bob for 100 tokens, then changes the approval to 50 tokens, Bob can observe the pending transaction and front-run it: spending the original 100 tokens before the new approval confirms, then spending the 50 tokens after — netting 150 total.
Mitigations to look for
- Incremental approve functions:
increaseAllowanceanddecreaseAllowance(present in OpenZeppelin v4; removed in v5 as EIP-2612 is now preferred). - EIP-2612 permit: Off-chain signed approvals that combine approval and spend in a single transaction, eliminating the race window entirely. Verify the
permitimplementation validatesdeadline, usesecrecovercorrectly, and checks thevvalue is 27 or 28. - Require reset to zero: Some tokens require the spender's allowance to be set to zero before it can be changed to a non-zero value. This is a friction-based mitigation, not a cryptographic one.
// Vulnerable: direct re-approval
function approve(address spender, uint256 amount) public returns (bool) {
_approve(msg.sender, spender, amount);
return true;
}
// Safer: require zero-reset first (friction mitigation)
function approve(address spender, uint256 amount) public returns (bool) {
require(
amount == 0 || allowance(msg.sender, spender) == 0,
"Reset to zero first"
);
_approve(msg.sender, spender, amount);
return true;
}
Integer Arithmetic and Overflow Checks
Since Solidity 0.8.0, arithmetic overflow and underflow revert by default, which eliminates the classic balance-wrap attacks that plagued earlier tokens. However, there are still arithmetic issues to watch for:
- Deliberate use of
uncheckedblocks — common in gas-optimized transfer loops — that re-introduce overflow risk if the surrounding logic does not validate inputs. - Precision loss in fee calculations using integer division, causing cumulative rounding errors.
- Contracts compiled with Solidity < 0.8.0 that rely on SafeMath but have paths where SafeMath is not applied.
For a deeper treatment of this vulnerability class, see Integer Overflow and Underflow in Smart Contracts.
Tooling: What to Run and Why
No single tool covers the entire attack surface of an ERC-20 contract. A useful audit pipeline combines static analysis, linting, symbolic execution, and fuzzing.
Recommended tool stack
- Slither — Fast static analysis. Run
slither . --detect erc20-interface,arbitrary-send-erc20,reentrancy-ethas a baseline. Catches interface violations, unchecked transfers, and reentrancy paths. - Aderyn — Rust-based AST analyzer with strong centralization and access-control detectors. Complements Slither on ownership patterns.
- Semgrep — Rule-based pattern matching. Useful for custom rules that encode project-specific invariants, such as "the fee must never exceed 10%."
- Solhint — Linter that enforces style and naming conventions. Catches missing NatSpec, shadowed state variables, and unsafe function visibility.
- Mythril / SMTChecker — Symbolic execution and formal verification. Slow but capable of finding arithmetic edge cases and reachability issues that static analysis misses.
- Echidna — Property-based fuzzer. Write invariants like
totalSupply == sum(balances)and let Echidna try to break them across thousands of random transaction sequences.
VRF7 runs all of these tools in parallel against a submitted contract and maps each finding to the originating tool, giving you a consolidated report without needing to configure each tool individually. That said, automated tooling — including this pipeline — surfaces deterministic and pattern-matched issues. It does not replace the judgment a human auditor applies to business logic, tokenomics incentive design, or novel attack vectors. For guidance on where automated scanning ends and manual review begins, see Automated Scanners vs Manual Audits: What Is the Difference?.
Checklist Summary
- Verify all nine ERC-20 functions and two events are correctly implemented and emit expected data.
- Identify every code path that calls
_mint; confirm access controls, role management, and hard supply caps. - Review fee-on-transfer logic: rate changeability, exemption lists, rounding behavior.
- Assess blacklist and pause controls: who holds the keys, whether a timelock exists, and whether the features can be weaponized.
- Check for approval race conditions; prefer EIP-2612 permit for modern deployments.
- Audit all
uncheckedblocks and verify fee division does not accumulate material rounding errors. - Run the tool stack above; treat automated findings as a starting point for manual review, not a final verdict.
Scan your contract before you ship
Run an automated, transparent security scan — seven industry tools in parallel, every finding labeled with its source tool. It is not a substitute for a full manual audit, but it is a fast first line of defense.
Scan a contractFrequently asked questions
What is the most dangerous vulnerability in a typical ERC-20 token contract?
Unrestricted or poorly gated minting is the most directly destructive vulnerability because it allows an attacker or malicious insider to inflate supply and drain value from all existing holders. Close behind it are owner-controlled fee rates that can be set to 100% — effectively a honeypot that prevents any holder from selling. Both issues require reviewing every privilege escalation path in the contract, not just the obvious mint function.
Does a fee-on-transfer token violate the ERC-20 standard?
Technically yes: EIP-20 states that the recipient must receive exactly the amount specified in the transfer call. Fee-on-transfer tokens violate this guarantee, which is why integrating protocols must explicitly handle the discrepancy by measuring the balance change rather than trusting the amount argument. The token itself may function correctly within its own ecosystem, but the non-standard behavior must be clearly documented and accounted for by any protocol that accepts the token.
Can automated tools fully audit an ERC-20 token contract?
No. Automated tools — including static analyzers like Slither and Aderyn, symbolic execution tools like Mythril, and fuzzers like Echidna — are effective at detecting known vulnerability patterns, interface violations, and arithmetic edge cases. They cannot evaluate whether the tokenomics design is sound, whether access controls make sense for the project's trust model, or whether a novel business logic combination creates an unexpected exploit path. Automated scanning is a necessary first step, not a complete substitute for manual review.
What is the ERC-20 approval race condition and how serious is it?
The approval race condition allows a spender to observe a pending approval change transaction in the mempool and front-run it, spending the old allowance before the new one is confirmed and then spending the new allowance afterward — potentially extracting far more than the token owner intended. In practice the risk is highest when a user is changing a large allowance for a contract they do not fully trust. The cleanest mitigation is EIP-2612 permit, which combines approval and action in a single atomic transaction signed off-chain, eliminating the window for front-running.
How do blacklist and pause features create centralization risk in ERC-20 tokens?
Blacklist and pause mechanisms give a privileged address the power to freeze any wallet or halt all transfers indefinitely. If that privileged address is a single externally owned account rather than a multisig with a timelock, the entire token economy depends on that one key not being lost, stolen, or misused. Attackers who compromise the owner key can blacklist the largest holders or the Uniswap liquidity pool before draining value. Auditors should confirm these roles are held by a well-secured multisig and that any pause has a bounded duration enforced on-chain.