Analysis of the Nomad Bridge Exploit

9 min read

Learn how the Nomad Bridge was exploited, resulting in a loss of funds worth $190 million.

TL;DR#

On August 1, 2022, the Nomad Bridge was exploited on Ethereum, Avalanche, Moonbeam, Evmos, MilkomedaC1, and Gnosis Chain, resulting in a loss of funds worth over $190 million.

Introduction to Nomad#

Nomad is an optimistic interoperability protocol that enables cross-chain communication.

Vulnerability Assessment#

The exploit was possible because of a lack of input validation.

Steps#

Step 1:

We attempt to analyze the first attack transaction executed by the exploiter. It can be seen that the attacker first withdrew just 100 WBTC, calling a single `process` function.

undefined

Step 2:

The `process` function of the Replica contract is responsible for executing cross-chain messages. The security of this function is extremely paramount, as every bridge message ends up here.

/**
 * @notice Given formatted message, attempts to dispatch
 * message payload to end recipient.
 * @dev Recipient must implement a `handle` method (refer to IMessageRecipient.sol)
 * Reverts if formatted message's destination domain is not the Replica's domain,
 * if message has not been proven,
 * or if not enough gas is provided for the dispatch transaction.
 * @param _message Formatted message
 * @return _success TRUE iff dispatch transaction succeeded
 */
function process(bytes memory _message) public returns (bool _success) {
  // ensure message was meant for this domain
  bytes29 _m = _message.ref(0);
  require(_m.destination() == localDomain, "!destination");
  // ensure message has been proven
  bytes32 _messageHash = _m.keccak();
  require(acceptableRoot(messages[_messageHash]), "!proven");
  // check re-entrancy guard
  require(entered == 1, "!reentrant");
  entered = 0;
  // update message status as processed
  messages[_messageHash] = LEGACY_STATUS_PROCESSED;
  // call handle function
  IMessageRecipient(_m.recipientAddress()).handle(_m.origin(), _m.nonce(), _m.sender(), _m.body().clone());
  // emit process results
  emit Process(_messageHash, true, "");
  // reset re-entrancy guard
  entered = 1;
  // return true
  return true;
}

Step 3:

This part of the code from the above function:

bytes32 _messageHash = _m.keccak();
require(acceptableRoot(messages[_messageHash]), "!proven");

ensures that the message has been proven. It means that the message should be verified, and there should be an acceptable root for the message hash in the `messages` mapping. If the message is not proven, it reverts with the error message "!proven."

The messages are also marked with the identifier `LEGACY_STATUS_PROCESSED`, ensuring that the valid messages are executed only once.

Step 4:

The bridge keeps an internal database of valid messages as a Merkle tree. The accepted messages are included in the tree by an off-chain updater. Then, the tree root at the time of inclusion acts as a receipt i.e. it is proven.

function update(bytes32 _oldRoot, bytes32 _newRoot, bytes memory _signature) external {
  // ensure that update is building off the last submitted root
  require(_oldRoot == committedRoot, "not current update");
  // validate updater signature
  require(_isUpdaterSignature(_oldRoot, _newRoot, _signature), "!updater sig");
  // Hook for future use
  _beforeUpdate();
  // set the new root's confirmation timer
  confirmAt[_newRoot] = block.timestamp + optimisticSeconds;
  // update committedRoot
  committedRoot = _newRoot;
  emit Update(remoteDomain, _oldRoot, _newRoot, _signature);
}

function prove(bytes32 _leaf, bytes32[32] calldata _proof, uint256 _index) public returns (bool) {
  // ensure that message has not been processed
  // Note that this allows re-proving under a new root.
  require(messages[_leaf] != LEGACY_STATUS_PROCESSED, "already processed");
  // calculate the expected root based on the proof
  bytes32 _calculatedRoot = MerkleLib.branchRoot(_leaf, _proof, _index);
  // if the root is valid, change status to Proven
  if (acceptableRoot(_calculatedRoot)) {
    messages[_leaf] = _calculatedRoot;
    return true;
  }
  return false;
}

