Wormhole Bridge Hack — $326M From a Missing Signature Check
In February 2022, Wormhole's Solana bridge accepted a forged 'guardian' signature set because it skipped real verification. The attacker minted 120,000 wETH out of thin air. One missing check, $326M gone.
The Bug in One Sentence
Wormhole's Solana bridge let the attacker supply a `SignatureSet` account that *claimed* it had been verified by the guardians, without re-checking that claim against the current guardian set on every mint.
The contract called a deprecated Solana sysvar helper (`load_current_index`) to confirm a prior `verify_signatures` instruction had run in the same transaction. After a Solana SDK upgrade, the correct call became `load_instruction_at_checked`. Wormhole updated some places. They missed one. The attacker found it before the audit did.
How a Bridge Mint Is *Supposed* to Work
A token bridge like Wormhole has two sides. Lock on chain A, mint on chain B. The link between them is a VAA (Verified Action Approval) — a payload signed by a threshold of 19 guardian nodes. Solana receives the VAA, checks the guardian signatures with `secp256k1_recover`, and only then mints wrapped tokens.
Here's the simplified Solidity-flavored pseudocode of what the check should look like:
```solidity
function completeTransfer(bytes calldata vaa) external {
(bytes32 hash, Signature[] memory sigs, Payload memory p) = parse(vaa);
uint256 valid;
for (uint i; i < sigs.length; i++) {
address signer = ecrecover(hash, sigs[i].v, sigs[i].r, sigs[i].s);
require(isGuardian[signer], "not a guardian");
valid++;
}
require(valid >= QUORUM, "insufficient signatures");
require(!used[hash], "replay");
used[hash] = true;
IERC20Mintable(p.token).mint(p.recipient, p.amount);
}
```
Every mint re-derives signers from signatures and a hash. No trust in external state.
What Wormhole Actually Did
The Solana program split verification across two instructions. Instruction 1: `verify_signatures` writes results into a `SignatureSet` account. Instruction 2: `post_vaa` reads that account and proceeds if it looks valid.
The vulnerable Rust path:
```rust
// vulnerable: uses deprecated solana_program sysvar helper
let current_instruction = solana_program::sysvar::instructions
::load_current_index(&instruction_sysvar_account_info.try_borrow_data()?);
// pulls the *previous* instruction and trusts that it was verify_signatures
let sig_ix = solana_program::sysvar::instructions
::load_instruction_at(
(current_instruction - 1) as usize,
&instruction_sysvar_account_info.try_borrow_data()?,
)?;
```
The Solana runtime had introduced a *new* sysvar account, `Sysvar1nstructions1111111...`, used by the safer `load_instruction_at_checked`. The old helper didn't validate that the account passed in was actually the system instructions sysvar. So the attacker passed a fake account they controlled, populated with data that looked like a successful `verify_signatures` call, and the program happily moved on to mint.
Result: 120,000 wETH minted on Solana, no ETH locked on Ethereum. ~$326M at the time. Jump Crypto refilled the treasury within 24 hours to prevent a wETH depeg — generous, but the bug was real.
The Fix
Wormhole's patch replaced the deprecated call with the checked variant, which verifies the sysvar account is the canonical one:
```rust
// fixed: load_instruction_at_checked validates the sysvar account address
use solana_program::sysvar::instructions::{
load_current_index_checked,
load_instruction_at_checked,
};
let current = load_current_index_checked(instruction_sysvar_account_info)?;
let sig_ix = load_instruction_at_checked(
(current as usize).checked_sub(1).ok_or(InvalidPrevious)?,
instruction_sysvar_account_info,
)?;
require!(sig_ix.program_id == BRIDGE_ID, BadProgram);
require!(sig_ix.data[0] == VERIFY_SIGNATURES_DISCRIMINATOR, BadIx);
```
The deeper fix is architectural: don't split signature verification across instructions if you can avoid it, and never trust an account's contents without verifying its address.
The Pattern, Generalized to Solidity
EVM bridges have their own version of this bug. Anywhere you see a contract trusting a struct that was "verified earlier" without re-checking signatures at the action site, you have the same class of vulnerability.
Bad:
```solidity
mapping(bytes32 => bool) public verifiedDigests;
function verify(bytes32 digest, bytes[] calldata sigs) external {
// ... check sigs ...
verifiedDigests[digest] = true;
}
function mint(bytes32 digest, address to, uint256 amt) external {
require(verifiedDigests[digest], "not verified");
token.mint(to, amt);
}
```
If an attacker can set `verifiedDigests[digest] = true` through any other code path — admin function, upgrade, storage collision, a buggy `verify` — they mint forever. Single-transaction atomic verification is safer.
Good:
```solidity
function mint(
bytes32 digest,
bytes[] calldata sigs,
address to,
uint256 amt
) external {
require(!used[digest], "replay");
uint256 ok;
address last;
for (uint i; i < sigs.length; i++) {
address s = ECDSA.recover(digest, sigs[i]);
require(isGuardian[s] && s > last, "bad signer");
last = s;
ok++;
}
require(ok >= quorum, "quorum");
used[digest] = true;
token.mint(to, amt);
}
```
Notice: signatures verified in the same call that performs the privileged action, sorted-signer check to prevent duplicates, replay protection on the digest, no external state trusted.
How To Catch This Before Mainnet
Three things every bridge team should do:
1. **Property-based fuzzing on the verification path.** Echidna / Foundry invariant tests that try to mint with arbitrary calldata. Invariant: `totalMinted <= totalLocked`.
2. **Cross-chain dependency audits when SDKs change.** The Wormhole bug shipped because a Solana SDK deprecation note was missed. Diff your dependencies on every release.
3. **Get a second pair of eyes on every bridge contract.** Run a [free AI audit](https://www.cryptohawking.com/audit) first to catch the obvious stuff, then commission a [manual audit](https://www.cryptohawking.com/audit/manual) for the bridge logic — bridges are too high-stakes for a single pass.
Other Bridges That Learned The Hard Way
Wormhole isn't alone. Ronin lost $625M to compromised validator keys (a different class — multisig governance). Nomad lost $190M because a zero `committedRoot` made every message look valid. Different bugs, same theme: the verification step was either bypassed or trusted blindly.
Bridges concentrate value across chains and have the largest attack surface in DeFi. Treat every signature verifier like it's guarding the entire TVL — because it is.
FAQ
Why didn't Wormhole's audits catch this?
The bug was introduced by a Solana SDK change after the initial audits. The deprecated `load_instruction_at` helper became unsafe only when the runtime started accepting an alternative sysvar account address. Audits are point-in-time; dependency drift is one of the most underrated sources of new vulnerabilities. The lesson is to re-audit after any major upstream upgrade, and to write invariant tests that don't rely on the auditor remembering every SDK quirk.
What is a VAA in Wormhole?
A VAA (Verified Action Approval) is the message format Wormhole guardians sign to attest that something happened on a source chain — a token lock, a governance vote, a generic message. It contains a payload, a timestamp, a nonce, an emitter address, and a set of 13-of-19 guardian signatures. The destination chain validates those signatures against the current guardian set and then executes the payload, typically minting a wrapped asset.
Could the same bug happen on an EVM bridge?
Yes, in spirit. The EVM equivalent is any pattern where one transaction marks a digest as "verified" and a later transaction trusts that flag without re-checking signatures. Storage collisions during upgrades, missing access control on the verifier, or unsafe delegatecalls can flip that flag. The defense is the same as on Solana: verify signatures atomically in the same call as the privileged action, and never trust mutable state as your only gate.
Did Wormhole users lose money?
End users didn't, because Jump Crypto — Wormhole's parent — backstopped the ~$326M loss within 24 hours by depositing equivalent ETH into the bridge. This prevented wETH on Solana from depegging and unwinding into a death spiral. It was one of the largest single-entity bailouts in crypto history and a reminder that most bridges are only as solvent as their backers' willingness to make users whole.
What's the minimum bar for shipping a bridge safely?
At minimum: signature verification in the same atomic transaction as the mint, sorted-signer deduplication, replay protection per digest, quorum thresholds enforced on every action, no admin function that can mint without a VAA, monitored guardian-set rotation, invariant tests asserting `mintedOnChainB <= lockedOnChainA`, and at least one independent manual audit focused on the cross-chain message handler. Skipping any one of these has historically cost nine figures.