Token risk
Honeypot Token Detection: How Checkers Work and the Four Contract Patterns That Beat Them
Every honeypot token detection service answers with a simulated trade, not with a statement about the token. A checker buys and sells against one live pair at one block, then reports what happened to that trade. This guide takes the method apart, names the four contract patterns that survive it, and reports our own run of 150 tokens through two public checkers on September 4, 2026.

Key facts
- Clean is not safe
- Of 89 tokens that honeypot.is and GoPlus both called clean, 49 carried at least one owner gated GoPlus capability: is_mintable on 20, is_blacklisted on 11, slippage_modifiable on 11, owner_change_balance on 10
- The SQUID control
- The Squid Game token of 2021 passes a honeypot simulation today, with isHoneypot false, all taxes zero and 3,098 of 3,098 holder sells successful. GoPlus returns a full result for it with no is_honeypot key at all
- What honeypot.is measures
- One simulated buy and one simulated sell against one live pair on Ethereum, BNB Smart Chain or Base, with the router, the pair and the chain named in the response and no API key required
- Capability is not verdict
- Live USDT returns is_honeypot 0 while is_blacklisted, is_mintable, owner_change_balance, slippage_modifiable and transfer_pausable are all set to 1
- Static analysis baseline
- HoneyBadger analyzed more than 2 million smart contracts, flagged 690 honeypots, confirmed 87 percent of them by hand and found 240 victims with over $90,000 in profit, USENIX Security 2019
How buy-sell simulation works
A honeypot token is one that takes money on the way in and refuses to let it out. The buy confirms. Selling reverts, or costs more than the position is worth, or quietly routes the proceeds somewhere else. Checkers answer that question the direct way, by running the trade before you do.
The method fits into one sentence, and the vendor supplies it: the service "simulates a buy and a sell transaction to determine if the token is a honeypot or not." The honeypot.is homepage claims "extra checks to minimize false results" and names three chains, Ethereum, BNB Smart Chain and Base. Arbitrum is not on that list.
Call the v2 API
yourself and the shape of the answer becomes clear. No key is needed, and
the docs say so outright: "API Key system is not yet implemented."
simulationSuccess is its own boolean with
simulationError beside it, which keeps the sell failed apart
from we could not run the test. Verdicts sit in
honeypotResult.isHoneypot with a honeypotReason
string. Trade economics live in a different object:
simulationResult.buyTax, sellTax,
transferTax, maxBuy and maxSell. One
warning repeats across several of those objects in the reference: "This
field is not guaranteed to be present".
The response also names what the trade ran against: our USDT call came back carrying the router, the exact pair and the chain. One pair. One block. That is the entire scope of the measurement, and every claim built on it inherits that scope.
Two other signals ride along. holderAnalysis is not a
simulation at all, it is a retrospective count of
holders, successful, failed,
siphoned and averageTax, assembled from sells that real wallets already made.
contractCode reports openSource,
rootOpenSource, isProxy and
hasProxyCalls as separate booleans, which is the vendor
conceding that source verification and proxy status get checked outside the
trade.
GoPlus Security takes the other road. Instead of trading against a DEX pair it reads the contract and the ownership graph, then returns roughly thirty string flags. Vendor wording on the headline field is careful: a honeypot result "means that the token maybe cannot be sold because of the token contract's function". Maybe. RugDoc, which simulates too, is blunt about its own status: "This is an experimental service, provided as-is."
Owner-gated blacklists applied after launch
CertiK's write-up of lists the blacklist first: "Once a victim purchases the scam token, they are added to a blacklist." Selling checks the wallet against that list and reverts. None of it exists at launch, which is the point. The list starts empty.
Disguise matters as much as mechanism. CertiK records the maintaining
function under the name ApprovalForAll, with entries kept in a
list called _snapshot. A reader skimming verified source for the word blacklist finds nothing.
mapping(address => bool) private _snapshot;
function ApprovalForAll(address holder, bool blocked) external onlyOwner {
_snapshot[holder] = blocked;
}
function _transfer(address from, address to, uint256 amount) internal {
require(!_snapshot[from], "transfer failed");
// ... normal balance update
}
GoPlus exposes the capability as is_blacklisted: "If there is a blacklist, some addresses may not be able to trade normally". Its blunter
sibling is transfer_pausable, "whether trading can be pausable
by token contract". Neither field is a verdict. Both describe what an owner
may do later.
Measurement separates the two ideas better than argument does. USDT on
Ethereum returns is_blacklisted: "1" and
is_honeypot: "0" in the same response. Tether can freeze an
address, and USDT is not a honeypot.
Capability is not intent, and one boolean cannot carry
both.
Technique two skips the block and rewrites the arithmetic instead: "Rather
than block users from selling the tokens they buy, this method changes a
user's token balance to an amount specified by the contract creator",
executed through a function named increaseAllowance(). Sell
whatever you like. Your balance is whatever the deployer last wrote, and
GoPlus keeps a field for exactly that, owner_change_balance,
which live USDT also returns as 1.
Time-delayed and supply-threshold sell locks
Technique three is arithmetic rather than a list. Holders "can only sell
tokens above a certain threshold which is set to an extremely high number",
tracked in a structure CertiK found named hulkinfo. Required
sell size exceeds the victim's entire holding, so every attempt fails on a
bounds check that looks like dust protection.
Time works the same way. GoPlus calls that one
trading_cooldown, and set wide enough a cooldown is a lock.
Supply-side variants get their own fields:
cannot_sell_all, is_anti_whale and
anti_whale_modifiable, the last being the limit plus a setter.
uint256 private hulkinfo = type(uint256).max;
uint256 private cooldown = 365 days;
mapping(address => uint256) private lastBuyAt;
function _beforeSell(address seller, uint256 amount) internal view {
require(amount >= hulkinfo, "amount too low");
require(block.timestamp >= lastBuyAt[seller] + cooldown, "cooldown");
}
Simulation meets this pattern on unequal terms. Caps do get reported, as
maxBuy and maxSell, yet the verdict comes from a
trade whose size the service chose.
Squid Game remains the clearest version. Selling required Marbles earned through gameplay, and entry cost 456 SQUID, worth $456 at a dollar a token.
Dynamic fee setters
A tax is a number in storage. Numbers in storage have setters.
GoPlus names the capability slippage_modifiable, "whether the
trading tax can be modifiable by token contract", and keeps a per-address
version called personal_slippage_modifiable, "whether the owner
can set a different tax rate for every assigned address". That second field describes a contract able to charge you 99 percent and everybody else 3. An empty string means the tax is unknown.
uint256 public sellFee = 300; // basis points
mapping(address => uint256) public personalSellFee;
function setSellFee(uint256 bps) external onlyOwner { sellFee = bps; }
function setSellFeeFor(address holder, uint256 bps) external onlyOwner {
personalSellFee[holder] = bps;
}
USDT again, one response: slippage_modifiable: "1" next to
sell_tax: "0". Zero now. Settable later. Two statements about
two different moments, and a simulation only ever reads the first.
In our own run, 11 of the 89 tokens both checkers called clean carried
slippage_modifiable and one carried the per-address version.
Six of the 150 addresses sampled already charged a sell tax above 10 percent on one checker or the other. That problem, at least, is visible before you buy.
Upgradeable transfer logic behind a proxy
Everything above assumes the code you read is the code that runs. Proxies break that assumption cleanly.
GoPlus reports is_proxy with no judgment attached. Danger
comes from the neighbors, hidden_owner and
external_call.
address private implementation;
function upgradeTo(address newLogic) external onlyOwner {
implementation = newLogic; // transfer() is now whatever newLogic says
}
fallback() external payable {
// delegatecall into implementation, storage stays here
}
The simulator splits the same question in two,
contractCode.isProxy and
contractCode.hasProxyCalls, and keeps
openSource apart from rootOpenSource. A proxy can
be verified on the explorer while the implementation behind it is not. Our
call on the SQUID contract returned that exact shape:
openSource: false with rootOpenSource: true,
isProxy: true and hasProxyCalls: true.
Five of the 89 clean tokens in our sample were proxies and four carried
hidden_owner, with no token in either group appearing in the
other. One of the five proxies sits on
BSC at
0x2598c30330d5771ae9f983979209486ae26de875, a token calling
itself AI, clean on both checkers with is_proxy set. Nothing
there is evidence of a scam. It is evidence that the sell path just tested
is replaceable by whoever controls the proxy, and no checker announces the
swap. Upgrade review is its own discipline, and the question it asks is who
holds the proxy admin slot rather than who owns the logic, which is the
whole subject of an
upgradeable contract audit.
| Pattern | Mechanism | GoPlus field | What a simulation sees |
|---|---|---|---|
| Owner-gated blacklist | A list the transfer function checks, empty at launch and written after the buy | is_blacklisted, transfer_pausable, owner_change_balance | A sell that succeeds, because the probe wallet is not on the list |
| Threshold and cooldown lock | A minimum sell size above the whole position, or a cooldown wide enough to be a lock | trading_cooldown, cannot_sell_all, is_anti_whale, anti_whale_modifiable | One trade at a size and a moment the service chose, so the bound never binds |
| Dynamic fee setter | A tax stored as a number, with a global setter and a per-address setter | slippage_modifiable, personal_slippage_modifiable | Today's tax reading, on a scale from 0 to 1 |
| Upgradeable transfer logic | A proxy pointing at logic the admin can replace | is_proxy, hidden_owner, external_call | The code in place at the block it ran, with no notice of a later swap |
Why a clean scan is a statement about one block
The vendor says it first and says it plainly: "This is not a foolproof method. Just because it's not a honeypot now, does not mean it won't change!" A simulation measures one pair at one block, and it expires the moment the owner sends a transaction.
- Launch block. The pair opens and selling works.
- Scan block. A checker simulates one buy and one sell, then returns a clean verdict that is valid in that block alone.
- Owner block. One owner transaction lands.
- Sell block. That transaction wrote a blacklist entry, raised the sell fee or pointed the proxy at new code, so a holder trying to sell now watches the call revert, while the clean verdict published two blocks ago is unchanged and still on screen.
Academic work lands in the same place. RPHunter, published , reports 229 incidents that "exhibit no obvious code risks and primarily initiate Rug Pulls through unlocked liquidity", then concludes that "relying solely on code or transaction information to detect Rug Pulls can lead to false negatives and false positives."
Our run of 150 tokens through two checkers
So we measured how far a clean verdict travels.
- Run date
- Addresses sampled
- 150 distinct token addresses
- Chains
- Ethereum, BNB Smart Chain and Base
- Source feeds
- DexScreener's public token profile and boost feeds plus its search endpoint
- Search terms
- over 31 generic terms from a 34 term list, moon, gold, based, trump and floki among them
- Both verdicts known
- 91 of the 150
- No usable simulation
- 48 of the 150
Each address went through honeypot.is v2 and the GoPlus
token_security endpoint.
Of the 150, exactly 91 came back with a usable verdict from both services.
Eighty-nine were clean on both. Two were flagged by honeypot.is alone, both
on BNB Smart Chain:
0xfb6115445bff7b52feb98650c87f44907e58f802, which GoPlus called
clean while reporting is_proxy, and
0xd0dfc951cd7787859f0ed61966bc243038888888, clean on GoPlus
with is_blacklisted set. Zero were flagged by GoPlus alone.
Disagreement is rare in this sample.
Agreement is not the same as safety.
Those two disagreements are worth a second look, because the reflex is to
treat the odd tool out as a false positive. Read
simulationError against isHoneypot before you
believe that. A honeypot verdict arriving with
simulationSuccess: false and an error string is a service
reporting that it could not run the trade at all, which is a gap and not a
finding. Both BSC tokens here came back the other way, with
simulationSuccess: true, no error and
honeypotReason reading HONEYPOT DETECTED, so a sell was
simulated and it failed. GoPlus calling the same addresses clean does not
contradict that, since its answer describes a capability rather than a
trade.
Among the 89 both called clean, 49 carried at least one owner-gated GoPlus
capability, which is 55 percent. That is a sample of what a retail buyer
meets in a search box rather than a random draw. Frequencies: is_mintable on 20, is_blacklisted on 11,
slippage_modifiable on 11, owner_change_balance on
10, transfer_pausable on 8,
anti_whale_modifiable on 7, is_proxy on 5 and
hidden_owner on 4, then
can_take_back_ownership,
personal_slippage_modifiable and
trading_cooldown on one each.
| Result | Tokens | Out of | Share |
|---|---|---|---|
| Both verdicts known | 91 | 150 sampled | |
| Both call it clean | 89 | 91 | |
| Flagged by honeypot.is only | 2 | 91 | |
| Flagged by GoPlus only | 0 | 91 | |
| One or more owner switches, clean on both | 49 | 89 | 55 percent |
| No owner switch, clean on both | 40 | 89 | |
| Exactly one switch, clean on both | 32 | 89 | |
| Two switches, clean on both | 8 | 89 | |
| Three or more switches, clean on both | 9 | 89 | |
| Sell tax above 10 percent on either checker | 6 | 150 | |
| Closed source | 5 | 150 | |
| No usable simulation returned | 48 | 150 | |
| GoPlus returned no is_honeypot key | 21 | 150 |
A rule for reading the flags
Flags are not verdicts, so a usable rule has to say what each one costs a buyer. Ours has two tiers.
-
Stop outright on
hidden_owner,can_take_back_ownershiporpersonal_slippage_modifiablein any token at all, because each one hands an owner a lever that can be aimed at a single holder. -
Stop on
is_proxyas well, unless the proxy admin can be named and its holder checked. -
Treat
is_mintableandis_blacklistedas conditional. They are ordinary on a stablecoin run by an identified issuer, and disqualifying on a token whose deployer cannot be identified.
Applied to the 89 tokens both checkers called clean on
, that rule rejects 10
of the 89 outright on one of the first four fields, and sends another 26 of
the 89 into the conditional group, carrying
is_mintable or is_blacklisted with none of the
outright flags set. The remaining 53 pass the rule, which is not the same
as passing an audit.
Two denominators sit outside the headline. The 48 with no usable
simulation were mostly dead or illiquid pairs from the boost feeds, and
GoPlus returned no is_honeypot key for 21 of the 150. Silence
is the failure mode a hurried buyer is least likely to notice.
Two GoPlus API traps
Running these endpoints produced two findings worth recording. GoPlus documents multi-address batching, and a batch did return code: 1 with the message OK. It returned data for only the first address of every batch, on
all three chains, while the same addresses queried one at a time answered
correctly. Our script therefore sends one address per request.
Throttling is the second trap, and it shaped the script rather than showing
up in this run: GoPlus has been observed signaling a rate limit inside an
HTTP 200 body carrying code: 4029 and the message too many
requests rather than as an HTTP 429, so the survey throttled to one call
every two seconds and checked the body code instead of the status. No 4029
was hit on .
What an auditor checks that a simulation cannot
Count the outputs. A simulation returns one verdict, while GoPlus needs 18 booleans to describe what an owner may do to a token.
is_honeypot- the headline flag, worded by the vendor as the token maybe cannot be sold
cannot_sell_all- the holder cannot sell the entire position in one transaction
cannot_buy- buying is blocked by the contract
transfer_pausable- trading can be pausable by token contract
is_blacklisted- some addresses may not be able to trade normally
is_whitelisted- a list of addresses exempt from the rules the other holders meet
slippage_modifiable- the trading tax can be modifiable by token contract
personal_slippage_modifiable- whether the owner can set a different tax rate for every assigned address
trading_cooldown- the contract has a trading-cool-down mechanism
is_anti_whale- a cap on the size of a single trade or a single holding
anti_whale_modifiable- that same cap with a setter behind it
owner_change_balance- the owner can write a holder's balance to a number of its choosing
is_proxy- 1 for a proxy and 0 for anything else
hidden_owner- the contract has owners that ordinary inspection misses
selfdestruct- the contract can destroy itself
external_call- a primary method can call into other contracts while it executes
is_mintable- new supply can be created after launch
can_take_back_ownership- ownership can return to a previous holder after it looks renounced
That ratio is the argument. An auditor's unit of analysis is the privilege, not the trade.
Ownership context is the second thing a trade never sees. GoPlus returns
creator_address, creator_percent,
owner_address, owner_percent,
holder_count and a holders array with per-address
percent, is_contract and is_locked.
One field does work no single-token check can: honeypot_with_same_creator ties the token to earlier honeypots from the same deployer.
Deployer history is where the real casework sits. CertiK traced one funding
address, 0xC5535F839071b854a8deC65d3b30E36AB91229AB, to 979
honeypot creators between and
, at a peak rate of one
contract every 30 minutes, for roughly $58.7k in total and about $60 taken
per victim. Per-contract analysis would have found 979 tiny frauds. Cluster
analysis found one operator.
Code analysis at scale works too, when it looks for the right thing. HoneyBadger examined more than 2 million smart contracts, flagged 690 as honeypots, confirmed 87 percent of those by hand and identified 240 victims with profits above $90,000, published at USENIX Security in .
An audit reaches the parts none of that touches: who holds the admin keys, what a timelock does and does not cover, whether the LP position is locked and until when, and which invariants survive adversarial input. Property testing is the practical form of that last question, described in our guide to smart contract fuzzing. Reading the document is a separate skill, so start at the scope statement and the dates as the audit report walkthrough sets out, then confirm the file is genuine before trusting any badge, which our note on fake audit reports covers. Our CertiK company analysis shows how far method varies between firms, the member directory lists the firms that publish, and the checkers described here sit in tools.
Tokens that passed a checker and still exited
SQUID is the canonical case and it is also our control. The token launched in at a cent, reached $4.42 within 72 hours and peaked at $2,861.80 at 09:35 London time on . Five minutes later it traded at $0.0007926, a fall of 99.9999 percent.
Ask honeypot.is about that address today and it says the token is fine.
| Token | Chain and address | honeypot.is | GoPlus |
|---|---|---|---|
| SQUID | BNB Smart Chain, 0x87230146E138d3F296a9a77e497A2A83012e9Bc5 |
|
full result returned with no is_honeypot key at all |
| USDT | Ethereum, 0xdAC17F958D2ee523a2206206994597C13D831ec7 | isHoneypot false | is_honeypot 0, with is_blacklisted, is_mintable, owner_change_balance, slippage_modifiable and transfer_pausable all 1 |
| USDC | Ethereum, 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 | isHoneypot false | is_honeypot 0, is_proxy 1 |
| WETH | Ethereum, 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 | isHoneypot false | is_honeypot 0, no owner switch raised |
| PEPE | Ethereum, 0x6982508145454Ce325dDbE47a25d4ec3d2311933 | isHoneypot false | is_honeypot 0, with anti_whale_modifiable, is_blacklisted and transfer_pausable set |
Read the SQUID row carefully, because it is easy to over-read. It proves that a simulation describes one pair at the block where it ran: selling SQUID against the PancakeSwap pair works today, on whatever liquidity remains. It does not prove the tool is broken, or that the 2021 trap never existed. Dating the measurement is the finding rather than a formality.
Comparison rows say the rest. Five tokens, five risk shapes, one verdict word between them.
Scale, from the academic side. Sampling from the same period found over 10,000 scam tokens on Uniswap and 39,762 potential victims, roughly half of the listed tokens in that labeled sample.
Practical shape for a buyer, then.
- Run the checker, and record the verdict together with the block and the day it ran in.
- Read the flags it declined to turn into a verdict, because owner privileges outlast today's answer.
-
Check the deployer, which matters more than either, through
honeypot_with_same_creatorand the creator address on the explorer.
Where a token is worth real money that work belongs to a firm, and our ranking of top cybersecurity companies is a starting point. Standing checks before a purchase are a crypto due diligence checklist of your own making, and owner backdoors are a separate subject from honeypots, since a smart contract backdoor can sit in a token that sells perfectly well today.
Frequently asked questions
Two checkers disagree about the same token. Which one do I believe?
Neither, until you know why they differ, because the two are answering different questions. A simulator says a sell executed against one pair at one block. A static scanner says the contract contains a function that could block a sell later. In our run of 91 tokens with both verdicts known, disagreement happened twice and both times the simulator was the one flagging, with GoPlus reporting a capability rather than a verdict on the same address. Treat any disagreement as a stop signal and go read the contract, or skip the token.
The owner renounced ownership. Does that end the honeypot risk?
Not when a proxy is in the picture. Renouncing sets the owner of the implementation to the zero address, and it says nothing about who may swap the implementation the proxy points at, which is often a separate admin slot with a separate holder. Check whether the token is a proxy first, then find the admin of the proxy rather than the owner of the logic. GoPlus flags the first half as is_proxy and the second as hidden_owner or can_take_back_ownership, and a token showing any of those with a renounced owner deserves the same suspicion it had before.
My token will not sell. How do I find out why?
Start with the checker's diagnostics rather than its verdict. Read simulationError against isHoneypot first: an error string with simulationSuccess false means the trade never ran, while isHoneypot true with simulationSuccess true means a sell was simulated and it reverted. Then check maxSell against the size of your position, because a cap below your holding fails a sell that a smaller one would pass. Check trading_cooldown next, since a sell attempted inside the window reverts on a timer rather than on anything about you. Then check is_blacklisted and look up your own address, because a list that started empty at launch may now hold exactly one entry. One limit applies to all of it: honeypot.is covers Ethereum, BNB Smart Chain and Base, so on other chains the GoPlus fields and a manual read of the transfer function are what remain.
How often should I re-check a token I already hold?
Before every sell of consequence, and after any announcement that mentions the contract. A clean result describes the block it ran in, so the useful habit is a check immediately before a transaction rather than a check once at purchase. Where the token carries slippage_modifiable, transfer_pausable, is_blacklisted or is_proxy, add a watch on the owner address, because the transaction that changes your position will come from there and will be visible on the explorer before your sell fails.