VIEW THIS AS

Auto mode follows the Route Engine until you choose a viewpoint.

YOU ARE HERE

ROUTE CHECK

CONNECTED TO

WHAT NEXT

Use the canonical route for this room, or HELP if you are unsure.

Smart-Contract Upgrades: Preserve Stored State and Its Meaning

Three students in school uniforms work through open books at a classroom table, with textbooks and stationery nearby and study notes on the whiteboard behind them.

中文版 · Blockchain learning guide · Previous article

Blockchain learning · Extension BCX005

Three students in school uniforms work through open books at a classroom table, with textbooks and stationery nearby and study notes on the whiteboard behind them.
Study photograph for illustration; it does not establish that the pictured students are studying blockchain.

An unchanged contract address does not prove an unchanged application. In a proxy design, replacement logic can read and write existing state through the same access point. A safe-looking interface can therefore expose the wrong values if the new implementation interprets old storage differently, or if a required migration never ran.

This lesson reviews a fictional registry upgrade from a complete storage dossier. You will compare a compatible layout with a swapped layout, distinguish implementation construction from proxy initialization, trace a guarded migration and test a claim that changing the implementation pointer back restores the old state.

All addresses, roles and values are symbolic. There is no deployed target, private key, administrative command or real upgrade. The calculations are an educational rehearsal, not a production migration recipe or a certification that a particular proxy pattern is safe.

Choose your reading route

In this guide

Sections 1–3 establish where the state lives and define the old layout. Sections 4–8 evaluate replacement and migration cases. Sections 9–11 examine rollback, evidence and acceptance. The exercise includes exact before-and-after answers. For a different question, whether an audit covers the current implementation, see Reading a Blockchain Audit Packet.

Back to contents · Next section

1. Separate the access point, implementation and stored values

In our fictional design, users address proxy P. P delegates the specified application operation to an implementation, initially V1. The operation uses P’s application storage. Changing the selected implementation changes the code used to interpret and update that storage; it does not automatically recreate the stored values.

OpenZeppelin’s proxy explanation describes this separation between implementation logic and proxy state. It also distinguishes collisions involving proxy administration fields from collisions between successive application layouts. Solving one of those problems does not solve the other.

Our dossier stipulates that implementation selection and administrative information occupy separate locations disjoint from the application slots below. We therefore isolate the version-to-version application problem rather than inventing an accidental overlap with the implementation pointer.

The same public access point can remain P throughout every case. That continuity is useful for identifying where calls go, but it cannot establish compatible layout, valid initialization, authorised migration or preserved business meaning by itself.

Back to contents · Next section

2. Read the simplified V1 layout exactly

This toy uses one full-width unsigned value per numbered slot, with no packing, inheritance, mappings, arrays or custom storage base. The application slots have these specified meanings and values:

  • Slot 0: recordCount = 12.
  • Slot 1: nextRecordId = 19.
  • Slot 2: schemaVersion = 1.
  • Slot 3: unused, with supplied value 0.

The registry contains twelve records, but its next identifier is 19. Counts and identifiers are not interchangeable. The dossier allows gaps in historical identifier use and does not say that every number below 19 is an active record.

V1 has already been initialized in P’s storage. Its initialization history is part of the evidence, not something inferred from the implementation’s existence. P’s selected implementation and these four values are independently supplied and correctly identified for the exercise.

Real Solidity layout can involve packing and inheritance. The Solidity 0.8.30 storage-layout documentation explains those rules. Our explicit full-word model deliberately avoids deriving real slot positions from an incomplete source-code declaration. It is not a layout description for every proxy.

Back to contents · Next section

3. Define V2’s intended change and migration

Candidate V2 preserves the meanings of slots 0, 1 and 2, and adds reviewedCount at slot 3. A separately supplied, trusted registry fixture establishes that seven of the twelve records have the required review status. The lesson does not infer that count from the number 19.

The approved migration M has four effects: set reviewedCount to 7, reserve the five identifiers beginning at the old nextRecordId, advance nextRecordId by 5, and set schemaVersion to 2. Reserving identifiers does not create five new records, so recordCount remains 12.

M requires the designated authorised migration role G and schemaVersion 1. Its state updates occur together under the toy’s stated execution rule. A rejected call leaves all four tracked slots unchanged. While V2 is selected but schemaVersion is not 2, its ordinary business operations are blocked by an explicit version check.

The purpose of the five reserved identifiers is a fictional legacy-reconciliation batch. At the supplied starting state, they are 19, 20, 21, 22 and 23. This makes the migration visibly state-changing, so a repeated run or a pointer rollback can be evaluated precisely.

Back to contents · Next section

4. Case A: preserving slots is necessary but not the whole decision

