Security audit of Nodle by Halborn

Findings:

title: TCR voting design should be improved
reported_impact: 3
description: 3.1 (HAL-01) HAL-01 TCR VOTING DESIGN SHOULD BE IMPROVED - MEDIUM 
 Description:
It was observed that it is possible to:
• Vote with 0 amount
• Challenge yourself
• Counter yourself
• Vote for yourself
By combining these properties, some scenarios might be possible:
• A whale can influence any challenge/counter decision by voting for itself.
A whale can also farm additional tokens upon success by countering any application and then voting to itself.
By countering your application and voting with 0 amount, it is possible to fill up the storage since the values are pushed into vector
To remove yourself from members in the root of trust.

Risk Level:
Likelihood - 3
Impact - 4

Recommendation:
Consider improving the design by not letting the same account to:
• Vote to itself
• Counter itself
• Challenge itself
• Vote with 0 deposit

Remediation Plan:
NOT APPLICABLE: The issue is marked as not applicable by the Nodle team as the TCR and root of trust pallets will be removed.

title: Denomination logic should be improved
reported_impact: 2
description: 3.2 (HAL-02) HAL-02 DENOMINATION LOGIC SHOULD BE IMPROVED - MEDIUM
 Description:
It was observed that if a nominator has a single validator, it is not possible to remove a validator through nominator_denominate since it has a check for <StakingMinNominatorTotalBond<T>>. In that case, nominator_denominate_all has to be used, which bypasses that check, which is not intentional.

Code Location:
Listing 1: pallets/staking/src/lib.rs
1 if !do_force {
   ensure!(
      remaining >= <StakingMinNominatorTotalBond<T>>::get(),
      <Error<T>>::NominatorBondBelowMin
   );
}

Risk Level:
Likelihood - 4
Impact - 2

Recommendation:
Consider having a conditional statement in nominator_denominate that allows the forced removal of a validator if the nominator has only one validator.

Remediation Plan:
SOLVED: The issue was solved by the Nodle team.
• Fix Commit

title: Emergency shutdown not used in critical functions
reported_impact: 4
description: 3.3 (HAL-03) HAL-03 EMERGENCY SHUTDOWN NOT USED IN MANY CRITICAL FUNCTIONS - MEDIUM
 Description:
It was observed that the emergency shutdown pallet is used only in the allocate function in the allocations pallet. However, there are more public functions across different pallets that might be problematic if, at any point in time, there is a bug (security/non-security) discovered within them. There should be a functionality to shut them down before new fixes are pushed.

Code Location:
These functions should have a shutdown functionality:

Grants pallet
• add_vesting_schedule

Staking pallet
• validator_join_pool
• validator_exit_pool
• validator_bond_more
• validator_bond_less
• nominator_nominate
• nominator_denominate
• nominator_bond_more
• nominator_bond_less
• nominator_move_nomination
• unbond_frozen
• withdraw_unbonded
• withdraw_staking_rewards

Risk Level:
Likelihood - 3
Impact - 4

Recommendation:
Consider enabling shutdown functionality in critical public functions.

Example Code:
Listing 2
1 ensure!(
   !pallet_emergency_shutdown :: Pallet::<T>::shutdown(),
   Error::<T>::UnderShutdown
);

Remediation Plan:
PENDING: In a future release, the Nodle team will modify the emergency shutdown pallet to better generalize.

title: Missing sanity checks
reported_impact: 2
description: 3.4 (HAL-04) HAL-04 MISSING SANITY CHECKS - LOW
 Description:
It was observed that the set_staking_limits privileged function is missing sanity checks on provided values. Even though it is a protected function, it is still advised to have some sanity checks to avoid any human error.

