Coverage, Fuzzing, and a Mainnet Upgrade
A protocol has 92% line coverage in its unit tests and a green CI badge. It is about to upgrade a proxy that holds user deposits. The upgrade adds a new function that reads a storage variable, and the team reordered two existing state variables in the implementation to "clean up" the layout. The test suite passes. A reviewer notices that the new function is covered but no test exercises the upgrade path itself, and there is no fuzz or invariant test. The team argues that 92% coverage is high and that the proxy pattern is standard. The upgrade is scheduled for Friday.
1. Assess the risk of the storage reordering and the coverage claim.
Read the worked answer
The storage reordering is a serious, potentially fund-losing bug. In a proxy pattern, the implementation's storage layout must match the layout the proxy already uses. Reordering two state variables shifts every subsequent slot, so the new implementation will read existing deposits and balances from the wrong slots. The unit tests pass because they deploy a fresh proxy and implementation together, so the layout is internally consistent; they never simulate an upgrade over pre-existing state. Line coverage does not catch this because the lines execute fine - they just read the wrong data.
The 92% coverage claim is therefore misleading for this change. Coverage measures which lines ran, not whether the upgrade preserves state. The missing test is an upgrade test: deploy the old implementation and proxy, write representative state, upgrade to the new implementation, and assert every existing variable reads back correctly and the new function works. Tools that compare storage layouts between builds should run in CI and fail on any incompatible change. The reorder should be reverted unless a migration is explicitly designed and tested. Friday should slip if the upgrade test cannot be written and passed.
2. What testing would you require before the upgrade, and how would you structure it?
Read the worked answer
Require four layers. First, a storage-layout diff between the deployed implementation and the new one, enforced in CI, with any change requiring explicit sign-off and a migration plan. Second, an upgrade simulation test: deploy old implementation and proxy, seed state for several users, upgrade, and assert balances, owners, and any counters are unchanged and the new function behaves. Third, invariant and fuzz tests on the core accounting: total deposits equal the sum of user balances, no user can withdraw more than deposited, and the proxy's admin cannot be changed by a non-admin. Fuzzing the upgrade path with random initial state catches packing and offset errors that hand-written cases miss. Fourth, a fork test against mainnet state at a pinned block, running the upgrade against a copy of real balances.
Structure the suite so the upgrade test is a first-class test, not an afterthought, and make it fail the build. Add a rehearsal on a testnet with the real deployment scripts. Only after these pass should the Friday upgrade proceed, and even then use a timelock or multisig so a problem can be caught before it is irreversible.