Nomad

QuantstampFiles named in the report

Quantstamp reviewed Nomad on 11 May 2022: 40 findings, 0 critical; 10 marked fixed, 24 acknowledged, 6 open.

No open critical findings
Open the reportOpen Nomad’s security record

Report hosted on certificate.quantstamp.com.

Trustblock does not review code.

Findings40

Statuses are the security firm’s own, from its report.

SeverityOpenAcknowledgedFixed
Critical000
High001
Medium204
Low4245
Low`_dust` Method Can Be AbusedAcknowledged

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.

Low`_sendToAllRemoteRouters` Will Not Complain If The Remote Router Is Not PresentAcknowledged

When 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.

LowApprove Race ConditionAcknowledged

The 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.

LowBridgeToken Allows The Owner Of The Contract To Burn Tokens Even Without Their ApprovalAcknowledged

The 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.

LowCustom Tokens Could Be MaliciousAcknowledged

Custom 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.

LowDiverge In The Updater During Updater RotationAcknowledged

To 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.

LowIncompatibility With Deflationary TokensAcknowledged

In 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.

LowInitializer Not Disabled On Implementation ContractAcknowledged

There 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.

LowMass Un-enrollmentAcknowledged

From 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`.

LowMissing EventsAcknowledged

Nomad 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.

Show 10 more
LowMissing Input ValidationAcknowledged

``` - 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 ```

LowModifier Allows This Contract To Be The CallerAcknowledged

The `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.

LowNo Enforcement On The Governance Messages To Be DeliveredAcknowledged

On 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.

LowPossible To Change `BridgeToken` DecimalsAcknowledged

The `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.

LowPossible To Run `executeCallBatch` While Local Governance Router Is On RecoveryAcknowledged

Most 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.

LowProving With An Empty LeafAcknowledged

The 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.

LowReplay Attack In The `improperUpdate`Acknowledged

In 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.

LowSignature Replay Attack: Hard ForkAcknowledged

The 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.

LowSignature Replay Attack: Multiple DeploymentsAcknowledged

The 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.

LowSusceptible To Signature MalleabilityAcknowledged

`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.

Show 4 more
LowTighten Validations Around MessageAcknowledged

Message 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.

LowTrusted Actors Risk And External Agents Have Too Much PowerAcknowledged

We 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.

LowUpdater Stop Signing New RootsAcknowledged