Select V2 while preserving the four stored values. Immediately after that selection, the storage is still 12, 19, 1, 0. V2 reads recordCount 12 and nextRecordId 19 correctly, but reviewedCount has not yet been populated and schemaVersion remains 1.

The intended layout is compatible with the old field positions in this model. That is a useful positive finding. It does not establish that the application is ready for ordinary V2 operations, because the required migration is incomplete.

The explicit version check blocks those operations until M succeeds. Report the state as “V2 selected; migration pending; ordinary V2 operations blocked.” Do not report reviewedCount 0 as a newly discovered fact that none of the twelve records was reviewed. It is merely the unmigrated value of the new field.

This is why an upgrade review needs both layout evidence and a transition plan. A code replacement can be structurally compatible while still depending on state preparation that has not happened.

Back to contents · Next section

5. Case B: swapping interpretations without moving values

The independent bad candidate Vbad reads slot 0 as nextRecordId and slot 1 as recordCount. It does not migrate or move the existing values. When selected against the original state, it therefore reports nextRecordId 12 and recordCount 19.

The stored words have not changed. Their interpretation has. The returned values can look plausible individually, but they contradict the supplied registry meanings: twelve records and next identifier 19.

Reject this candidate under the declared compatibility requirement. Do not claim that changing field names in a user interface repairs the issue. Every read and write using the wrong interpretation must be assessed. We need not speculate that identifier 12 definitely collides with an existing record; the dossier does not enumerate all occupied identifiers.

The example demonstrates a semantic failure before any malicious actor appears. An authorised administrator can select incompatible code by mistake. Authorisation of the upgrade and correctness of its state interpretation are independent review questions.

Back to contents · Next section

6. Case C: constructing an implementation is not initializing P

Suppose a constructor on the separately deployed V2 implementation sets its own reviewedCount to 7. That does not, by itself, set P’s slot 3 to 7. The constructor ran for the implementation’s deployment context, not as the required state transition in P’s storage.

OpenZeppelin’s proxy documentation identifies this constructor distinction. Its upgradeable-contract guidance discusses initialization and layout restrictions. For this lesson, the important consequence is to inspect the initialization evidence for the actual proxy context rather than borrowing a value from the implementation instance.

Under the independent Case C premise, P remains 12, 19, 1, 0 until the authorised M transition occurs there. A screenshot showing 7 when directly inspecting a separate implementation does not establish the state of calls through P.

Initialization also needs an authority and repetition policy. “There is an initialize function” is not evidence that the correct actor called it once with the correct data in the correct context. Our dossier supplies those requirements explicitly for M; it does not claim a function name enforces them.

Back to contents · Next section

7. Case D: run the authorised migration once

Starting from the original state with V2 selected, G invokes M while schemaVersion is 1. The preconditions pass. The reviewed fixture supplies 7. The five reserved identifiers are 19 through 23, and nextRecordId advances to 24. The marker becomes 2.

The resulting four slots are 12, 24, 2, 7. RecordCount is still 12 because reservations are not new records. ReviewedCount 7 does not exceed the total 12, and the next identifier is 24 under the supplied reservation policy.

This result satisfies the stated migration postconditions. It permits ordinary V2 operations under the toy’s version check, assuming their other requirements hold. It is not a proof that every possible V2 function is safe, that the review fixture was independently audited in real life or that production governance is adequate.

An independent negative case uses an unauthorised actor instead of G. M rejects before changing the tracked state, leaving 12, 19, 1, 0. That test distinguishes access control from the successful migration arithmetic.

Back to contents · Next section

8. Case E: reject a repeated migration

After successful M, attempt M again as G. The actor is authorised, but schemaVersion is 2 rather than the required 1. The call rejects and leaves 12, 24, 2, 7 unchanged. A valid role does not override the version precondition.

Now inspect a separate defective variant with no effective repetition guard. If it performs the same “advance nextRecordId by 5” action again, nextRecordId becomes 29. It reserves another five identifiers 24 through 28, although the approved migration called for one batch only. The other supplied assignment reviewedCount=7 need not visibly change for the repeated execution to be wrong.

This is why testing only the new reviewed count would miss the defect. The acceptance record should compare every intended state effect and the version history, including values that are not prominent in the user interface.

The example does not assert that every initialization routine increments a counter or that every repeated call is harmful in the same way. The harm follows from this particular nonrepeatable reservation action and the approved once-only transition.

Back to contents · Next section

9. Case F: changing the pointer back does not undo writes

Begin with the successfully migrated state 12, 24, 2, 7. An authorised pointer-only rollback selects V1 again but performs no storage writes. The four slots remain 12, 24, 2, 7. They do not return to 12, 19, 1, 0.