Code Location:
Listing 3: pallets/staking/src/lib.rs
201 pub fn set_staking_limits (
   origin: OriginFor<T>,
   max_stake_validators: u32,
   min_stake_session_selection: BalanceOf<T>,
   min_validator_bond: BalanceOf<T>,
   min_nominator_total_bond: BalanceOf<T>,
   min_nominator_chill_threshold: BalanceOf<T>
) -> DispatchResultWithPostInfo {

Risk Level:
Likelihood - 3
Impact - 2

Recommendation:
It is recommended to add sanity checks to ensure:
• max_stake_validators != 0
• min_stake_session_selection != 0
• min_validator_bond != 0
• min_nominator_total_bond != 0

Remediation Plan:
SOLVED: The issue was solved by the Nodle team.
• Fix Commit

title: Vesting to yourself is allowed
reported_impact: 2
description: 3.5 (HAL-05) HAL-05 VESTING TO YOURSELF IS ALLOWED - LOW
 Description:
It was observed that you can create a vesting schedule to yourself.

Code Location:
Listing 4: pallets/grants/src/lib.rs
157 pub fn add_vesting_schedule (
   origin: OriginFor<T>,
   dest: <T::Lookup as StaticLookup>::Source,
   schedule: VestingScheduleOf<T>
) -> DispatchResultWithPostInfo {
   let from = ensure_signed(origin)?;
   let to = T::Lookup::lookup(dest)?;
   Self::do_add_vesting_schedule(&from, &to, schedule.clone())?;
   Self::deposit_event(Event::VestingScheduleAdded(from, to, schedule));
   Ok(().into())
}

Risk Level:
Likelihood - 3
Impact - 2

Recommendation:
Please add a check that ensures that from != to in fn add_vesting_schedule.

Remediation Plan:
SOLVED: The issue was solved by the Nodle team.
• Fix Commit

title: Missing zero value check
reported_impact: 2
description: 3.6 (HAL-06) HAL-06 MISSING ZERO VALUE CHECK - LOW
 Description:
It was observed that the allocate function should have a zero value check on the amount argument.

Code Location:
Listing 5: pallets/grants/src/lib.rs (Line 88)
85 pub fn allocate (
   origin: OriginFor<T>,
   to: T::AccountId,
   amount: BalanceOf<T>,
   proof: Vec<u8>
) -> DispatchResultWithPostInfo {
   Self::ensure_oracle(origin)?;
   ...

Risk Level:
Likelihood - 2
Impact - 2

Recommendation:
Consider adding zero value checks to these functions to avoid performing redundant operations if a zero value is received.

Remediation Plan:
SOLVED: The issue was solved by the Nodle team.
• Fix Commit

title: Vesting schedules less than a current block can be created
reported_impact: 1
description: 3.7 (HAL-07) HAL-07 VESTING SCHEDULES LESS THAN A CURRENT BLOCK CAN BE CREATED - INFORMATIONAL
 Description:
It was observed that the pallet allows the creation of vesting schedules that are less than the current block number. Those vesting schedules are not more than the regular transfers with extra steps. Therefore, those are redundant.

Example:
Listing 6
1 Current Block: 100

3 Vesting Schedule Start: 1st Block
5 Period: 10 Blocks
7 Period_count: 2
9 Per Period: 1 knodl

13 Vesting Duration: 10 * 2 + 1 = 21 Blocks
14 Initial Transfer sent: 2 knodl
16 Next Claims: 0 since Vesting Duration < Current Block

Risk Level:
Likelihood - 1
Impact - 1

Recommendation:
Consider adding a check that ensures that:
(period * period_count) + start > current_block_number

Remediation Plan:
NOT APPLICABLE: The issue was marked as not applicable by the Nodle team, stating:
This can be useful to keep as it is. In fact, we may have to create retroactive awards that may have been partially vested.

title: Redundant check
reported_impact: 1
description: 3.8 (HAL-08) HAL-08 REDUNDANT CHECK - INFORMATIONAL
 Description:
It was observed that the grants pallet contains a redundant check.

Code Location:
There is no need for a second new_lock.is_zero() since it was already checked prior. Removing of the vestingSchedule can be performed within the first check.

Listing 7: pallets/grants/src/lib.rs (Line 253)
247 if new_lock . is_zero () {
248 T :: Currency :: remove_lock ( VESTING_LOCK_ID , & target );
249 }
250 else {
251 T :: Currency :: set_lock ( VESTING_LOCK_ID , & target , new_lock , WithdrawReasons :: all () );
252 }
253 if new_lock . is_zero () {
254 // No more claimable , clear
255 VestingSchedules :: <T >:: remove ( target . clone () );
256 }
257 else {
258 T :: Currency :: set_lock ( VESTING_LOCK_ID , & target , new_lock , WithdrawReasons :: all () );
}

Risk Level:
Likelihood - 1
Impact - 1

Recommendation:
Please remove the second new_lock.is_zero() check and remove the vestingSchedule within the first check.

Listing 8: pallets/grants/src/lib.rs
247 if new_lock . is_zero () {
248 T :: Currency :: remove_lock ( VESTING_LOCK_ID , & target );
249 VestingSchedules :: <T >:: remove ( target . clone () );
250 }
251 else {
252 T :: Currency :: set_lock ( VESTING_LOCK_ID , & target , new_lock , WithdrawReasons :: all () );

Remediation Plan:
SOLVED: The issue was solved by the Nodle team.
• Fix Commit

title: Redundant variable
reported_impact: 1
description: 3.9 (HAL-09) HAL-09 REDUNDANT VARIABLE - INFORMATIONAL
 Description:
It was observed that the old1 variable in on_finalize function in tcr pallet is redundant. Tuple returned from commit_applications is Ok(( new_members, Vec::new())). Therefore, old1 is always going to be an empty vector. Hence, extending it with old2 does not make any difference. In this scenario, we only care about new_1.

Code Location:
Listing 9: pallets/tcr/src/lib.rs
138 fn on_finalize ( block : T :: BlockNumber ) {
139 let ( mut new_1 , mut old_1 ) =
140 Self :: commit_applications ( block ) . unwrap_or (( Vec :: new () , Vec :: new () ) );
141 let ( new_2 , old_2 ) =
142 Self :: resolve_challenges ( block ). unwrap_or (( Vec :: new () , Vec :: new () ) );
143 // Should never be the same , so should not need some uniq checks
145 new_1 . extend ( new_2 ) ;
146 old_1 . extend ( old_2 ) ;
148 new_1 . sort () ;
149 old_1 . sort () ;
151 Self :: notify_members_change ( new_1 , old_1 ) ;

Listing 10: pallets/tcr/src/lib.rs (Line 478)
460 fn commit_applications ( block : T :: BlockNumber ) -> FinalizeHelperResultFrom <T > {
461 let new_members = < Applications <T , I > >:: iter ()
462 . filter (|( _account_id , application )| {
463 block
464 . checked_sub (& application . clone () . created_block )
465 . expect ( " created_block should always be smaller than block ; qed ")
466 >= T :: FinalizeApplicationPeriod :: get ()
})
468 . map (|( account_id , application ) | {
469 < Applications <T , I > >:: remove ( account_id . clone () );
470 < Members <T , I > >:: insert ( account_id . clone () , application . clone () ) ;
471 Self :: unreserve_for ( account_id . clone () , application . candidate_deposit );
472 Self :: deposit_event ( Event :: ApplicationPassed ( account_id . clone () ) ) ;
474 account_id
475 })
476 . collect :: < Vec <T :: AccountId > >() ;
478 Ok (( new_members , Vec :: new () ) ) // === HERE ===
}

Risk Level:
Likelihood - 1
Impact - 1

Recommendation:
Consider omitting old1 and remove all actions performed on it.

Listing 11: pallets/tcr/src/lib.rs
247 fn on_finalize ( block : T :: BlockNumber ) {
248 let ( mut new_1 , _ ) =
249 Self :: commit_applications ( block ) . unwrap_or (( Vec :: new () , Vec :: new () ) );
250 let ( new_2 , mut old ) =
251 Self :: resolve_challenges ( block ). unwrap_or (( Vec :: new () , Vec :: new () ) );
253 // Should never be the same , so should not need some uniq checks
254 new_1 . extend ( new_2 ) ;
257 new_1 . sort () ;
258 old . sort () ;
260 Self :: notify_members_change ( new_1 , old ) ;

Remediation Plan:
SOLVED: The issue was solved by the Nodle team.
• Fix Commit

title: Usage of vulnerable crates
reported_impact: 1
description: 3.10 (HAL-10) HAL-10 USAGE OF VULNERABLE CRATES - INFORMATIONAL
 Description:
It was observed that the project uses crates with known vulnerabilities.

Code Location:
ID
RUSTSEC-2020-0159
RUSTSEC-2020-0071
RUSTSEC-2021-0130
RUSTSEC-2021-0067
RUSTSEC-2021-009
RUSTSEC-2021-0079
RUSTSEC-2021-0078
RUSTSEC-2021-0076
RUSTSEC-2021-0070
RUSTSEC-2021-0073
RUSTSEC-2021-0013
RUSTSEC-2021-0089
RUSTSEC-2021-0124
RUSTSEC-2021-0110
RUSTSEC-2021-0115

FINDINGS & TECH DETAILS
package: chrono
Short Description: Potential segfault in 'localtime_r' invocations
package: time
Short Description: Potential segfault in the time crate
package: lru
Short Description: Use after free in lru crate
package: cranelift-codegen
Short Description: Memory access due to code generation flaw in Cranelift module
package: crossbeam-deque
Short Description: Data race in crossbeam-deque
package: hyper
Short Description: Integer overflow in hyper's parsing of the Transfer-Encoding header leads to data loss
package: hyper
Short Description: Lenient hyper header parsing of Content-Length could allow request smuggling
package: libsecp256k1
Short Description: libsecp256k1 allows overflowing signatures
package: nalgebra
Short Description: VecStorage Deserialize Allows Violation of Length Invariant
package: prost-types
Short Description: Conversion from prost_types::Timestamp to SystemTime can cause an overflow and panic
package: raw-cpuid
Short Description: Soundness issues in raw-cpuid
package: raw-cpuid
Short Description: Optional Deserialize implementations lacking validation
package: tokio
Short Description: Data race when sending and receiving after closing a oneshot channel
package: wasmtime
Short Description: Multiple Vulnerabilities in Wasmtime
package: zeroize-derive
Short Description: #[zeroize(drop)] doesn’t implement Drop for enums

Risk Level:
Likelihood - 2
Impact - 1

Recommendation:
Even if those vulnerable crates cannot impact the underlying application, it is advised to be aware of them and attempt to update them to non-vulnerable versions. Furthermore, it is necessary to set up dependency monitoring to always be alerted when a new vulnerability is disclosed in one of the project’s crates.

Remediation Plan:
ACKNOWLEDGED: The issue was acknowledged by the Nodle team and will be fixed later.

title: Outdated Rust edition
reported_impact: 1
description: 3.11 (HAL-11) HAL-11 OUTDATED RUST EDITION - INFORMATIONAL
 Description:
It was observed that the project is using outdated rust edition (2018). Recently, 2021 rust edition came out, which includes a lot of stability improvements and new features that might make the code more readable.

Code Location:
• Cargo.toml

Risk Level:
Likelihood - 1
Impact - 1

Recommendation:
Consider updating the Rust to the latest edition to use the latest features and stability improvements.

Reference:
Rust 2021 Edition Guide

Remediation Plan:
SOLVED: The issue was solved by the Nodle team.
• Fix Commit