3.10.7 Wormhole (February 2022)
The Wormhole bridge exploit drained $326 million worth of wrapped ETH on February 2, 2022 — at the time the second-largest DeFi exploit in history (after Poly Network, Section 3.10.4). The attack worked by submitting a single Solana transaction that bypassed the signature-verification step entirely, causing the bridge to mint 120,000 wETH on Solana without any corresponding ETH being locked on Ethereum. The wrapped tokens were then bridged back across to Ethereum and converted to real ETH, leaving the Wormhole bridge structurally insolvent.
The case is the only one in this section that is not a Solidity exploit. Wormhole's vulnerable code was a Solana program written in Rust using the Anchor-adjacent Solana SDK. But the underlying class of bug — trusting an account address you were given instead of verifying it is the account you expected — generalizes directly to Solidity contracts that accept user-supplied addresses without checking them. The lesson is platform-independent: any value passed by an untrusted caller, including an account or contract address that ostensibly identifies a system component, must be validated against an expected reference.
Two other things make Wormhole instructive. First, the fix had been published to GitHub days before the exploit. A Wormhole engineer had identified the bug, committed a fix to a public repository, and was working through the deployment process when an attacker — almost certainly monitoring the public commits — exploited the still-deployed vulnerable code before the fix shipped. Second, the loss was repaid in full by Wormhole's investor Jump Crypto, who replenished all 120,000 ETH from its own reserves within 24 hours. This is one of the few cases where a single private party absorbed a nine-figure loss to make users whole; the precedent has rarely been repeated.
Section 3.8.4 (Access Control Failures) and Section 3.8.8 (Signature & Replay Issues) both draw on the underlying pattern. Section 3.11.5 (Cross-Chain & Bridge Security) covers the larger architectural questions.
Context
Wormhole is a generic cross-chain message-passing protocol — at the time of the exploit, it connected Ethereum, Solana, Terra, Binance Smart Chain, Polygon, Avalanche, and Oasis. The token bridge built on top of Wormhole let users lock tokens on one chain and mint wrapped equivalents on another.
The on-Solana side of the token bridge consisted of multiple programs, with two relevant for the exploit:
bridge(the Wormhole core) — verifies signatures from the "Guardian" set (a permissioned multi-sig of validators) and produces signed messages called VAAs (Verifiable Action Approvals). A VAA is essentially an authenticated cross-chain message: "the guardians have signed off that X happened on chain Y."token_bridge— receives VAAs from the core bridge and executes their instructions. In particular, thecomplete_wrappedfunction mints wrapped tokens on Solana corresponding to a VAA that asserts equivalent tokens were locked on the source chain.
The intended security flow:
- User locks ETH on Ethereum side
- Guardians observe the lock, sign a VAA attesting to it
- User submits signatures to Solana's
verify_signaturesfunction - After verification, user calls
post_vaato create the on-Solana VAA record - User calls
complete_wrappedto mint wrapped ETH on Solana
Step 3 — verify_signatures — was where the bug lived. The function was supposed to verify that the Guardian signatures had been validated by Solana's built-in Secp256k1 precompile (Solana's native ECDSA verification, similar to Ethereum's ecrecover but architected as a separate program). The verification depended on inspecting Solana's "instructions sysvar," a special account that exposes the contents of the current transaction's other instructions to the executing program.
Wormhole's verify_signatures looked at this sysvar to confirm that an earlier Secp256k1 instruction in the same transaction had actually executed and produced a valid signature check. If the sysvar said "yes, the previous instruction was a valid Secp256k1 verification," then verify_signatures would create a SignatureSet account marking the signatures as valid.
The flaw was in which sysvar the function was reading.
The Bug: Trusting an Unverified Sysvar Account
The Solana programming model passes accounts to programs as a list, with the program responsible for validating that each account is what it claims to be. The sysvar::instructions account has a well-known fixed address (Sysvar1nstructions1111111111111111111111111), and a properly-defensive program should check that the account it was handed has exactly this address before reading its contents.
Wormhole's code used the function solana_program::sysvar::instructions::load_instruction_at to read the prior Secp256k1 instruction from what it believed to be the instructions sysvar:
#![allow(unused)] fn main() { // Wormhole's vulnerable verify_signatures (simplified) let secp_ix = solana_program::sysvar::instructions::load_instruction_at( secp_ix_index as usize, &accs.instruction_acc.try_borrow_mut_data()?, )?; // Check that the instruction we just loaded was indeed Secp256k1 if secp_ix.program_id != solana_program::secp256k1_program::id() { return Err(ErrorCode::InvalidSecpInstruction.into()); } // If we got here, we believe the signatures have been verified mark_signatures_valid(...) }
The function load_instruction_at accepts a data buffer (the second parameter) and parses it as instructions sysvar content. It does not check that the account from which that buffer came is the legitimate instructions sysvar.
The deeper issue: accs.instruction_acc was a user-supplied account in the accounts list. Wormhole's code took whatever account was at that position, read its data, and parsed it as instruction-sysvar contents. The expected case — that the user passed the real Sysvar1nstructions... account — worked correctly. The attack case — that the user passed a different account whose data happened to look like a valid instructions sysvar — was not anticipated.
Solana had already deprecated load_instruction_at in favor of load_instruction_at_checked, which performs the account-address verification automatically. The deprecation predated the exploit by several months. Wormhole's code had not yet been updated.
The Attack
The attacker prepared the exploit over multiple transactions, then executed the drain in a single one. The full chain:
Step 1: Construct a Forged Instructions Sysvar
The attacker created a normal Solana account (not the real sysvar, just a regular account) and populated its data to look like a valid instructions-sysvar payload. Specifically, the data:
- Encoded an instruction at the position Wormhole's code would read
- Claimed that instruction was a call to the
Secp256k1precompile - Claimed the call had succeeded — i.e., that signatures had been verified
The actual content of the data was entirely under the attacker's control. They populated it to assert whatever signatures they liked.
The attacker's forged sysvar account address: 2tHS1cXX2h1KBEaadprqELJ6sV9wLoaSdX68FqsrrZRd (a regular account, not the real sysvar address Sysvar1nstructions...).
Step 2: Invoke verify_signatures with the Forged Sysvar
The attacker called verify_signatures on the Wormhole bridge, passing:
- A fake set of guardian signatures
- The forged account
2tHS1...in the position where the real instructions sysvar should have been
Wormhole's code:
- Called
load_instruction_atagainst the data from2tHS1... - Got back what looked like a valid prior Secp256k1 instruction (because the attacker had crafted the data that way)
- Checked that the loaded instruction's
program_idwas the Secp256k1 program — and the forged data said yes - Marked the signatures as verified, creating a
SignatureSetaccount
The SignatureSet account now existed, claiming valid guardian signatures over a message specifying minting 120,000 wETH.
Step 3: post_vaa
With the SignatureSet in hand, the attacker called post_vaa, which produced a VAA account formally representing "the guardians have signed off on this cross-chain message."
The post_vaa function itself validated that the SignatureSet accumulated enough valid signatures — but it trusted the SignatureSet's claim that the signatures were valid. The bug was upstream; once verify_signatures had certified a forged signature set, every downstream check passed.
Step 4: complete_wrapped
The attacker called complete_wrapped on the token bridge, passing the freshly-minted VAA. The token bridge:
- Validated the VAA's structure
- Verified the VAA was a
Transfermessage - Minted 120,000 wETH on Solana to the attacker's address
The mint succeeded. The attacker now held 120,000 wETH on Solana that corresponded to no actual ETH locked on Ethereum.
Step 5: Bridge the wETH Back to Ethereum
The attacker began moving the wETH back to Ethereum via the bridge. Of the 120,000:
- 93,750 wETH was bridged back to Ethereum and withdrawn as real ETH from the bridge's Ethereum-side liquidity
- The remaining ~26,250 wETH was swapped on Solana for SOL and USDC
The Ethereum-side bridge contract had no way to know the wETH had been minted illegitimately. From its perspective, valid wormhole-bridged ETH was returning home; it released the corresponding ETH from the locked liquidity.
The total drained at then-current prices: approximately $326M.
The Pre-Exploit Timeline
The most uncomfortable detail of this case is the timing. A Wormhole engineer had identified the load_instruction_at issue and committed a fix to the public Wormhole repository on the day of the upgrade. The fix replaced load_instruction_at with the checked variant. The fix was visible in the public diff.
The attack happened approximately 19 hours after the fix was committed, before the new code had been deployed to mainnet. The strongly-supported hypothesis: an attacker monitoring Wormhole's public commits identified the unpatched vulnerability from the diff, recognized that the deployment had not yet happened, and exploited the still-live vulnerable code before the fix could ship.
This is a real risk for open-source security-critical projects: publishing a fix to a public repository announces the vulnerability. Coordinated security disclosure best practices (which Wormhole likely did not follow rigorously here) involve either deploying patches before disclosing them, or keeping critical fixes private until deployment is complete.
The Aftermath
Within hours of the exploit, Wormhole's parent company Jump Crypto announced it would replenish the bridge's reserves. Within 24 hours, Jump had deposited 120,000 ETH from its own treasury into the bridge, making users completely whole. The actual realized user loss was zero.
A $10M white-hat bounty was offered to the attacker. There was no response. The funds — already converted into SOL, USDC, and re-bridged ETH — were never recovered.
The Jump Crypto reimbursement is one of the only times in DeFi history that a private party has absorbed a nine-figure loss to make users whole. The pattern requires:
- A protocol with a wealthy and committed backer
- An ecosystem-protection motive (Jump had positions across Solana DeFi that depended on Wormhole continuing to function)
- A legal/regulatory structure that permits the transfer
Subsequent bridge exploits have generally not produced equivalent outcomes.
Root Cause
The Wormhole exploit had several compounding causes:
1. Account validation missing (Section 3.8.4). The proximate cause: verify_signatures accepted an arbitrary account where it should have required the specific Sysvar1nstructions... address. A single check — require!(accs.instruction_acc.key == &solana_program::sysvar::instructions::id(), Error::InvalidSysvar) — would have prevented the entire attack.
2. Deprecated API used in security-critical path. Solana had already shipped load_instruction_at_checked and deprecated load_instruction_at. The whole reason the new function existed was that the old one was unsafe. Wormhole's failure to upgrade was a known-issue debt that came due catastrophically.
3. Signature verification result trusted without re-verification. Once verify_signatures certified a SignatureSet, downstream functions trusted it. A defense-in-depth approach would have re-verified signatures (or at least cross-checked critical claims) at the complete_wrapped layer — though in practice this is expensive and rarely done in production. The trust relationship between security-critical operations and the operations that depend on them must be explicit and auditable.
4. Open-source disclosure timing (operational, not code). Publishing the fix before deploying it telegraphed the vulnerability to anyone watching. This is a coordination failure rather than a code bug, but it materially shortened the exploit window. Section 2.9 covers responsible disclosure timing.
5. Bridge architecture concentrates risk. $326M sat in a bridge whose security reduced to "the Solana program correctly verifies guardian signatures." A single bug in that single program produced a $326M loss. Every architectural pattern that reduces the bridge's single-point-of-failure footprint (e.g., delayed finality, withdrawal caps, fraud proofs) would have mitigated some portion of the loss.
6. Insufficient defensive coverage of the Solana program. Wormhole's Solana program had been audited. The audit had not identified this specific bug. This is the general lesson — audits catch many bugs, miss some — and is not specific to Wormhole. But it is worth noting that the pattern of "this function reads from an account it didn't verify" is exactly the kind of thing manual review should catch. Section 3.9 covers what audit practices catch and miss.
Lessons
The Wormhole exploit produced lessons that crossed both Solana and broader smart-contract security:
1. Verify every account that is supposed to identify a specific entity. In Solana, this means checking that sysvar accounts have the expected address, that owner-restricted accounts are signed by the expected owner, that token accounts have the expected mint, etc. In Solidity, the equivalent is verifying that contract addresses passed by users actually point to the expected contract (often via IERC165 or constructor-set immutable references). The principle: never trust an account/address that purports to be a specific entity unless you've verified it.
2. Use checked APIs over deprecated ones, consistently. When a platform deprecates a function for security reasons, prioritize the migration. This sounds obvious; in practice, security-deprecated functions stay in production code for years across the industry. Section 3.9.1 covers tooling (linters, dependency scanners) that can flag deprecated API usage in CI.
3. Open-source security fixes need careful disclosure timing. A patch committed to a public repository announces the bug. For high-value targets, fixes should be deployed before they are visible publicly — or at least the public commit should not include enough detail to reconstruct the exploit. Trade-offs exist (open-source review is valuable; secret patches undermine open development); the right answer depends on the threat model.
4. Defense in depth at integration points. The complete_wrapped function trusted that VAAs were authenticated. The VAA was trusted because post_vaa accepted the SignatureSet. The SignatureSet was trusted because verify_signatures had marked it valid. Each link in the chain was a place where additional verification could have caught the upstream forgery. In practice, every protocol must choose between performance/cost and defense-in-depth; high-value protocols should err strongly toward the latter.
5. Token bridges concentrate risk in proportion to TVL. A bridge holding nine-figure value is among the most attractive targets in DeFi. Wormhole, Ronin, Poly Network, and Nomad collectively account for over $1.5 billion in losses. The bridge security category remains an open problem; new architectures (light-client verification, ZK proofs, optimistic verification with fraud proofs) are being developed but have not yet established themselves as clearly superior.
6. Private reimbursement is not a substitute for security. Jump Crypto's replenishment turned a $326M user loss into a $326M Jump loss. This was generous and culture-positive, but it is not a security pattern any protocol can rely on. Without Jump, the Wormhole users would have absorbed the loss. Designing security around the assumption that a backer will bail out failed protocols is not security design.
7. Solana-specific patterns recur across protocols. The Wormhole exploit was one of several major Solana exploits in 2022 that depended on missing account validation (Cashio in March 2022, $48M, was a similar pattern). The Solana programming model requires programs to validate every account they receive; programs that fail to do so universally are vulnerable to forged-account attacks. Solana frameworks like Anchor now provide attribute-based account validation that catches many of these errors at compile time.
Modern Reproduction
Because the vulnerable code was Rust on Solana rather than Solidity on EVM, the direct reproduction is in a different language. The conceptual pattern translates cleanly to Solidity, however.
The Solana Pattern (the actual bug)
#![allow(unused)] fn main() { // Vulnerable: doesn't verify which account is being read pub fn verify_signatures(ctx: Context<VerifySignatures>) -> ProgramResult { let ix_acc = &ctx.accounts.instruction_acc; // BUG: ix_acc could be any account; we don't check it's the real sysvar let secp_ix = sysvar::instructions::load_instruction_at(0, &ix_acc.data.borrow())?; require!(secp_ix.program_id == secp256k1_program::id(), Error::InvalidSecp); // Mark signatures valid based on what the (possibly forged) data said Ok(()) } // Fixed: verify the account is the real sysvar pub fn verify_signatures_safe(ctx: Context<VerifySignatures>) -> ProgramResult { let ix_acc = &ctx.accounts.instruction_acc; require!(ix_acc.key == &sysvar::instructions::id(), Error::WrongSysvar); let secp_ix = sysvar::instructions::load_instruction_at_checked(0, ix_acc)?; require!(secp_ix.program_id == secp256k1_program::id(), Error::InvalidSecp); Ok(()) } }
The Solidity Equivalent
The same class of bug appears in Solidity any time a contract trusts a user-supplied address that's supposed to identify a specific entity:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
// Vulnerable: trusts user-supplied oracle address
contract VulnerableBridge {
function mintFromVerifiedTransfer(
address oracle,
bytes calldata data
) external {
// BUG: 'oracle' could be any contract that returns the expected shape
bool ok = IOracle(oracle).verify(data);
require(ok, "not verified");
_mint(msg.sender, _parseAmount(data));
}
}
// Fixed: oracle is set at deployment and immutable
contract SafeBridge {
IOracle public immutable oracle;
constructor(IOracle _oracle) {
oracle = _oracle;
}
function mintFromVerifiedTransfer(bytes calldata data) external {
// 'oracle' is the contract we trust; no user-supplied alternative
bool ok = oracle.verify(data);
require(ok, "not verified");
_mint(msg.sender, _parseAmount(data));
}
function _mint(address, uint256) internal {}
function _parseAmount(bytes calldata) internal pure returns (uint256) {}
}
A Foundry test demonstrating the vulnerable pattern:
contract MaliciousOracle is IOracle {
function verify(bytes calldata) external pure returns (bool) {
return true; // approves anything
}
}
function test_VulnerableBridge_acceptsFakeOracle() public {
VulnerableBridge bridge = new VulnerableBridge();
MaliciousOracle fakeOracle = new MaliciousOracle();
// Anyone can pass any contract that returns 'true'
bridge.mintFromVerifiedTransfer(address(fakeOracle), hex"");
// Tokens have been minted with no real authentication
}
The principle is the same as the Solana case: any account/contract/address passed by an untrusted caller, where the caller is asserting it is a particular trusted entity, must be verified against the expected reference. The verification is platform-specific — sysvar checks on Solana, immutable references or explicit address comparison on Solidity — but the principle is universal.
Cross-References
- Access control failures — Section 3.8.4 covers the trust-without-verification pattern as a general vulnerability class
- Signature & replay issues — Section 3.8.8 covers the signature verification patterns that Wormhole's code was supposed to implement
- Anti-patterns — Section 3.7.7 covers the broader "trust user-supplied references" anti-pattern
- Audit practices — Section 3.9 covers what audits should be catching; this is precisely the kind of bug manual review can find with sufficient attention
- Subsequent bridge exploits — Sections 3.10.4 (Poly Network), 3.10.5 (Ronin), and 3.10.6 (Nomad) cover other bridge failure modes
- Cross-chain security — Section 3.11.5 covers bridge architecture in depth
- Disclosure timing — Section 2.9 covers responsible vulnerability disclosure, which Wormhole's timing illustrates the cost of getting wrong