V1 reads its original recordCount and nextRecordId fields as 12 and 24. The five reserved identifiers have not been unreserved by changing the code pointer. V1 does not gain knowledge of V2’s reviewedCount feature merely because the word 7 remains stored.

This proves a narrow point: implementation rollback is not automatic state rollback. It does not establish that V1 is always unusable against the migrated state. That compatibility would require separate evidence about V1’s behaviour, version handling and the meaning of the already reserved identifiers.

A proposed compensating migration would be a new state-changing action with its own authority, preconditions and consequences. It cannot be assumed safe merely because it subtracts 5. If subsequent activity had consumed identifiers or depended on the migration, blindly restoring numbers could create new inconsistencies. No such compensation is executed or authorised by this lesson.

Back to contents · Next section

10. Know what a standard implementation slot does and does not prove

ERC-1967 standardises locations for proxy-specific information such as implementation, admin and beacon references. Its page identifies it as Final and records creation on 24 April 2019. Such conventions can help tools locate proxy information.

Finding the expected implementation reference still does not prove that V2 interprets every application slot compatibly, that M ran, or that the selected administrator follows an acceptable governance policy. The pointer identifies code selection, not the complete safety argument.

Likewise, appending a field in our simplified linear layout does not guarantee all behavioural invariants. Replacement logic can alter permissions or update rules without moving any old field. Real projects may use namespaced layouts or reserved gaps, each with declared rules and validation requirements. Our four-slot model does not assess those alternatives.

This lesson concerns application logic and state behind a proxy. A network protocol upgrade changes a different layer. Use the protocol compatibility and adoption lesson for that question rather than treating every upgrade as a proxy replacement.

Back to contents · Next section

11. Build an evidence packet that follows the state

The packet should identify P, the old and proposed implementations, the effective authority rule, the old layout, the candidate layout and the actual prestate being assessed. Include compiler and layout metadata where relevant, but do not let a tool’s green result replace the application’s semantic checks.

For M, record the trusted source of reviewedCount 7, the reservation interval, permitted actor, schema precondition, atomic update assumption and exact poststate. Include omitted, unauthorised and repeated-migration cases. A migration plan that documents only the happy path leaves important states unexplained.

For rollback, state whether the proposal changes code selection only or also changes storage. Provide the expected poststate and identify which effects cannot simply be forgotten. Keep current storage, retained history and indexed views distinct; the storage, events and indexes lesson explains that broader evidence separation.

A model decision is: “V2’s declared field positions preserve the old layout in this fixture, but activation of ordinary operations requires successful authorised M. Vbad fails interpretation compatibility. Pointer-only selection of V1 after M does not restore the pre-migration values. The supplied tests support these specific conclusions, not production release.”

Back to contents · Next section

Exercise: compute and classify

Use the original four-slot fixture unless a question explicitly starts after migration.

  1. What does Vbad report for recordCount and nextRecordId? It reports 19 and 12, respectively, because it swaps slot interpretations.
  2. What is P’s state after selecting V2 without M? 12, 19, 1, 0. Ordinary V2 operations remain blocked under the supplied version check.
  3. Does a constructor setting 7 in the implementation establish P’s reviewedCount? No. The storage context is different.
  4. What is the state after one authorised M? 12, 24, 2, 7, with identifiers 19 through 23 reserved.
  5. What happens to a second correctly guarded M? It rejects; the state remains 12, 24, 2, 7.
  6. In the defective unguarded-repeat variant, what nextRecordId follows? 29, demonstrating an extra reservation batch.
  7. Does pointer-only rollback restore nextRecordId 19? No. It remains 24 after the successful migration.

For a final check, suppose someone claims that 7 reviewed records plus five reserved identifiers equals the twelve existing records. Reject the reasoning. Reviewed records and reserved identifiers are different categories; their numbers happen to sum to 12 but do not partition the registry.

Back to contents · Next section

Frequently asked questions

Does an unchanged proxy address mean the application is unchanged?

No. It preserves an access point while the selected logic and its interpretation of stored state may change. Inspect the implementation, layout, migration and authority evidence.

Is a layout-compatible addition enough?

Not by itself. In this dossier it still needs reviewedCount initialization, once-only reservation and version transition. Other applications have their own invariants and permissions.

Can a rollback always fix a bad upgrade?

No such guarantee follows. Selecting earlier code does not automatically undo writes or restore all assumptions. The actual stored state and subsequent effects must be considered.

Is this a deployment guide?

No. It is a fixed-state review exercise without commands, real targets or keys. Production changes require their actual specifications, tooling, governance and independent review.

Back to contents · Next section

Sources and further reading

A sound upgrade explanation follows not only which code is selected, but which stored values survive and what each version believes those values mean.

Back to contents