Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/magicblock-labs/magicblock-engine-examples/llms.txt

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

Account delegation is the process of temporarily transferring ownership of an account to the MagicBlock delegation program. Once delegated, the account can be used in an Ephemeral Rollup where transactions execute with minimal latency.

Overview

Delegation enables accounts to be processed by Ephemeral Rollup validators while maintaining state consistency with the Solana base layer. During delegation:
  • The account’s owner is temporarily changed to the delegation program
  • The account can be modified through the ER with low latency
  • State automatically commits back to Solana at configured intervals
  • The account can be undelegated to return full control to the original owner

Delegation with Anchor

The Anchor framework provides the simplest way to add delegation support to your programs.

Setup

1
Add the SDK dependency
2
Add the ephemeral-rollups-sdk to your Cargo.toml:
3
cargo add ephemeral-rollups-sdk
4
Import required modules
5
Import the delegation macros and functions:
6
use ephemeral_rollups_sdk::anchor::{delegate, ephemeral};
use ephemeral_rollups_sdk::cpi::DelegateConfig;
7
Mark your program
8
Add the #[ephemeral] attribute to your program module:
9
#[ephemeral]
#[program]
pub mod anchor_counter {
    // Your program instructions
}
10
Create a delegate instruction
11
Add a delegate instruction with the #[delegate] attribute:
12
pub fn delegate(ctx: Context<DelegateInput>) -> Result<()> {
    ctx.accounts.delegate_pda(
        &ctx.accounts.payer,
        &[COUNTER_SEED],
        DelegateConfig {
            validator: ctx.remaining_accounts.first().map(|acc| acc.key()),
            ..Default::default()
        },
    )?;
    Ok(())
}

#[delegate]
#[derive(Accounts)]
pub struct DelegateInput<'info> {
    pub payer: Signer<'info>,
    /// The PDA to delegate
    #[account(mut, del)]
    pub pda: AccountInfo<'info>,
}
The #[delegate] macro automatically adds the required accounts for delegation (buffer, record, metadata, delegation program, etc.).

Complete Anchor example

From the anchor-counter example:
/home/daytona/workspace/source/anchor-counter/programs/anchor-counter/src/lib.rs
use anchor_lang::prelude::*;
use ephemeral_rollups_sdk::anchor::{delegate, ephemeral};
use ephemeral_rollups_sdk::cpi::DelegateConfig;

pub const COUNTER_SEED: &[u8] = b"counter";

#[ephemeral]
#[program]
pub mod anchor_counter {
    use super::*;

    pub fn delegate(ctx: Context<DelegateInput>) -> Result<()> {
        ctx.accounts.delegate_pda(
            &ctx.accounts.payer,
            &[COUNTER_SEED],
            DelegateConfig {
                // Optionally set a specific validator
                validator: ctx.remaining_accounts.first().map(|acc| acc.key()),
                ..Default::default()
            },
        )?;
        Ok(())
    }
}

#[delegate]
#[derive(Accounts)]
pub struct DelegateInput<'info> {
    pub payer: Signer<'info>,
    #[account(mut, del)]
    pub pda: AccountInfo<'info>,
}

Delegation with native Rust

For programs that don’t use Anchor, you can delegate accounts using the native Rust SDK.

Manual delegation

From the rust-counter example:
/home/daytona/workspace/source/rust-counter/src/processor.rs
use ephemeral_rollups_sdk::cpi::{delegate_account, DelegateAccounts, DelegateConfig};

pub fn process_delegate(_program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
    // Get accounts
    let account_info_iter = &mut accounts.iter();
    let initializer = next_account_info(account_info_iter)?;
    let system_program = next_account_info(account_info_iter)?;
    let pda_to_delegate = next_account_info(account_info_iter)?;
    let owner_program = next_account_info(account_info_iter)?;
    let delegation_buffer = next_account_info(account_info_iter)?;
    let delegation_record = next_account_info(account_info_iter)?;
    let delegation_metadata = next_account_info(account_info_iter)?;
    let delegation_program = next_account_info(account_info_iter)?;
    let validator_account = account_info_iter.next();

    // Optional: client-provided validator or default validator
    let validator_pubkey: Option<Pubkey> = 
        validator_account.map(|acc_info| acc_info.key.clone());

    // Prepare counter PDA seeds
    let seed_1 = b"counter";
    let seed_2 = initializer.key.as_ref();
    let pda_seeds: &[&[u8]] = &[seed_1, seed_2];

    let delegate_accounts = DelegateAccounts {
        payer: initializer,
        pda: pda_to_delegate,
        owner_program,
        buffer: delegation_buffer,
        delegation_record,
        delegation_metadata,
        delegation_program,
        system_program,
    };

    let delegate_config = DelegateConfig {
        validator: validator_pubkey,
        ..Default::default()
    };

    delegate_account(delegate_accounts, pda_seeds, delegate_config)?;

    Ok(())
}

