The contract is clean - for now: catching scams that survive launch-time checks

A launch-time verdict is a snapshot, and scammers know it. Two ways a token passes every check at T0 and turns hostile later: the delayed honeypot and the proxy whose implementation is the real payload. Here is how we now catch both.

Most scam detectors, including ours until recently, make one big implicit assumption: the contract you analyze at launch is the contract people will trade. Read the source, simulate a buy and a sell, cluster the deployer, score it, done.

That assumption is a snapshot. And a snapshot is exactly what a patient scammer plays against. Two token designs pass every launch-time check and then turn hostile later. This week we shipped detections for both. Here is how they work and why the launch snapshot was never enough.

Design 1: the delayed honeypot

A honeypot is a token you can buy but cannot sell. The classic version is non-sellable from block one, so a buy-then-sell simulation catches it instantly. We have done that for a long time.

The patient version is sellable at launch. Early buyers sell fine. The chart looks healthy, a little volume comes in, the token earns a clean verdict on every checker that judged it at T0. Then, days later, the operator flips a switch:

  • a timed blacklist that starts rejecting transfers after a block height or timestamp,
  • a setTrading(false) / pause() kill switch pulled once enough liquidity has accumulated,
  • a fee setter cranked to 100% on sells.

From that moment the token is a honeypot. But the only verdict anyone recorded was the clean one from launch day. The detection ran once, at the worst possible time to run it.

The fix: re-simulate at J7

We already keep post-launch snapshots of every token at J0, J7 and J30 to catch slow rugs (volume collapse, late LP burns). The new piece re-runs the full buy/sell honeypot simulation at the J7 snapshot, but only for tokens that were genuinely sellable at J0. A clean-to-honeypot flip is the signal:

// Re-run the buy/sell simulation a week later.
// Only for tokens that were sellable + tradable at J0 - a clean->honeypot
// flip is the whole point. Bounded per run because it is RPC-heavy.
const eligible = !j0.risk_flags.some((f) => J0_SKIP_RESIM_FLAGS.has(f));
if (rpc && eligible && resims < resimLimit) {
  const isNowHoneypot = await detectLateHoneypot(rpc, tokenAddress);
  if (isNowHoneypot) {
    flags.push("late_honeypot");   // +40 risk on the J7 snapshot
  }
}

If a token that you could sell on day zero reverts on a sell simulation a week later, that is not noise - that is the trap arming itself. It shows up on the token’s timeline as a late_honeypot flag at J7, so the verdict you read is no longer frozen at launch.

The honest engineering note: this is best-effort. An RPC hiccup never fabricates a late_honeypot - a failed re-simulation returns “not a honeypot” rather than inventing one. We would rather miss a flip than cry wolf on a healthy token.

Design 2: the proxy whose implementation is the payload

The second design is sneakier because the malicious code is not where you are looking.

A proxy is a thin contract that holds storage and forwards every call, via delegatecall, to a separate implementation contract that holds the logic. This is a completely legitimate, extremely common pattern (every upgradeable token uses it). That is exactly why it is good cover.

Here is the trick: you analyze the proxy address. Its bytecode is tiny - basically “delegatecall to whatever address is in this storage slot.” There is nothing to flag. The source, if verified, is a boilerplate proxy. Clean. Meanwhile the real logic - the blacklist, the mint, the drain - lives in the implementation the proxy points to, which nobody looked at. Worse, an admin can change that implementation in a single transaction. The proxy address never changes, so explorers and checkers keep showing the same “contract.” Yesterday’s audited code becomes tomorrow’s drain function, invisibly.

Until this week, our analysis stopped at the shell. So we taught it to follow the delegatecall.

Resolving the implementation on-chain

You do not need the source to find the implementation. The proxy standards store the implementation address in well-known storage slots. We read them on-chain, in order:

// EIP-1967: slot = keccak256("eip1967.proxy.implementation") - 1
let implementation = await readSlotAddress(rpc, address, EIP1967_IMPL_SLOT);

// Fall back to a beacon proxy (the beacon holds the address)...
if (!implementation) {
  const beacon = await readSlotAddress(rpc, address, EIP1967_BEACON_SLOT);
  if (beacon) implementation = await callImplementation(rpc, beacon);
}
// ...then the OpenZeppelin legacy slot, then EIP-1822 / UUPS.

Once we have the implementation address, we run the same bytecode analysis we run on any deployed contract against it - selectors, dangerous opcodes, and the known-scam-factory hash match. The shell was camouflage; the implementation is where the verdict actually lives. If the implementation’s bytecode is byte-identical to a known mass-scam factory, the token gets proxy_implementation_known_scam - a clean-looking proxy delegating to a confirmed scam template.

Two more signals fall straight out of this:

  • proxy_implementation_missing - the proxy points at an address with no code. A proxy delegatecalling into nothing is a loaded trap: the operator deploys malicious logic to that address (or repoints the proxy) the moment enough liquidity has piled up.
  • mutable_proxy_admin - the EIP-1967 admin slot holds a live address. That admin can swap the implementation whenever it wants. It is a rug switch sitting in plain sight, and without a time-lock the flip is instant.

Why “the admin slot is empty” is not “safe”

One nuance we were careful about: an empty admin slot does not mean immutable. UUPS proxies keep their upgrade authority inside the implementation, not in the admin slot. So we only raise mutable_proxy_admin when the admin slot is genuinely populated - a positive signal of mutability - and we never claim immutability from an empty slot. Over-claiming safety is its own kind of lie.

The common thread: stop trusting the snapshot

Both detections come from the same shift in posture. A launch-time verdict answers “is this token a scam right now?” The questions that actually protect a buyer are “will this token still be safe next week?” and “is the code I’m reading the code that will actually run?”

  • The delayed honeypot attacks the time axis: clean now, hostile later. Answer: re-judge at J7.
  • The proxy attacks the indirection axis: clean here, hostile one delegatecall away. Answer: follow the pointer.

A nice side effect: cost stays bounded. The honeypot re-simulation only runs on tokens that were sellable at J0 and is capped per cycle; the proxy resolution only fires when the bytecode actually contains a DELEGATECALL opcode, so the millions of non-proxy tokens pay nothing.

Where you see it

These signals flow through to the token view and, for late_honeypot, onto the J0 -> J7 -> J30 timeline, so you can watch a token’s risk drift instead of reading one frozen launch verdict. A token that scored clean at launch and picked up late_honeypot or proxy_implementation_known_scam afterward is precisely the case the old one-shot check missed.

If you only remember one thing: a clean verdict at launch is a statement about launch, not a promise about next week. We stopped treating it as one.