Solidity is a programming language, which makes "read the contract" sound like advice only developers can follow. It's not. You don't need to understand inheritance, gas optimization, storage slots, or assembly. You need to recognize five function patterns. That's the difference between being a target and being informed.
This post is the framework: what to search for, what to ignore, and how to form a verdict in about ten minutes per contract.
The mental model
A smart contract is a set of functions. Some functions anyone can call; some only the owner can. Your job when auditing is to find every function that gives the owner power over your tokens, then decide whether that power is reasonable.
Most scams are built with the same primitives. Once you know the five shapes, you'll see them immediately — even in a 2,000-line contract.
Where to look
- Open the contract on the block explorer (Etherscan, Basescan, etc.)
- Click the Contract tab, then Code
- If it says "Contract source code not verified," close the tab. You cannot safely interact with unverified contracts.
- If verified, you'll see the Solidity source. Use Ctrl+F — that's your primary tool.
Pattern 1: onlyOwner functions
Search: onlyOwner
Every function with onlyOwner (or similar custom modifiers like onlyAdmin, onlyRole) can only be called by a specific address. List every single one. Then ask: what does each one do?
Normal onlyOwner usage:
- Updating a frontend URL
- Setting a metadata base URL
- Transferring ownership to a multisig
Suspicious onlyOwner usage:
- Changing fees, taxes, or limits
- Pausing transfers
- Blacklisting addresses
- Minting new tokens
- Upgrading the contract
- Withdrawing funds
The test: if the owner ran this function tomorrow, could it damage you? If yes, it's a risk vector. If every onlyOwner function in the contract is reasonable, that's a good sign.
Pattern 2: The _transfer override
Search: _transfer or function transfer
Standard ERC20 has a simple _transfer(from, to, amount). Anything that overrides it is doing something special — and special is where scams live.
Here's a benign override (legitimate reflection token):
function _transfer(address from, address to, uint256 amount) internal override {
uint256 reflectionFee = amount / 100;
super._transfer(from, to, amount - reflectionFee);
_distributeReflection(reflectionFee);
}
Here's a hostile override:
function _transfer(address from, address to, uint256 amount) internal override {
require(canTransfer[from], "not allowed");
if (to == pair) {
uint256 fee = amount * sellTax / 100;
amount -= fee;
}
super._transfer(from, to, amount);
}
The difference is what conditions gate the transfer. If the conditions are owner-controlled (canTransfer[from], sellTax, tradingEnabled), then the owner controls whether you can sell. That's a red flag regardless of today's settings.
Pattern 3: The mint function
Search: _mint or function mint
In a finished, launched token, _mint should appear in exactly one place: the constructor. It runs once at deployment, sets the total supply, and is never called again.
constructor() ERC20("Token", "TKN") {
_mint(msg.sender, 1_000_000 * 10**18); // initial supply
}
If _mint is reachable through any other function — especially an onlyOwner function — the total supply is not fixed. The owner can dilute every holder at will:
function mint(address to, uint256 amount) external onlyOwner {
_mint(to, amount); // unlimited dilution
}
Check: search _mint, count the references. If there's exactly one and it's in the constructor, the supply is capped. If there's more, trace who can call each one.
Pattern 4: The withdraw / rescue function
Search: withdraw, rescue, sweep, recover, emergencyWithdraw
These functions transfer assets out of the contract. In a token contract, they shouldn't exist at all — the contract doesn't hold user funds. In a staking/vault contract, they should only let users withdraw their own funds.
What you don't want to see:
function rescueETH() external onlyOwner {
payable(owner).transfer(address(this).balance);
}
function rescueTokens(address token) external onlyOwner {
IERC20(token).transfer(owner, IERC20(token).balanceOf(address(this)));
}
Framed as "rescue in case of accidental transfers," but if the contract holds real user deposits, this is a drain function.
Safe version (user withdraws only their own funds):
function withdraw(uint256 amount) external {
require(deposits[msg.sender] >= amount, "insufficient");
deposits[msg.sender] -= amount;
token.transfer(msg.sender, amount);
}
Pattern 5: The upgrade function
Search: upgradeTo, _authorizeUpgrade, setImplementation
If any of these exist, the contract is a proxy and its code can be changed. Whether that's dangerous depends on who can change it and how quickly.
Instant upgrade by EOA owner: dangerous. The entire contract you audited can be replaced with a malicious version before you next read it.
Upgrade gated by a Timelock contract: reasonable. There's typically a 24-72 hour delay between scheduling an upgrade and executing it, giving users time to exit.
Upgrade gated by a multisig + Timelock: best practice. Multiple signers required, plus the time delay.
Check: find the upgrade function, then find what address can call it. Then check if that address is a plain EOA, a multisig (look it up on Etherscan — multisigs usually identify themselves), or a Timelock.
What you can safely skip
To save time, you can usually skip:
- View/pure functions (
returns (uint256)without state changes): they don't do anything dangerous - Constructor after the
_mintcall: just initialization - Event emissions: just logging
- Standard OpenZeppelin imports:
import "@openzeppelin/contracts/..."is generally safe as long as the import path hasn't been modified - Interface definitions: just type signatures
You're looking for places where state changes based on conditions. If nothing changes, nothing is risky.
The 10-minute process
- Minute 0-1: Verify the contract is verified. If not, stop.
- Minute 1-3: Ctrl+F
onlyOwner. List every result. - Minute 3-5: Ctrl+F
_transfer. Read the overrides if present. - Minute 5-6: Ctrl+F
_mint. Confirm it only appears in constructor. - Minute 6-8: Ctrl+F
withdraw,rescue,sweep,recover. Read each. - Minute 8-9: Ctrl+F
upgradeTo,setImplementation. Check for proxy patterns. - Minute 9-10: Form a verdict. Does the owner have ability to (a) change rules after deployment, (b) take your funds, (c) freeze your tokens? If yes to any, the contract is higher risk.
That's the job. It takes practice to do in 10 minutes, but you can do it in 30 minutes from the start, and each additional contract is faster.
Where AI fits
AI auditors don't replace this process — they accelerate it. Instead of manually searching for six keywords, an AI can read the entire contract and surface the exact functions that match each red-flag category, with plain-English explanations. Tools like Unrugify are tuned for exactly this pattern-matching task. The free preview gives you the verdict in 30 seconds; the $1 paid report gives you the function-by-function breakdown.
But the principles here are worth knowing regardless. AI is a force multiplier; it's not a substitute for understanding what the output means.
Apply this checklist to any contract: run a free Unrugify scan and see the structured breakdown.