Step 5:

Given that the exploiter was able to invoke a call to the process function directly without calling the update or prove function, we can conclude that the issue was vested in this function.

Step 6:

The Replica contract introduced a newer set of features, as seen in this GitHub commit from May 27, 2022. This includes an introduction to three different constant values:

bytes32 public constant LEGACY_STATUS_NONE = bytes32(0);
bytes32 public constant LEGACY_STATUS_PROVEN = bytes32(uint256(1));
bytes32 public constant LEGACY_STATUS_PROCESSED = bytes32(uint256(2));

According to this logic, the `acceptableRoot` function in the later part of the contract should only process or execute valid or proven messages with value 1. However, this function was not entirely correct in its logic.

Step 7:

The `acceptableRoot` function failed to account for the conditional checks of `LEGACY_STATUS_NONE`. This means that for legacy messages, both 'proven' with status 1 and 'processed' with status 2 cases were handled, but not the case 'none' with status 0.

function acceptableRoot(bytes32 _root) public view returns (bool) {
  // this is backwards-compatibility for messages proven/processed
  // under previous versions
  if (_root == LEGACY_STATUS_PROVEN) return true;
  if (_root == LEGACY_STATUS_PROCESSED) return false;
  uint256 _time = confirmAt[_root];
  if (_time == 0) {
    return false;
  }
  return block.timestamp >= _time;
}

Step 8:

Now, let's verify the working nature of certain functions of this contract by pasting the source code ABI and the associated contract address into our set of web3 tools in the online Solidity ABI Encoder.

Once you connect your Metamask or other similar wallet on the Ethereum Mainnet and read the contract at the `acceptableRoot` by entering the 32-byte zero-address `0x0000000000000000000000000000000000000000000000000000000000000000`, the output of this operation is `true`. This means that the address `0x00...` was also a trusted and verified root, and thus all messages were accepted.

undefined

Step 9:

The remaining part of the `acceptableRoot` function is:

function acceptableRoot(bytes32 _root) public view returns (bool) {
  ...
  uint256 _time = confirmAt[_root];
  if (_time == 0) {
    return false;
  }
  return block.timestamp >= _time;
}

This means that the output value at `confirmAt[0]` needs to be non-zero for the transaction to succeed. Let's check the output of this function operation as well.

undefined

Step 10:

Therefore, the routine upgrade of the contract marked the messages with a value of 0 to be read by default as 0x00, which was defined in the upgrade as a trusted and valid root. The situation turned out to be catastrophic, as this case passed the validation requirement as `proven`.

Aftermath#

The team has provided an in-depth post-mortem analysis of the incident, revealing a critical lapse in the Replica contract's message authentication process. This oversight permitted message forgery, provided the message hadn't been processed previously.

It's noted that this modification to the contract was instituted in May 2022, with the production environment receiving an upgrade on June 21, 2022. The commit hash provided for the post-remediation re-audit attests to Quantstamp's thorough audit of this alteration in late May and early June 2022.

While the Nomad watchers are designed to identify and address compromises of the updater key, they weren't configured to detect potential threats stemming from smart contract vulnerabilities. Given that this vulnerability was rooted in the smart contract's function and wasn't tied to a private key compromise, no watchers were activated. As a result, the malicious activity persisted for over three hours post-community notification until the team decisively deactivated the system.

An expert exploiter could have crafted the exploit contract, which would be able to drain the entirety of the bridge for themselves in a single transaction.

Following the breakout of the news due to large-value transfers, copy-cat attackers, security researchers, the MEV bot, and other white-hat hackers were able to copy and paste the original attacker's call data, add their own address, and loot Nomad.

