Solana Upgrade Authority Security: The $40M Lesson Most Protocols Haven't Learned
Source: Dev.to
The Problem
Every Solana program deployed with solana program deploy is upgradeable by default.
The upgrade authority – a single keypair – has god‑mode access to your protocol: it can replace the entire program binary. No timelock. No multisig. No warning.
The January 2026 Step Finance collapse ($40 M drained) wasn’t a smart‑contract bug.
It was a compromised executive device that held the upgrade‑authority keys. The attacker didn’t need a code vulnerability – they only needed the key to replace the program.
If your Solana program’s upgrade authority is a single hot wallet, you’re one phishing email away from the same fate.
How Upgrade Authority Works
When you deploy a Solana program, the BPF Loader creates:
- A program account that points to a data account containing your executable bytes.
- An upgrade‑authority pubkey stored in the program account’s metadata.
Anyone who controls that key can call BPFLoaderUpgradeable::Upgrade and swap the entire program binary.
Example: Checking Who Controls Your Program
solana program show Output
Program Id: YourProgram111111111111111111111111
Owner: BPFLoaderUpgradeab1e11111111111111111111111
ProgramData Address: ProgramData111111111111111111111
Authority: HotWa11et1111111111111111111111111 ← single point of failure
Last Deployed In: Slot 250000000
Data Length: 832456 bytesThe Authority field is the attack target. If an attacker obtains this key they can:
- Deploy a malicious binary that drains all PDAs.
- Replace program logic to bypass access controls.
- Insert a backdoor that siphons funds over time.
- Brick the program entirely.
Default authority = developer’s local keypair (
~/.config/solana/id.json).
This is how 60 %+ of Solana programs ship to mainnet. Your program’s security equals your laptop’s security.
Risks: malware, phishing, device theft, insider threat.
Securing the Upgrade Authority
1. Transfer Authority to a Ledger (Hardware Wallet)
solana program set-upgrade-authority \
--new-upgrade-authority \
--keypair usb://ledgerPros: the key never touches a hot machine.
Cons: still a single key held by a single person → bus factor = 1.
Risks: physical theft, coercion, single‑person compromise.
2. Use a Multisig (Squads Protocol – the standard Solana multisig)
import { Squads } from "@sqds/sdk";
// Create a multisig vault for upgrade authority
const multisigAccount = await squads.createMultisig(
2, // threshold: 2‑of‑3 required
createKey,
[
member1.publicKey, // CTO – hardware wallet
member2.publicKey, // Lead Dev – hardware wallet
member3.publicKey, // Security Lead – hardware wallet
]
);
// Transfer program upgrade authority to multisig
await squads.createProposalForProgramUpgrade(
multisigAccount,
programId,
bufferAddress,
spillAddress,
"v2.1.0 – Fix oracle validation"
);Result: 2‑of‑3 keyholders must approve any upgrade. An attacker must compromise multiple parties across different security boundaries.
Risks: social engineering of multiple members, governance attacks, emergency‑response latency.
3. Add a Mandatory Delay (Timelock) Between Proposal and Execution
Example: Custom Upgrade Program with a 48‑hour Timelock (Anchor)
// ── Propose Upgrade ───────────────────────────────────────────────────────
pub fn propose_upgrade(ctx: Context, buffer: Pubkey) -> Result {
let proposal = &mut ctx.accounts.proposal;
proposal.buffer = buffer;
proposal.proposed_at = Clock::get()?.unix_timestamp;
proposal.execution_time = proposal.proposed_at + TIMELOCK_DURATION; // 48 h
proposal.approvals = 0;
proposal.executed = false;
emit!(UpgradeProposed {
program: ctx.accounts.program.key(),
buffer,
execution_time: proposal.execution_time,
});
Ok(())
}
// ── Execute Upgrade ───────────────────────────────────────────────────────
pub fn execute_upgrade(ctx: Context) -> Result {
let proposal = &ctx.accounts.proposal;
let now = Clock::get()?.unix_timestamp;
require!(proposal.approvals >= THRESHOLD, ErrorCode::InsufficientApprovals);
require!(now >= proposal.execution_time, ErrorCode::TimelockNotExpired);
require!(!proposal.executed, ErrorCode::AlreadyExecuted);
// Execute the BPFLoaderUpgradeable::Upgrade instruction via CPI
// ...
Ok(())
}
// ── Cancel Upgrade ───────────────────────────────────────────────────────
pub fn cancel_upgrade(ctx: Context) -> Result {
// Any single guardian can cancel during the timelock window
let proposal = &mut ctx.accounts.proposal;
require!(!proposal.executed, ErrorCode::AlreadyExecuted);
proposal.cancelled = true;
emit!(UpgradeCancelled {
program: ctx.accounts.program.key(),
cancelled_by: ctx.accounts.guardian.key(),
});
Ok(())
}Effect: Even if attackers compromise the multisig, the community has 48 h to:
- Detect an unauthorized upgrade proposal.
- Cancel it via any single guardian.
- Alert users to withdraw funds.
Risk: Emergency bugs take longer to patch (mitigated by an emergency fast‑path with a higher threshold).
4. Nuclear Option: Revoke Upgrade Authority Permanently
solana program set-upgrade-authority --finalThe program can never be changed again. This is the gold standard for security but means:
- No bug fixes possible.
- No feature additions.
- Must be absolutely certain the code is correct.
Best for: core AMM logic, token contracts, bridges (after extensive auditing).
Putting It All Together – A Maturity Pattern
| Path | Governance | Timelock | Execution Speed |
|---|---|---|---|
| Normal Upgrade | 2‑of‑3 multisig | 48 h | Standard |
| Emergency | 4‑of‑5 multisig (expanded council) | 4 h | Fast |
| Critical (active exploit) | 5‑of‑5 multisig | 0 h (immediate) | Immediate + automatic pause |
Key insight: Higher urgency requires higher consensus, not lower. If something truly needs immediate deployment, gathering all 5 signers should be achievable because the stakes are highest.
TL;DR Checklist
- Never leave the default hot‑wallet authority on mainnet.
- Move authority to hardware wallets (preferably a multisig).
- Add a timelock to any upgrade path.
- Consider finalizing the program once it’s battle‑tested.
- Document the upgrade process and train the team on phishing/physical‑security best practices.
Secure the upgrade authority today – it’s the single most critical key protecting your Solana protocol.
Upgrade Authority Monitoring & Security Checklist
Detection is as important as prevention. Set up alerts for any interaction with your program’s upgrade authority:
import asyncio
from datetime import datetime
from solders.pubkey import Pubkey
from solana.rpc.websocket_api import connect
PROGRAM_ID = Pubkey.from_string("YourProgram111111111111111111111111")
BPF_LOADER = Pubkey.from_string("BPFLoaderUpgradeab1e11111111111111111111111")
async def monitor_upgrades():
async with connect("wss://api.mainnet-beta.solana.com") as ws:
# Subscribe to any transaction mentioning the BPF Loader + your program
await ws.logs_subscribe(
filter_={"mentions": [str(BPF_LOADER)]},
commitment="confirmed"
)
async for msg in ws:
logs = msg[0].result.value.logs
signature = msg[0].result.value.signature
# Check if this upgrade targets our program
if any("Upgrade" in log for log in logs):
if any(str(PROGRAM_ID) in log for log in logs):
await send_alert(
f"🚨 PROGRAM UPGRADE DETECTED!\n"
f"Program: {PROGRAM_ID}\n"
f"Tx: {signature}\n"
f"Time: {datetime.utcnow()}\n"
f"ACTION REQUIRED: Verify this was authorized"
)
async def send_alert(message: str):
# Send to Telegram, Discord, PagerDuty, etc.
print(message)
asyncio.run(monitor_upgrades())Also monitor for SetAuthority instructions
An attacker’s first move might be to transfer upgrade authority to their own key:
# Quick check: has your authority changed?
solana program show | grep AuthorityAudit Checklist (based on 2026 incidents such as Step Finance)
- Upgrade authority is NOT a single hot wallet
- Multisig with ≥2‑of‑3 threshold
- Each signer uses a hardware wallet
- Signers are geographically distributed
- No single person has access to the threshold number of keys
- Backup/recovery procedure documented and tested
- Timelock of ≥24 hours for normal upgrades
- Emergency path with higher threshold (not lower timelock)
- Program binary verified on‑chain before execution
- Buffer account contents independently verified by ≥2 parties
- Upgrade proposals publicly announced before execution
- Real‑time alerts for
UpgradeandSetAuthorityinstructions - Authority pubkey checked on every deployment
- On‑chain program hash verified against source code
- Solana Explorer / Anchor Verifiable Builds used
- Program pause mechanism exists (separate from upgrade authority)
- Emergency contact list for all multisig signers
- Written runbook for “upgrade authority compromised” scenario
- Regular drills (quarterly) to test emergency upgrade flow
Staged Path to Immutability
| Phase | Timeline | Requirements |
|---|---|---|
| Phase 1 (Launch) | Immediate | 2‑of‑3 multisig, 24 h timelock |
| Phase 2 | 6 months | 3‑of‑5 multisig, 48 h timelock, ≥2 audits completed |
| Phase 3 | 1 year | Core logic frozen; only peripheral modules upgradeable |
| Phase 4 | 2+ years | Full immutability after extensive battle‑testing |
Each phase is a public commitment to your users. Document your current phase and the criteria for advancing to the next one.
Key Takeaways
- The $40 M loss at Step Finance shows that a program is only as secure as its upgrade authority.
- A perfectly audited contract is worthless if the key to replace it sits on an executive’s laptop.
- The fix isn’t exotic cryptography; it’s operational discipline:
- Move to multisig today.
- Add a timelock this quarter.
- Plan your path to immutability.
- Monitor everything.
Your users trust you with their funds. The least you can do is ensure a single compromised laptop can’t betray that trust.
DreamWork Security publishes weekly research on DeFi security patterns. Follow us for more Solana and EVM security analysis.