Skip to content

Add Migration<'info, From, To> account type#4060

Merged
jacobcreech merged 17 commits intosolana-foundation:masterfrom
blueshift-gg:master
Jan 7, 2026
Merged

Add Migration<'info, From, To> account type#4060
jacobcreech merged 17 commits intosolana-foundation:masterfrom
blueshift-gg:master

Conversation

@L0STE
Copy link
Copy Markdown
Contributor

@L0STE L0STE commented Nov 17, 2025

What does this do

This PR adds a new account container type that abstracts away the hurdle of migrating Solana accounts from one schema to another.

How it works

The Migration<'info, From, To> type wraps an account that can be in either the old format (From) or new format (To). It automatically detects which format the account is in during deserialization, lets you migrate with a simple .migrate() call, and guarantees the account is migrated before exit.

Example:

#[derive(Accounts)]
pub struct MigrateAccount<'info> {
    #[account(mut)]
    pub payer: Signer<'info>,
    #[account(
        mut,
        realloc = 8 + AccountV2::INIT_SPACE,
        realloc::payer = payer,
        realloc::zero = false
    )]
    pub my_account: Migration<'info, AccountV1, AccountV2>,
    pub system_program: Program<'info, System>,
}

pub fn migrate(ctx: Context<MigrateAccount>) -> Result<()> {
    // Access old data via deref
    let old_data = ctx.accounts.my_account.data;
    
    // Migrate
    ctx.accounts.my_account.migrate(AccountV2 {
        data: old_data,
        new_field: 0,
    })?;
    
    Ok(())
}

Smart Migration

By default, the migration type accepts both From and To accounts. It tries to deserialize as To first (already migrated), then falls back to From (needs migration).

Why? This enables gradual migrations without downtime. Instead of running a costly loop to migrate all existing accounts upfront, you can deploy the migration instruction and let accounts migrate organically as users interact with your protocol.

Some accounts might already be in V2 format (migrated earlier), while others are still V1: the type handles both seamlessly. This is especially useful for live protocols where you can't afford downtime or want to avoid the compute costs and operational complexity of batch migrations.

Calling .migrate() on an already-migrated account is a no-op; it means that if the account has already migrated it won't fail or corrupt state.

The serialization on exit is heavily constrained: if you forget to call .migrate(), the account exit will fail with AccountNotMigrated preventing accidentally leaving accounts in an inconsistent state. You can't forget to migrate because the type system forces you to handle it explicitly. This is critical for data integrity in production.

Strict mode

Add #[account(migrate = "strict")] to reject accounts that are already migrated.

Sometimes you want to ensure an account hasn't been migrated yet. For example, if you want to enforce that migration happens through a specific code path. Strict mode makes this explicit.

#[account(mut, migrate = "strict")]
pub my_account: Migration<'info, AccountV1, AccountV2>,

Realloc

We added the realloc constraint to work with Migration types since account migrations usually involve resizing. Instead of forcing developers to manually realloc in a separate logic you can do it directly in the account struct like this:

#[account(
    mut,
    realloc = 8 + AccountV2::INIT_SPACE,
    realloc::payer = payer,
    realloc::zero = false
)]
pub my_account: Migration<'info, AccountV1, AccountV2>,

What Changed

  • Added lang/src/accounts/migration.rs with the core Migration type
  • Added AccountAlreadyMigrated and AccountNotMigrated error codes
  • Added migrate = "strict" constraint in the parser/codegen
  • Made realloc work with Migration types
  • Added type parsing for Migration<'info, From, To> in the macro

Testing

You can see an example of how this work here: https://github.com/L0STE/anchor-migration-test

And everything compiles successfully with cargo check -p anchor-lang.

@vercel
Copy link
Copy Markdown

vercel bot commented Nov 17, 2025

@L0STE is attempting to deploy a commit to the Solana Foundation Team on Vercel.

A member of the Team first needs to authorize it.

@jamie-osec jamie-osec added enhancement New feature or request lang labels Nov 17, 2025
@L0STE
Copy link
Copy Markdown
Contributor Author

L0STE commented Nov 19, 2025

Based on @jacobcreech and @chen-robert inputs I rewrote the migration feature to only accept the strict version.

The only thing I wasn't super happy was the fact that we could Deref only the From value so I created 2 helpers to migrate and grab the values both statically and mutably (into_inner and into_inner_mut).

@nutafrost nutafrost moved this to Security Review Required in Anchor 1.0 Nov 28, 2025
Copy link
Copy Markdown
Collaborator

@jamie-osec jamie-osec left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall implementation looks good to me, just some notes about the API.
I'd also potentially suggest an API like

impl<'info, From, To> Migration<'info, From, To>
where From: Default {
    pub fn migrate_with<F: FnOnce(From) -> To>(&mut self, func: F) -> Result<()> {
        let MigrationInner::From(f) = &mut self.inner else {
            return Err(ErrorCode::AccountAlreadyMigrated.into());
        };
        let from = std::mem::take(f);
        self.inner = MigrationInner::To(func(from));
        Ok(())
    }
}

To allow constructing the new type directly from an owned instance of the old one.

Finally, please add a test case demonstrating the usage and edge cases of this account.

@L0STE L0STE requested a review from jamie-osec December 9, 2025 07:04
@L0STE
Copy link
Copy Markdown
Contributor Author

L0STE commented Dec 9, 2025

I'd also potentially suggest an API like

The only thing that I'm worried about this is the fact that From needs to implement Default if done this way.

All other comments have been addressed!

@jamie-osec
Copy link
Copy Markdown
Collaborator

The only thing that I'm worried about this is the fact that From needs to implement Default if done this way.

That is true - potentially we could instead use unsafe to leave the account in an invalid state temporarily, though this would need some careful handling about panic safety. Probably better to leave to a followup if this API is desired in practice.

@nutafrost nutafrost removed this from Anchor 1.0 Dec 10, 2025
@L0STE
Copy link
Copy Markdown
Contributor Author

L0STE commented Dec 10, 2025

Probably better to leave to a followup if this API is desired in practice.

Agreed. Anything else that I need to fix to get this into the next version?

@jamie-osec
Copy link
Copy Markdown
Collaborator

I think we can land this in v1/1.1 with the following:

@jamie-osec
Copy link
Copy Markdown
Collaborator

Hey, changes look good but we're still hitting formatting issues. Can you please fix that?

@L0STE
Copy link
Copy Markdown
Contributor Author

L0STE commented Jan 5, 2026

I thought I fixed it but there was a trailing whitespace that I must have added by accident while committing. I think now it should be good

@vercel
Copy link
Copy Markdown

vercel bot commented Jan 5, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
anchor-docs Error Error Jan 7, 2026 6:37am

@jacobcreech jacobcreech merged commit d7ace7a into solana-foundation:master Jan 7, 2026
58 of 59 checks passed
@kostyamospan
Copy link
Copy Markdown

@L0STE @jamie-osec i believe you should add a note somewhere in the docs that its not part of a latest stable release. Was a bit confused when found it in the docs but didnt see it in 0.32.1

Otter-0x4ka5h pushed a commit to Otter-0x4ka5h/anchor that referenced this pull request Mar 25, 2026
…n#4060)

* Added a migration type

* Fixed my midcurved implementation

* Fixed some errors + added realloc for migration type

* Last nit then up only

* Better naming

* Updated to always be stricted

* Simplified and improved

* Last nit then really up only

* Additions and Fixes based on comments

* clippy

* last addition

* last nits

* Update error.rs

* Update migration.rs

* fix: fmt error

---------

Co-authored-by: Jacob Creech <82475023+jacobcreech@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request lang Merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants