Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/blueshift-gg/quasar/llms.txt

Use this file to discover all available pages before exploring further.

When an upgradeable Solana program needs to change its account data layout — adding fields, removing fields, or changing field types — existing on-chain accounts must be migrated to the new format. Migration<From, To> is Quasar’s type-safe mechanism for performing this transition atomically in a single instruction. During account parsing it validates the account as From; in the handler the migrate() method rewrites it as To in-place.

Why Account Migration Is Needed

Quasar validates accounts using a discriminator (a developer-specified byte prefix) and an exact data length check. When you add a field to an account struct, the discriminator changes and/or the required data length grows. Existing on-chain accounts still have the old discriminator and old length — they would fail the new Account<To> validation. Migration<From, To> bridges this gap:
  1. At parse time, it validates the account as From (old layout is accepted)
  2. In the handler, migrate() reallocates if needed and writes the To layout
  3. The account is now valid as Account<To> and can be used as such

Compile-Time Contracts

Quasar enforces several invariants at compile time when you use Migration<From, To>:
// From and To must have the same owner program
const _OWNER_EQ: () = assert!(
    keys_eq_const(&From::OWNER, &To::OWNER),
    "migration source and target must have the same Owner"
);

// Discriminators must not be prefixes of each other
const _DISC_NEQ: () = { /* prefix overlap check */ };

// Target data must fit in the 4KB sBPF stack frame
const _STACK_BUDGET: () = assert!(
    size_of::<To::Target>() < 3584,
    "migration target type too large for sBPF 4KB stack frame"
);

// To::SPACE must be at least disc_len + size_of::<To::Target>()
const _TARGET_FITS_SPACE: () = assert!(
    To::SPACE >= To::DISCRIMINATOR.len() + size_of::<To::Target>(),
    "migration target Space must cover discriminator plus target data"
);
These assertions are evaluated at compile time via const expressions. If any contract is violated, you get a compile error, not a runtime panic.

How Migration<From, To> Works

Migration<From, To> is #[repr(transparent)] over AccountView:
#[repr(transparent)]
pub struct Migration<From, To> {
    __view: AccountView,
    _marker: PhantomData<(From, To)>,
}

Parse Time (From validation)

AccountLoad::check validates the account as From:
impl<From, To> AccountLoad for Migration<From, To>
where
    From: CheckOwner + AccountLoad,
    To: Space + Discriminator,
{
    fn check(view: &AccountView) -> Result<(), ProgramError> {
        From::check_owner(view)?;  // owner must match From::OWNER
        From::check(view)          // discriminator and length check for From
    }
}

Reading From Data

Migration<From, To> implements Deref<Target = From::Target>, giving read access to the current From account data:
impl<From, To> Deref for Migration<From, To>
where
    From: Deref + Discriminator,
    From::Target: Sized,
{
    type Target = From::Target;

    fn deref(&self) -> &From::Target {
        let disc_len = From::DISCRIMINATOR.len();
        // pointer cast past the From discriminator
        unsafe { &*(self.__view.data_ptr().add(disc_len) as *const From::Target) }
    }
}

The migrate() Method

migrate() performs the actual transition:
pub fn migrate(
    &mut self,
    payer: &impl AsAccountView,
    new_data: To::Target,
) -> Result<&mut Account<To>, ProgramError>
1

Guard checks

Asserts all compile-time migration contracts are satisfied. Verifies the account still has the From discriminator (not already migrated). Returns ProgramError::AccountAlreadyInitialized if the To discriminator is already present.
2

Realloc if needed

Calls realloc_account(&mut self.__view, To::SPACE, payer, None). If To::SPACE > From::SPACE, lamports are transferred from payer to make the account rent-exempt at the new size. If To::SPACE < From::SPACE, excess lamports are returned to payer.
3

Write To layout

Copies To::DISCRIMINATOR bytes to the start of account data, then copies the new_data: To::Target bytes immediately after the discriminator.
4

Validate and return

Runs Account::<To>::check() on the freshly written account data. If validation passes, returns &mut Account<To>. The account is now fully initialized as To and can be used for the rest of the handler.
The SVM enforces a maximum of 10,240 bytes (10 KiB) of total reallocation per transaction (MAX_PERMITTED_DATA_INCREASE). If your migration grows the account by more than 10 KiB in a single transaction (across all accounts in that transaction), the transaction will fail with ProgramError::InvalidRealloc. Split large migrations across multiple transactions or design schemas to minimize space growth.

Declaring Migration in an Accounts Struct

Declare the migration field as a plain owned Migration<From, To> value in your accounts struct. The handler mutably borrows the field through &mut self when calling .migrate():
use quasar_lang::prelude::*;

#[derive(Accounts)]
pub struct MigrateConfig {
    #[account(mut)]
    pub payer: Signer,
    pub system_program: Program<SystemProgram>,

    #[account(constraints(config.authority == *authority.address()))]
    pub config: Migration<ConfigV1, ConfigV2>,

    pub authority: Signer,
}

Complete Migration Example

The following is taken from the Quasar test suite’s test-migrate program:
use quasar_lang::prelude::*;

// Old layout: discriminator = 1
#[account(discriminator = 1)]
pub struct ConfigV1 {
    pub authority: Address,
    pub value: PodU64,
}

// New layout: discriminator = 2, adds an extra field
#[account(discriminator = 2)]
pub struct ConfigV2 {
    pub authority: Address,
    pub value: PodU64,
    pub extra: PodU32,
}

Discriminator Handling

The discriminator transition is explicit and safe:
  • Before migration: account data starts with From::DISCRIMINATOR
  • After migration: account data starts with To::DISCRIMINATOR
Quasar checks that the account is not already migrated (rejects if To discriminator is present) and that it has a valid From discriminator (rejects if neither is present). This makes migration idempotent — calling it twice on the same account fails on the second call rather than corrupting data. The compile-time _DISC_NEQ assertion ensures that From::DISCRIMINATOR and To::DISCRIMINATOR are not prefixes of each other, preventing ambiguous discriminator matching.

Realloc and Rent

migrate() calls realloc_account which handles rent adjustment automatically:
  • Growing (To::SPACE > From::SPACE): transfers rent_exempt(To::SPACE) - current_lamports from payer to the account
  • Shrinking (To::SPACE < From::SPACE): returns current_lamports - rent_exempt(To::SPACE) to payer
  • Same size (To::SPACE == From::SPACE): no lamport change
Rent is always fetched via syscall by migrate(). The migrate() method passes None to realloc_account, which triggers a syscall to read the current rent parameters. There is no API to provide a pre-loaded Sysvar<Rent> to migrate().
Migration<From, To> requires that From and To have the same owner program. This is enforced at compile time. Cross-program account migration (changing the owner) is not supported through this wrapper — use a manual CPI to the system program’s assign instruction instead.

Build docs developers (and LLMs) love