Oct 27, 20268 min read

The DeFi audit is there. Is it the code we're actually using?

Follow an audit from its scope and reviewed commit to the proxy, deployed code and strategy a vault actually uses.

JohnnyTimeFounder, Ginger Security

A team is ready to put money into a DeFi vault. They send me the audit PDF, the contract address and a link to the verified source code.

At first glance, everything looks reassuring. The report is there. The explorer has a green checkmark. The vault has been running for a while.

Then I notice two details: the address is a proxy, and the strategy that handles withdrawals was changed recently.

Now I have a different question: does the audit describe the code that will actually handle this money today?

In my work with hedge funds, trading firms and institutions, that’s a question I want answered before a team relies on an audit as part of an investment decision. A report can be useful and still leave a gap between what was reviewed and what is deployed.

In the due-diligence article, we met Cedar Vault. The vault code matched the reviewed deployment, but a new strategy withdrawal path had no review coverage. Here I’ll show how I’d follow the trail to reach that conclusion, and what I’d ask for before treating the gap as closed.

Cedar is an illustrative example. There’s also a small local demo below that reproduces the proxy and strategy mismatch. It has no real audit and doesn’t make a claim about a live protocol.

What did the report actually cover?

I start by looking at the scope of the audit.

I want the repository and commit, the files and contracts the reviewers examined, and any exclusions, time limits or assumptions. For Cedar, I’d check whether the engagement included the vault, proxy, strategy and withdrawal path. If fixes were made, I’d also check whether the reviewers examined the fixed revision.

Imagine the report covers the vault and the original strategy at a specific commit. The protocol later changes the strategy’s withdrawal path. The report may still be accurate about its original scope, but it doesn’t automatically cover that later change.

I’d record the full commit hash, repository, report version, scope and any follow-up review. Then I’d get the exact source at that commit, including pinned dependencies, and compare it with the source used for the deployment. If the report only lists a branch or a short identifier that can’t be resolved unambiguously, I’d ask for the full revision and supporting records.

A matching commit doesn’t mean every line in that repository was reviewed. The scope and any exclusions still matter.

A review trail from audit scope to reviewed commit and build, then proxy implementation and configured strategy; the changed strategy needs separate coverage.
Each link answers a different question. Those checks connect the report to the contracts in use.

Then find out what the proxy is running

A proxy is an address users interact with that delegates execution to another contract, called the implementation. The proxy can keep its address and storage while its implementation changes. The address the team sent me starts the trail; I still need to find the code it uses.

For a proxy that follows ERC-1967, the implementation address is usually stored in a standard slot. Using Foundry’s Cast, I can inspect it and fetch its runtime bytecode:

cast chain-id --rpc-url "$RPC_URL"
cast block-number --rpc-url "$RPC_URL"
cast implementation "$PROXY" --block "$BLOCK" --rpc-url "$RPC_URL"
cast code "$IMPLEMENTATION" --block "$BLOCK" --rpc-url "$RPC_URL"

I’d independently confirm the chain and proxy address, record the block number and block hash, and use the same block for related reads. If the RPC supports it, use a finalized block suitable for the decision. A provider’s answer reflects what that provider reports, so for a material position I’d cross-check it with another trusted node or provider.

Not every proxy uses ERC-1967. A beacon proxy needs the beacon resolved to its implementation; other designs can route calls differently. A blank or unexpected slot calls for a closer look at the proxy code and its upgrade paths. The explorer’s proxy label is a useful starting point.

Can I reproduce the code on-chain?

Once I know the implementation, I compare its deployed runtime bytecode with the result of compiling the relevant source. Runtime bytecode is the code stored at the contract address; it is different from the creation bytecode used to deploy it.

For a meaningful comparison I need the exact source and dependencies, compiler version, optimizer settings, EVM target and metadata settings. Linked libraries can change the result too, so I need their actual addresses and the code at those addresses. Solidity’s metadata documentation explains how source references and build settings support reproducible verification.

There is a wrinkle: constructor-set immutables can be embedded in deployed runtime code. The compiler artifact may contain placeholders before deployment, so a raw comparison against that template can fail even when the correct source was used. I’d account for the deployed immutable values and library links using the deployment records and a verification tool that handles them, then examine any remaining difference. I wouldn’t simply strip unexplained bytes until a comparison passes.

Explorer verification and Sourcify can help establish a source-to-bytecode match. I’d inspect whether the result is an exact match or a more limited match and what was actually compared. A mismatch can come from build inputs, metadata, libraries or immutables; it can also mean we have the wrong source. Until it’s explained, the link remains open.

