private audit field notes / evm / cross-chain defi

Between Two Chains: How I Audit Cross-Chain DeFi Protocols

A bridge is not one transaction stretched across two networks. It is a distributed state machine with money moving through the gaps. This is the approach I used during a recent private EVM engagement with Three Sigma.

0xTheBlackPanther 10 min read EVM, Cross-chain, DeFi Security
NDA
What stays private

This article does not name the client or disclose findings, affected code, severities, proof-of-concepts, or remediation details. It shares the reusable reasoning process and bug classes behind the review.

I recently reviewed a cross-chain DeFi system during a private engagement with Three Sigma. The architecture belonged to a category that is becoming increasingly common: a hub chain holds canonical accounting and backing, outpost chains expose faster user flows, and asynchronous messages keep rates, supplies, vaults, and liquidity aligned.

At first glance, each contract looked familiar. There were gateways, vaults, receipt tokens, message codecs, role checks, nonces, and ERC-4626-style accounting. The interesting attack surface did not live inside any one of them. It lived in the time and state between them.

The key mental shift was simple: stop reading the bridge as a sequence of Solidity calls and start reading it as a distributed protocol that can pause after any step.

Why Cross-Chain Auditing Is Different

On one chain, a revert usually restores the entire transaction. In a cross-chain flow, the source transaction may already be final before the destination even sees the message. A later revert cannot travel backward in time and undo the source-chain state.

The dangerous states are often not the starting state or the intended final state. They are the intermediate states:

This is why bridge audits cannot stop at access control and message authentication. Authentic bytes can still carry stale truth. Correct functions can still compose into an incorrect protocol.

The Five-Ledger Model

Before hunting for bugs, I reduced the system to five ledgers. Every relevant state transition had to update one or more of them:

01

custody

Where are the real assets right now: user, gateway, connector, vault, or farm?

02

liabilities

How many redeemable receipt or staked tokens exist on every chain?

03

messages

Which transitions are queued, processed, retried, skipped, or still in flight?

04

time

When was a value produced, sent, received, processed, and considered fresh?

05

recovery

If execution stops here, who can restore the queue, accounting, and assets?

This model made the protocol easier to reason about than a contract-by-contract reading. For every bridge direction, I wrote down the before state and asked what each ledger should contain after every step—not only after the happy path finishes.

cross-chain invariant worksheet before: custody: who holds the backing? liabilities: what supply can be redeemed? messages: what nonce is expected? time: how old is the snapshot or rate? recovery: what happens if the next step fails? after each step: - what changed? - what has not changed yet? - who can act during this gap? - can a user redeem, bridge, report, or retry now? - can the protocol return to a fully backed state?

The Workflow I Used

1. Start With Scope, Trust, and Known Issues

I first pinned the exact review commit, listed every in-scope contract, and separated supporting dependencies from reportable root causes. Then I documented the trust model: governance, keepers, connectors, pausers, minters, burners, farms, and ordinary users.

This prevented two common audit failures. The first is inventing an exploit that requires a trusted role to become malicious. The second is spending hours rediscovering an issue that the team already knows.

I maintained a living known-issue file for both manual review and AI-assisted scans. Every confirmed lead was added immediately so later passes could focus on new behavior instead of producing increasingly polished duplicates.

2. Build The Protocol Story Before The Bug Story

I traced the main user journeys end to end:

For each journey, I tracked the exact token, amount, unit, owner, supply, vault balance, chain ID, connector, nonce, and timestamp. This is tedious on paper and incredibly effective in practice.

3. Turn The Story Into Invariants

The strongest invariants crossed contract and chain boundaries:

4. Explore Adversarial Schedules

Once the invariants were clear, I stopped asking only “can this function be exploited?” and started asking “in which order can legitimate operations violate this invariant?”

I permuted events across the independent lanes:

bridge funded report queued rate updated message delayed user redeems nonce skipped

The best leads often required no forged message or compromised role. They emerged from honest actions executing in a legal but unsafe order.

5. Reproduce The Smallest Real State

For each promising lead, I built the smallest Foundry test that still used the protocol’s real contracts and call path. Mocks were useful for transport timing, but never allowed to invent authentication or token behavior that the production system did not provide.

The test needed to prove:

  1. the initial balances, supplies, nonces, and configuration;
  2. the exact authorized or permissionless trigger;
  3. the harmful intermediate or final state;
  4. why rollback, access control, and recovery did not prevent the impact;
  5. whether a later update truly repaired the system or only hid the mismatch.

