DEV Community

Cover image for Delta Analysis: How to Audit a Uniswap Fork Without Wasting a Week
vault vult
vault vult

Posted on

Delta Analysis: How to Audit a Uniswap Fork Without Wasting a Week

Delta Analysis: How to Audit a Uniswap Fork Without Wasting a Week

Every DeFi protocol on an EVM chain is, statistically, a fork of something. A Uniswap V2 fork here, a PancakeSwap V3 fork there, a Compound fork in the lending corner. Bug bounty programs list these contracts as critical targets, and researchers — reasonably — point their tools at them.

Then nothing happens. Slither is silent. The audit report from 2021 says "no critical findings." The code has been live for years, processing billions in volume, with no exploit.

The mistake is treating an audited fork as a fresh codebase. The productive approach is delta analysis: identify exactly what the fork changed, and aim only at the delta. Everything else is a waste of time.

This post walks through the methodology I use, with public examples.


Why "just run Slither" fails

Static analysis tools are pattern matchers. They catch reentrancy, unchecked math, unsafe external calls — the classes of bug that have existed since 2016. On an audited fork, those patterns have already been flagged and resolved.

The bugs that survive audits are not pattern-detectable. They live in:

  • The fork's own additions (a reward mechanism, a migration path, a proxy layer)
  • Interactions between two audited contracts
  • Economic invariants that break under specific, legal user behavior
  • Access control that is present but wrong

Slither cannot find these. You have to read the code — but you have to read the right code.


The five-phase methodology

Phase 1 — Recon and scope mapping

Before any review, answer these questions in writing:

  1. Which contracts are in scope? Copy the addresses verbatim.
  2. Which of them are proxies? Run cast codesize on every address. Anything under ~1KB is a proxy; you need the implementation.
  3. Who controls authority? Call owner(), admin(), getOwners(), getThreshold(). Then call cast codesize on that address. If it returns 0, the owner is an EOA — log it, but do not chase it. Bounty programs almost universally exclude "attacks requiring privileged addresses."
  4. What does the protocol hold? For each contract, call balanceOf on the reward or value token. That number is your maximum damage estimate, and it tells you where to spend time.

Output: a SCOPE.md with all of the above. This document travels with you through the rest of the review.

Phase 2 — Delta analysis against the canonical source

For each contract, identify its upstream parent:

  • Uniswap V2 → Uniswap/v2-core, Uniswap/v2-periphery
  • PancakeSwap V3 → pancakeswap/pancake-v3-contracts
  • Compound → compound-finance/compound-protocol
  • WETH9 → gnosis/weth9 (canonical reference: etherscan.io/address/0xC02aaA39...)

Clone both the fork and the parent, then diff the function signatures:

diff <(grep -E "^    function" fork/Contract.sol) \
     <(grep -E "^    function" parent/Contract.sol)
Enter fullscreen mode Exit fullscreen mode

Every line with < or > is a candidate. Three outcomes:

Diff shape What it means
No diff in a function body Dead — it's canonical
Renamed function, identical body Dead — probably a rebrand
New function, or changed logic This is the target

For a slower but more thorough pass, run git log on the parent repo to find the exact commit that matches the fork's deployment date, then diff full file contents. For an even faster first look, extract the deployed function selectors:

cast code 0x<CONTRACT> --rpc-url $RPC | \
  grep -oE "63[0-9a-f]{8}" | sed 's/^63//' | sort -u | \
while read sel; do
  echo -n "0x$sel -> "
  cast 4byte "0x$sel" 2>/dev/null || echo "(custom)"
done
Enter fullscreen mode Exit fullscreen mode

Selectors that 4byte-decodes to known functions tell you what the contract claims to be. Selectors that come back as (custom) are either compiler error strings pushed into the bytecode (a common source of false positives — look for byte patterns like 0x61 0x6c 0x6c which spell "all" ASCII) or genuine fork additions.

Phase 3 — Invariant testing on a Foundry fork

This is where most of the value is. Canonical code has known invariants. Your fork may break them.

Test pattern:

function test_Invariant() public {
    vm.createSelectFork(vm.envString("RPC"), BLOCK_NUMBER);
    // Assert something that should always be true for a correct implementation
    assertLe(protocol.value(), protocol.maxValue(), "invariant broken");
}
Enter fullscreen mode Exit fullscreen mode

For a Uniswap V3 pool, the invariants are well-documented:

  • feeGrowthGlobal0X128 and feeGrowthGlobal1X128 are non-decreasing over time
  • slot0.sqrtPriceX96 is within [MIN_SQRT_RATIO, MAX_SQRT_RATIO]
  • slot0.unlocked == true after any completed transaction

For a MasterChef-style farm:

  • totalSupply of the receipt token equals balanceOf of the staking contract
  • Sum of user.rewardDebt equals totalRewardsDistributed
  • emergencyWithdraw does not leave residual claims

For a proxy:

  • implementation.slot0 is initialized (i.e., _disableInitializers() was called)
  • implementation.owner() is either non-zero or upgradeTo requires onlyProxy

