Why smart contract migration breaks in production
A contract is immutable: logic can be updated only in two ways — transfer state to a new address or replace implementation behind a wrapper contract. Both options work with storage layout, and here is the first failure boundary.
If order and types of fields in slots diverge, new code reads foreign values as its own: accounted balances stop matching actually held funds, and caller sees zeros where money lies.
The second boundary is the switching moment. While transfer is being prepared, incomplete calls remain in old contract: external transfers, oracle responses, scheduled deferred payouts.
Transferring address without clearing the queue leaves them in limbo: debit passed, credit did not happen. Stuck operations do not roll back themselves — they have to be either manually completed or compensated.
The third scenario is an orphaned address. Owner rights, operator roles, external service bindings often remain on old contract. As long as it is alive, anyone who remembers its address continues to call methods, and accounting diverges between two copies.
Blocked funds here are not a hypothesis: at Parity an error in library initialization left wallets permanently inaccessible while code formally worked.
The common root of all three failures during smart contract migration is one: transfer is tested on empty state, not on live. Therefore we analyze how this is closed — run on copy of production state, "before/after" invariants and phased switching.
What managed migration gives: measurable effects
Reducing downtime window during switching
The migration is assembled so that new environment is raised alongside old: state copied in advance, logic warmed up on copy of mainnet, and switching reduced to changing implementation address in proxy pattern OpenZeppelin. No manual steps remain at switch moment — they are performed during preparation.
Hence time effect: downtime window, which previously took from half an hour to two hours of manual operations, shrinks to 2-4 minutes, and main part is waiting for network confirmations. Switching time becomes predictable, and share of migrations with noticeable downtime falls to zero.
Class of failures that goes away after transfer
Managed migration removes not individual errors but a whole class: desynchronization of balances and allowances between old and new contract, lost storage records, repeated initialization, stuck debits.
State integrity is confirmed by reconciliation — checksums for key structures, invariants on aggregate balances, selective address verification before and after.
Added to this is recovery after failure: checkpoints, migration log and pre-written rollback to previous address. If switching is not confirmed, traffic returns to old contract within minutes, and users do not notice difference.
Approaches to state transfer: comparison of modes
Transferring logic is half the work; harder is deciding what to do with state: balances, allowances, access roles and references to previously created objects. In practice, one of three modes is chosen, and each limits verifiability of changes in its own way.
| Option | When suitable | Limitations |
|---|---|---|
| Upgradeable proxy | Address and storage must remain the same | Slot layout is fixed; an error in it breaks state reads |
| Full state transfer | Data model or architecture changes | Expensive in number of transactions; requires snapshot and re-verification |
| Gradual migration: old contract read-only | Users migrate at their own pace | Two sources of truth live for a long time |
Upgradeable proxy
Logic is moved behind proxy, address and storage remain the same. The key is slot layout: new version is built with same variable order, reserve is left for future fields and types are not changed, otherwise state reads as garbage.
Before deployment, automatic schema divergence check is mandatory: framework is provided by OpenZeppelin, but storage invariants are described by team.
Full state transfer — when there is no other way
Data model or architecture changes — proxy does not save: snapshot is taken, records transferred in batches and old contract made read-only. Each batch is reconciled by tree checksum before and after, share of transferred records and discrepancy report.
While old contract is alive, two sources of truth exist — this risk is closed by a deadline, not a promise.
Work stages: from audit to handover of regulations
Order of work is arranged so that irreversible actions come last: first we fix current state, then reproduce transfer on network copy, and only after results match do we touch production environment. Each stage is closed with an artifact — report, diagram or regulation.
- Code audit: analyze contracts, connections between them, role permissions and accumulated states.
- Transfer scheme: mark what moves as is, what is recalculated, and what remains in previous contract read-only.
- Assembling migration scripts: make calls repeatable — re-running does not create duplicates or corrupt remains.
- Rehearsal on network copy: full cycle on testbed with same volumes of records as in production environment.
- Behavior verification: reconcile amounts, balances and rights before and after, separately go through rollback scenario.
- Switching: change addresses in integrations, switch old contract to read mode.
- Handover of regulations: operating instructions, failure procedure, responsibility matrix.
Inventory
Without it, transfer scheme is built on guesses. We count record volumes, find contracts called from outside, and separately mark keys that must not be touched — they are tied to already issued rights.
Rehearsal on network copy
We raise testbed with copy of state at transfer moment to catch discrepancies on real data, not synthetic. We repeat run twice: transfer and rollback with subsequent repeat — this verifies that system returns to initial state without losses. If numbers match, same script goes to production environment.
What remains with customer: diagrams, policies, regulations
Migration is closed not at moment when contract is deployed at new address, but when your team can perform next update without us. Therefore we hand over not an archive of files, but a working system — from transfer documentation to update regulation.
Documentation composition
- Transfer documentation: map of old and new addresses, list of transferred state, report of reconciliation of balances and rights before and after migration.
- Deployment diagrams: order of contract rollout with connections, dependencies and points where manual confirmation is mandatory.
- Instructions for team: scenarios for updating logic, pausing and scheduled rollback to previous version.
- Update regulation: rollout windows, change review requirements and criteria that trigger rollback.
- Access policies: who holds each role, which operations require multiple signatures, what and to whom is delegated.
- Verification log: which classes of failures tests already catch and at which rollout stage.
Access and key regulation
Keys do not remain with us: administrative roles are transferred to your addresses, critical operations are closed with multiple signatures, and signers are within your team.
For each role we fix owner, replacement procedure when employee leaves, and sequence of actions on key compromise. Separately describe which rights remain with us during support and how they are disabled with one action.
Case: migrating a service to a new scheme without stopping
Balance accounting and payout service worked on old contract scheme: logic was spread across several connected addresses, and part of state was delivered by manual exports.
New requirements for limits and roles did not fit such scheme, so we prepared smart contract migration to Solidity with unified registry of positions — so that users would not notice transition.
Initial situation
Any logic update meant stopping service for several hours: remove load, wait for operations to complete, transfer balances and raise new scheme.
Transfer went by exports, and about a tenth of positions required manual reconciliation — discrepancies surfaced after launch, via user reports. Rollback was a separate operation taking hours and with risk of duplicating records.
Result by metrics
We split balance transfer into batches and added state acceptance mode to new contract: old environment continued serving requests while loading proceeded.
Switching reduced to address change — service downtime decreased from hours to minutes, 100% of positions transferred with byte-by-byte checksum comparison before and after.
Post-switch verification — running all operations on shadow environment — revealed no calculation failures, rollback to old scheme remains possible with one transaction.
What to do if code is already deployed and holds funds
Deployed code cannot be edited in place. If contract is uploaded directly, without intermediate layer and stop function, the only way is to deploy a new one alongside, and leave old readable.
Migration task here is not to "rewrite" but to transfer user funds without losing their rights.
We start with inventory: which assets contract holds, who owns them, which administrative roles and limits are set.
In parallel we take state — balances, allowances, unfulfilled obligations — and fix it immutably so there is something to reconcile against: without snapshot, any post-transfer check turns into dispute about numbers.
We design rollback plan before first transaction. Old contract remains working until new one is confirmed, and switching is made reversible: user route returns back, transfer has limits by amount per wave. Discrepancy in reconciliation is a reason to stop, not to patch live code.
Gradual transfer goes in waves: first own addresses, then limited group, then the rest. After each wave we reconcile invariants — how many assets came, how many remained, how many went to fees.
Frozen assets are described by separate procedure: snapshot at stop date and clear procedure for those who did not make transfer window.
In short: we do not touch live code, fix state, prepare reverse path in advance, transfer in waves and keep procedure for frozen balances. This order we embed into migration scripts.
Technical doubts before work starts
Before transferring logic, same questions almost always come up: what will happen to funds at old addresses, how to prove new version calculates the same, and whether service will have to be stopped. Below are short answers for each, without general words.
Work timelines and contractor responsibility are fixed in writing before start, and result guarantee is tied to specific acceptance criterion, not to wording "should work".
What will happen to user funds?
Funds remain on source contract until it is switched to read-only mode. New contract gets access to balances by pre-fixed list of addresses or through delayed withdrawal from old storage.
Scenario in which access to funds is lost is not considered: with such risk, transfer does not start.
Is old logic compatible with new?
Part of state is transferred as is, part is recalculated. Compatibility is checked on snapshot of main state — we run historical operations on new logic and see where behavior diverges. Most discrepancies come from rounding and order of calculations, so we reconcile them separately.
How to prove behavior is identical?
We assemble set of scenarios from real history: normal calls, boundary values, failures. We run them on old and new version and compare results, including events and error codes. Match across entire set is the acceptance criterion fixed in documentation.
Is it necessary to stop service?
In most cases no. New contract is deployed in parallel, state is synchronized, switching takes minutes. If pause is still inevitable, we agree window in advance, choose minimum load and keep ready rollback plan.
What to do if calculations diverge?
Discrepancy in checks blocks switching. We analyze it to root cause, fix logic or transfer conditions and repeat reconciliation. Until discrepancy is closed, old contract remains working — rollback requires no additional actions and does not depend on reaction speed.
Let's discuss the task: audit, scheme and transfer plan
Contract migration starts not with code, but with inventory.
To discuss task concretely and return with timeline estimate, not with "from and to" range, we need snapshot of current state: where deployed, what is stored, who manages access and what must not be lost during transfer. Below is what should be sent and clarified before start.
- Contract and network addresses, including testbeds where releases were checked.
- Source code and confirmation that it matches bytecode on network: build artifacts, compiler parameters, optimization settings.
- Description of stored state: which structures hold balances, roles, lists and limits, and what of this must be transferred.
- Access scheme: where admin keys are, whether multisig and timelock exist, who signs transactions.
- External dependencies: oracles, pools, third-party contracts that call yours or that you call.
- Transfer requirements: keep addresses or switch to new proxy, whether downtime window is acceptable.
- Constraints: limits on contract size and gas per transaction, storage warm-up order.
- Verification history: audit reports, known defects, past incidents and their postmortems.
Then we return with audit and plan: transfer scheme, order of steps, rollback points and acceptance criteria.
Timeline estimate and scope of work are fixed before project start, and after start we proceed in stages with migration run on copy of mainnet and state reconciliation before and after.