Delegation with Bolt

The Bolt framework provides ECS-style delegation using component attributes.

Component delegation

From the bolt-counter example:
/home/daytona/workspace/source/bolt-counter/programs-ecs/components/counter/src/lib.rs
use bolt_lang::*;

#[component(delegate)]
#[derive(Default)]
pub struct Counter {
    pub count: u64,
}
The #[component(delegate)] attribute automatically makes the component delegatable.

Client-side delegation

import { createDelegateInstruction, FindComponentPda } from "@magicblock-labs/bolt-sdk";

const counterPda = FindComponentPda({
  componentId: counterComponent.programId,
  entity: entityPda,
});

const delegateIx = createDelegateInstruction({
  entity: entityPda,
  account: counterPda,
  ownerProgram: counterComponent.programId,
  payer: provider.wallet.publicKey,
});

const tx = new anchor.web3.Transaction().add(delegateIx);
const txSign = await provider.sendAndConfirm(tx);

Delegation configuration

The DelegateConfig struct allows you to customize delegation behavior:
pub struct DelegateConfig {
    /// How often to commit state to base layer (in milliseconds)
    pub commit_frequency_ms: u32,
    /// Optional specific ER validator to delegate to
    pub validator: Option<Pubkey>,
}

Commit frequency

The commit_frequency_ms parameter controls how often the ER automatically commits state back to Solana:
let delegate_config = DelegateConfig {
    commit_frequency_ms: 30_000, // Commit every 30 seconds
    validator: None,
};
Setting a very low commit frequency increases base layer transaction costs. Balance latency needs with cost considerations.

Validator selection

You can optionally specify which ER validator should handle the delegation:
// Anchor example
pub fn delegate(ctx: Context<DelegateInput>) -> Result<()> {
    ctx.accounts.delegate_pda(
        &ctx.accounts.payer,
        &[COUNTER_SEED],
        DelegateConfig {
            // Use validator from remaining accounts if provided
            validator: ctx.remaining_accounts.first().map(|acc| acc.key()),
            ..Default::default()
        },
    )?;
    Ok(())
}
For local development, you typically need to specify the local validator identity:
const remainingAccounts = connectionER.rpcEndpoint.includes("localhost")
  ? [
      {
        pubkey: new PublicKey("mAGicPQYBMvcYveUZA5F5UNNwyHvfYh5xkLS2Fr1mev"),
        isSigner: false,
        isWritable: false,
      },
    ]
  : [];

let tx = await program.methods
  .delegate()
  .accounts({ payer: wallet.publicKey, pda: counterPDA })
  .remainingAccounts(remainingAccounts)
  .transaction();

PDA delegation

Delegation works seamlessly with Program Derived Addresses (PDAs). You must provide the seeds used to derive the PDA:
// Single seed
const COUNTER_SEED: &[u8] = b"counter";
ctx.accounts.delegate_pda(&ctx.accounts.payer, &[COUNTER_SEED], config)?;

// Multiple seeds
let seed_1 = b"counter";
let seed_2 = initializer.key.as_ref();
let pda_seeds: &[&[u8]] = &[seed_1, seed_2];
delegate_account(accounts, pda_seeds, config)?;
The SDK uses these seeds to verify PDA ownership and recreate the account after undelegation.

Undelegating accounts

To return an account to the base layer and restore original ownership:
import { createUndelegateInstruction } from "@magicblock-labs/bolt-sdk";

const undelegateIx = createUndelegateInstruction({
  payer: provider.wallet.publicKey,
  delegatedAccount: pda,
  ownerProgram: program.programId,
  reimbursement: provider.wallet.publicKey,
});

let tx = new anchor.web3.Transaction().add(undelegateIx);
await provider.sendAndConfirm(tx);
Undelegation automatically commits the latest state to the base layer before returning ownership.

Examples using delegation

Next steps

Transaction execution

Learn how to execute transactions in Ephemeral Rollups

Ephemeral Rollups

Understand the full ER architecture

Build docs developers (and LLMs) love