When an invariant fails, you have a lead. When all invariants pass, the canonical claim holds and you can move on.

Phase 4 — Proxy and authority analysis

Modern contracts are almost always behind a proxy. Three patterns exist:

Pattern How to detect Attack surface
TransparentUpgradeableProxy EIP-1967 admin slot ≠ 0 Admin is a contract (timelock/multisig) or EOA
UUPS (ERC-1822) EIP-1967 admin slot = 0, impl has proxiableUUID Implementation initialization — CVE-2021-41251
Beacon EIP-1967 beacon slot ≠ 0 Beacon upgrade gate

The UUPS case is the most dangerous because the implementation holds the initializer state. If _disableInitializers() was not called in the constructor, an attacker can:

  1. Call initialize() on the implementation directly
  2. Become the implementation's owner
  3. Call upgradeToAndCall(malicious, "") on the implementation
  4. malicious.selfdestruct() → implementation has no code → proxy bricked

This is CVE-2021-41251, and it has destroyed production protocols.

Test:

cast call $IMPL "owner()(address)" --rpc-url $RPC      # should be 0x0
cast storage $IMPL 0 --rpc-url $RPC                   # should be 0x...ff
cast call $IMPL "initialize()" --rpc-url $RPC         # should revert
Enter fullscreen mode Exit fullscreen mode

If initialize() succeeds on the implementation, stop everything and write the PoC.

Phase 5 — The discipline of stopping

This is the phase no blog post talks about, and it is the most important one.

The instinct after 20 dead ends is to lower the bar: "well, this receipt-token bug could matter if someone deployed a market tomorrow." That is how researchers waste submissions on findings the triager rejects in 30 seconds.

The bar is not "is this technically a bug?" The bar is:

  1. Is there real value at risk right now, on-chain? Not "could be in the future."
  2. Does the exploit work in a single transaction, on a fork, with the current state?
  3. Is the impact measurable in tokens held by the protocol, not just in emitted events?
  4. Does it fall outside the program's explicit exclusions? Most programs exclude "privileged addresses," "devaluing protocol incentive rewards," and "design issues." Read those clauses before writing the PoC, not after.

If any of those four answers is "no," the finding is a portfolio piece, not a submission.


Two public case studies

Case study 1: The four.meme pool pre-creation (BSC, 2025)

A launchpad migrated liquidity to PancakeSwap V3 via createAndInitializePoolIfNecessary. This function only initializes the pool if slot0.sqrtPriceX96 == 0. An attacker pre-created the pool and initialized it at an extreme price. The migration succeeded silently, and the attacker drained the pool for ~$183,000.

The delta: the launchpad's code assumed "create" meant "initialize at a fair price." The canonical function only meant "initialize if empty."

The lesson: when a fork calls a canonical function, check what the canonical function actually guarantees. The fork author's mental model is not the source of truth.

Case study 2: UUPS uninitialized implementation (CVE-2021-41251)

Multiple protocols deployed their UUPS implementations without calling _disableInitializers() in the constructor. Attackers called initialize() on the implementation directly, became the owner, and upgraded to a selfdestructing contract — bricking the proxy and locking all funds.

The delta: nothing in the proxy was wrong. The bug was in the implementation's constructor, which is invisible from the proxy address.

The lesson: audit both layers of every proxy. The proxy's bytecode is short and easy to verify, but the implementation is where the actual state lives — including the initializer.


A small tooling checklist

# Recon
cast codesize <addr> --rpc-url $RPC             # proxy detection
cast call <addr> "owner()(address)" --rpc-url $RPC
cast storage <addr> 0 --rpc-url $RPC             # slot-0 fingerprint
cast code <addr> --rpc-url $RPC | \
  grep -oE "63[0-9a-f]{8}" | sed 's/^63//' | \
  xargs -I{} cast 4byte "0x{}"                   # selector extraction

# Fork testing
forge test --match-contract <Name> -vvvv         # with trace

# Diff
diff <(grep -E "^    function" a.sol) <(grep -E "^    function" b.sol)
Enter fullscreen mode Exit fullscreen mode

Foundry's -vvvv trace is the single most useful tool in the workflow. It shows every SSTORE, CALL, and LOG in a call. A function that "succeeds" for a random caller but has zero SSTORE is a no-op, not a bug. The trace proves it in five seconds.


Conclusion

Most of a smart contract review is proving what is not vulnerable. The findings are a byproduct of the process, not the process itself.

The five phases — scope, delta, invariants, proxies, discipline — are not novel. They are the same approach professional audit firms use. What separates a productive hunt from a frustrating one is resisting the urge to test code you know is clean. Aim at the delta. Test the invariants. Check the proxies. Then stop.

If nothing is vulnerable, you have still produced a documented security review — which is worth more to your portfolio than a single rejected submission.

Methodology refined over multiple bounty programs. Public examples referenced: the four.meme incident (February 2025), CVE-2021-41251, and the canonical Uniswap V2 / PancakeSwap V3 source repositories.

Top comments (0)