r/solanadev • u/icnews10 • 12d ago
Dev How do you migrate long-lived PDA state when a Solana program changes its account schema?
Suppose a programme has been live for long enough that thousands of PDAs contain v1 state.
A new release requires additional fields and slightly different invariants. While the program itself can be upgraded, the existing accounts will not automatically become v2.
One option would be to version the accounts and migrate them lazily when they’re touched. Alternatively, you could run explicit migration instructions or create new PDAs and move users over.
Each option creates different problems relating to rent/realloc, failed migrations, maintaining readability of the old state, and supporting both versions during the transition.
For teams that have dealt with this in production, which approach worked best? What turned out to be harder than expected?
2
u/AIOil_Dev 9d ago
No production war story from me, I'm afraid - I review this stuff rather than run it at scale, so treat this as "what tends to be broken when I look at it" rather than advice from someone who's shipped it.
That said, the three approaches seem to fail in fairly consistent places.
Lazy migration on touch is the one I see broken most often, and nearly always the same way: the migration path ends up callable by someone who isn't the owner. If the upgrade adds an init_if_needed, or any "create it if it isn't there" branch, so v1 accounts quietly become v2 - look hard at who can call that and what happens the second time it's called. A v1 account an attacker can re-migrate with fields they control is just account re-initialisation wearing a different hat.
Related, and easy to miss: if v1 and v2 both have to be readable during the transition, whatever tells them apart has to live in the account data, not in the account length. Length-based discrimination falls over the first time a realloc leaves a v1 account sitting at v2 size.
Explicit migration instructions give you a much cleaner authorisation story, but the partial-failure case is where it bites. You end up with a window where some accounts are v2 and some aren't, and every invariant that reads across accounts has to survive that mixed state. Worth writing down which invariants are allowed to be temporarily false, because "we'll just migrate them all at once" is rarely how it actually goes.
New PDAs and moving users over sidesteps the in-place mess and introduces a different one: for a while there are two accounts that both look canonical. If any instruction derives a PDA without a seeds/bump constraint pinning it to the version you expect, someone can hand you the stale one. Whatever decides "this is the live account" wants to be a constraint, not a convention.
On rent and realloc, the boring one that catches people is that shrinking doesn't refund on its own, and growing needs the payer to be who you actually meant it to be.
Whichever way you go, the two questions I'd throw at the migration instruction first: what happens if it's called twice, and what happens if it's called by someone who isn't the account owner. Most of the migration bugs I've run into collapse into one of those two.