Owner & Authority

Understanding the distinction between owner and authority is crucial for Solana development. These concepts determine who can modify account data and who controls account functionality.

Owner vs Authority: The Core Distinction

Text
OWNER vs AUTHORITY
══════════════════

OWNER (on-chain, protocol level):
├── Stored in account metadata
├── Always a PROGRAM public key
├── Determines which program can modify data
├── Cannot be an EOA (wallet)
└── Set at account creation, rarely changed

AUTHORITY (application level):
├── Stored in account DATA
├── Typically a USER public key
├── Determined by program logic
├── Can be an EOA or PDA
└── Can be changed via program instruction

Visual Comparison

Text
┌────────────────────────────────────────────────────────────────────┐
│                       TOKEN ACCOUNT EXAMPLE                        │
├────────────────────────────────────────────────────────────────────┤
│                                                                    │
│  Account Metadata (protocol controlled):                           │
│  ┌────────────────────────────────────────────────────────────┐   │
│  │ owner: TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA       │   │
│  │        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^      │   │
│  │        Token Program (PROGRAM, not user)                   │   │
│  └────────────────────────────────────────────────────────────┘   │
│                                                                    │
│  Account Data (program controlled):                                │
│  ┌────────────────────────────────────────────────────────────┐   │
│  │ mint: 7i5KKs...                                            │   │
│  │ authority: 9WzDXw...USER's wallet (can transfer tokens)│   │
│  │            ^^^^^^^^^                                       │   │
│  │            This is what people mean by "owner" colloquially│   │
│  │ amount: 1000000000                                         │   │
│  │ delegate: null                                             │   │
│  │ state: initialized                                         │   │
│  └────────────────────────────────────────────────────────────┘   │
│                                                                    │
└────────────────────────────────────────────────────────────────────┘

The Owner Field

What Owner Means

TypeScript
// Every account has an owner in its metadata
interface AccountInfo {
  lamports: number;
  owner: PublicKey; // ← This is the OWNER
  data: Uint8Array;
  executable: boolean;
  rentEpoch: number;
}

// The owner is ALWAYS a program
// It determines which program can:
// - Modify the account's data field
// - Debit lamports from the account

// Common owners:
const SYSTEM_PROGRAM = new PublicKey("11111111111111111111111111111111");
const TOKEN_PROGRAM = new PublicKey(
  "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
);
const TOKEN_2022 = new PublicKey("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb");

Owner-Based Access Control

Rust
// In a Solana program, the runtime enforces owner checks

// This will FAIL at runtime if account.owner != my_program_id
pub fn modify_data(ctx: Context<ModifyData>) -> Result<()> {
    let account = &mut ctx.accounts.my_account;
    account.data = new_value; // Only works if we're the owner
    Ok(())
}

// Runtime check (conceptual):
fn runtime_check(account: &AccountInfo, program_id: &Pubkey) -> bool {
    // Can this program modify this account?
    if instruction_modifies_data {
        return account.owner == program_id;
    }
    true
}

Changing Owners

TypeScript
// Owner can only be changed by:
// 1. The current owner program
// 2. The System Program (for accounts it owns)

import { SystemProgram, Transaction } from "@solana/web3.js";

// Transfer ownership to a program
// (Only System Program-owned accounts can do this)
const assignIx = SystemProgram.assign({
  accountPubkey: accountToReassign,
  programId: newOwnerProgram,
});

// After this, only newOwnerProgram can modify the account

Authority Patterns

Authority is a logical concept implemented in account data, not protocol-enforced.

Single Authority

Rust
// Anchor pattern for single authority
#[account]
pub struct SingleAuthorityAccount {
    pub authority: Pubkey,  // User who controls this account
    pub data: [u8; 32],
}

// Instruction to modify requires authority signature
#[derive(Accounts)]
pub struct ModifyWithAuthority<'info> {
    #[account(
        mut,
        has_one = authority,  // Enforces authority check
    )]
    pub account: Account<'info, SingleAuthorityAccount>,

    pub authority: Signer<'info>,  // Must sign transaction
}

Multiple Authorities

Rust
// Different authorities for different operations
#[account]
pub struct MultiAuthorityAccount {
    pub admin: Pubkey,       // Can change settings
    pub operator: Pubkey,    // Can perform operations
    pub withdraw: Pubkey,    // Can withdraw funds
    pub data: [u8; 64],
}

