Security audit of ReefChain by Halborn

Findings:

title: Integer overflow
reported_impact: 3
description: 3.1 (HAL-01) INTEGER OVERFLOW - MEDIUM
Description:
An overflow happens when an arithmetic operation reaches the maximum size of a type. For instance, in the ethereum_signable_message() method, an if statement is summing up a few u32 values, which may end up overflowing the integer. In computer programming, an integer overflow occurs when an arithmetic operation attempts to create a numeric value that is outside of the range that can be represented with a given number of bits—either larger than the maximum or lower than the minimum representable value.

Code Location:
Listing 1: modules/evm-accounts/src/lib.rs (Lines 182)
180 pub fn ethereum_signable_message(what: &[u8], extra: &[u8]) -> Vec<u8> {
181 let prefix = b"reef evm:";
182 let mut l = prefix.len() + what.len() + extra.len();
183 let mut rev = Vec::new();

Listing 2: modules/evm-accounts/src/lib.rs (Lines 313,314)
312 pub fn to_ascii_hex(data: &[u8]) -> Vec<u8> {
313 let mut r = Vec::with_capacity(data.len() * 2);
314 let mut push_nibble = |n| r.push(if n < 10 { b'0' + n } else { b'a' - 10 + n });

Listing 3: modules/evm-bridge/src/lib.rs (Lines 183,186,191)
182 let offset = U256::from_big_endian(&output[0..32]);
183 let length = U256::from_big_endian(&output[offset.as_usize()..offset.as_usize() + 32]);
184 ensure!(
// output is 32-byte aligned. ensure total_length >= offset + string length + string data length.
186 output.len() >= offset.as_usize() + 32 + length.as_usize(),
187 Error::<T>::InvalidReturnValue
188 );
189 let mut data = Vec::new();
191 data.extend_from_slice(&output[offset.as_usize() + 32..offset.as_usize() + 32 + length.as_usize()]);

Risk Level:
Likelihood - 3
Impact - 3

Recommendations:
It is recommended to use vetted safe math libraries for arithmetic operations consistently throughout the smart contract system. Consider replacing the addition and multiplication operators with Rust’s checked_add and checked_mul methods.

Remediation:
SOLVED: Reef fixed the issue in commit 6e4153498a28d03b8600739709cb200065c88781.

title: Total issuance not updated on mint
reported_impact: 3
description: 3.2 (HAL-02) TOTAL ISSUANCE NOT UPDATED ON MINT - MEDIUM
Description:
The update_balance dispatchable defined in modules/currencies/src/lib.rs does not update the total issuance of the currency (identified by user-supplied ID) which is minted to the target address. This may lead to discrepancies in token data.

Code Location:
Listing 4: modules/currencies/src/lib.rs (Lines 168)
159 #[pallet::weight(T::WeightInfo::update_balance_non_native_currency())]
160 pub fn update_balance(
161 origin: OriginFor<T>,
162 who: <T::Lookup as StaticLookup>::Source,
163 currency_id: CurrencyIdOf<T>,
164 amount: AmountOf<T>,
165 ) -> DispatchResultWithPostInfo {
166 ensure_root(origin)?;
167 let dest = T::Lookup::lookup(who)?;
168 <Self as MultiCurrencyExtended<T::AccountId>>::update_balance(currency_id, &dest, amount)?;
169 Ok(().into())
170 }

Risk Level:
Likelihood - 3
Impact - 3

Recommendations:
Total issuance should be updated every time tokens are minted or burned.

Remediation Plan:
ACKNOWLEDGED: Reef states that the affected function is sudo only and will be deprecated in a future release.

title: Casting overflow
reported_impact: 3
description: 3.3 (HAL-03) CASTING OVERFLOW - LOW
Description:
When converting or casting between types, an “overflow”/wrapping may occur and result in logic bugs leading to thread panic. The decode_string utility method defined in modules/evm-bridge/src/lib.rs does not validate if the values of the offset and length variables can be cast to the usize type. Although the method is not exported and available externally, the method is vulnerable still and the risk could increase in the future if the method is used before it’s patched.

Code Location:
Listing 5: modules/evm-bridge/src/lib.rs (Lines 183,186,191)
182 let offset = U256::from_big_endian(&output[0..32]);
183 let length = U256::from_big_endian(&output[offset.as_usize()..offset.as_usize() + 32]);
184 ensure!(
// output is 32-byte aligned. ensure total_length >= offset + string length + string data length.
186 output.len() >= offset.as_usize() + 32 + length.as_usize(),
187 Error::<T>::InvalidReturnValue
188 );
189 let mut data = Vec::new();
191 data.extend_from_slice(&output[offset.as_usize() + 32..offset.as_usize() + 32 + length.as_usize()]);

Risk Level:
Likelihood - 2
Impact - 3

Recommendations:
Check the value against maximum type value before casting.

Listing 6:
1 if (x <= usize::MAX) {
2 // logic ...
3 }

Remediation:
SOLVED: Reef fixed the issue in commit 313439bb7940afa0f0d5060fbcbbe26d5a3e5298.

title: Slash amount validation missing
reported_impact: 2
description: 3.4 (HAL-04) SLASH AMOUNT VALIDATION MISSING - LOW
Description:
The slash_reserved method defined in modules/currencies/src/lib.rs does not validate if the value of the user-supplied value parameter exceeds the actual balance of the account owned by the address that is to have its ERC20 tokens slashed.

Code Location:
Listing 7: modules/currencies/src/lib.rs (Lines 396)
394 fn slash_reserved(currency_id: Self::CurrencyId, who: &T::AccountId, value: Self::Balance) -> Self::Balance {
395 match currency_id {
396 CurrencyId::ERC20(_) => value,
397 CurrencyId::Token(TokenSymbol::REEF) => T::NativeCurrency::slash_reserved(who, value),
398 _ => T::MultiCurrency::slash_reserved(currency_id, who, value),
399 }
400 }

Risk Level:
Likelihood - 2
Impact - 2

Recommendations:
The slashed amount should always be lesser or equal to the account balance that is to be slashed.

Remediation:
SOLVED: Reef fixed the issue in commit bd43bec58890be763b32bfdfd18ba85a8c0ef9e5.

title: Currency ID validation missing
reported_impact: 2
description: 3.5 (HAL-05) CURRENCY ID VALIDATION MISSING - LOW
Description:
Many dispatchables and helper methods defined in modules/currencies/src/lib.rs do not check if the user-supplied currency ID matches any of the existing ones before calling the possibly resource-intensive underlying utility functions.

Code Location:
Listing 8: modules/evm-accounts/src/lib.rs (Lines 125)
121 #[pallet::weight(T::WeightInfo::transfer_non_native_currency())]
122 pub fn transfer(
123 origin: OriginFor<T>,
124 dest: <T::Lookup as StaticLookup>::Source,
125 currency_id: CurrencyIdOf<T>,
126 #[pallet::compact] amount: BalanceOf<T>,
127 ) -> DispatchResultWithPostInfo {
128 let from = ensure_signed(origin)?;
129 let to = T::Lookup::lookup(dest)?;
130 <Self as MultiCurrency<T::AccountId>>::transfer(currency_id, &from, &to, amount)?;
131 Ok(().into())
132 }

List of all the functions that fail to validate the currency ID:
Listing 9: (Lines 2,3)
1 auditor@halborn:~/projects/reef/reef-chain/modules/currencies$ \
2 > grep -ne 'fn.*CurrencyId' src/lib.rs \
3 > | cut -d '-' -f 1
4 178: fn minimum_balance(currency_id: Self::CurrencyId)
5 186: fn total_issuance(currency_id: Self::CurrencyId)
6 199: fn total_balance(currency_id: Self::CurrencyId, who: &T::AccountId)
7 217: fn free_balance(currency_id: Self::CurrencyId, who: &T::AccountId)
8 235: fn ensure_can_withdraw(currency_id: Self::CurrencyId, who: &T::AccountId, amount: Self::Balance)
9 290: fn deposit(currency_id: Self::CurrencyId, who: &T::AccountId, amount: Self::Balance)
10 303: fn withdraw(currency_id: Self::CurrencyId, who: &T::AccountId, amount: Self::Balance)
11 316: fn can_slash(currency_id: Self::CurrencyId, who: &T::AccountId, amount: Self::Balance)
12 324: fn slash(currency_id: Self::CurrencyId, who: &T::AccountId, amount: Self::Balance)
13 336: fn update_balance(currency_id: Self::CurrencyId, who: &T::AccountId, by_amount: Self::Amount)
14 376: fn remove_lock(lock_id: LockIdentifier, currency_id: Self::CurrencyId, who: &T::AccountId)
15 386: fn can_reserve(currency_id: Self::CurrencyId, who: &T::AccountId, value: Self::Balance)
16 394: fn slash_reserved(currency_id: Self::CurrencyId, who: &T::AccountId, value: Self::Balance)
17 402: fn reserved_balance(currency_id: Self::CurrencyId, who: &T::AccountId)
18 423: fn reserve(currency_id: Self::CurrencyId, who: &T::AccountId, value: Self::Balance)
19 445: fn unreserve(currency_id: Self::CurrencyId, who: &T::AccountId, value: Self::Balance)

Risk Level:
Likelihood - 2
Impact - 2

Recommendations:
It is recommended to validate all user-supplied input in order to avoid executing unnecessary operations and mitigate the risk of resource exhaustion.

Remediation Plan:
ACKNOWLEDGED: Reef states that there is only 1 currency id in use, and there likely won’t be more going forward.

title: Vector capacity validation missing
reported_impact: 2
description: 3.6 (HAL-06) VECTOR CAPACITY VALIDATION MISSING - INFORMATIONAL
Description:
The to_ascii_hex utility function defined in modules/evm-accounts/src/lib.rs when creating a new Vec<u8> from the user-supplied data slice with a Vec::with_capacity method does not validate if the capacity of the new vector exceeds the maximum allowed capacity.

Code Location:
Listing 10: modules/currencies/src/lib.rs (Lines 313)
312 pub fn to_ascii_hex(data: &[u8]) -> Vec<u8> {
313 let mut r = Vec::with_capacity(data.len() * 2);
314 let mut push_nibble = |n| r.push(if n < 10 { b'0' + n } else { b'a' - 10 + n });
315 for &b in data.iter() {
316 push_nibble(b / 16);
317 push_nibble(b % 16);
318 }
319 r
320 }

Risk Level:
Likelihood - 1
Impact - 2

Recommendations:
Validate if the new capacity (data.len() * 2) does not exceed isize::MAX bytes.

Remediation:
SOLVED: Reef fixed the issue in commit 6b826f7ca16d1a30f3fa55f0606d0b94b69b2b3a.