if (_time == 0) {
return false;
}
uint256 _time = confirmAt[_root];
return false;
}
return block.timestamp >= _time;
uint256 _time = confirmAt[_root];
if (_time == 0) {
__NomadBase_initialize(_updater);
// set storage variables
entered = 1;
remoteDomain = _remoteDomain;
committedRoot = _committedRoot;
// pre-approve the committed root.
confirmAt[_committedRoot] = 1;
_setOptimisticTimeout(_optimisticSeconds);
uint32 _remoteDomain,
address _updater,
bytes32 _committedRoot,
uint256 _optimisticSeconds
}
) public initializer {
function initialize(
// ...
// ...
require(messages[_messageHash] == MessageStatus.Proven, "!proven");
}
function process(bytes memory _message) public returns (bool _success) {
// ...
require(acceptableRoot(messages[_messageHash]), "!proven");
// ...
}
function process(bytes memory _message) public returns (bool _success) {
This change was part of a broader revision of the message processing semantics. Previously, the require operator would permanently block the processing of valid messages if they were associated with an invalid attestation. The new require operator prevents a malicious updater from blocking the delivery of valid messages by providing false attestation. To implement this, a mechanism was introduced that maps messages to roots, as described in the "Acceptable Root" section above.
Previously, the require operator mandated that the value in the messages mapping was not equal to 0. This value could only be set through a prior call to the prove function. After the change, a value of 0 could be loaded from the mapping and passed into the acceptableRoot function.
Previously, the require statement in the process function was written as follows:
Nomad also encouraged a public code review through the ImmuneFi bounty program, which was launched on June 9. However, this issue was not identified as part of the bounty program.
The Replica contracts have been removed from the Nomad Bridge and Nomad Governance. This prevents any messages from being processed by the bridge and reduces the impact of the vulnerability in the process function. At the same time, new bridge transactions can be initiated but will not be processed. The NomadBridge graphical interface has been disabled. Users are advised to refrain from using the bridge until a full fix and system restart are completed.
Nomad Watchers monitor for key compromise of the Updater and respond accordingly, but so far they do not track suspicious activity related to smart contract errors. Since the vulnerability was inside the process function of the contract, messages did not require a forged Updater signature and therefore did not trigger the Watchers. As a result, no Watcher took any action.
What is the current state of the system?
Why did Nomad Watchers not respond to this situation?
The updates to the production environments were deployed on June 21, 2022, in the following transactions:
When was this change deployed to the network?
This update to the Nomad system was reviewed by Quantstamp in May and early June 2022. The final report was received on June 9. The change was implemented on May 26, during the audit period, as part of corrections and improvements related to the audit (specifically addressing issues QSP-2 and QSP-34), and was included in a commit with the specified hash used for re-auditing after addressing the comments.
What details of the audit of the Replica contract update conducted by Quantstamp?
The corresponding code was implemented in the smart contract update on June 21, 2022.
When was this code introduced?
Since _time equals 1, this block is skipped.
This allows unconfirmed messages to pass the subsequent check in the process function. Consequently, it enables the processing of messages without prior confirmation.
Because _time is equal to 1, any valid block timestamp will be greater than or equal to 1. As a result, the function acceptableRoot(bytes32(0)) will always return true for such Replica contracts.
When a message is sent to the process function, the protocol retrieves the root from this mapping and checks whether the acceptableRoot function returns true. This function should return true only if the waiting period for the valid root has elapsed. It also accounts for special deprecated root values (from a previous system version) as well as roots that have not been confirmed (i.e., have a timestamp of 0 in the roots mapping). The function determines whether the timer has expired by comparing it to the current block time: return block.timestamp >= _time;.
After the root is transmitted to another chain, the inclusion of a message in the tree is verified using a Merkle proof. The root under which the message was proven is stored in a mapping (mapping(bytes32 => bytes32)) within the Replica contract. This mapping links the hash of the message to the root that confirmed it. In this case, the default value for bytes32 is bytes32(0). Therefore, any message that has not been confirmed will have bytes32(0) as its root in this mapping.
The Replica contract tracks roots from other chains using a mapping (mapping(bytes32 => uint256)). This mapping associates roots with timestamps indicating when they become valid. Message processing is prohibited until the optimistic waiting period for the corresponding root has elapsed. When reading from the mapping, if a value has not yet been set, it returns the default value (the so-called zero value). For the uint256 type, this default value is 0. Therefore, any root that has not been confirmed will have a timestamp of 0 in this mapping.
However, if the Replica is deployed simultaneously with the corresponding Home contract—such as during initial deployment—the Home's Merkle tree contains no messages. In the Nomad implementation, a Merkle tree without leaves has a root of bytes32(0). Therefore, a Replica deployed at the same time as the Home contract will be initialized with the root bytes32(0), and confirmAt[bytes32(0)] will be set to 1.
During initialization, the value of confirmAt[_committedRoot] is set to 1. This ensures that messages included in the initial root can be processed. As a result, a newly initialized Replica can accept messages that were created prior to its deployment.
First, the value of confirmAt[_root] is loaded into a variable named _time. For unknown messages (including forged ones), this _root equals bytes32(0). In any Replica contract initialized with _committedRoot set to bytes32(0), the value of confirmAt[bytes32(0)] was set during initialization to 1.
Here are the relevant lines of the acceptableRoot function.
Line by line:
At the same time
When the Replica contract is deployed after its associated Home contract, it is initialized in a specific state. This allows new deployments to skip replaying all previous updates from the remote Home for message processing. During deployment, a parameter _committedRoot can be provided, which indicates the starting point of the message tree history from which updates will be processed.
Initialization
Nomad records cross-chain messages as a Merkle tree (referred to as the "message tree"). The root of this tree is propagated to remote chains using an optimistic mechanism.
Details
The implementation flaw caused the Replica contract to fail in properly verifying the authenticity of messages. This allowed attackers to forge any messages, provided they had not yet been processed. As a result, contracts relying on Replica for authenticating incoming messages faced security breaches. This authentication vulnerability led to the transmission of counterfeit messages to the Nomad BridgeRouter contract.
The high-level issue
}
return block.timestamp >= _time;
uint256 _time = confirmAt[_root];
if (_time == 0) {
return false;
// 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;
return block.timestamp >= _time;
function acceptableRoot(bytes32 _root) public view returns (bool) {