And even a perfect source match answers only: does this source correspond to this deployed code? It doesn’t tell me that an auditor reviewed it or that its current settings are acceptable.

The vault matches. What about the strategy?

Suppose the vault implementation matches the reviewed vault code. That’s a useful match, but the vault’s storage points to a separate strategy contract. The strategy’s code determines where and how assets can move on withdrawal.

I’d read the strategy address through the proxy at the same block, using the actual contract’s ABI and getter. For a contract with a strategy() getter, the shape is:

cast call "$PROXY" 'strategy()(address)' --block "$BLOCK" --rpc-url "$RPC_URL"
cast code "$STRATEGY" --block "$BLOCK" --rpc-url "$RPC_URL"

Then I’d identify whether the strategy is itself a proxy, resolve it if necessary, and compare its deployed code with its own build and the report’s scope. I’d also inspect the vault’s relevant configuration: strategy, asset, withdrawal destination or pool, privileged roles and upgrade controls. The exact getters and storage layout depend on the contracts. Read state through the proxy where the live application keeps its state; reading implementation storage instead can produce a misleading answer.

In our example, the strategy now in use has a changed withdrawal path outside the original review. Even if its source is verified, I still need a review of that change and a mapping from the reviewed revision to the deployed strategy. I’d hold the deposit until that review and mapping are available, just as in the earlier article.

Try it locally

I built a small downloadable Foundry demo to make the difference visible. It deploys a vault implementation, a proxy and two strategies into a temporary local EVM. For the exercise, we assume the vault and the original strategy were reviewed; the changed strategy was not. There is no actual audit report or reviewed commit in this demo.

Unzip it, open the extracted folder and run:

forge test -vvvv

The trace emits the proxy, active implementation, configured strategy and original strategy, plus their code hashes. The test reads the ERC-1967 slot, confirms the active vault runtime matches its locally compiled runtime, and confirms the configured strategy matches the changed build but differs from the original. It also checks the changed strategy’s example withdrawal destination.

The test shows that the vault build matches, while the configured strategy differs from the one designated reviewed. We supplied the assumed audit scope ourselves; the blockchain cannot establish it. In a real review I’d need the report, commit, build artifacts and records of any subsequent review to establish it.

The demo contains no linked libraries or immutables and doesn’t implement asset transfers or production access controls. Its contracts are teaching code; keep funds away from them. The README lists the build settings and what the test proves.

Once the team has mapped the deployment, the DeFi monitoring article shows how I would watch for changes that could affect the decision.

Where tools and AI agents help

I’d use Git to check out the reported commit and inspect changes, Foundry to build and test, and Cast to query code, storage and configuration. An explorer or Sourcify can provide source and verification artifacts; I’d still check them against the specific chain, address, block and scope of the decision.

An AI agent can speed up the investigation: extract candidate scope and commit references from the report, build a dependency map, summarize a diff, propose Cast commands and assemble a table of sources and results. I would give it the actual documents and read-only access, then check its claims against the report, source and raw chain results. An agent can miss an exclusion, mistake a proxy for an implementation or invent a link between two revisions. I use its summary to find the relevant records and check the underlying sources myself.

For stronger assurance about a particular on-chain storage value, an RPC can provide an account and storage proof, for example with Cast’s proof command. The proof must be verified against a trusted block header’s state root; merely receiving a proof from the same RPC isn’t independent verification. A proof of the implementation slot establishes its value at that block. Audit coverage, safe behavior and future state each need separate checks.

What changes after today?

The result describes the deployment at a particular point in time. A proxy upgrade, strategy replacement, configuration change or dependency upgrade can invalidate the conclusion without changing the vault address. I’d record the chain, block, addresses, code hashes, build inputs, report and scope, relevant state, unresolved gaps and owner of the decision. Then I’d monitor the important changes and reopen the review when they occur.

The practical distinction I keep in front of the team is simple:

  • Source verification: does this source and build correspond to the deployed code?
  • Audit coverage: did the reviewers examine the relevant version and behavior, and what happened to their findings?
  • Configuration: what code, addresses, permissions and settings are actually in use at the recorded block?

All three can be true, and the protocol can still carry risk. But if we can’t answer one of them, an audit badge won’t answer it for us.

If you’d like help tracing an audit to the contracts and configuration your team actually uses, we can discuss it through my fractional security advisory service.