Nomad Bridge
Summary
ExpandThe Nomad Bridge codebase contains a critical vulnerability in the Replica contract's initialization logic that permits acceptance of a zero-value Merkle root as valid. Because `confirmAt[bytes32(0)]` is set to `1` (a timestamp permanently in the past), an attacker can prove arbitrary messages against the zero root if the `committedRoot` is ever initialized or reset to `bytes32(0)`. This is an existential risk to all funds bridged through the protocol.
Beyond the critical initialization flaw, the codebase has a secondary vector in `Replica.update()` that permits the updater to transition `committedRoot` to `bytes32(0)`, which is compounded by the fact that the `UpdaterManager.slashUpdater` function is a no-op — meaning there is no economic penalty for a malicious updater. The GovernanceRouter also contains an unbounded array growth pattern that could eventually cause denial-of-service on governance operations.
The most urgent recommendation is to add zero-root validation in the Replica contract's `initialize` and `update` functions, and to implement actual updater slashing to provide economic security guarantees.
Findings
4 issues identified
Replica accepts zero committed root, enabling arbitrary message proving
Replica.sol:112-127
View Details
Description
The Replica.initialize() function accepts bytes32(0) as _committedRoot and sets confirmAt[bytes32(0)] = 1. Since timestamp 1 is permanently in the past, acceptableRoot(bytes32(0)) always returns true. An attacker can craft a Merkle proof that computes to bytes32(0), prove any arbitrary message, and process it to steal bridged funds. This is the exact vulnerability exploited in the August 2022 Nomad bridge hack (~$190M stolen).
Impact
Complete theft of all bridged funds. Arbitrary message injection to any recipient on the destination chain. Total protocol compromise.
Recommendation
Add require(_committedRoot != bytes32(0), “committed root cannot be zero”) to the initialize function.
Fix
Adding a zero-root check prevents bytes32(0) from being set as a valid committed root during initialization, eliminating the attack vector where confirmAt[0x0] = 1 makes the zero root permanently acceptable.
solidity/nomad-core/contracts/Replica.sol
UpdaterManager.slashUpdater is a no-op, removing economic security guarantee
UpdaterManager.sol:80
View Details
Description
The slashUpdater() function only emits a FakeSlashed event and performs no actual slashing. The updater faces no economic penalty for submitting fraudulent updates, completely undermining the optimistic fraud proof security model that the protocol depends on.
Impact
A compromised or malicious updater can submit fraudulent merkle roots with no economic consequence. While the Home enters FAILED state, fraudulent roots already relayed to Replicas may be processed during the optimistic timeout window. The lack of deterrence fundamentally weakens the security model.
Recommendation
Implement actual slashing by at minimum rotating the updater to address(0) and transferring any held ETH to the reporter.
Fix
Sets the updater to address(0) to prevent any further use of the compromised updater, and transfers any ETH held by the contract to the reporter as a bounty. This provides minimal economic incentive for fraud reporting. A complete solution should add a bonding mechanism.
solidity/nomad-core/contracts/UpdaterManager.sol
GovernanceRouter _removeDomain leaves gaps causing unbounded array growth
GovernanceRouter.sol:404-412
View Details
Description
_removeDomain uses delete domains[i] which sets the element to 0 but does not reduce the array length. Over time, the domains array grows with zero-value holes that are never reclaimed. Multiple functions iterate over the entire array (_sendToAllRemoteRouters, _setRouterGlobal), and eventually the gas cost could exceed the block gas limit, permanently bricking governance operations.
Impact
Governance operations become increasingly expensive. In extreme cases, governance could become permanently unusable if the array grows beyond the block gas limit, preventing governor transfers and router updates.
Recommendation
Use swap-and-pop pattern to keep the array compact.
Fix
The swap-and-pop pattern replaces the removed element with the last element and reduces array length by one, keeping the array compact and preventing unbounded growth. Domain ordering is not required by any consuming code.
solidity/nomad-core/contracts/governance/GovernanceRouter.sol
Stale Replica remains trusted after domain re-enrollment in XAppConnectionManager
XAppConnectionManager.sol:118
View Details
Description
When ownerEnrollReplica is called to enroll a new Replica for a domain that already has an enrolled Replica, the function calls _unenrollReplica on the NEW replica (clearing its previous domain mapping) but does not unenroll the OLD replica that was previously registered for the target domain. The old Replica retains its replicaToDomain entry, so isReplica(oldReplica) continues to return true. The stale Replica passes onlyReplica checks in GovernanceRouter and other xApp consumers.
Impact
A compromised or stale Replica that has been replaced continues to be treated as valid. Messages delivered through the old Replica are accepted by governance and xApp routers, potentially allowing injection of fraudulent cross-chain messages even after the Replica was supposedly replaced.
Recommendation
Before enrolling the new Replica, unenroll the existing Replica for the target domain to clear its replicaToDomain mapping.
Fix
By unenrolling the existing Replica for the target domain before enrolling the new one, the old Replica's replicaToDomain entry is cleared to 0, causing isReplica to correctly return false for the stale Replica.
solidity/nomad-core/contracts/XAppConnectionManager.sol
Conclusion
ExpandThe Nomad Bridge codebase contains a critical vulnerability in the Replica contract that permits arbitrary cross-chain message injection when the committed root is zero-valued. This exact vulnerability was exploited in August 2022 for approximately $190M, making it one of the largest bridge hacks in DeFi history. The root cause is the combination of accepting bytes32(0) as a valid committed root and setting its confirmation time to 1 (always in the past).
The protocol’s security model assumes that updater misbehavior is economically punished through bonding and slashing, but the UpdaterManager.slashUpdater function is a no-op placeholder. This means even if the zero-root attack via initialize is patched, a compromised updater can submit a zero-root update through Replica.update() with no financial penalty.
The remaining findings (unbounded governance array growth, domain-0 edge case) are lower severity but should be addressed to improve protocol robustness. The codebase demonstrates generally sound architecture with the optimistic verification model, but the zero-root validation gap represents a fundamental safety check that must be present for the protocol to maintain its security guarantees. Deployment to mainnet should not proceed without addressing C-1 and H-1.
Legal Disclaimer: This report covers the code submitted for analysis. It does not account for infrastructure, deployment configuration, third-party dependencies, or changes made after the audit date. Automated analysis may produce false positives or miss context-dependent vulnerabilities. audited.xyz provides this report “as is” without warranty of any kind.