Security audit of MantaNetwork by Veridise

Findings:

title: Static fee charged despite dynamic storage accesses
reported_impact: Medium
vulnerability_class_audit: Bad Extrinsic Weight
description: 4.1.1 V-MANC-VUL-001: Static fee charged despite dynamic storage accesses

Severity: Medium
Type: Bad Extrinsic Weight
Commit: 45ba60e1d
Status: Acknowledged

File(s): parachain-staking/lib.rs
Location(s): go_online, go_offline, candidate_bond_more

Blockchain computations must have appropriate fees to prevent network congestion. For substrate extrinsics, these fees are set by computing an associated weight for the operation where the weight is intended to capture the maximum computational cost. As reads from and writes to storage are expensive, these weights should consider the number of these operations that are performed. The following extrinsics, however, have a fixed weight despite requiring a dynamic number of reads or writes due to insert or remove operations being performed on CandidatePool.

- go_online
- go_offline
- candidate_bond_more
- execute_candidate_bond_less
- delegate
- execute_leave_delegators
- delegator_bond_more
- execute_delegation_request
- schedule_leave_delegators
- schedule_delegator_bond_less
- cancel_leave_delegators

Also note that similar functions in the same pallet, such as schedule_leave_candidates charge the users dynamic fees. An example can be seen in Snippet 4.1.

Impact: As the size of the CandidatePool grows, the cost of insert and remove will increase linearly since vector inserts in Rust are linear in the size of the vector. This allows malicious actors to add many candidates to the pool for a fixed monetary cost despite an increasing computational cost. If the size of the pool becomes too large, this could effectively create a DoS.

Recommendation: Similar to schedule_leave_candidates, calculate the weights dynamically rather than charging a fixed cost.

Developer Response: The developers have acknowledged the issue and are determining how to address it.

Snippet 4.1: go_offline calls remove on the CandidatePool but charges users a fixed weight in WeightInfo::go_offline

#[pallet::call_index(12)]
#[pallet::weight(<T as Config>::WeightInfo::go_offline())]
/// Temporarily leave the set of collator candidates without unbonding
pub fn go_offline(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
   let collator = ensure_signed(origin)?;
   let mut state = <CandidateInfo<T>>::get(&collator).ok_or(Error::<T>::CandidateDNE)?;
   ensure!(state.is_active(), Error::<T>::AlreadyOffline);
   state.go_offline();
   let mut candidates = <CandidatePool<T>>::get();
   if candidates.remove(&Bond::from_owner(collator.clone())) {
      <CandidatePool<T>>::put(candidates);
   }
   <CandidateInfo<T>>::insert(&collator, state);
   Self::deposit_event(Event::CandidateWentOffline { candidate: collator });
   Ok(().into())
}

title: Users can use any previously seen Merkle root
reported_impact: Medium
vulnerability_class_audit: Hash Collision
description: 4.1.2 V-MANC-VUL-002: Users can use any previously seen Merkle root

Severity: Medium
Type: Hash Collision
Commit: 45ba60e1d
Status: Acknowledged

File(s): pallets/manta-pay/src/lib.rs
Location(s): has_matching_utxo_accumulator_output

The MantaPay protocol maintains a Merkle tree on the ledger where the leaves of the ledger are the hashes of the UTXOs generated during the protocol’s lifetime. In order to spend a UTXO, users must supply a ZK proof that the UTXO belongs to the Merkle tree on the ledger. The membership proof takes as input the root of the Merkle tree (public input), the inner node hashes (private inputs), and proves that the root can be derived from the inner node hashes and leaf.

Ideally, the ledger would check that the root provided is equal to the latest root on-chain. However, this isn’t done in practice as the transaction could easily be front-runned since every transaction changes the root. Instead, the ledger maintains a set of all previously generated roots and just checks that the root provided belongs to that set.

However, by allowing the root provided by the user to be any previously generated root, an attacker simply needs to find a hash collision with any previously generated root to steal assets. The likelihood of finding a collision grows quadratically with the number of previously seen hashes. In particular, given an output size of b bits and n previously generated hashes, the likelihood of finding a collision with any of the n hashes is approximately n^2/2^(b+1).