The Lessons That Changed My Review

An Honest Snapshot Can Still Be Dangerous

A snapshot does not need to be forged to be wrong when applied. The world may change while it travels. Funds may move, tokens may mint or burn, and another message may execute before the snapshot is processed.

The question is not only “is this report authentic?” It is also “what events does this report know about?” If the payload carries no acknowledgement of processed inbound work, the receiver may be unable to distinguish settled state from in-flight state.

FIFO Is Local, Not Global

Per-chain nonces can guarantee ordering inside one message stream. They do not automatically order the opposite direction. A message from A to B and a report from B to A can each be perfectly FIFO and still cross in a dangerous order.

Per-direction ordering prevents local replay. It does not create global causality.

Intermediate State Is Real State

Two reconciliation sequences can end with the same arithmetic target and behave very differently during execution. If the first operation changes supply, a later external call may observe that temporary state and enforce liquidity, yield, or solvency checks against it.

This changed how I reviewed “mint then burn” and “burn then mint” paths. The final equation is not enough. Every external interaction between the two operations turns temporary accounting into a security boundary.

Recovery Has Three Separate Meanings

During triage, I found it useful to split recovery into three questions:

liveness

Can valid messages continue after a failure?

accounting

Are supplies, backing, and vault balances correct again?

assets

Can the original owner recover funds already committed?

Skipping a nonce may restore liveness while leaving stale accounting or locked assets behind. “The admin can fix it” is therefore not the end of severity analysis; it is the start of a concrete recovery trace.

Shared Configuration Can Leak Permission Between Flows

A token may be valid for reserve transfers and invalid for receipt-token minting. If both operations use one generic mapping as their only allowlist, validity in one context silently becomes permission in another.

I now distinguish identity mappings from capability mappings. Knowing that token X corresponds to token Y does not tell you whether X may be bridged, minted, deposited, withdrawn, or used only for operational rebalancing.

A Preview Is A Promise

ERC-4626-style previews, quotes, and view functions shape user expectations and often feed minimum-output checks. If the preview ignores a downstream fee while execution applies it, the safety check can reject an otherwise ordinary withdrawal.

My review rule is now explicit: compare previews with real execution under non-zero fees, zero idle liquidity, conversion rounding, stale rates, and external calls.

One Wei Can Be Real And Still Not Be A Finding

Some rounding differences are mechanically real but economically meaningless. Before reporting one, I ask whether it accumulates, creates repeatable profit, blocks execution, violates an explicit exactness guarantee, or changes another protocol calculation materially.

Fuzz tests are especially valuable here. A test that deliberately accepts a one-unit tolerance may be a better statement of protocol intent than a comment promising exact synchronization.

Turning Leads Into Evidence

I used AI heavily during this engagement, but never as the final judge. It helped me map entry points, generate adversarial schedules, search for variants, draft test scaffolding, and challenge assumptions. The compiler, execution trace, state assertions, deployment evidence, and protocol intent remained authoritative.

generateAI proposes hypotheses
challengemanual review attacks assumptions
provetests demonstrate state impact
triagescope and intent decide reporting

Every serious lead went through the same gate:

  1. Reachability: can a realistic actor create the required state?
  2. Mechanics: does the exact call sequence execute against the pinned code?
  3. Impact: what changes in balances, supply, liquidity, accounting, or liveness?
  4. Recovery: is the effect temporary, manually recoverable, or permanent?
  5. Materiality: can the issue move beyond dust or theoretical inconsistency?
  6. Novelty: is it fresh, in scope, and distinct from known issues?

This last step matters. A high-quality private audit is not measured by the number of leads generated. It is measured by how many incorrect claims are removed before the client has to review them.

My Cross-Chain DeFi Audit Checklist

If I audit another hub-and-outpost DeFi protocol tomorrow, these are the first questions I will ask.

Custody And Supply

Messages And Ordering

Rates, Fees, And Units

Configuration And Recovery

The Final Lesson

The most useful finding strategy was not “look for a missing require.” It was to locate every moment when the two chains temporarily disagreed and ask what users, keepers, reports, and contracts could do before they agreed again.

Cross-chain protocols are built from familiar Solidity components, but their security lives in distributed-systems properties: causality, partial failure, delayed truth, idempotency, and recovery. That is the layer auditors need to model explicitly.

The engagement details remain private. The lesson is reusable: trace the money, trace the liabilities, trace the message, and then audit every state that exists before all four tell the same story.