Solana Upgrade Authority Security: The $40M Lesson Most Protocols Haven't Learned
How to secure your Solana program upgrade authority before an attacker does it for you
Every Solana program deployed with solana program deploy is upgradeable by default. That 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 ($40M drained) wasn't a smart contract bug. It was a compromised executive device that held upgrade authority keys. The attacker didn't need to find a vulnerability in the code — they just needed the key to replace it.
If your Solana program's upgrade authority is a single hot wallet, you're one phishing email away from the same fate.
The Upgrade Authority Attack Surface
When you deploy a Solana program, the BPF Loader creates a program account pointing to a data account containing your executable bytes. The upgrade authority is a pubkey stored in the program account's metadata. Anyone who controls that key can call BPFLoaderUpgradeable::Upgrade and swap the entire program binary.
Here's what that looks like in practice:
# Check who controls your program
solana program show <PROGRAM_ID>
# Output:
# Program Id: YourProgram111111111111111111111111
# Owner: BPFLoaderUpgradeab1e11111111111111111111111
# ProgramData Address: ProgramData111111111111111111111
# Authority: HotWa11et1111111111111111111111111 ← single point of failure
# Last Deployed In: Slot 250000000
# Data Length: 832456 bytes
That Authority field is the attack target. If an attacker obtains this key, they can:
- Deploy a malicious program binary that drains all PDAs
- Replace the program logic to bypass access controls
- Insert a backdoor that siphons funds over time
- Brick the program entirely
The 5 Tiers of Upgrade Authority Security
Tier 0: Hot Wallet (🔴 Critical Risk)
Authority = developer's local keypair (~/.config/solana/id.json)
This is the default. It's also how 60%+ of Solana programs ship to mainnet. Your program's security equals your laptop's security.
Risk: Malware, phishing, device theft, insider threat.
Tier 1: Hardware Wallet (🟠 High Risk)
# Transfer authority to Ledger
solana program set-upgrade-authority <PROGRAM_ID> \
--new-upgrade-authority <LEDGER_PUBKEY> \
--keypair usb://ledger
Better — the key never touches a hot machine. But it's still a single key held by a single person. Bus factor = 1.
Risk: Physical theft, coercion, single-person compromise.
Tier 2: Multisig (🟡 Medium Risk)
Using 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"
);
Now 2 of 3 keyholders must approve any upgrade. An attacker needs to compromise multiple parties across different security boundaries.
Risk: Social engineering of multiple members, governance attacks, emergency response latency.
Tier 3: Multisig + Timelock (🟢 Low Risk)
Add a mandatory delay between upgrade proposal and execution:
// Custom upgrade program with timelock
pub fn propose_upgrade(ctx: Context<ProposeUpgrade>, 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 hours
proposal.approvals = 0;
proposal.executed = false;
emit!(UpgradeProposed {
program: ctx.accounts.program.key(),
buffer,
execution_time: proposal.execution_time,
});
Ok(())
}
pub fn execute_upgrade(ctx: Context<ExecuteUpgrade>) -> 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(())
}
pub fn cancel_upgrade(ctx: Context<CancelUpgrade>) -> 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(())
}
The 48-hour timelock means even if attackers compromise the multisig, the community has time to:
- Detect the unauthorized upgrade proposal
- Cancel it via any single guardian
- Alert users to withdraw funds
Risk: Emergency bugs take 48h to patch (mitigated by having an emergency fast-path with higher threshold).
Tier 4: Immutable (🔵 Lowest Risk, Highest Cost)
# Nuclear option: revoke upgrade authority permanently
solana program set-upgrade-authority <PROGRAM_ID> \
--final
The 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).
The Emergency Upgrade Dilemma
Every security team faces this tension: timelocks protect against attackers but slow down emergency patches. Here's the pattern used by mature Solana protocols:
Normal Upgrade Path:
2-of-3 multisig → 48h timelock → execution
Emergency Path:
4-of-5 multisig (expanded council) → 4h timelock → execution
Critical Path (active exploit):
5-of-5 multisig → immediate execution + automatic pause
The key insight: higher urgency requires higher consensus, not lower. If something truly needs immediate deployment, getting all 5 signers should be achievable because they're all aware of the active incident.
Monitoring Your Upgrade Authority
Detection is as important as prevention. Set up alerts for any interaction with your program's upgrade authority:
import asyncio
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 <PROGRAM_ID> | grep Authority
The Step Finance Post-Mortem Checklist
Based on what went wrong with Step Finance and other 2026 incidents, here's your audit checklist:
Key Management
- [ ] 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 threshold number of keys
- [ ] Backup/recovery procedure documented and tested
Upgrade Process
- [ ] 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
Monitoring
- [ ] 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
Incident Response
- [ ] 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
The Road to Immutability
The safest Solana program is one that can't be changed. But getting there requires a staged approach:
Phase 1 (Launch): 2-of-3 multisig, 24h timelock
Phase 2 (6 months): 3-of-5 multisig, 48h timelock, 2+ audits complete
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 your criteria for advancing to the next one.
Conclusion
The $40M lesson from Step Finance is clear: your program is only as secure as its upgrade authority. A perfect smart contract audited by the best firms is worthless if the key to replace it sits on an executive's MacBook.
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 are trusting you with their funds. The least you can do is make sure a single compromised laptop can't betray that trust.
DreamWork Security publishes weekly research on DeFi security patterns. Follow for more Solana and EVM security analysis.
Top comments (0)