The current version of the Protocol uses the Poseidon hash function, which produces 255-bit hashes and, in theory, should be safe even with billions of previously seen roots. However, this is contingent on the safety of the Poseidon hash. While there has been a significant amount of research and analysis conducted on the function, including various attacks and optimizations, there is no formal proof of its security and correctness, let alone any proofs about concrete implementations.

Impact: Storing all previously seen roots significantly increases the likelihood of collisions. If any attack or weakness is found in the Poseidon hash, this can be an additional means of attacking the protocol.

Recommendation: There are a few ways to mitigate this. Protocols like Semaphore maintain a timeout period TIMEOUT and associate each root with a timestamp indicating when it was created. Any root created before now() - TIMEOUT is rejected. Another option is to only store the N previously generated roots and only allow a root if it belongs to the set of N previously generated roots. The latter option would have the additional benefit of not needing to store every root on-chain.

Developer Response: The developers acknowledged the issue and will either use a timestamp or only maintain the N previously generated roots.

title: MantaPay weights calculated with a small database
reported_impact: Medium
vulnerability_class_audit: Bad Extrinsic Weight
description: 4.1.3 V-MANC-VUL-003: MantaPay weights calculated with a small database

Severity: Medium
Type: Bad Extrinsic Weight
Commit: 45ba60e1d
Status: Acknowledged

File(s): pallets/manta-pay/src/lib.rs
Location(s): to_private, to_public, private_transfer

Transactions to_public, to_private, and private_transfer take as input nullifiers and membership proofs and generate UTXOs. These UTXOs are then added to a Merkle tree on the ledger.

MantaPay shards this Merkle tree into 256 buckets where each bucket has its own Merkle tree. Instead of storing the entire tree at each bucket, the Ledger just stores the last path added to the tree. When adding a UTXO, the Ledger first computes its corresponding bucket, then computes the new path pointing to that UTXO, and finally adds that path to the bucket.

Computing the new path should take time proportional to log(n) where n is the size of the Merkle Tree. The current benchmarking scheme only covers cases where the previous path is small, i.e., at most size 1. However, if the number of transactions gets large, on the order of hundreds of millions or billions, then the size of the path can get to 24-28 (taking shards into account). If the tree grows to this size, this means each execution of the extrinsic will perform 24-28 hashes, multiplied by the number of UTXOs to be added.

The benchmarking scheme should take into account the size of the tree to ensure that the existing weights are sufficient to offset the computation of the new Merkle tree path.

Impact: Setting the weight too low can allow users to perform a large number of transactions with little cost, potentially allowing malicious users to launch a DoS attack.

Recommendation: There are several ways to address this. One strategy is to take an additional parameter corresponding to the logarithm of the Merkle Tree's size on the ledger. The weight charged can be proportional to this value. In the implementation, this value (technically 2^value) can be compared against the actual size, and the transaction will only proceed if it is larger than or equal to the actual size.

Another strategy is to benchmark the pallets by taking into account the tree's size. If MantaPay is expected not to exceed more than a billion transactions, then the pallet could be benchmarked assuming the current path length is around 24-28.

Developer Response: The developers acknowledged that the weights should be recalculated with a more saturated database.

title: Total supply of native assets can exceed the set limit
reported_impact: Medium
vulnerability_class_audit: Logic Error
description: 4.1.4 V-MANC-VUL-004: Total supply of native assets can exceed the set limit

Severity: Medium
Type: Logic Error
Commit: 45ba60e1d
Status: Acknowledged

File(s): pallets/asset-manager/src/lib.rs
Location(s): mint_asset

One invariant underlying the correctness of MantaPay is that the total supply of an asset cannot exceed the maximum amount that can be held in a particular account. This is because MantaPay uses a dedicated account A to store the value of all the private assets. As such, A should, in principle, be able to hold all the supply in the case where all of that asset is privatized.

In more detail, when privatizing a user’s public assets (via to_private), MantaPay constructs opaque UTXOs to encode the amount privatized, and then transfers those public assets into A. This transfer is expected not to fail because of the invariant described above. However, we found a case where the transfer can fail.

Manta enforces this invariant for NonNative assets because every time an asset is minted into an account, the total supply is increased. If the total supply would exceed the maximum that can be held in an account, the mint fails with the error Overflow. However, there is no such check for Native assets. Thus, if the total supply of Native assets exceeds the maximum that can be held in an account, u128::MAX, then to_private calls that should succeed can fail if the amount held in A is close to the maximum allowed. This is demonstrated in Snippet 4.2.

