Wormhole is a message-passing protocol enabling the transfer of tokenized assets between supported chains. In other words, it allows one to send ETH from Ethereum and receive a wrapped wormhole ETH (wETH) on Solana. Thanks to that, developers can also create cross-chain dApps.
The basic principle of operation is that each supported chain has a wormhole contract that emits or processes wormhole messages. The system relies on so-called ‘Guardians‘, a distributed set of nodes that monitor the state of transactions and after consensus sign the messages emitted by the wormhole contracts. When a user sends ETH to Solana, the system locks the tokens into a contract on the source chain (Ethereum) and when the Guardian nodes approve that it happened, they sign the message, and another contract on the destination chain is called to issue the parallel ‘wrapped’ token.
In February 2022, an attacker stole assets worth roughly $338M (at the time of the exploit) by circumventing the signing process and minting 120k wrapped ETH tokens on Solana without locking an equivalent on Ethereum. Even though the Wormhole team sent a message to the hacker offering a white hat agreement, there was no response. The hacker used 93,750 of the minted wrapped ETH to redeem back to equivalent ETH tokens on Ethereum, thus draining the money from Ethereum liquidity pool. The rest of the tokens were sold for SOL and USDC, as seen in the hacker’s wallet. Wormhole’s investor, Jump Crypto replenished all 120k ETH, virtually bailing Wormhole out.
The Wormhole project is open-source, and the public repository is accessible to anyone. Interestingly enough, the hack happened only a few hours after a critical bug was fixed and committed to the public repository and before a new version of the program was deployed.
The bug’s root cause was a deprecated function
being used during the Wormhole signature verification. The way to build custom instructions that ‘do’ signature verification is by sending a transaction made of (at least) two instructions and checking that the native program instruction was sent. As described in the secp256k1_instruction.rs documentation, it is necessary to:
- Load the secp256k1 instruction data with
load_instruction_at_checked or get_instruction_relative;
- Check that the
program ID is equal to
so that the signature verification cannot be faked with a malicious program;
- Check that the public keys and messages are the expected values per the program’s requirements.
Unfortunately, Wormhole’s implementation of the signature verification did not use the suggested
function and instead used a deprecated version
This function does not check that the origin of data to deserialize is actually from the sysvar account and not a fake account. That way, the attacker was able to:
- Create an account 2tHS1cXX2h1KBEaadprqELJ6sV9wLoaSdX68FqsrrZRd with
instruction data earlier within different transaction;
where he replaced the sysvar::instructions account with the malicious account created in the first step.
Signature verification normally requires two instructions within one transaction:
secp256k1 and verify_certificate.
However, the attacker managed to call only the single
instruction and fake the instruction input data, so the
did not recognize it was not called together with the
instruction and passed the verification. You can check the transaction yourself. Once the verification was complete, it was possible to call the usual instruction
to obtain the so-called ‘validator action approval’ and subsequently complete_wrapped instruction to mint the 120,000 ETH.
In simple words, the Wormhole hack was quite sophisticated on one side, but after all, the root cause was a missing ownership check on one account, which allowed the hacker to fake that he had money on his account on one blockchain. And due to the lack of checks, he managed to mint the money on another blockchain.