Governance Overview

Solana programs can be upgraded, and governance systems enable decentralized decision-making for these upgrades and other protocol changes.

Program Upgradability

Text
┌─────────────────────────────────────────────────────────────────┐
│                  Program Upgrade Architecture                   │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Program Account                    Program Data Account        │
│  ┌───────────────────┐             ┌──────────────────────┐    │
│  │ Address: Prog123  │             │ Address: Data456     │    │
│  │ Owner: BPFLoader  │────────────▶│ Owner: Prog123       │    │
│  │ Executable: true  │             │ Executable: false    │    │
│  │                   │             │                      │    │
│  │ Data:             │             │ Data:                │    │
│  │  └─ Pointer to ───┼─────────────┤  └─ Actual bytecode │    │
│  │     data account  │             │     (can be updated) │    │
│  └───────────────────┘             └──────────────────────┘    │
│                                                                 │
│  Upgrade Authority Account                                      │
│  ┌───────────────────────────────────────────────────────────┐ │
│  │ Controls: Who can upgrade the program                      │ │
│  │                                                            │ │
│  │ Options:                                                   │ │
│  │  • Single keypair (centralized)                           │ │
│  │  • Multisig (semi-decentralized)                          │ │
│  │  • Governance PDA (fully decentralized)                   │ │
│  │  • None (immutable - renounced)                           │ │
│  └───────────────────────────────────────────────────────────┘ │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Program Upgrade Process

TypeScript
import {
  Connection,
  Keypair,
  PublicKey,
  Transaction,
  SystemProgram,
  BpfLoader,
  LAMPORTS_PER_SOL,
} from "@solana/web3.js";
import * as fs from "fs";

const BPF_LOADER_UPGRADEABLE = new PublicKey(
  "BPFLoaderUpgradeab1e11111111111111111111111",
);