Impact: By not constraining the amount of Native assets to be less than the maximum amount that can be held in an account, to_private transactions that should succeed will fail.

Recommendation: We recommend a similar check be done for Native assets as is done for NonNative assets to enforce that the total supply cannot exceed the maximum that can be held in a given account.

Developer Response: The developers have acknowledged the issue and are determining how to address it.

Snippet 4.2: Failed to_private due to count of native assets exceeding u128::MAX

#[test]
fn public_account_issue() {
let mut rng = OsRng;
new_test_ext().execute_with(|| {
let asset_id = NATIVE_ASSET_ID;
let value = 1000u128;
let id = NATIVE_ASSET_ID;
let metadata = AssetRegistryMetadata {
metadata: AssetStorageMetadata {
name: b"Calamari".to_vec(),
symbol: b"KMA".to_vec(),
decimals: 12,
is_frozen: false,
},
min_balance: TEST_DEFAULT_ASSET_ED2,
is_sufficient: true,
};
assert_ok!(MantaAssetRegistry::create_asset(
id, metadata.into(), TEST_DEFAULT_ASSET_ED2,
true
));
assert_ok!(FungibleLedger::<Test>::deposit_minting(id, &ALICE, 2*value));
assert_ok!(FungibleLedger::<Test>::deposit_minting(id, &MantaPay::account_id(), u128::MAX));
let mut utxo_accumulator = UtxoAccumulator::new(UTXO_ACCUMULATOR_MODEL.clone());
let spending_key = rng.gen();
let address = PARAMETERS.address_from_spending_key(&spending_key);
let mut authorization =
Authorization::from_spending_key(&PARAMETERS, &spending_key, &mut rng);
let asset_0 = Asset::new(Fp::from(asset_id), value);
let (to_private_0, pre_sender_0) = ToPrivate::internal_pair(
&PARAMETERS, &mut authorization.context,
address, asset_0,
Default::default(), &mut rng,
);
let to_private_0 = to_private_0
.into_post(
FullParametersRef::new(&PARAMETERS, utxo_accumulator.model()),
&PROVING_CONTEXT.to_private,
None, Vec::new(), &mut rng,
)
.expect("Unable to build TO_PRIVATE proof.")
.expect("Did not match transfer shape.");
assert_ok!(MantaPay::to_private(
MockOrigin::signed(ALICE),
PalletTransferPost::try_from(to_private_0).unwrap()
));
}
}


title: Missing updates in update_asset_metadata
reported_impact: Medium
vulnerability_class_audit: Logic Error
description: 4.1.5 V-MANC-VUL-005: Missing updates in update_asset_metadata

Severity: Medium
Type: Logic Error
Commit: 45ba60e1d
Status: Acknowledged

File(s): pallets/asset-manager/src/lib.rs
Location(s): update_asset_metadata

Manta Chain has an asset-manager pallet responsible for registering and minting assets. Each asset has a unique id and is associated with various metadata like a name, symbol, decimal places, etc. One important metadata is called min_balance. To store an account with some quantity of assets on the ledger, it must have more than min_balance quantity. This metadata is also used when validating transfers.

In particular, many asset transfers take an “existential parameter” as input, called KeepAlive, which decides what to do if the transfer would take the account’s balance (with respect to the asset) below min_balance. If KeepAlive is set, the transfer will fail if the amount goes below the min_balance. If it is not set, other configurations come into play, and the account may be removed and the remaining balance burned.

The asset-manager pallet exposes an extrinsic called update_asset_metadata, which takes as input the new metadata for that asset and updates the ledger to associate the asset with that metadata. While the implementation took a new min_balance as input, it did not update the ledger to associate the asset with this metadata.

Additionally, this API also took as input a new value for the metadata is_sufficient, but similarly did not update the ledger to associate the asset with this metadata.

Impact: While it is rare for the min_balance to be changed, it is sometimes necessary if it was originally set too high, for example. The current API made it appear that min_balance could be changed, so users might think the min_balance was changed when, in fact, it wasn’t.

Recommendation: The main issue with this extrinsic is its interface makes it appear as though the metadata min_balance and is_sufficient could be changed when it actually didn’t. Either the API should be changed to only take the metadata that should be changed, or it should appropriately update min_balance and is_sufficient.

Developer Response: The developers acknowledged this issue and are discussing two possible fixes. The first is to update both parameters in the asset pallet, and the second is to change the interface to prevent updating the min_balance or is_sufficient parameters.

title: No slashing mechanism for collators
reported_impact: Medium
vulnerability_class_audit: Consensus
description: 4.1.6 V-MANC-VUL-006: No slashing mechanism for collators

Severity: Medium
Type: Consensus
Commit: 45ba60e1d
Status: Acknowledged

File(s): parachain-staking
Location(s): N/A

Proof of Stake blockchains often have a slashing mechanism to detect poorly performing stakers and punish them. Typically, a significant portion of the staker’s stake is taken by the chain as punishment for poor performance.

Currently, Manta Chain does not have any slashing mechanism. Instead, it uses a combination of social pressure and manual slashing to incentivize good behavior. Specifically, when the owners detect a poorly performing collator, they contact the collator over Discord and warn them of the poor performance. If their performance does not improve, the owners manually slash the collator’s funds.

While this approach may work when the blockchain is small, it will be challenging to enforce as the chain grows. Therefore, we recommend that Manta Chain implement a slashing mechanism.

Impact: Manta Chain’s current method of using social pressure will only work with a small set of trusted collators. However, as the chain expands, this mechanism is unlikely to sufficiently incentivize collators to perform well.

Recommendation: We recommend that Manta Chain establish a slashing mechanism to implement if/when the current process becomes inadequate.

Developer Response: The developers acknowledged the lack of a slashing mechanism and plan to include one in the future if/when the current process stops working.

title: Collators given full rewards regardless of quality
reported_impact: Medium
vulnerability_class_audit: Consensus
description: 4.1.7 V-MANC-VUL-007: Collators given full rewards regardless of quality

Severity: Medium
Type: Consensus
Commit: 45ba60e1d
Status: Open

File(s): pallets/parachain-staking/src/lib.rs
Location(s): pay_one_collator_reward

Manta Chain rewards collators by first allocating a fixed number of points (20) for every block they author and then giving the collator a fixed percentage of those allocated points as rewards. However, there is no check on the quality of the blocks authored by the collator: an empty block will result in just as many rewards as a full block.

Currently, Manta relies on the owners to monitor the blocks on-chain and manually punish collators who perform poorly. However, as the chain grows, this misbehavior may not be easy to detect.

One relatively simple way to address this issue is to adjust the reward system to incentivize high quality blocks.

Impact: Collators can effectively steal funds from Manta by authoring low-quality blocks (i.e., empty or partial blocks) and reaping full rewards.

Recommendation: We recommend the developers adjust the rewards system to either reward the collators for high-quality blocks or punish them for authoring poor ones.

Developer Response: TBD

title: Missing validation in pull_ledger_diff
reported_impact: Low
vulnerability_class_audit: Data Validation
description: 4.1.8 V-MANC-VUL-008: Missing validation in pull_ledger_diff

Severity: Low
Type: Data Validation
Commit: 45ba60e1d
Status: Acknowledged

File(s): pallets/manta-pay/src/lib.rs
Location(s): Line 593

pull_ledger_diff takes as input a Checkpoint, which is a struct with two fields: receiver_index and sender_index. It pulls sender and receiver data from the ledger, starting at sender_index (resp. receiver_index) up to at most sender_index + PULL_MAX_SENDER_UPDATE_SIZE (resp. receiver_index + PULL_MAX_RECEIVER_UPDATE_SIZE). However, there is no check to ensure that this sum cannot overflow for both the sender and receiver index in pull_senders, pull_receivers, pull_senders_for_shard, and pull_receivers_for_shard.

Impact: If the code is compiled without the --release flag, a malicious user could crash the node by passing in bad values. If it is built with --release, the call will be reported as successful and no senders or receivers will be returned. However, if a benign end user calls the API with incorrect indexes, it might be preferable to return an Error informing them that the index is invalid.

Recommendation: We recommend adding bounds checks for safety and to return an Error.

Developer Response: The developers are aware and agree that it would be better to check and return an error.

title: increase_count_of_associated_assets can overflow
reported_impact: Low
vulnerability_class_audit: Logic Error
description: 4.1.9 V-MANC-VUL-009: increase_count_of_associated_assets can overflow

Severity: Low
Type: Logic Error
Commit: 45ba60e1d
Status: Acknowledged

File(s): pallets/asset-manager/src/lib.rs
Location(s): Line 590

The asset_manager pallet maintains a mapping of paraids to a count of assets associated with that paraid. Each paraid can be associated with at most u32::MAX assets. When registering an asset or moving its location, the pallet calls increase_count_of_associated_assets, which takes as input a paraid and increments the number of assets associated with that paraid. However, this function does not check whether increasing the number of assets will result in an overflow.

Impact: If the runtime is compiled using --debug, this can crash the node. However, if built under --release, the asset count will go to zero.

Recommendation: Make this function check if the addition will result in an overflow, i.e., check if the current count is u32::MAX and return an error.

Developer Response: Acknowledged

title: Account checks are incorrect
reported_impact: Low
vulnerability_class_audit: Logic Error
description: 4.1.10 V-MANC-VUL-010: Account checks are incorrect.

Severity: Low
Type: Logic Error
Commit: 45ba60e1d
Status: Acknowledged

File(s): pallets/manta-pay/src/lib.rs
Location(s): check_sink_accounts, check_source_accounts

When validating a transaction, the source and sink accounts are checked by check_sink_accounts and check_source_accounts. These functions iterate over pairs (account, value) and check that value can be safely deposited (withdrawn) from account. The logic is correct only if every account appears in at most one pair. While this is fine for the current APIs, if the APIs change to allow multiple sink or multiple source accounts, then this code needs to be refactored or the uniqueness needs to be enforced elsewhere.

Impact: Currently there is no impact since the current APIs only allow one account for the source and sink accounts.

Recommendation: To be safe, we recommend adding an additional check in the validation step to ensure the accounts are distinct for both sources and sinks.

Developer Response: The developers acknowledged the issue and plan to add a check during validation to ensure that the accounts are distinct.

title: Unstaked user may be selected as collator
reported_impact: Low
vulnerability_class_audit: Logic Error
description: 4.1.11 V-MANC-VUL-011: Unstaked user may be selected as collator

Severity: Low
Type: Logic Error
Commit: 45ba60e1d
Status: Acknowledged

File(s): parachain-staking/lib.rs
Location(s): select_top_candidates

Parachains use collators to combine transactions into blocks that are then checked by Validators on the relay chain. Notably, this allows collators to remain relatively untrusted as validators ensure blocks are created correctly. On Manta’s chain, collators are selected from a group of staked users who receive rewards for creating blocks. Requiring that collators be staked provides additional security guarantees as, if a collator misbehaves (e.g., submits no blocks for validation, submits multiple conflicting blocks), governance can step in and slash the user’s staked funds. As such, unstaked collators have less incentive to maintain parachain stability and should be avoided.

However, in the collator selection process, if no sufficiently staked collator can be found, collators from the previous round will be selected. As there is no validation on the current state of the previous collators’ stake, this could select unstaked validators who lack the incentive to ensure network stability.

Here is a simple test case demonstrating this:

#[test]
fn test_failed_candidate_selection() {
  ExtBuilder::default()
    .with_balances(vec![(10, 10)])
    .with_candidates(vec![(10, 10)])
    .build()
    .execute_with(|| {
      roll_to(2);
      assert_ok!(ParachainStaking::schedule_leave_candidates(Origin::signed(10), 6u32));
      roll_to(5);
      let candidate: Vec<u64> = ParachainStaking::selected_candidates();
      assert_ne!(candidate[0], 10u64);
    });
}

Impact: Collators will not be incentivized to ensure network stability. Another set of partially staked or “trusted” collators could provide better stability.

Recommendation: Consider maintaining a set of “trusted” collators to fall back on if no staked collators can be found.

Developer Response: Acknowledged.

title: XCM instructions can charge 0 weight
reported_impact: Low
vulnerability_class_audit: Bad Extrinsic Weight
description: 4.1.12 V-MANC-VUL-012: XCM instructions can charge 0 weight

Severity: Low
Type: Bad Extrinsic Weight
Commit: 45ba60e1d
Status: Acknowledged

File(s): runtime/(calamari, dolphin)/src/weights/xcm/mod.rs
Location(s): Every use of weigh_multi_assets

The Polkadot ecosystem uses the XCM messaging standard to enable parachains and the relay chain to communicate with each other. For example, if a parachain P1 wants to deposit an asset onto another parachain P2, they can construct an XCM message stating they wish to deposit an asset into an account associated with P1 and send it to P2.

Each XCM message consists of a sequence of low-level XCM instructions that get executed by the XCM executor on the destination parachain. To offset the cost of executing these instructions, parachains set weights for each instruction, so the sender of the XCM message is charged fees for the destination parachain executing their message.

Manta Chain configured the weights of multiple instructions so that senders could generate messages with a total weight of 0. For example, in the code snippet below, the function deposit_asset sets the weight for the XCM instruction deposit_asset based on a parameter called assets. When assets is an empty vector, the weight_multi_assets function returns 0, resulting in a total weight of 0 for the instruction:

fn deposit_asset(
    assets: &MultiAssetFilter,
    _max_assets: &u32,
    _dest: &MultiLocation,
) -> Weight {
    let hardcoded_weight: u64 = 1_000_000_000;
    let weight = assets.weigh_multi_assets(XcmFungibleWeight::<Runtime>::deposit_asset());
    cmp::min(hardcoded_weight, weight)
}

This setup allows malicious or incompetent senders to spam Manta with messages costing 0, even though the instruction is successfully executed by the XCM executor. While a denial of service may be unlikely due to the fast execution of 0-length vectors, spam prevention is recommended. Adding a minimal base fee for instructions that can be executed with 0 weight would help prevent spam.

Impact: Malicious users may spam Manta with XCM messages of weight 0, potentially slowing down blockchain performance and risking denial of service.

Recommendation: We recommend always charging a base fee to prevent spam.

Developer Response: The developers acknowledged the issue and plan to change weigh_multi_assets to charge the benchmarked weight of a single asset execution for any successfully executed multiasset XCM message.

title: Missing validation in set_units_per_second
reported_impact: Low
vulnerability_class_audit: Data Validation
description: 4.1.13 V-MANC-VUL-013: Missing validation in set_units_per_second

Severity: Low
Type: Data Validation
Commit: 45ba60e1d
Status: Acknowledged

File(s): pallets/asset-manager/src/lib.rs
Location(s): set_units_per_second

The asset-manager pallet manages a hashmap called UnitsPerSecond which maps assetIds to a u128 value units_per_second, used to determine the price for an XCM transfer. It provides a function, set_units_per_second, to set the units_per_second for a given asset. This value determines the cost (in the corresponding asset) to purchase a certain weight for a transaction. The following code snippet calculates the amount:

let units_per_second = M::units_per_second(&asset_id).ok_or({
    log::debug!(
        target: "FirstAssetTrader::buy_weight",
        "units_per_second missing for asset with id: {:?}",
        id,
    );
    XcmError::TooExpensive
})?;

let amount = units_per_second * (weight as u128) / (WEIGHT_PER_SECOND as u128);

if amount.is_zero() {
    return Ok(payment);
}

This calculation of amount uses multiplication, which can overflow. Currently, both units_per_second and amount are of type u128. If units_per_second exceeds u128::MAX / u64::MAX, a large weight (u64::MAX) can be bought for a small asset amount, allowing a malicious parachain to potentially perform a DoS attack.

There is currently no validation in set_units_per_second to ensure units_per_second is small enough. Although only the root can call set_units_per_second, there is a risk if the root user mistakenly sets an excessive value or is tricked into doing so.

Impact: If units_per_second is set above u128::MAX / u64::MAX, large amounts of weight can be purchased at low cost, potentially enabling a DoS attack on the chain.

Recommendation: Change the type of units_per_second to map assetId to a u64 value or validate that the amount is sufficiently small.

Developer Response: The developers acknowledged the issue and plan to fix the weight calculation to use saturating arithmetic, which should address this vulnerability.

title: Collator is a single point of failure for a round
reported_impact: Low
vulnerability_class_audit: Consensus
description: 4.1.14 V-MANC-VUL-014: Collator is a single point of failure for a round

Severity: Low
Type: Consensus
Commit: 45ba60e1d
Status: Acknowledged

The Manta parachain uses the Aura consensus mechanism to select collators to author blocks. Aura selects a primary collator for a round, and only that collator is allowed to produce blocks in that round. However, if that collator goes down, then no blocks will be produced, making that collator a single point of failure.

Other parachains, like Moonbeam, address this by selecting multiple collators for a given round.

Impact: If a collator goes down, no blocks will be produced for that round, thereby impacting the transaction throughput of Manta.

Recommendation: We recommend that Manta adopt a consensus mechanism that selects multiple collators, ideally geographically separated, so if one collator fails, the likelihood of others failing remains low.

Developer Response: The developers are aware of this issue and have plans to move away from the Aura consensus mechanism.

title: Unchecked index calculation in spend_all
reported_impact: Warning
vulnerability_class_audit: Logic Error
description: 4.1.15 V-MANC-VUL-015: Unchecked index calculation in spend_all

Severity: Warning
Type: Logic Error
Commit: 45ba60e1d
Status: Acknowledged

File(s): pallets/manta-pay/src/lib.rs
Location(s): spend_all

The spend_all function in the Manta-Pay pallet does the following:
1. Adds the nullifier commitments in the TransactionPost to the NullifierCommitmentSet.
2. Inserts each (nullifier, outgoingNote) pair into the NullifierSetInsertionOrder structure.
3. Updates a global variable, NullifierSetSize, which stores the size of the nullifier commitment set.

The index where the pair gets inserted, along with the new nullifier size, is calculated as index + i, where i is the index of the corresponding SenderPost, and index is the current size of the set. However, this arithmetic is unchecked and could result in an overflow.

Impact: When the size of the commitment set reaches u64::MAX, the index calculation may overflow, causing the pair to be inserted at the beginning of the list and setting the nullifier set size to 1. However, reaching this value through normal execution is extremely unlikely.

Recommendation: Add an overflow check and return an error.

Developer Response: TBD.

title: Excess fees not refunded
reported_impact: Warning
vulnerability_class_audit: Bad Extrinsic Weight
description: 4.1.16 V-MANC-VUL-016: Excess fees not refunded

Severity: Warning
Type: Bad Extrinsic Weight
File(s): parachain-staking/lib.rs
Location(s): (cancel_leave, execute_leave, schedule_leave, join)_candidates
Commit: 45ba60e1d
Status: Intended Behavior

When a substrate extrinsic is created, its weight must be carefully considered to ensure it correctly reflects the computational cost of the operation as extrinsic weight is directly related to the fees that are charged to the user. This weight should capture the maximum number of computational resources that will be consumed by the extrinsic as excess fees can be returned. In several functions, though, the weights are computed based on the value of an argument provided by the user which might not always reflect the true cost of the computation.

For example, consider the following:

1#[pallet::call_index(11)]
2#[pallet::weight(<T as Config>::WeightInfo::cancel_leave_candidates(*candidate_count))]
3/// Cancel open request to leave candidates
4/// - only callable by collator account
5/// - result upon successful call is the candidate is active in the candidate pool
pub fn cancel_leave_candidates(
7origin: OriginFor<T>,
8#[pallet::compact] candidate_count: u32,
) -> DispatchResultWithPostInfo {
...
12let mut candidates = <CandidatePool<T>>::get();
13ensure!(
14candidates.0.len() as u32 <= candidate_count,
15Error::<T>::TooLowCandidateCountWeightHintCancelLeaveCandidates
);
...
18Ok(().into())
}

In this function, the weight is computed using the candidate_count argument, and in order for the function to execute successfully, candidate_count must be greater than or equal to the current size of the candidate pool. A user might need to call this function with a candidate_count that is larger than the size of the pool to prevent a front-running attack where a malicious user would add candidates to prevent the transaction from executing successfully. In such a case, the weight would be larger than necessary, but no fees are returned to the user.

Impact: Such functions can charge unnecessary fees to the user.

Recommendation: Refund the user additional fees that are not consumed.

Developer Response: Since these extrinsics are likely to be executed sparingly and since the additional fees are likely to be small, we feel like the additional computational cost of determining the excess does not.

title: Assets can be registered at unsupported locations
reported_impact: Warning
vulnerability_class_audit: Data Validation
description: 4.1.17 V-MANC-VUL-017: Assets can be registered at unsupported locations

Severity: Warning
Type: Data Validation
File(s): pallets/asset-manager/src/lib.rs
Location(s): register_asset
Commit: 45ba60e1d
Status: Acknowledged

The asset-manager pallet allows assets to be registered, managed, and minted. In particular, register_asset takes as input an asset, location, and corresponding asset_metadata and registers the asset. Every asset must be associated with a location; however, Manta only supports assets from specific locations. The current implementation of asset-manager does not perform any validation on the locations passed into register_asset, potentially allowing assets to be registered from untested locations. The pallet also exposes a method called update_asset_location, which is supposed to update the location of an asset. It similarly does not perform any validation on the new location of the asset.

Impact: The current implementation allows assets to be registered from untested locations.

Recommendation: The asset-manager pallet already implements the Contains trait, which exposes a method contains that takes as input a location and returns true if and only if the location is supported. Currently, that method is unused and can be used to validate the locations passed in.

Developer Response: The developers acknowledged the issue and plan to add a check in register_asset.

title: Minimum delegator funds is not MinDelegatorStk
reported_impact: Warning
vulnerability_class_audit: Logic Error
description: 4.1.18 V-MANC-VUL-018: Minimum delegator funds is not MinDelegatorStk

Severity: Warning
Type: Logic Error
File(s): parachain-staking/lib.rs
Location(s): N/A
Commit: 45ba60e1d
Status: Acknowledged

In the case where MinDelegation < MinDelegatorStk, it is possible for the delegator’s staked funds to be less than MinDelegatorStk. This can occur through the following sequence of calls:

1. delegate amount N from delegator D to candidate C1 where N >= MinDelegatorStk
2. delegate amount M from delegator D to candidate C2 where M < MinDelegatorStk and M >= MinDelegation
3. schedule_leave_candidates and execute_leave_candidates for C1

This results in D having M funds staked, where M < MinDelegatorStk.

Impact: If MinDelegation is less than MinDelegatorStk, a delegator may end up with less than MinDelegatorStk funds actually staked.

Note: This is not currently exploitable because MinDelegation == MinDelegatorStk in all production runtimes. However, if these values are adjusted in the future, this bug may become exploitable.

Recommendation: There are two options:

1. When starting a runtime, ensure that MinDelegation >= MinDelegatorStk
2. Whenever a delegation is removed (such as in execute_leave_candidates), ensure that the remaining locked funds for the delegator are at least MinDelegatorStk.

Developer Response: The developers acknowledged the issue and are considering removing MinDelegation as there is no apparent reason for it being different from MinDelegatorStk.

title: Unintended test crashes
reported_impact: Info
vulnerability_class_audit: Maintainability
description: 4.1.19 V-MANC-VUL-019: Unintended test crashes

Severity: Info
Type: Maintainability
File(s): pallets/manta-pay/src/lib.rs
Location(s): to_private_should_work
Commit: 45ba60e1d
Status: Open

Many of the manta-pay tests randomly generate an asset id, total supply, and an amount to make private. To ensure the total supply of the asset is greater than the minimum balance, the minimum balance is always added to the randomly generated total supply, as seen in this test:

fn to_private_should_work() {
  let mut rng = OsRng;
  for _ in 0..RANDOMIZED_TESTS_ITERATIONS {
    new_test_ext().execute_with(|| {
      let asset_id = rng.gen();
      let total_free_supply = rng.gen();
      initialize_test(asset_id, total_free_supply + TEST_DEFAULT_ASSET_ED);
      mint_private_tokens(
        asset_id,
        &value_distribution(5, total_free_supply, &mut rng),
        &mut rng,
      );
    });
  }
}

If the random number generator generates a value for the total_free_supply which is greater than u128::MAX - TEST_DEFAULT_ASSET_ED, then the test will fail even though it is expected to succeed.

Impact: May cause tests to fail when they are expected to succeed.

Recommendation: Change the test to generate a value for total_free_supply between [0, u128::MAX - TEST_DEFAULT_ASSET_ED).

Developer Response: TBD