August 2nd, 2022
In August 2022, crypto bridge Nomad was the victim of a chaotic hack in which many different users piled on to drain value from the project. By taking advantage of an error accidentally introduced in an update, exploiters were able to drain over $190 million in value from the blockchain protocol.
The incident was discovered based on a series of transactions over the Nomad Bridge between the Moonbeam and Ethereum networks. Transactions sending 0.01 WBTC to the bridge from Moonbeam released 100 WBTC on the Ethereum network.
In theory, processing these messages should be a two-step process. The first step should be proving the validity of the transaction, followed by processing it. However, the transactions to the bridge only called the process() within Replica.sol without proving validity. While the proof and processing could be split across multiple transactions and blocks, no earlier proof existed for these transactions.
Within the process() function is an assert (line 185) that validates that the message for the transfer is associated with a valid root. By default, a root for an unproven message would be 0x00.
In an upgrade to the protocol, Nomad decided to initialize the value of trusted roots to 0x00. While this is common practice, it also matches the value for an untrusted root, so all messages are automatically viewed as proven.
Once the issue was discovered, it was exploited in a stream of transactions. Even if a user didn’t understand the actual mechanics of why it worked, exploiting it simply required taking a successful exploit transaction and submitting it with their own account address.
As a result, approximately $190 million was drained from the bridge.
This attack against the Nomad bridge was made possible by an oversight during an update to the project’s contracts. While changing the initialization value of trusted roots to 0x00 was, in itself, logical, it had unforeseen implications.
This exploit demonstrates the importance of performing a comprehensive security audit on smart contract code before deployment. This issue with the process() function’s validation of message roots could have been detected via fuzzing or other common test techniques.