Amongst these notorious hackers, three addresses each labeled Nomad Bridge Exploiter 1 took away almost $47 million, Nomad Bridge Exploiter 2 took away $40 million, and Nomad Bridge Exploiter 3 took away approximately $8 million worth of funds. The Exploiter 1 also transferred approximately 23,073.401 ETH, worth $37,461,266.41 at the time of this writing, to this address. The Exploiter 3, however, transferred approximately 1,110.9 ETH, worth $1,804,328 at the time of this writing, to yet another address.

Nomad also announced a 10% bounty reward to all those users who return more than 90% of the funds they had looted during the exploit. Luckily, the actions taken by white-hat hackers provided a sigh of relief to the team, as over $36 million worth of the compromised assets were returned to the Nomad recovery address from more than 40 addresses. 

Solution#

In the world of blockchain, no system can achieve absolute infallibility. Nevertheless, by embracing a layered defense strategy, we can notably diminish vulnerabilities. The emphasis must always be on proactive measures rather than reactive responses.

Prior to the deployment of any smart contract—especially those entrusted with substantial assets or crucial functions—it's imperative for teams to undertake extensive security audits from esteemed auditing firms. An audit isn't a mere checkpoint; it's an ongoing commitment. While audits enhance system integrity, it's vital to acknowledge that they don't render it invulnerable.

A case in point is Nomad's audit by Quantstamp in June 2022. Their report, under the issue QSP-19, specifically highlighted a vulnerability that allowed an empty leaf message to be erroneously flagged as proven. This glitch could subsequently misalign the message mapping. Unfortunately, it appears the gravity of this issue was misjudged by the Nomad team, leading them to overlook the auditor's crucial observations.

In the realm of DeFi, there's a pressing need to adopt formal verification methods whenever feasible. This rigorous process involves establishing the mathematical correctness of a system, ensuring it aligns perfectly with its intended specifications. While the intricacies of formal verification might deter some, its unparalleled depth offers robust protection against unpredictable glitches and weak spots.

Additionally, the significance of a well-delineated emergency protocol cannot be overstressed. This involves having a clear blueprint detailing the immediate steps to undertake, the responsible communication channels, and the mechanisms to secure the system when breaches are detected. Preparedness can spell the difference between a manageable disruption and a calamitous downfall. Highlighting the importance of timely intervention, Nomad's breach endured for a staggering two and a half hours before any official acknowledgment. Although they ultimately countered the exploit by revoking the Replica contract's ownership status, the prolonged response time meant the assets were irrevocably compromised.

While robust security measures form the first line of defense against vulnerabilities, it's equally critical to have a contingency plan in place for potential breaches. Enter Neptune Mutual, an innovative safeguard in the complex world of DeFi. We were not available as a marketplace at the time of this incident. Therefore, the affected parties due to this exploit probably had no way of recovering their lost funds.

Had Nomad sought collaboration with us at Neptune Mutual and established a dedicated cover pool before the calamity struck, the repercussions might have been substantially less devastating. Our expertise lies in shielding users from losses stemming from smart contract vulnerabilities. We understand the intricacies of blockchain and the unpredictability that sometimes accompanies it. Thus, we've pioneered cutting-edge parametric policies to ensure users are insulated against unexpected losses.

What sets us apart is our commitment to user-centricity. We believe that in moments of crisis, the last thing users should fret about is the cumbersome process of recovering their funds. With Neptune Mutual, the claims process is transparent and streamlined. Users can confidently bypass traditional bureaucratic hurdles and promptly receive their payouts after a thorough incident resolution. This system not only offers financial protection but also provides peace of mind to our users.

Moreover, our footprint isn't limited to a single blockchain. Our marketplace spans prestigious networks like EthereumArbitrum, and the BNB chain, amplifying our reach across the DeFi domain. Such vast accessibility ensures that a diverse array of DeFi enthusiasts can avail of our services, guarding them against unforeseen risks. In essence, Neptune Mutual isn't just a protective measure—it's a trust builder, aiming to fortify the faith of users in the burgeoning world of DeFi.

Reference Source RektsamczsunZellic

By

Tags