#[derive(Accounts)]
pub struct AdminOperation<'info> {
    #[account(mut, has_one = admin)]
    pub account: Account<'info, MultiAuthorityAccount>,
    pub admin: Signer<'info>,
}

#[derive(Accounts)]
pub struct OperatorOperation<'info> {
    #[account(mut, has_one = operator)]
    pub account: Account<'info, MultiAuthorityAccount>,
    pub operator: Signer<'info>,
}

#[derive(Accounts)]
pub struct WithdrawOperation<'info> {
    #[account(mut, has_one = withdraw)]
    pub account: Account<'info, MultiAuthorityAccount>,
    pub withdraw: Signer<'info>,
}

Authority Transfer

Rust
// Transferring authority to a new user
pub fn transfer_authority(
    ctx: Context<TransferAuthority>,
    new_authority: Pubkey,
) -> Result<()> {
    let account = &mut ctx.accounts.account;

    // Old authority must sign (enforced by Accounts struct)
    account.authority = new_authority;

    msg!("Authority transferred to: {}", new_authority);
    Ok(())
}

#[derive(Accounts)]
pub struct TransferAuthority<'info> {
    #[account(mut, has_one = authority)]
    pub account: Account<'info, SingleAuthorityAccount>,

    pub authority: Signer<'info>,  // Current authority signs

    /// CHECK: New authority, no signature needed
    pub new_authority: AccountInfo<'info>,
}

PDA Authorities

PDAs (Program Derived Addresses) can serve as authorities, enabling program-controlled accounts.

Rust
// PDA as authority pattern
#[account]
pub struct VaultAccount {
    pub authority: Pubkey,    // PDA, not a user
    pub admin: Pubkey,        // User who can manage
    pub balance: u64,
}

// The PDA authority can "sign" via CPI
pub fn withdraw_from_vault(
    ctx: Context<WithdrawFromVault>,
    amount: u64,
) -> Result<()> {
    // Transfer tokens where vault PDA is the authority
    let seeds = &[
        b"vault",
        ctx.accounts.vault.admin.as_ref(),
        &[ctx.bumps.vault_authority],
    ];
    let signer = &[&seeds[..]];

    // CPI with PDA signer
    transfer(
        CpiContext::new_with_signer(
            ctx.accounts.token_program.to_account_info(),
            Transfer {
                from: ctx.accounts.vault_token_account.to_account_info(),
                to: ctx.accounts.destination.to_account_info(),
                authority: ctx.accounts.vault_authority.to_account_info(),
            },
            signer,
        ),
        amount,
    )?;

    Ok(())
}

#[derive(Accounts)]
pub struct WithdrawFromVault<'info> {
    #[account(
        seeds = [b"vault", admin.key().as_ref()],
        bump,
    )]
    pub vault_authority: AccountInfo<'info>,  // PDA

    pub admin: Signer<'info>,  // User who controls the vault

    // ... token accounts
}

Common Authority Patterns

1. Upgrade Authority

Programs can have upgrade authority:

TypeScript
import { PublicKey } from "@solana/web3.js";

// Get program's upgrade authority
async function getProgramAuthority(
  connection: Connection,
  programId: PublicKey,
): Promise<PublicKey | null> {
  const programAccount = await connection.getAccountInfo(programId);

  if (!programAccount) return null;

  // Program accounts point to program data account
  // which contains the upgrade authority
  const programDataAddress = new PublicKey(programAccount.data.slice(4, 36));

  const programData = await connection.getAccountInfo(programDataAddress);
  if (!programData) return null;

  // Authority is at offset 13 in program data
  const authorityOption = programData.data[12];
  if (authorityOption === 0) {
    return null; // No authority (immutable)
  }

  return new PublicKey(programData.data.slice(13, 45));
}

2. Mint Authority

Token mints have mint and freeze authorities:

TypeScript
import { getMint } from "@solana/spl-token";

async function getMintAuthorities(
  connection: Connection,
  mintAddress: PublicKey,
) {
  const mint = await getMint(connection, mintAddress);

  return {
    mintAuthority: mint.mintAuthority, // Can mint new tokens
    freezeAuthority: mint.freezeAuthority, // Can freeze accounts
    supply: mint.supply,
    decimals: mint.decimals,
  };
}