If 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)`.

LowUpgradeable Contracts Can Be Working With Older VersionsAcknowledged

Communication 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.

Contracts in scope

No deployed address is recorded for this audit.

The report names a repository or files. Open the report to see its scope.

Repository and files the report names (66)

  • Nomad/monrepo/packages/contracts-bridge/contracts/TokenRegistry.sol
  • Nomad/monrepo/packages/local-environment/tests/utils/token/contract/IERC20.sol
  • Nomad/monrepo/packages/contracts-core/contracts/UpdaterManager.sol
  • Nomad/monrepo/packages/contracts-router/contracts/XAppConnectionClient.sol
  • Nomad/monrepo/packages/contracts-bridge/contracts/BridgeMessage.sol
  • Nomad/monrepo/packages/contracts-core/contracts/test/TestRecipient.sol
  • Nomad/monrepo/packages/contracts-bridge/contracts/Encoding.sol
  • Nomad/monrepo/packages/contracts-core/contracts/test/TestMerkle.sol
  • Nomad/zodiac/contracts/test/Imports.sol
  • Nomad/monrepo/packages/contracts-bridge/contracts/test/TestBridgeRouter.sol
  • Nomad/monrepo/packages/contracts-core/contracts/XAppConnectionManager.sol
  • Nomad/monrepo/packages/local-environment/tests/utils/token/contract/Token.sol
  • Nomad/monrepo/packages/contracts-core/contracts/test/MysteryMathV1.sol
  • Nomad/monrepo/packages/contracts-core/contracts/test/bad-recipient/BadRecipient2.sol
  • Nomad/monrepo/packages/contracts-core/contracts/libs/Message.sol
  • Nomad/monrepo/packages/contracts-core/contracts/upgrade/UpgradeBeaconController.sol
  • Nomad/monrepo/packages/contracts-core/contracts/test/TestMessage.sol
  • Nomad/monrepo/packages/contracts-core/contracts/test/TestCommon.sol
  • Nomad/monrepo/packages/contracts-core/contracts/test/bad-recipient/BadRecipient6.sol
  • Nomad/monrepo/packages/contracts-core/contracts/interfaces/IMessageRecipient.sol
  • Nomad/monrepo/packages/contracts-core/contracts/test/TestHome.sol
  • Nomad/monrepo/packages/contracts-core/contracts/test/TestGovernanceMessage.sol
  • Nomad/monrepo/packages/contracts-bridge/contracts/interfaces/IBridgeToken.sol
  • Nomad/monrepo/packages/contracts-core/contracts/libs/TypeCasts.sol
  • Nomad/monrepo/packages/contracts-core/contracts/interfaces/IUpdaterManager.sol
  • Nomad/monrepo/packages/contracts-bridge/contracts/test/MockCore.sol
  • Nomad/monrepo/packages/contracts-core/contracts/test/bad-recipient/BadRecipientHandle.sol
  • Nomad/monrepo/packages/contracts-core/contracts/test/bad-recipient/BadRecipient3.sol
  • Nomad/monrepo/packages/contracts-core/contracts/test/bad-recipient/BadRecipient5.sol
  • Nomad/monrepo/packages/contracts-core/contracts/Queue.sol
  • Nomad/monrepo/packages/contracts-bridge/contracts/BridgeToken.sol
  • Nomad/zodiac/contracts/test/TestAvatar.sol
  • Nomad/monrepo/packages/contracts-core/contracts/Home.sol
  • Nomad/monrepo/packages/contracts-core/contracts/upgrade/UpgradeBeaconProxy.sol
  • Nomad/monrepo/packages/contracts-core/contracts/test/MysteryMath.sol
  • Nomad/zodiac/contracts/test/Mock.sol
  • Nomad/monrepo/packages/contracts-bridge/contracts/test/TestBridgeMessage.sol
  • Nomad/monrepo/packages/contracts-core/contracts/Version0.sol
  • Nomad/monrepo/packages/contracts-bridge/contracts/vendored/OZERC20.sol
  • Nomad/monrepo/packages/contracts-core/contracts/libs/Queue.sol
  • Nomad/monrepo/packages/contracts-core/contracts/Replica.sol
  • Nomad/monrepo/packages/contracts-core/contracts/test/TestQueue.sol
  • Nomad/monrepo/packages/local-environment/tests/utils/token/contract/ERC20.sol
  • Nomad/zodiac/contracts/test/TestFactory.sol
  • Nomad/monrepo/packages/contracts-core/contracts/test/TestReplica.sol
  • Nomad/monrepo/packages/contracts-bridge/contracts/test/MockWeth.sol
  • Nomad/monrepo/packages/contracts-core/contracts/libs/Merkle.sol
  • Nomad/monrepo/packages/contracts-router/contracts/Router.sol
  • Nomad/monrepo/packages/contracts-bridge/contracts/interfaces/ITokenRegistry.sol
  • Nomad/monrepo/packages/contracts-core/contracts/Merkle.sol
  • Nomad/monrepo/packages/contracts-core/contracts/test/TestXAppConnectionManager.sol
  • Nomad/zodiac/contracts/GnomadModule.sol
  • Nomad/monrepo/packages/contracts-core/contracts/test/TestGovernanceRouter.sol
  • Nomad/monrepo/packages/contracts-core/contracts/NomadBase.sol
  • Nomad/monrepo/packages/contracts-core/contracts/governance/GovernanceRouter.sol
  • Nomad/zodiac/contracts/test/MockConnectionManager.sol
  • Nomad/monrepo/packages/contracts-core/contracts/upgrade/UpgradeBeacon.sol
  • Nomad/monrepo/packages/contracts-bridge/contracts/test/TestEncoding.sol
  • Nomad/monrepo/packages/contracts-bridge/contracts/ETHHelper.sol
  • Nomad/monrepo/packages/contracts-core/contracts/governance/GovernanceMessage.sol
  • Nomad/monrepo/packages/contracts-bridge/contracts/BridgeRouter.sol
  • Nomad/monrepo/packages/contracts-core/contracts/test/bad-recipient/BadRecipient4.sol
  • Nomad/monrepo/packages/contracts-bridge/contracts/interfaces/IWeth.sol
  • Nomad/monrepo/packages/contracts-core/contracts/test/bad-recipient/BadRecipient1.sol
  • Nomad/monrepo/packages/contracts-core/contracts/test/MysteryMathV2.sol
  • Nomad/monrepo/packages/local-environment/tests/utils/token/contract/Context.sol

Part of Nomad’s security record

Nomad
Audits1 audit by 1 firmLast audit May 2022, by Quantstamp.
Critical findingsNo open critical findingsIn 1 report.
IncidentIncident 31 Jul 2022Source: De.Fi incident database.
Open Nomad’s security record

Security firm

Quantstamp

236 projects reviewed · 332 reports · latest Oct 2026

See Quantstamp’s audits

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