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: Draft → Voting → Succeeded/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
| Type | Description | Use Case |
|---|---|---|
| Program Governance | Controls upgrades to a program | Protocol upgrades |
| Token Governance | Controls a token mint | Token supply changes |
| Account Governance | Controls any account | Treasury management |
| Native SOL Governance | Controls SOL transfers | Fund 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.