How the $285M Drift hack happened: durable nonces + a fake oracle — a defensive read for Solana builders
On April 1, 2026, Drift Protocol was drained for $285M. The tooling I ship — cipher-solana-wallet-audit — now catches the three anti-patterns the attacker used, and a new x402-gated API (cipher-drift-exposure.vercel.app) lets any AI agent check whether a given wallet had exposure for $0.01 USDC on Base.
This post walks through the attack chain with enough detail to be useful to a Solana builder, maps each step to a defensive control, and shows exactly what the v1.1.0 release of the audit action does (and, just as honestly, does not) catch.
Disclaimer. Not financial advice. Not a complete security audit. Pattern-based static analysis is a first line of defense, not a substitute for a full review by a real auditor. I am not affiliated with Drift, Chainalysis, or Cyfrin. All sources linked below.
The four moves
Based on the public post-mortems from Chainalysis, CoinDesk, and Cyfrin, the attack unfolded in four steps.
1. Social engineering against the Security Council
Drift's admin keys sat behind a 2-of-5 Squads multisig staffed by a "Security Council." The attacker, linked to DPRK-affiliated groups, targeted those signers with a months-long social-engineering campaign — fake recruiter contacts, bogus VC intros, doctored calendly links. Two signers eventually signed a malicious transaction they believed was routine.
Defensive reading. Multisig is not magic. A 2-of-5 that all live on commodity laptops in the same professional network is one well-crafted LinkedIn DM away from 2/5. The fix is not "more signers." The fix is (a) hardware-enforced signing for every admin op, (b) a second out-of-band check on the content of every proposed tx, and (c) a culture where "I'm not sure what this does" is an acceptable answer that blocks the tx.
A few more concrete controls worth borrowing from the post-mortems: maintain a signed changelog of every multisig proposal (timestamp, proposer, hash of the tx bytes, human-language summary) that every signer has to acknowledge before signing. Keep the proposer and the reviewer on separate teams. Require a 24-hour minimum delay on any proposal that touches program-data, upgrade-authority, or oracle-config accounts. None of these cost much; all of them would have blocked step 1.
2. Durable-nonce pre-signed admin transfer
This is the part most people missed. Solana supports durable nonces (nonceAdvance on the System Program) so a transaction can be signed now and submitted later — they're great for hardware-wallet workflows and for cold-signed treasury ops. But that same property means a signed transaction can sit in an attacker's inbox for weeks. When the attacker finally landed it, the two compromised signers had probably forgotten they'd ever seen it.
The malicious tx bundled:
-
System::nonceAdvance(nonce_account)— to make the tx submittable against the current nonce state. -
SetAuthorityon the Drift program data account — transferring control to the attacker.
A defensive signer reviewing a single tx that contains both AdvanceNonce and SetAuthority/UpgradeProgram should refuse to sign it, full stop. Those two things never need to be in the same message for any legitimate workflow.
That's rule #1 in the v1.1.0 release.
# patterns.py (excerpt — cipher-solana-wallet-audit v1.1.0)
NONCE_ADVANCE_IN_MULTISIG = Rule(
id="NONCE_ADVANCE_IN_MULTISIG",
severity="critical",
description=(
"Durable-nonce AdvanceNonce instruction built alongside a "
"SetAuthority / TransferAuthority / UpgradeProgram instruction "
"(within 50 lines)."
),
scope="tree",
tree_scan=_scan_nonce_advance_in_multisig,
)
The scanner correlates AdvanceNonce/nonceAdvance/advance_nonce with SetAuthority/TransferAuthority/UpgradeProgram/SetUpgradeAuthority within a 50-line window across .rs, .ts, .js, .tsx, .jsx, .py. It's flagged critical because there is no legitimate code path that needs both in one tx.
3. Fake "CarbonVote Token" as $100M oracle collateral
The more creative move: the attacker deployed a fresh SPL token with essentially no trading volume ("CarbonVote Token" was the label in the post-mortem), then pushed it onto Drift's oracle allow-list so the protocol would treat it as priceable collateral. They minted $100M+ of it to themselves, deposited it, and now had $100M of borrowable credit against a token no one else held or traded.
The bug was that Drift's add_oracle / addCollateral path checked admin authority but not liquidity. Any sufficiently well-scoped admin could whitelist anything — and after step 2, the attacker was the admin.
Rule #2 catches the static-analysis shape of this bug: an allow-list mutation with no preceding liquidity / volume / depth check.
// SYNTHETIC fixture — not Drift's real code.
pub fn add_oracle_bad(ctx: Context<AddOracle>, mint: Pubkey) -> Result<()> {
require_keys_eq!(ctx.accounts.admin.key(), ctx.accounts.oracle_config.admin);
// BAD: no liquidity / volume / depth check anywhere above this push.
ctx.accounts.oracle_config.oracle_whitelist.push(mint);
Ok(())
}
LOW_LIQUIDITY_ORACLE_WHITELIST scans for oracleWhitelist.push / oracle_config.add_asset / addCollateral( etc. and walks the preceding 30 lines looking for a call that mentions check_liquidity / requireMinDepth / min_volume. No call, no pass. Severity high.
Worth noting what a good add_oracle path looks like. At minimum it should (a) require the asset has N days of continuous trading history on at least two independent venues, (b) require a minimum rolling 24-hour dollar volume and bid-ask depth, (c) require a minimum number of independent holders above a dust threshold, and (d) rate-limit itself to one oracle addition per governance epoch with a mandatory time-lock. The CarbonVote token would have failed every single one of those tests, which is why the controls matter — not because they're clever, but because they're boring enough that nobody builds them until something blows up.
4. Unbounded admin-instruction bundle to drain
The final tx packed a ComputeBudgetProgram.setComputeUnitLimit instruction (needed because the drain touched many vaults in one message) plus multiple SetAuthority and UpgradeProgram instructions. One message, one block, game over.
The defensive intuition is simple: any tx that mutates more than one admin authority in a single message is already suspicious. Compute-budget + 2+ authority changes is almost a fingerprint of a drain.
Rule #3 implements this.
// SYNTHETIC fixture — not Drift's real code.
export function buildDrainBundle(): Transaction {
const tx = new Transaction();
tx.add(ComputeBudgetProgram.setComputeUnitLimit({ units: 1_400_000 }));
tx.add(ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 10_000 }));
tx.add(setAuthorityIx());
tx.add(upgradeProgramIx());
tx.add(setAuthorityIx());
return tx;
}
UNBOUNDED_ADMIN_INSTRUCTION_BUNDLE looks inside every .add( / .push( / instructions: [ builder chain. If a 40-line window contains 2+ distinct admin-level instructions (or 1 admin + compute-budget + another admin), it flags the file. Severity high.
If your workflow legitimately needs to touch multiple authorities (migrations do, sometimes), the fix is ergonomic: split the operations across separate transactions, require the multisig to approve each independently, and require a human-readable summary attached to each proposal. The audit rule will still flag the builder, but you'll have a one-line code comment explaining why it's intentional and the multisig signers will have one decision to make per message instead of five buried in a single bundle. That's the whole point — make it impossible for a signer to accidentally approve a drain because it was tucked behind a ComputeBudget instruction they didn't read.
Drop-in usage
The action is already on the GitHub Marketplace. Add one step to your workflow:
# .github/workflows/wallet-audit.yml
name: Wallet Security
on: [push, pull_request]
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: cryptomotifs/cipher-solana-wallet-audit@v1
The v1 sliding tag now points at v1.1.0. Findings are surfaced as inline annotations on the PR diff; the job fails on any high or critical match.
Eight other rules come along for free — plaintext private keys, JSON-keypair leaks, seed phrases in comments, .env files that slipped past .gitignore, hardcoded RPC URLs with embedded API keys, id.json files tracked in git, etc.
The other half: check a wallet's Drift exposure
The static check is nice. The on-chain check is the one a user actually wants: "did my wallet get hit?"
I shipped a companion x402-gated API for that:
GET https://cipher-drift-exposure.vercel.app/api/drift-exposure/<wallet>
- Unpaid →
HTTP 402with the v2 accept-list. $0.01 USDC on Base, payTo0xa0630fAD18C732e94D56d2D5F630963eb8fB9640. - Paid → JSON with
hadDriftPosition,hadExposureTo(attacker addresses from the post-mortems),estimatedLossUsd,recommendation, and diagnostics.
The heavy lifting runs server-side: Helius RPC getSignaturesForAddress against the 30-day window ending April 1, 2026, filtered for Drift program-ID interactions, cross-referenced with the attacker-address list from the Chainalysis/CoinDesk/Cyfrin write-ups. If Helius is down the endpoint fails open to a pattern-based score and labels the response helius-rpc-error-fallback so callers can tell the difference. The full response shape is documented on the landing page and includes a diagnostics object with signaturesScanned and, when relevant, an error code so a calling agent can reason about confidence.
Why $0.01 and not free? Because spam is real and AI agents are about to be the dominant traffic source on any public API. Pricing at one cent means a well-behaved agent costs approximately nothing and a badly-behaved agent running a million checks a minute costs $10,000/hour — which is exactly the kind of pricing signal that keeps an endpoint alive. The payout wallet is the same Base address as the rest of the CIPHER micro-payment stack, so all the revenue converges in one place and I can keep the infrastructure free for humans who copy-paste the URL into Claude or Perplexity and let the agent handle the payment.
This is built on the same x402 protocol as the existing cipher-x402.vercel.app endpoint — no login, no account creation, no email capture. Any x402-aware agent (Claude Code, GPT Actions, Perplexity Comet) will receive the 402, auto-pay the cent, and receive the JSON on the refetch. Humans copy the URL into the same agents for the same result.
What these rules don't catch
Important to say out loud.
-
Runtime-only bugs. If the
AdvanceNonceis in a compiled program you don't control, a source scan finds nothing. Use simulation-diff tools + hardware-wallet tx inspectors for that layer. - Social engineering. No static rule stops a signer from clicking a phishing link. Hardware-isolated review and second-channel verification are the actual controls.
- Oracle manipulation from the other side. These rules catch the addition of a bad oracle. They don't catch a Pyth/Switchboard feed that gets corrupted upstream. For that, see Cyfrin's write-up on cross-verification between oracles.
-
False positives. All three tree-rules are heuristic. Expect to annotate legitimate liquidity-gated paths with a comment and re-scan. Severity
high, notcritical, on the two rules that are more prone to FPs for this reason.
Related reading
Previous posts in this series:
- The Ontario NI 31-103 four hard lines for solo crypto-quant devs — exactly which regulatory lines you cross when you ship a "recommendation" vs. a "screener."
- Jito tip math for $1k/tx bundles — closed-form for the tip that minimizes inclusion probability × expected MEV leak.
Free resources:
- cipher-starter — 150-page Solana quant playbook, MIT-licensed, no paywall.
- cipher-x402 — four gated premium chapters, $0.25/fetch.
- cipher-drift-exposure — this article's companion API.
- cipher-solana-wallet-audit — the action described above.
Sources
- Chainalysis — "Lessons from the Drift hack"
- CoinDesk — "How a Solana feature designed for convenience let an attacker drain $270M from Drift"
- Cyfrin — "Drift hack learnings"
- Release notes v1.1.0
If you found this useful, the free audit action is one line of YAML. The paid exposure check is one $0.01 USDC call your AI agent can make on its own. The playbook is free on GitHub. Ship something.
Top comments (0)