Incident,
Source: De.Fi incident database.
- Audits before it
- 1.
- Audits since
- None on Trustblock.
As of 14 Oct 2026, Nomad has 1 audit report on Trustblock, by Quantstamp, dated 6 Jun 2022. That report lists no open critical findings. It also holds 1 incident (latest 31 Jul 2022).
Record last updated . It holds audits and findings, contracts and incidents up to Oct 2026. Statuses are the security firms’ own. Trustblock does not review code.
Newest first
| Audit | Security firm | Date | Scope | Critical findings | Report |
|---|---|---|---|---|---|
| Nomad | Security firm: Quantstamp | Date: | Scope: Files named in the report | No open critical findings | Open · certificate.quantstamp.com |
Statuses are the security firm’s own, from each report.
| Severity | Open | Acknowledged | Fixed |
|---|---|---|---|
| Critical | 0 | 0 | 0 |
| High | 0 | 0 | 1 |
| Medium | 2 | 0 | 4 |
| Low | 4 | 24 | 5 |
The `_dust` method is used to send recipients a substantial amount of `Ether` for gas bootstrapping (0.06 `Ether`, which is around 140 USD currently). The method only checks that the recipient ETH balance is less than the `DUST_AMOUNT`. A malicious trader could repetitively send small trades, to constantly receive the `DUST_AMOUNT` and drain the `BridgeRouter` from Ether.
Open the reportWhen transferring the governor, the `GovernanceRouter` will call the `_sendToAllRemoteRouters` method to send the message to all remote chains. However, the method does not validate if a domain has no router configured (empty router). If the domain has no router, the method will still dispatch the message without any indication that the method will fail on the receiving domain.
Open the reportThe standard ERC20 implementation contains a widely known racing condition in it approve function, wherein a spender can witness the token owner broadcast a transaction altering their approval and quickly sign and broadcast a transaction using transferFrom to move the current approved amount from the owner's balance to the spender. If the spender's transaction is validated before the owner's, the spender will be able to get both approval amounts of both transactions.
Open the reportThe owner of a `BridgeToken` instance (`BridgeRouter`) can burn tokens of any address, even without the token owner's consent. While the router is unlikely to steal tokens, future iterations of the router could mistakenly burn tokens without the user consent.
Open the reportCustom ERC20 tokens might not work as expected due to faulty or malicious implementations (for example, not transferring tokens or burning tokens as expected). **Note**: This is marked low, as only the owner can enrol custom ERC20 tokens.
Open the reportTo rotate the `updater`, the governor will need to send the command via the `GovernanceRouter` to call the `UpdaterManager.sol:setUpdater` function to update the `updater` in `Home` and call the `Replica.sol:setUpdater` to update the one in the `Replica`. However, the governance domain (with a valid governor) will have the updater rotated immediately, while the other domains remain the old updater until the `optimisticSeconds` (30 min) pass. During this period, the signature required in the governance domain will be different from the other domains. Relayer relies on the `Update` event on-chain to collect the signature of the updater when `Home.sol:update` is called. The divergence of the updater across domains will cause the relayer to fail to collect the signature that the replay can send to the replica, since the home and replica can require a different signature of the updater.
Open the reportIn the `_handleTransfer` function `(L348)`, when transferring standard ERC20 deflationary tokens, the input amount may not be equal to the received amount due to the charged (and burned) transaction fee. As a result, this may not meet the assumption behind these low-level asset-transferring routines and will bring unexpected balance inconsistencies, this technique is used for the same chain.
Open the reportThere are several contracts in the Nomad codebase that are deployed as proxies to enable easy upgrades. When proxies are created the deployment script calls the corresponding method to initialize important functionality, like the owner of the contract. Further, calls to this method are not allowed, as it is protected by the `initializer` modifier. However, this initialize method is never called on the implementation contract itself. Therefore, an attacker could call the initializer method and take ownership of the implementation contract.
Open the reportFrom the discussion with the Nomad dev team, the default and the recommended way to add a new dapp on top of the Nomad message system is to reuse the `Replica` contract, but each `Router` will have its own `XAppConnectionManager`. The `XAppConnectionManager` can configure the watcher that the dapp trusts. Suppose the updater provides an invalid update to the `Replica`. In that case, the watcher can call `XAppConnectionManager.sol:unenrollReplica` within the `optimisticSeconds` to stop the message from being processed on the `Router` of the dapp. However, a single invalid update on the replica will trigger un-enrollment for all the routers connected to the replica. There is a capacity (gas) limit on how many un-enrollment calls the chain can handle within the `optimisticSeconds`.
Open the reportNomad architecture heavily relies on the off-chain components to sync with the on-chain status. It is crucial to emit an event on critical state updates.
Open the report``` - In `contracts-core` - `Home.sol:update`: there should be check on `_newRoot != _committedRoot` - `Home.sol:setUpdater` and `Replica.sol:setUpdater`: should check the `_updater` is not zero address. Furthermore, we can add validation that the updater should not be a contract, as the signature of the updater is required. - `Replica.sol:update`: there should be check on `_newRoot != _committedRoot`. Updater can try to override the `confirmAt` timestamp in `Replica.sol:L153` without this check. - `XAppConnectionManager.sol:unenrollReplica`: - `_domain` should not be 0 - `_updater` should not be empty `bytes32.` - `_signature` should not be empty `bytes.` - `XAppConnectionManager.sol:setHome`: - `_home` should not be zero address - `XAppConnectionManager.sol:ownerEnrollReplica`: - `_replica` should not be zero address - `_domain` should not be 0 - `_domain` should be checked with the public variable `remoteDomain` of the replica. e.g. `Replica(_replica).remoteDomain() == _domain`. - `XAppConnectionManager.sol:ownerUnenrollReplica`: - `_replica` should not be zero address - `XAppConnectionManager.sol:setWatcherPermission`: - `_watcher` should not be zero address - `_domain` should not be 0 - `governance/GovernanceRouter.sol:transferRecoveryManager`: should check `_newRecoveryManager` is not zero address. - `governance/GovernanceRouter.sol:initialize`: should check `_xAppConnectionManager` and `_recoveryManager` is not zero address - `upgrade/UpgradeBeacon.sol:constructor`: should check `_controller` is a smart contract. - In `contracts-bridge` - `BridgeRouter.sol:send` should check `_destination` not being 0 - `BridgeRouter.sol:enrollCustom` should check `_domain` is not 0 and `_id` is not empty. Also, it should check that the input `_domain` is not the local domain. - `BridgeRouter.sol:migrate` should check `_oldRepr` is not zero address - `BridgeRouter.sol:preFill` should check `_origin` not being 0 - In `contracts-router` - `Router.sol:enrollRemoteRouter` should check `_domain` is not 0, and `_router` is not empty bytes - In `zodiac-module-gnomad` - `GnomadModule.sol:constructor` - check `_avatar`, `_target`, `_manager` is contract. (e.g., with open-zeppelin library: [doc](https://docs.openzeppelin.com/contracts/4.x/api/utils#Address-isContract-address-)) - check `_controller` is not a zero address - check `_controllerDomain` is not zero - `GnomadModule.sol:setUp - check `_avatar`, `_target`, `_manager` is contract - check `_controller` is not a zero address - check `_controllerDomain` is not zero - `GnomadModule.sol:onlyValid`: check that `_sender` starts with only padding 0s for the first 12 bytes. - `GnomadModule.sol:setManager`: check `_manager` is contract - `GnomadModule.sol:setController` - check `_controller` is not a zero address - check `_controllerDomain` is not zero ```
Open the reportThe `onlyGovernor` and the `onlyGovernorOrRecoveryManager` modifiers check that the `msg.sender` is an address registered as `governor` (or `recoveryManager`) or if the address is the `GovernanceRouter` contract itself. However, the `_callLocal` method is using Solidity's low-level call function, so methods dispatched from it will have the `GovernanceRouter` as the `msg.sender`. While we couldn't find any invalid use, governance calls executed through `_callLocal` could be used to bypass the `onlyGovernor` or `onlyGovernorOrRecoveryManager` modifiers.
Open the reportOn top of the regular way of sending messages, governance actions need an external agent to call the `executeCallBatch` method. If this external agent does not call this method, the batch of actions will stay forever in `Pending` status and the governance actions will never be executed.
Open the reportThe `setDetails` method allows anyone to change the token information (name, symbol, and decimals) as long as the new details match the `detailsHash` set by the owner. This means that, with the owner's consent, it's possible to change the decimals of the token. If mishandled (mistakenly or maliciously), this could have serious consequences, as the decimals are needed to perform proper token math.
Open the reportMost methods in the `GovernanceRouter` contract are annotated with modifiers to prevent them from being executed when the contract is in recovery mode. However, the `executeCallBatch` is not annotated with any of these modifiers; it can be called regardless of the recovery status. It is unclear if this is intentional or if it's missing the right modifier. **Note**: While this method could bypass the recovery mode, the calls to execute originated from the governor router, so they are unlikely to be malicious.
Open the reportThe function `Replica.sol:prove` accepts the input `_leaf` and checks if it is part of the Merkle tree. Nomad architecture uses a sparse Merkle tree, in which all the non-used leaves default with empty `bytes32`. This nature of the sparse Merkle tree makes it possible for one to pass an empty `bytes32` as the `_leaf` and some artificial Merkle proof with a specified index to pass the inclusion check. The “empty leaf” message status can later be flagged as `PROVEN`, resulting in the `messages` mapping in an undesired state.
Open the reportIn the `improperUpdate` function, we are taking the `_signature` as a parameter to verify the correctness of the transaction. The issue here is that the signature is not associated to any nonce or address. Thus, the signature can be replayed since there is no nonce associated.
Open the reportThe contracts have a hard-coded “domain” value set on deployment. The hardcoded domains will work until the chain is hard forked. Once a hard fork occurs, the signature of both chains will be considered valid on both the original and the hard-forked chain.
Open the reportThe attacker can replay the message's signature if there are multiple deployments in the same chain running with the same updater. In the worst case, one can use it to call `Home.sol:improperUpdate` to fail the home contract.
Open the report`BridgeToken.sol:permit` function receives the signature and uses `ecrecover` to get the recovered address to verify the signature. However, the `ecrecover` call does not protect from the malleable signatures of ECDSA (see: [SWC-117](https://swcregistry.io/docs/SWC-117)). As a result, it will be possible for another user to replay the call with a different signature even without holding the address's private key and still passes the verification. Fortunately, the current implementation includes a `nonce` which will increase each time when building the `digest` of the signature. The immediate increase in the nonce avoids replay attacks with this vulnerability.
Open the reportMessage is the first-class citizen of the Nomad system. However, it uses a dependency `TypedMemView.sol` in which the implementation aggressively uses assembly. We recommend taking extra care on all the messages and adding stricter validation to reduce the risk of an invalid message passed across the domain.
Open the reportWe will describe the risk from a dapp operator's point of view and a user of the dapp's perspective. From the dapp operator's point of view, the most considerable risk is a fraudulent updater. The private key of the updater is mostly going to be in a hot wallet, as it has to sign the update frequently. If the key is ever leaked, the hacker can use the key to stop the Nomad service by signing invalid updates to the replica. The invalid update forces the dapp operator/watcher to cut off the router's replica connection. However, this usually only solves half of the problem. If we look at the standard pattern of a cross-chain action: 1. First, perform some action in the home domain. 2. Dispatch the message via the home contract 3. Updater signs the update 4. Relayer sends the update to the replica. 5. Process it in the router. The problem is in the first step. Some action is already performed in the home domain. Although the watcher can stop invalid messages from being processed, we cannot roll back the changes on the home domain. For instance, for a token bridge, the token is already locked in the bridge router before dispatching the “transfer” message. If any fraud was to occur, we could not easily roll back the result of the token-locking in the home domain. Unless we introduce a better flow, the dapp on top of Nomad will likely need some privileged roles to recover from the failure state in the worst case. From the dapp users' point of view, they have to trust the dapp operator (watcher). If the watcher colludes with the updater, they can process any invalid messages to the router. Also, they should be aware of the crypto-economy nature of the bond-slashing mechanism of Nomad. For instance, in a token bridge dapp, it might be best practice not to send tokens value more than X% (e.g., 50%) of the slash-able bonds from the updater so that it is less motivated to perform fraud. The correct operation of the system is completely reliant on external agents performing crucial tasks. If one or more of them stops working as expected (due to malfunction or maliciously), it could put the whole system at risk and endanger the operations happening in it. In particular, we have detected the following problems: - Updater: Currently, there's no slashing mechanism to punish updaters that have committed fraud. Without it, external updaters will have significant incentive to participate on fraud. - Relayer: Relayers are supposed to forward updates from the home contract to the replicas in other chains. However, relayers might delay forwarding the updates (maliciously or due to heavy load) to one or more chains. The impact from this could range from delays to full denial-of-service (messages won't be able to be delivered to their destination). - Processor: Processors are supposed to prove and send the messages to the replica, who will eventually send them to the end recipients. However, there is no mechanism to ensure that the processor sends ALL messages or that it sends them in a timely manner; a malicious processor might decide not to send one or more messages that are not convenient to it. - Watcher: Watchers are the agents that keep the other participants honest. They are supposed to observe homes and replicas to ensure that they are working properly. However, if watchers are not working properly (due to malfunction or if they are colluding with other malicious actors) fraudulent operations might go through the system. **Note:** The documentation does not describe how external agents are chosen, rotated, how they stake their funds, and how they are slashed if a malicious operation is detected. Without this information, it's not possible to assess if the economic incentives are sufficient to discourage fraud throughout the system.
Open the reportIf the updater stops signing new messages, it can freeze the system without penalty. However, the impact on the dapps on top of it can be huge. We will analyse some exploit scenarios that this attack can cause. - Governance Router : First, the governance router relies on the Nomad message system to dispatch the governance commands from the main governance domain to other domains. Nonetheless, the updater can attack the governance by refusing to sign the governance commands or trying to update an invalid root to force the governance router to unroll from the replica and not be able to force-drop the governance command. The risk of not applying the governance commands can be huge to the nomad system, as it is usually about system updates or incidence handling. Fortunately, the current implementation has the role of `recoveryManager`, which can switch the `GovernanceRouter` to the recovery mode and take over the control to do actions directly on the `GovernanceRouter`. Note that there will be a one-day delay of `recoveryTimelock` according to the configuration in the deployment script. - Updater: Secondly, the updater can also apply this attack to the token bridge. Suppose the updater denies signing the token bridge update with the current bridge implementation. In that case, it will lock the user's token without a new one minted in the destination domain. The token is locked because `BridgeRouter.sol:send` will call `IERC20(_token).safeTransferFrom` to secure the funds needed to mint on the other side of the bridge. **Exploit Scenario: loss of governor**: 1. The governor calls `GovernanceRouter.sol:transferGovernor` and transfers the governor from domain 1 to domain 2. 2. The updater refused to sign the update. 3. Now, the variable `governor` in the `GovernanceRouter` in domain 1 and 2 becomes `address(0)`.
Open the reportCommunication between the different smart contracts across the system is supposed to work in perfect unison. However, if contracts are not upgraded carefully, it's possible that newer contracts will end up communicating with older (potentially not compatible) versions of other contracts; this could have unforeseen consequences.
Open the reportAttached through a shared contract address. Our incident data ends in Oct 2026.
Source: De.Fi incident database.
The reports name a repository or files. Open a report to see its scope.
Is this your project? Show this record on your own website. The label states what the record holds, links to this page and updates by itself. It is free and needs no sign-in.
Automatic: the three strongest facts of this project’s own record (its last audit and two more), highlighted. They update with the record. Picking a fact turns Automatic off.
<head><script src="https://widget.trustblock.run/project-widget.min.js" defer></script></head><body><divdata-tb-project-widgetdata-tb-project-slug="nomad"></div></body>
Paste the code where the label should appear. It uses no API credit and updates by itself.
Something wrong on this page? A missing report, a wrong date, a finding that was fixed since. Tell us and we check it against its source.
Tell us