```
$ cast code "$LOCKER" --block "$B" --rpc-url "$RPC"
0x
```
Two characters. That is what the chain returns when there is no contract at an address.
I got that answer in the ninth round of work on a target. Before it, I had read the in-scope
contracts, built hypotheses, screened them and written a proof of concept for the best one. Then I
finally ran a deployment inventory. Every address the repository recorded for the in-scope system
returned zero bytes on the chain the project documented. The contracts existed only as a demo on a
testnet. The program's rules said findings had to apply to the protocol as currently deployed.
Eight rounds of careful work on code that was not deployed.
The worst part is that I already had a gate for this. About a week earlier, a finding I submitted
came back closed as a duplicate. The triager's explanation was polite and specific: the behaviour I
described really was present in the source I tested. But the deployed code had been fixed months
before, an earlier report of the same bug had already been accepted, and the current version
rejected the write my test performed. My repository pin was older than the chain.
The day after that reply, I made deployed state a hard gate in my tooling. I put it at the last step
before a report is marked ready. The zero-bytes target never got near that step, so the gate never
fired, and I spent eight rounds learning what it would have told me on day one.
This article is that gate, moved to where it belongs, and written so you can run it with nothing but
`cast` and a text file.
---
## The checklist
```
DEPLOYED-STATE CHECK [~30 min, before reading code]
IDENTITY
[ ] The deployment authority is named: the project's own registry, docs page or SDK config
[ ] Every component a candidate depends on has an address from that authority
[ ] One block number is pinned and used for every read
EXISTENCE
[ ] Code exists at each address
[ ] Proxies resolved: the implementation the proxy points at is the one compared
PARITY
[ ] Deployed source compared with the pinned checkout, per file I rely on
[ ] The direction of any drift is known: repo ahead of chain, or chain ahead of repo
LIVENESS
[ ] Not paused, not disabled behind a flag, not capped at zero
[ ] Wired: the protocol actually routes to this component
[ ] Configured: the parameters the bug needs are the ones set on chain
RECORD
[ ] Each read saved to a file, with address, block, method and observed value
[ ] The file's hash recorded next to the verdict
```
---
## Why the repository is not the protocol
Bug bounty programs often scope through a repository: "the contracts in this repo at this commit".
That is convenient for everyone, and it quietly hides a gap. The program pays for the protocol that
holds money, not for a commit. When those two disagree, the protocol wins, and many programs say so
in their rules.
The gap runs both ways, and both directions matter:
- The repository is ahead of the chain. New code, a new module, a refactor not yet deployed. A bug
in it is a bug in nothing. At best it is a heads-up.
- The chain is ahead of the repository. The fix is deployed, and your pin predates it. Your PoC
reproduces faithfully against history.
The second direction has a useful inverse, and it is the one people miss. **A fix that exists in the
repository but is not deployed means the bug is still live.** The rule for a "fixed" item in a prior
audit is the same as for a new candidate: it is fixed when the guard is on chain, not when it is on
`main`.
---
## Seven shapes from my own notes
When I searched my engagement notes for "not deployed" and its cousins, the question came up in at
least nine engagements. The cases fall into seven shapes. Every one is from real work, described by
class with the target removed.
| Shape | What I found | What it did to the candidate |
|---|---|---|
| Absent | Zero bytes at every address the repository recorded, on the documented chain. Only a testnet demo existed. | Nothing to attack. Eight rounds spent first. |
| Older generation | The only deployment was an older testnet generation running a mock stablecoin. It did not contain the entry point the attack sequence needed. | Not applicable "as currently deployed", and no user funds at risk on it. |
| Unreleased | The interesting code lived on a development branch, ahead of the release that was deployed. | Killed before a PoC. |
| Stubs | The handlers for the operations I was hunting were placeholders that only logged. Work in progress. | No mechanism at all on chain. |
| Ambiguous identity | The build manifest named two different published addresses for the same package. | Recorded as a scope question. A finding cannot be sized until it is answered. |
| Behind the chain | The deployed code had fixed the bug months before my pinned commit was cut. | Submitted, closed as duplicate. |
| Wrong address | I read an address found by web search, an older deployment, and decided the audited code was "unshipped". The current deployment was a different address and byte-identical to my pin. | My verdict claimed a deployment discount it did not have. Corrected an hour later from a sibling session's notes. |
The last row is the one I would underline. **A deployed-state check can be wrong in the other
direction too.** An address from a search result, a blog post or an old README is not a deployment
authority. The project's own network registry was the authority in that case; it usually lives in the
SDK, the frontend config or a deployments page. Get addresses from there, and write down where you
got them.
---
## The reads
For an EVM target, all of this is `cast` against an RPC at one pinned block. Pin the block once and
reuse it, so every read describes the same moment.
```bash
B=$(cast block-number --rpc-url "$RPC") # pin once, reuse everywhere
# Existence: "0x" means nothing is deployed here
cast code "$ADDR" --block "$B" --rpc-url "$RPC"
# Proxy resolution: EIP-1967 implementation and admin slots
cast storage "$ADDR" 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc \
--block "$B" --rpc-url "$RPC"
cast storage "$ADDR" 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103 \
--block "$B" --rpc-url "$RPC"
# Liveness: whatever switch the contract exposes
cast call "$ADDR" "paused()(bool)" --block "$B" --rpc-url "$RPC"
# Wiring: does the protocol point at this component?
cast call "$REGISTRY" "<getter the protocol uses>" --block "$B" --rpc-url "$RPC"
# Use: has anything happened here recently? (RPCs cap the range; page it)
cast logs --address "$ADDR" --from-block $((B - 100000)) --to-block "$B" --rpc-url "$RPC"
```
A few notes from getting these wrong:
- An empty implementation slot is good news, not a gap. In the zero-bytes case above, the
contracts that did exist read zero at both EIP-1967 slots, which ruled out an unverified
implementation hiding behind a proxy. Write that down; it closes a question a reviewer will ask.
- Compare source, not raw bytecode. Runtime bytecode differs from your build because of the
metadata hash and immutables, so a byte comparison fails even when the code is identical. Diff the
verified source against your pin, file by file, for the files your finding relies on. If the code
is unverified, say so: that is a finding about the scope, not about the bug.
- No logs is a hint, not a proof. A component with no events in a long window is probably not in
use. Confirm through the wiring read before calling it disconnected.
- Read the parameters your bug needs. If the exploit needs a fee above zero, a cap above some
amount or a feature flag on, read that value at the pinned block. A mechanism at a configuration
nobody runs is the same problem in a different coat.
On chains where code is published as packages or programs rather than contracts at addresses, the
questions are the same: which version is live at the address the project's registry names, what its
upgrade history says, and whether it matches the source you read.
---
## Eight states, one is live
My gate records one row per component the finding depends on, and gives each a status. Only one
deployed status passes.
| Status | Meaning | Typical read |
|---|---|---|
| `active` | Deployed, wired, enabled, configured | all of the above, consistent |
| `disabled` | Deployed, switched off | `paused()` true, a flag off, a cap of zero |
| `unconfigured` | Deployed, never set up | the parameters the bug needs are unset |
| `disconnected` | Deployed, nothing routes to it | the registry points elsewhere |
| `deprecated` | Replaced, kept for history | the registry points at its successor |
| `absent` | Expected, not there | `cast code` returns `0x` |
| `not_deployed` | Declared undeployed by the project | a sourced document, not a chain read |
| `unknown` | Not yet checked | the default for every draft |
`not_deployed` passes only as an explicit, sourced declaration, for programs that pay for code before
it ships. Everything else blocks. No record at all blocks too: the gate is fail-closed, because an
unchecked draft is the normal state of a finding, and "I didn't look" must not read as "it's fine".
Each row carries the address, the pinned block, the method, the observed value, the path to a file
holding the raw read, and that file's SHA-256:
```yaml
component: position-market
chain: <the documented chain>
address: "0x…"
block: <pinned block>
method: eth_getCode
observed_value: "0x"
status: absent
evidence_source: raw/deployment_inventory.json
evidence_hash: "<sha256 of that file>"
rationale: Address from the repository's deployment record; no code on the documented chain.
```
The hash is there because of a hole I found in my own first version. It accepted a record that cited
any path with any hash, so a draft could claim a read that never happened. The evidence file must
now exist inside the workspace and hash to the recorded value. The same rule applies to
`not_deployed`: a document citation is cheaper to fake than a chain read, so it gets checked just as
hard.
---
## Where this sits in the workflow
My tooling still enforces this check at the last gate before a report can be marked ready for
review. That is where it is cheapest to enforce and least useful to learn from. The zero-bytes target
paid eight rounds for the difference.
The real value is at the very start, before the first line of code is read. Thirty minutes of reads
decide whether the next thirty hours are spent on the protocol or on a repository. That part is still
discipline, not code, and I am saying so plainly because the tooling makes it easy to forget.
---
## What this does not settle
- Audit contests are different. Contest code is often deliberately pre-deployment, and the contest
pays for the commit. This check is for bounty programs on live protocols. Read your program's rules
for which one you are in.
- Live is not the same as reachable. An `active` component can still sit behind a role, a
whitelist or an off-chain filter. This gate answers "is it there?", and the privilege and
attacker-model checks answer "can an outsider get to it?"
- One block is one moment. A paused component can be unpaused tomorrow. Some programs pay for
that latent risk and some do not. The record makes the claim precise; the program decides what it
is worth.
- Wiring reads need protocol knowledge. There is no universal "is this connected" call. The
registry getter is protocol-specific, and getting it wrong produces a confident wrong answer.
- I have no rate. I cannot tell you what fraction of rejected findings fail on deployed state,
because my rejection log was not categorised on this axis until the gate existed. "At least nine
engagements" is a count from my notes, not a measurement.
---
## What I would do with this if I were you
Before you read a single in-scope file, open the program's deployment page or the project's SDK
config, pin a block and run `cast code` on every address. If one returns `0x`, stop and find out why.
If they all return code, diff the verified source against your commit.
Then do it again before you submit. The chain moves, and the fix for your bug may already be on it.
The rule catalog behind my other checks, including the realistic-configuration rule that sits next to
this one, is public in [viability-gate](https://github.com/KhomDev/viability-gate). The rejection log
it was calibrated on is in [rejection-taxonomy](https://github.com/KhomDev/rejection-taxonomy).
The repository tells you what the team wrote. The chain tells you what you can break.