// Deploy upgradeable program
export async function deployUpgradeableProgram(
  connection: Connection,
  payer: Keypair,
  programPath: string,
  programKeypair: Keypair,
): Promise<{
  programId: PublicKey;
  programDataAddress: PublicKey;
}> {
  const programData = fs.readFileSync(programPath);

  // Calculate program data account address
  const [programDataAddress] = PublicKey.findProgramAddressSync(
    [programKeypair.publicKey.toBuffer()],
    BPF_LOADER_UPGRADEABLE,
  );

  // Calculate required space and rent
  const programDataLength = programData.length;
  const rent = await connection.getMinimumBalanceForRentExemption(
    programDataLength + 45, // Header size
  );

  // Create buffer account to hold program data temporarily
  const bufferKeypair = Keypair.generate();
  const bufferSize = programDataLength + 37; // Buffer header
  const bufferRent =
    await connection.getMinimumBalanceForRentExemption(bufferSize);

  // Write to buffer
  const createBufferIx = SystemProgram.createAccount({
    fromPubkey: payer.publicKey,
    newAccountPubkey: bufferKeypair.publicKey,
    lamports: bufferRent,
    space: bufferSize,
    programId: BPF_LOADER_UPGRADEABLE,
  });

  // Initialize buffer
  const initBufferIx = {
    programId: BPF_LOADER_UPGRADEABLE,
    keys: [
      { pubkey: bufferKeypair.publicKey, isSigner: false, isWritable: true },
      { pubkey: payer.publicKey, isSigner: true, isWritable: false },
    ],
    data: Buffer.from([0]), // InitializeBuffer instruction
  };

  // Write program data to buffer in chunks
  const chunkSize = 1000;
  const writeInstructions = [];

  for (let offset = 0; offset < programData.length; offset += chunkSize) {
    const chunk = programData.slice(offset, offset + chunkSize);
    writeInstructions.push({
      programId: BPF_LOADER_UPGRADEABLE,
      keys: [
        { pubkey: bufferKeypair.publicKey, isSigner: false, isWritable: true },
        { pubkey: payer.publicKey, isSigner: true, isWritable: false },
      ],
      data: Buffer.concat([
        Buffer.from([1]), // Write instruction
        Buffer.from(new Uint32Array([offset]).buffer), // Offset
        chunk,
      ]),
    });
  }

  // Deploy from buffer
  const deployIx = {
    programId: BPF_LOADER_UPGRADEABLE,
    keys: [
      { pubkey: payer.publicKey, isSigner: true, isWritable: true },
      { pubkey: programDataAddress, isSigner: false, isWritable: true },
      { pubkey: programKeypair.publicKey, isSigner: true, isWritable: true },
      { pubkey: bufferKeypair.publicKey, isSigner: false, isWritable: true },
      { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
      { pubkey: payer.publicKey, isSigner: true, isWritable: false }, // Upgrade authority
    ],
    data: Buffer.concat([
      Buffer.from([2]), // DeployWithMaxDataLen instruction
      Buffer.from(new BigUint64Array([BigInt(programData.length * 2)]).buffer),
    ]),
  };

  // Execute deployment (simplified - actual implementation needs chunking)
  console.log("Deploying program...");

  return {
    programId: programKeypair.publicKey,
    programDataAddress,
  };
}

// Upgrade existing program
export async function upgradeProgram(
  connection: Connection,
  payer: Keypair,
  programId: PublicKey,
  newProgramPath: string,
  upgradeAuthority: Keypair,
): Promise<string> {
  const newProgramData = fs.readFileSync(newProgramPath);

  // Get program data address
  const [programDataAddress] = PublicKey.findProgramAddressSync(
    [programId.toBuffer()],
    BPF_LOADER_UPGRADEABLE,
  );

  // Create and write to buffer (same as deploy)
  const bufferKeypair = Keypair.generate();
  // ... buffer creation and writing ...

  // Upgrade instruction
  const upgradeIx = {
    programId: BPF_LOADER_UPGRADEABLE,
    keys: [
      { pubkey: programDataAddress, isSigner: false, isWritable: true },
      { pubkey: programId, isSigner: false, isWritable: true },
      { pubkey: bufferKeypair.publicKey, isSigner: false, isWritable: true },
      { pubkey: payer.publicKey, isSigner: false, isWritable: true }, // Spill account
      { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
      { pubkey: upgradeAuthority.publicKey, isSigner: true, isWritable: false },
    ],
    data: Buffer.from([3]), // Upgrade instruction
  };

  const tx = new Transaction().add(upgradeIx);
  const signature = await connection.sendTransaction(tx, [
    payer,
    upgradeAuthority,
  ]);

  return signature;
}

// Set new upgrade authority
export async function setUpgradeAuthority(
  connection: Connection,
  programId: PublicKey,
  currentAuthority: Keypair,
  newAuthority: PublicKey | null, // null to make immutable
): Promise<string> {
  const [programDataAddress] = PublicKey.findProgramAddressSync(
    [programId.toBuffer()],
    BPF_LOADER_UPGRADEABLE,
  );

  const setAuthorityIx = {
    programId: BPF_LOADER_UPGRADEABLE,
    keys: [
      { pubkey: programDataAddress, isSigner: false, isWritable: true },
      { pubkey: currentAuthority.publicKey, isSigner: true, isWritable: false },
      ...(newAuthority
        ? [{ pubkey: newAuthority, isSigner: false, isWritable: false }]
        : []),
    ],
    data: Buffer.from([4, newAuthority ? 1 : 0]), // SetAuthority instruction
  };

  const tx = new Transaction().add(setAuthorityIx);
  const signature = await connection.sendTransaction(tx, [currentAuthority]);

  return signature;
}

SPL Governance

Text
┌─────────────────────────────────────────────────────────────────┐
│                   SPL Governance Flow                           │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  1. Create Realm                                                │
│     ┌─────────────────────────────────────────────────────┐    │
│     │ Realm = Governance organization                      │    │
│     │ • Community token (voting power)                     │    │
│     │ • Council token (optional, for admin actions)        │    │
│     │ • Configuration (thresholds, voting period)          │    │
│     └─────────────────────────────────────────────────────┘    │
│                           │                                     │
│                           ▼                                     │
│  2. Deposit Tokens                                              │
│     ┌─────────────────────────────────────────────────────┐    │
│     │ Token Owner Record = Voting power                    │    │
│     │ • Deposited tokens locked                            │    │
│     │ • Can delegate to another address                    │    │
│     └─────────────────────────────────────────────────────┘    │
│                           │                                     │
│                           ▼                                     │
│  3. Create Proposal                                             │
│     ┌─────────────────────────────────────────────────────┐    │
│     │ Proposal = Suggested action                          │    │
│     │ • Title, description                                 │    │
│     │ • One or more instructions to execute                │    │
│     │ • State: DraftVotingSucceeded/Defeated         │    │
│     └─────────────────────────────────────────────────────┘    │
│                           │                                     │
│                           ▼                                     │
│  4. Vote                                                        │
│     ┌─────────────────────────────────────────────────────┐    │
│     │ Vote Record = Individual vote                        │    │
│     │ • Yes/No/Abstain                                     │    │
│     │ • Weight = deposited tokens                          │    │
│     └─────────────────────────────────────────────────────┘    │
│                           │                                     │
│                           ▼                                     │
│  5. Execute (if passed)                                         │
│     ┌─────────────────────────────────────────────────────┐    │
│     │ Execute proposal instructions via governance PDA     │    │
│     │ • Upgrade programs                                   │    │
│     │ • Transfer tokens                                    │    │
│     │ • Update parameters                                  │    │
│     └─────────────────────────────────────────────────────┘    │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Creating a Governance Realm

TypeScript
import {
  Connection,
  Keypair,
  PublicKey,
  Transaction,
  TransactionInstruction,
} from "@solana/web3.js";
import {
  getGovernanceProgramId,
  MintMaxVoteWeightSource,
  VoteThreshold,
  VoteThresholdType,
  VoteTipping,
  withCreateRealm,
  withCreateGovernance,
  withCreateTokenOwnerRecord,
  withDepositGoverningTokens,
  withCreateProposal,
  withAddSignatory,
  withSignOffProposal,
  withCastVote,
  withFinalizeVote,
  withExecuteTransaction,
  Vote,
  YesNoVote,
} from "@solana/spl-governance";
import { getMint, getAssociatedTokenAddress } from "@solana/spl-token";

const GOVERNANCE_PROGRAM_ID = new PublicKey(
  "GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw",
);

interface RealmConfig {
  name: string;
  communityMint: PublicKey;
  councilMint?: PublicKey;
  minCommunityTokensToCreateGovernance: number;
  communityVotingPeriod: number; // seconds
}

export async function createGovernanceRealm(
  connection: Connection,
  payer: Keypair,
  config: RealmConfig,
): Promise<{
  realmAddress: PublicKey;
  communityTokenHolding: PublicKey;
}> {
  const instructions: TransactionInstruction[] = [];

  // Get mint info for decimals
  const communityMintInfo = await getMint(connection, config.communityMint);

  // Calculate realm address
  const [realmAddress] = PublicKey.findProgramAddressSync(
    [Buffer.from("governance"), Buffer.from(config.name)],
    GOVERNANCE_PROGRAM_ID,
  );

  // Community token holding account
  const [communityTokenHolding] = PublicKey.findProgramAddressSync(
    [
      Buffer.from("governance"),
      realmAddress.toBuffer(),
      config.communityMint.toBuffer(),
    ],
    GOVERNANCE_PROGRAM_ID,
  );

  // Create realm
  await withCreateRealm(
    instructions,
    GOVERNANCE_PROGRAM_ID,
    3, // Program version
    config.name,
    payer.publicKey, // Realm authority
    config.communityMint,
    payer.publicKey,
    config.councilMint,
    MintMaxVoteWeightSource.FULL_SUPPLY_FRACTION,
    BigInt(
      config.minCommunityTokensToCreateGovernance *
        Math.pow(10, communityMintInfo.decimals),
    ),
    undefined, // Community token config
  );

  const tx = new Transaction().add(...instructions);
  await connection.sendTransaction(tx, [payer]);

  return {
    realmAddress,
    communityTokenHolding,
  };
}

// Deposit tokens to gain voting power
export async function depositGovernanceTokens(
  connection: Connection,
  payer: Keypair,
  realmAddress: PublicKey,
  communityMint: PublicKey,
  amount: bigint,
): Promise<PublicKey> {
  const instructions: TransactionInstruction[] = [];

  // Get user's token account
  const userTokenAccount = await getAssociatedTokenAddress(
    communityMint,
    payer.publicKey,
  );

  // Token owner record tracks voting power
  const [tokenOwnerRecord] = PublicKey.findProgramAddressSync(
    [
      Buffer.from("governance"),
      realmAddress.toBuffer(),
      communityMint.toBuffer(),
      payer.publicKey.toBuffer(),
    ],
    GOVERNANCE_PROGRAM_ID,
  );

  // Create token owner record if it doesn't exist
  await withCreateTokenOwnerRecord(
    instructions,
    GOVERNANCE_PROGRAM_ID,
    3,
    realmAddress,
    payer.publicKey,
    communityMint,
    payer.publicKey,
  );

  // Deposit tokens
  await withDepositGoverningTokens(
    instructions,
    GOVERNANCE_PROGRAM_ID,
    3,
    realmAddress,
    userTokenAccount,
    communityMint,
    payer.publicKey,
    payer.publicKey,
    payer.publicKey,
    amount,
  );

  const tx = new Transaction().add(...instructions);
  await connection.sendTransaction(tx, [payer]);

  return tokenOwnerRecord;
}

Governance Types

TypeDescriptionUse Case
Program GovernanceControls upgrades to a programProtocol upgrades
Token GovernanceControls a token mintToken supply changes
Account GovernanceControls any accountTreasury management
Native SOL GovernanceControls SOL transfersFund distribution

Security Considerations

Text
┌─────────────────────────────────────────────────────────────────┐
│                  Governance Security Model                      │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Voting Thresholds                                              │
│  ├── Quorum: Minimum participation required                     │
│  │   └── e.g., 10% of total supply must vote                   │
│  │                                                              │
│  ├── Approval: Votes needed to pass                             │
│  │   └── e.g., 60% of votes must be Yes                        │
│  │                                                              │
│  └── Veto: Council can block malicious proposals               │
│      └── Time-locked execution allows review                   │
│                                                                 │
│  Time Locks                                                     │
│  ├── Voting Period: Time to vote (e.g., 3 days)                │
│  ├── Hold-up Period: Delay before execution (e.g., 1 day)      │
│  └── Cool-down Period: Delay to withdraw tokens                │
│                                                                 │
│  Attack Vectors & Mitigations                                   │
│  ├── Flash Loan Attack                                          │
│  │   └── Mitigation: Require tokens locked before vote         │
│  │                                                              │
│  ├── Proposal Spam                                              │
│  │   └── Mitigation: Minimum tokens to create proposal         │
│  │                                                              │
│  └── Governance Capture                                         │
│      └── Mitigation: Multisig council veto power               │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Common Governance Patterns

TypeScript
// Pattern 1: Timelock for sensitive operations
interface TimelockConfig {
  minDelay: number; // Minimum delay in seconds
  maxDelay: number; // Maximum delay
  gracePeriod: number; // Time window to execute after delay
}

// Pattern 2: Multi-tier governance
interface GovernanceTier {
  name: string;
  votingPeriod: number;
  quorum: number;
  approvalThreshold: number;
  allowedActions: string[];
}

const governanceTiers: GovernanceTier[] = [
  {
    name: "Emergency",
    votingPeriod: 3600, // 1 hour
    quorum: 0.33, // 33%
    approvalThreshold: 0.66, // 66%
    allowedActions: ["pause", "unpause"],
  },
  {
    name: "Standard",
    votingPeriod: 259200, // 3 days
    quorum: 0.1, // 10%
    approvalThreshold: 0.5, // 50%
    allowedActions: ["parameter_change", "treasury_spend"],
  },
  {
    name: "Critical",
    votingPeriod: 604800, // 7 days
    quorum: 0.2, // 20%
    approvalThreshold: 0.66, // 66%
    allowedActions: ["upgrade", "authority_change"],
  },
];

// Pattern 3: Optimistic governance
// Actions are queued and execute after delay unless vetoed
interface OptimisticGovernance {
  proposer: PublicKey; // Trusted proposer
  vetoCouncil: PublicKey; // Can veto within window
  delay: number; // Execution delay
}

Next: Program Upgrades - Safe program upgrade patterns and migration strategies.