// Disable minting forever
import { setAuthority, AuthorityType } from "@solana/spl-token";

async function disableMinting(
  connection: Connection,
  mint: PublicKey,
  currentAuthority: Keypair,
) {
  await setAuthority(
    connection,
    currentAuthority, // Payer
    mint, // Mint account
    currentAuthority, // Current authority
    AuthorityType.MintTokens,
    null, // New authority (null = disable)
  );
}

3. Delegate Authority

Token accounts can delegate spending:

TypeScript
import { approve, revoke, getAccount } from "@solana/spl-token";

// Approve delegate to spend tokens
async function approveDelegate(
  connection: Connection,
  tokenAccount: PublicKey,
  delegate: PublicKey,
  owner: Keypair,
  amount: bigint,
) {
  await approve(
    connection,
    owner, // Payer
    tokenAccount, // Token account
    delegate, // Who can spend
    owner, // Owner
    amount, // Max amount
  );
}

// Check delegation status
async function getDelegation(connection: Connection, tokenAccount: PublicKey) {
  const account = await getAccount(connection, tokenAccount);

  return {
    delegate: account.delegate,
    delegatedAmount: account.delegatedAmount,
    isDelegate: account.delegate !== null,
  };
}

Security Considerations

Common Vulnerabilities

Rust
// VULNERABLE: Missing authority check
pub fn bad_withdraw(ctx: Context<BadWithdraw>, amount: u64) -> Result<()> {
    // Anyone can call this!
    let vault = &mut ctx.accounts.vault;
    vault.balance -= amount;
    // Transfer happens...
    Ok(())
}

// SECURE: Proper authority check
pub fn good_withdraw(ctx: Context<GoodWithdraw>, amount: u64) -> Result<()> {
    // Authority check enforced by Accounts struct
    let vault = &mut ctx.accounts.vault;
    vault.balance -= amount;
    Ok(())
}

#[derive(Accounts)]
pub struct GoodWithdraw<'info> {
    #[account(
        mut,
        has_one = authority,  // ← This is critical
    )]
    pub vault: Account<'info, Vault>,

    pub authority: Signer<'info>,  // ← Must sign
}

Authority Best Practices

Rust
// 1. Always use has_one or custom constraints
#[account(has_one = authority)]
pub account: Account<'info, MyAccount>,

// 2. Make authority a Signer
pub authority: Signer<'info>,  // Not just AccountInfo

// 3. Consider multisig for high-value operations
#[account]
pub struct MultisigAccount {
    pub signers: [Pubkey; 3],
    pub threshold: u8,  // 2 of 3
}

// 4. Implement authority transfer with two-step process
#[account]
pub struct TwoStepAuthority {
    pub authority: Pubkey,
    pub pending_authority: Option<Pubkey>,
}

// Step 1: Propose new authority
// Step 2: New authority accepts
// Prevents accidental transfers to wrong address

Checking Authority Client-Side

TypeScript
// Verify authority before sending transaction
async function verifyAuthority(
  connection: Connection,
  accountAddress: PublicKey,
  expectedAuthority: PublicKey,
): Promise<boolean> {
  const account = await connection.getAccountInfo(accountAddress);

  if (!account) {
    throw new Error("Account not found");
  }

  // Parse authority from account data
  // (Exact offset depends on account structure)
  const authorityOffset = 8; // After discriminator for Anchor
  const authority = new PublicKey(
    account.data.slice(authorityOffset, authorityOffset + 32),
  );

  return authority.equals(expectedAuthority);
}

Owner vs Authority Summary

AspectOwnerAuthority
Stored inAccount metadataAccount data
TypeAlways a programAny pubkey (user/PDA)
ControlsData modification rightsApplication-level access
Set byAccount creationProgram logic
Changed byCurrent owner programProgram instruction
Enforced byRuntimeProgram code

Key Takeaways

  1. Owner is always a program - it's who can modify account data
  2. Authority is a convention - stored in data, enforced by programs
  3. Signer requirement - authorities should always be Signers
  4. PDA authorities enable program-controlled accounts
  5. Security depends on proper checks - always verify authority

Next: Rent & Exemption - Deep dive into rent mechanics.