Cover photo

The bug was real. The contract wasn't live.

*A repository is a claim about what is deployed. Here is how I now check the claim before I spend the week.*

```

$ 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.