Halborn Logo

Octopus Network


Prepared by:

Halborn Logo

HALBORN

Last Updated 04/26/2024

Date of Engagement by: February 1st, 2022 - December 1st, 2021

Summary

100% of all REPORTED Findings have been addressed

All findings

15

Critical

2

High

1

Medium

4

Low

5

Informational

3


1. INTRODUCTION

Octopus Network engaged Halborn to conduct a security assessment on their NEAR anchor smart contracts beginning on February 1st, 2022 and ending December 1st, 2021. Octopus Network is a multichain interoperable cryptonetwork for launching and running Web3.0 Substrate-based application-specific blockchains, aka appchains.

Though this security audit's outcome is satisfactory, only the most essential aspects were tested and verified to achieve objectives and deliverables set in the scope due to time and resource constraints. It is essential to note the use of the best practices for secure development.

2. AUDIT SUMMARY

The team at Halborn was provided eight weeks for the engagement and assigned one full time security engineer to audit the security of the assets in scope. The engineer is a blockchain and smart contract security expert with advanced penetration testing, smart-contract hacking, and deep knowledge of multiple blockchain protocols.

The purpose of this audit is to achieve the following:

    • Identify potential security issues within the NEAR smart contracts.

In summary, Halborn identified few security risks that were mostly addressed by the Octopus Network team.

3. TEST APPROACH & METHODOLOGY

Halborn performed a combination of manual view of the code and automated security testing to balance efficiency, timeliness, practicality, and accuracy in regard to the scope of the smart contract audit. While manual testing is recommended to uncover flaws in logic, process, and implementation; automated testing techniques help enhance coverage of smart contracts and can quickly identify items that do not follow security best practices. The following phases and associated tools were used throughout the term of the audit:

    • Research into architecture, purpose, and use of the platform.

    • Manual code read and walkthrough.

    • Manual Assessment of use and safety for the critical Rust variables and functions in scope to identify any arithmetic related vulnerability classes.

    • Fuzz testing. (cargo fuzz)

    • Checking the unsafe code usage. (cargo-geiger)

    • Scanning of Rust files for vulnerabilities.(cargo audit)

    • Deployment to devnet through near-cli

4. SCOPE

octopus-appchain-anchor

    • all in appchain-anchor folder is in scope

5. RISK METHODOLOGY

Vulnerabilities or issues observed by Halborn are ranked based on the risk assessment methodology by measuring the LIKELIHOOD of a security incident and the IMPACT should an incident occur. This framework works for communicating the characteristics and impacts of technology vulnerabilities. The quantitative model ensures repeatable and accurate measurement while enabling users to see the underlying vulnerability characteristics that were used to generate the Risk scores. For every vulnerability, a risk level will be calculated on a scale of 5 to 1 with 5 being the highest likelihood or impact.
RISK SCALE - LIKELIHOOD
  • 5 - Almost certain an incident will occur.
  • 4 - High probability of an incident occurring.
  • 3 - Potential of a security incident in the long term.
  • 2 - Low probability of an incident occurring.
  • 1 - Very unlikely issue will cause an incident.
RISK SCALE - IMPACT
  • 5 - May cause devastating and unrecoverable impact or loss.
  • 4 - May cause a significant level of impact or loss.
  • 3 - May cause a partial impact or loss to many.
  • 2 - May cause temporary impact or loss.
  • 1 - May cause minimal or un-noticeable impact.
The risk level is then calculated using a sum of these two values, creating a value of 10 to 1 with 10 being the highest level of security risk.
Critical
High
Medium
Low
Informational
  • 10 - CRITICAL
  • 9 - 8 - HIGH
  • 7 - 6 - MEDIUM
  • 5 - 4 - LOW
  • 3 - 1 - VERY LOW AND INFORMATIONAL

6. SCOPE

Out-of-Scope: New features/implementations after the remediation commit IDs.

7. Assessment Summary & Findings Overview

Critical

2

High

1

Medium

4

Low

5

Informational

3

Impact x Likelihood

HAL-03

HAL-01

HAL-02

HAL-09

HAL-10

HAL-04

HAL-08

HAL-05

HAL-06

HAL-07

HAL-11

HAL-12

HAL-14

HAL-15

HAL-13

Security analysisRisk levelRemediation Date
ANYONE CAN CHANGE OCT TOKEN ACCOUNTCriticalSolved - 10/21/2021
SMART CONTRACT MAIN FUNCTIONALITY DoSCriticalSolved - 12/02/2021
IMPROPER ROLE-BASED ACCESS CONTROL POLICYHighSolved - 12/14/2021
REGISTRY OWNER CAN SET ITSELF AS VOTER OPERATORMediumSolved - 11/05/2021
REGISTRY OWNER CAN BE SET AS APPCHAIN OWNERMediumPartially Solved
USAGE OF SIGNER ACCOUNT ID INSTEAD OF PREDECESSOR ID IN ACCESS CONTROLMediumSolved - 12/02/2021
APPCHAIN CAN BE REGISTERED WITHOUT CORE DETAILSMediumSolved - 11/05/2021
MISSING CARGO OVERFLOW CHECKSLowSolved - 11/10/2021
LACK OF PAUSABILITY OF SMART CONTRACTSLowSolved - 12/14/2021
USAGE OF VULNERABLE CRATESLowRisk Accepted
MISSING ZERO VALUE CHECKLowSolved - 12/02/2021
REDUNDANT CODELowSolved - 11/05/2021
MISSING REASSIGNMENT CHECKSInformationalSolved - 11/05/2021
CODE REFACTOR OPPORTUNITYInformationalSolved - 12/02/2021
OUTDATED RUST EDITIONInformationalAcknowledged

8. Findings & Tech Details

8.1 ANYONE CAN CHANGE OCT TOKEN ACCOUNT

// Critical

Description

It was observed that the change_oct_token is lacking the ownership check, which allows anyone to change the OCT token account.

Code Location

appchain-registry/src/sudo_actions.rs

impl SudoActions for AppchainRegistry {
    //
    fn change_oct_token(&mut self, oct_token: AccountId) {
        self.oct_token = oct_token;
    }

Score
Impact: 5
Likelihood: 5
Recommendation

SOLVED: The Octopus Network team solved this issue by removing this function.

8.2 SMART CONTRACT MAIN FUNCTIONALITY DoS

// Critical

Description

It was observed that the project is vulnerable to DoS of the main functionality. In NEAR, there is a validation that tells whether the account format is valid or not. During conclude_voting_score, the new sub_account is created by appending the appchain_id to the registry account:

appchain-registry/src/registry_owner_actions.rs

fn conclude_voting_score(&mut self) {
        self.assert_owner();
        assert!(
            !self.top_appchain_id_in_queue.is_empty(),
            "There is no appchain on the top of queue yet."
        );
        // Set the appchain with the largest voting score to go `staging`
        let sub_account_id = format!(
            "{}.{}",
            &self.top_appchain_id_in_queue,
            env::current_account_id()
        );

Then, at the end, smart contracts creates a new create_account promise action to create new sub account:

appchain-registry/src/registry_owner_actions.rs

   Promise::new(sub_account_id)
            .create_account()
            .transfer(APPCHAIN_ANCHOR_INIT_BALANCE)
            .add_full_access_key(self.owner_pk.clone());
    }

The issue is that no check ensures that the appchain_id complies with NEAR's validation rules. Therefore, if invalid appchain_id became top_appchain_id_in_queue and used during the creation of sub_account, the smart contract will inevitably panic during the creation of the account. Since there is no functionality to remove top_appchain_id_in_queue, the smart contract won't conclude votes anymore. The smart contract will get stuck it at that appchain_id.

Code Location

appchain-registry/src/registry_owner_actions.rs

   Promise::new(sub_account_id)
            .create_account()
            .transfer(APPCHAIN_ANCHOR_INIT_BALANCE)
            .add_full_access_key(self.owner_pk.clone());
    }
Score
Impact: 5
Likelihood: 5
Recommendation

SOLVED: The Octopus Network team solved the issue by using ValidAccountId helper class.

Fixed Code:

appchain-registry/src/lib.rs

        assert!(
            !appchain_id.trim().is_empty(),
            "Missing necessary field 'appchain_id'."
        );
        assert!(appchain_id.find(".").is_none(), "Invalid 'appchain_id'.");
        assert!(
            ValidAccountId::try_from(format!("{}.{}", appchain_id, env::current_account_id()))
                .is_ok(),
            "Invalid 'appchain_id'."
        );

8.3 IMPROPER ROLE-BASED ACCESS CONTROL POLICY

// High

Description

It was observed that most of the privileged functionality is controlled by the owner. Additional authorization levels are needed to implement the least privilege principle, also known as least-authority, which ensures only authorized processes, users, or programs can access the necessary resources or information. The ownership role is helpful in a simple system, but more complex projects require more roles by using role-based access control.

Code Location

The owner can access those functions:

  • All functions in sudo_actions.rs
  • All functions in registry_settings_actions.rs
  • All functions in registry_owner_actions.rs except count_voting_score
  • set_owner in lib.rs
Score
Impact: 5
Likelihood: 3
Recommendation

SOLVED: The Octopus Network team solved the issue by adding role based access control functionality..

8.4 REGISTRY OWNER CAN SET ITSELF AS VOTER OPERATOR

// Medium

Description

It was observed that the owner could set itself as a voter_operator. This functionality violates the principle of least privilege giving the owner additional privileges.

Code Location

appchain-registry/src/registry_settings_actions.rs

   fn change_operator_of_counting_voting_score(&mut self, operator_account: AccountId) {
        self.assert_owner();
        let mut registry_settings = self.registry_settings.get().unwrap();
        registry_settings.operator_of_counting_voting_score.clear();
        registry_settings
            .operator_of_counting_voting_score
            .push_str(&operator_account);
        self.registry_settings.set(&registry_settings);
    }

Score
Impact: 4
Likelihood: 3
Recommendation

SOLVED: The Octopus Network team solved the issue by adding relevant check.

Fixed Code:

appchain-registry/src/registry_settings_actions.rs

    fn change_operator_of_counting_voting_score(&mut self, operator_account: AccountId) {
        self.assert_owner();
        assert_ne!(
            operator_account, self.owner,
            "The account should NOT be the owner."
        );
        let mut registry_settings = self.registry_settings.get().unwrap();
        assert_ne!(
            operator_account, registry_settings.operator_of_counting_voting_score,
            "The account is not changed."
        );
        registry_settings.operator_of_counting_voting_score = operator_account;
        self.registry_settings.set(&registry_settings);
    }

8.5 REGISTRY OWNER CAN BE SET AS APPCHAIN OWNER

// Medium

Description

It was observed that the owner could be set as an appchain_owner. This functionality violates the principle of least privilege giving the owner additional privileges.

Code Location

appchain-registry/src/lib.rs: register_appchain

  • sender_id should not be equal to the registry owner

appchain-registry/src/appchain_owner_actions.rs: transfer_appchain_ownership

  • new_owner should not be equal to the registry owner
Score
Impact: 3
Likelihood: 3
Recommendation

PARTIALLY SOLVED: The Octopus Network team partially solved the issue by adding the required check only to appchain-registry/src/lib.rs.

Fixed Code:

appchain-registry/src/lib.rs

appchain-registry/src/lib.rs

 fn register_appchain(
        &mut self,
        sender_id: AccountId,
        appchain_id: AppchainId,
        register_deposit: Balance,
        website_url: String,
        function_spec_url: String,
        github_address: String,
        github_release: String,
        contact_email: String,
        premined_wrapped_appchain_token_beneficiary: AccountId,
        premined_wrapped_appchain_token: U128,
        ido_amount_of_wrapped_appchain_token: U128,
        initial_era_reward: U128,
        fungible_token_metadata: FungibleTokenMetadata,
        custom_metadata: HashMap<String, String>,
    ) {
        assert_ne!(
            sender_id, self.owner,
            "The register account should NOT be the contract owner."
        );

8.6 USAGE OF SIGNER ACCOUNT ID INSTEAD OF PREDECESSOR ID IN ACCESS CONTROL

// Medium

Description

It was observed that the env::signer_account_id() was used in the assert_appchain_owner to assert whether the caller is the appchain_owner.

  • env::signer_account_id(): The id of the account that either signed the original transaction or issued the initial cross-contract call.

  • env::predecessor_account_id(): The id of the account that was the previous contract in the chain of cross-contract calls. If this is the first contract, it is equal to signer_account_id.

From their definitions above, we can derive that the usage of env::signer_account_id() is risky in access control scenarios. There is a risk that the appchain owner can be phished to sign the cross contract call and hence unknowingly let the malicious contract execute functions in the project's contract under that owner's role.

Code Location

appchain-registry/src/lib.rs

fn assert_appchain_owner(&self, appchain_id: &AppchainId) {
        let appchain_basedata = self.get_appchain_basedata(appchain_id);
        assert_eq!(
            env::signer_account_id(),
            appchain_basedata.owner().clone(),
            "Function can only be called by appchain owner."
        );
    }
Score
Impact: 3
Likelihood: 3
Recommendation

SOLVED: The Octopus Network team solved the issue by changing env::signer_account_id() to env::predecessor_account_id().

Fixed Code:

appchain-registry/src/lib.rs

fn assert_appchain_owner(&self, appchain_id: &AppchainId) {
        let appchain_basedata = self.get_appchain_basedata(appchain_id);
        assert_eq!(
            env::predecessor_account_id(),
            appchain_basedata.owner().clone(),
            "Function can only be called by appchain owner."
        );
    }

8.7 APPCHAIN CAN BE REGISTERED WITHOUT CORE DETAILS

// Medium

Description

It was observed that it is possible to register an appchain without providing any core details such as appchain_id, website_url, and so on. Those details are needed for intended functionality of the application.

Code Location

Existence of those fields has to be enforced:

appchain-registry/src/lib.rs: register_appchain

  • appchain_id
  • website_url
  • function_spec_url
  • github_address
  • github_release
  • contact_email
  • premined_wrapped_appchain_token_beneficiary
  • fungible_token_metadata.name
  • fungible_token_metadata.symbol
Score
Impact: 3
Likelihood: 4
Recommendation

SOLVED: The Octopus Network team solved the issue by enforcing required fields.

Fixed Code:

appchain-registry/src/lib.rs

assert!(
            !appchain_id.trim().is_empty(),
            "Missing necessary field 'appchain_id'."
        );
        assert!(
            !website_url.trim().is_empty(),
            "Missing necessary field 'website_url'."
        );
        assert!(
            !function_spec_url.trim().is_empty(),
            "Missing necessary field 'function_spec_url'."
        );
        assert!(
            !github_address.trim().is_empty(),
            "Missing necessary field 'github_address'."
        );
        assert!(
            !github_release.trim().is_empty(),
            "Missing necessary field 'github_release'."
        );
        assert!(
            !contact_email.trim().is_empty(),
            "Missing necessary field 'contact_email'."
        );
        assert!(
            !premined_wrapped_appchain_token_beneficiary
                .trim()
                .is_empty(),
            "Missing necessary field 'premined_wrapped_appchain_token_beneficiary'."
        );
        fungible_token_metadata.assert_valid();
        assert!(
            !fungible_token_metadata.name.trim().is_empty(),
            "Missing necessary field 'fungible token name'."
        );
        assert!(
            !fungible_token_metadata.symbol.trim().is_empty(),
            "Missing necessary field 'fungible token symbol'."
        );

8.8 MISSING CARGO OVERFLOW CHECKS

// Low

Description

It was observed that there is no overflow-checks=true in Cargo.toml. By default, overflow checks are disabled in optimized release builds. Hence, if there is an overflow in release builds, it will be silenced, leading to unexpected behavior of an application. Even if checked arithmetic is used through checked_*, it is recommended to have that check in Cargo.toml.

Code Location

  • Cargo.toml
Score
Impact: 3
Likelihood: 2
Recommendation

SOLVED: The Octopus Network team solved the issue by adding overflow-checks=true.

8.9 LACK OF PAUSABILITY OF SMART CONTRACTS

// Low

Description

The project lacks ability to pause contracts. It is advised that in case of unexpected events temporarily disable some important functions to prevent further damage.

Score
Impact: 4
Likelihood: 1
Recommendation

SOLVED: The Octopus Network team solved the issue by adding the pausability to smart contracts.

8.10 USAGE OF VULNERABLE CRATES

// Low

Description

It was observed that the project uses crates with known vulnerabilities.

Code Location

\begin{center} \begin{tabular}{|l|p{2cm}|p{9cm}|} \hline \textbf{ID} & \textbf{package} & \textbf{Short Description} \ \hline \href{https://rustsec.org/advisories/RUSTSEC-2020-0159}{RUSTSEC-2020-0159} & chrono & Potential segfault in localtime\textunderscore r invocations \ \hline \href{https://rustsec.org/advisories/RUSTSEC-2021-0067}{RUSTSEC-2021-0067} & cranelift-codegen & Memory access due to code generation flaw in Cranelift module\ \hline \href{https://rustsec.org/advisories/RUSTSEC-2021-0013}{RUSTSEC-2021-0013} & raw-cpuid & Soundness issues in raw-cpuid \ \hline \href{https://rustsec.org/advisories/RUSTSEC-2021-0089}{RUSTSEC-2021-0089} & raw-cpuid & Optional Deserialize implementations lacking validation \ \hline \href{https://rustsec.org/advisories/RUSTSEC-2020-0071}{RUSTSEC-2020-0071} & time & Potential segfault in the time crate \ \hline \href{https://rustsec.org/advisories/RUSTSEC-2021-0110}{RUSTSEC-2021-0110} & wasmtime & Multiple Vulnerabilities in Wasmtime \ \hline \end{tabular} \end{center}

Score
Impact: 4
Likelihood: 1
Recommendation

RISK ACCEPTED: The Octopus Network team acknowledged the issue and is working on fixing it.

8.11 MISSING ZERO VALUE CHECK

// Low

Description

There are functions within the project that should have a zero value check.

Code Location

appchain-registry/src/registry_settings_actions.rs

  • change_minimum_register_deposit
    • value should be greater than 0
  • change_counting_interval_in_seconds

    • value should be greater than 0. Possibly bigger than 3600 seconds.
  • value should be greater than 0

  • value should be greater than 0. Possibly bigger than 3600 seconds.

appchain-registry/src/voter_actions.rs

  • withdraw_upvote_deposit_of

    • amount should be greater than 0.
  • withdraw_downvote_deposit_of

    • amount should be greater than 0.
  • amount should be greater than 0.

  • amount should be greater than 0.

Score
Impact: 2
Likelihood: 3
Recommendation

SOLVED: The Octopus Network team solved the issue by adding necessary zero-value checks.

8.12 REDUNDANT CODE

// Low

Description

It was observed that there is a redundant code in withdraw_upvote_deposit_of and withdraw_downvote_deposit_of in the voter_actions module. Even though there is already a variable called voter = env::predecessor_account_id(); new variable called account_id = env::predecessor_account_id(); is created and the same actions are performed for both of those variables for voter_upvote.

Code Location

appchain-appchain-registry/src/voter_actions.rs

fn withdraw_upvote_deposit_of(&mut self, appchain_id: AppchainId, amount: U128) {
        let voter = env::predecessor_account_id();
        let voter_upvote = self
            .upvote_deposits
            .get(&(appchain_id.clone(), voter.clone()))
            .unwrap_or_default();
        assert!(
            voter_upvote >= amount.0,
            "Not enough upvote deposit to withdraw."
        );
        let mut appchain_basedata = self.get_appchain_basedata(&appchain_id);
        let account_id = env::predecessor_account_id();
        let voter_upvote = self
            .upvote_deposits
            .get(&(appchain_id.clone(), account_id.clone()))
            .unwrap_or_default();
        appchain_basedata.decrease_upvote_deposit(amount.0);
        self.appchain_basedatas
            .insert(&appchain_id, &appchain_basedata);
        if amount.0 == voter_upvote {
            self.upvote_deposits
                .remove(&(appchain_id.clone(), account_id.clone()));
        } else {
            self.upvote_deposits.insert(
                &(appchain_id.clone(), account_id.clone()),
                &(voter_upvote - amount.0),
            );
        }



Score
Impact: 2
Likelihood: 3
Recommendation

SOLVED: The Octopus Network team solved the issue by removing redundant code.

Fixed Code:

appchain-appchain-registry/src/voter_actions.rs

fn withdraw_upvote_deposit_of(&mut self, appchain_id: AppchainId, amount: U128) {
        let voter = env::predecessor_account_id();
        let voter_upvote = self
            .upvote_deposits
            .get(&(appchain_id.clone(), voter.clone()))
            .unwrap_or_default();
        assert!(
            voter_upvote >= amount.0,
            "Not enough upvote deposit to withdraw."
        );
        let mut appchain_basedata = self.get_appchain_basedata(&appchain_id);
        appchain_basedata.decrease_upvote_deposit(amount.0);
        self.appchain_basedatas
            .insert(&appchain_id, &appchain_basedata);
        if amount.0 == voter_upvote {
            self.upvote_deposits
                .remove(&(appchain_id.clone(), voter.clone()));
        } else {
            self.upvote_deposits.insert(
                &(appchain_id.clone(), voter.clone()),
                &(voter_upvote - amount.0),
            );
        }

8.13 MISSING REASSIGNMENT CHECKS

// Informational

Description

It was observed that the project is missing reassignment checks. Reassignment checks make sure that redundant operations are not performed by not letting the reassignment of the existing value.

Code Location

appchain-registry/src/registry_settings_actions.rs

  • change_operator_of_counting_voting_score: value

appchain-registry/src/lib.rs

  • set_owner: owner
Score
Impact: 1
Likelihood: 2
Recommendation

SOLVED: The Octopus Network team added all necessary reassignment checks.

8.14 CODE REFACTOR OPPORTUNITY

// Informational

Description

It was observed that the project is manually restricts the usage of uninitialized smart contract. However, near_sdk already provides a PanicOnDefault macro that generates that code for you.

Code Location

appchain-registry/src/lib.rs

impl Default for AppchainRegistry {
    fn default() -> Self {
        env::panic(b"The contract needs be initialized before use.")
    }
}
Score
Impact: 1
Likelihood: 1
Recommendation

SOLVED: The Octopus Network team solved the issue by adding PanicOnDefault macro.

8.15 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

appchain-registry/Cargo.toml

[package]
name = "appchain-registry"
version = "1.0.5"
authors = ["Octopus Network"]
edition = "2018"
Score
Impact: 1
Likelihood: 1
Recommendation

ACKNOWLEDGED: The Octopus Network team acknowledged the issue and is working on fixing it.

9. Automated Testing

AUTOMATED ANALYSIS

Description

Halborn used automated security scanners to assist with detection of well-known security issues and vulnerabilities. Among the tools used was cargo audit, a security scanner for vulnerabilities reported to the RustSec Advisory Database. All vulnerabilities published in https://crates.io are stored in a repository named The RustSec Advisory Database. cargo audit is a human-readable version of the advisory database which performs a scanning on Cargo.lock. Security Detections are only in scope. All vulnerabilities shown here were already disclosed in the above report. However, to better assist the developers maintaining this code, the auditors are including the output with the dependencies tree, and this is included in the cargo audit output to better know the dependencies affected by unmaintained and vulnerable crates.

Results

\begin{center} \begin{tabular}{|l|p{2cm}|p{9cm}|} \hline \textbf{ID} & \textbf{package} & \textbf{Short Description} \ \hline \href{https://rustsec.org/advisories/RUSTSEC-2020-0159}{RUSTSEC-2020-0159} & chrono & Potential segfault in localtime\textunderscore r invocations \ \hline \href{https://rustsec.org/advisories/RUSTSEC-2021-0067}{RUSTSEC-2021-0067} & cranelift-codegen & Memory access due to code generation flaw in Cranelift module\ \hline \href{https://rustsec.org/advisories/RUSTSEC-2021-0013}{RUSTSEC-2021-0013} & raw-cpuid & Soundness issues in raw-cpuid \ \hline \href{https://rustsec.org/advisories/RUSTSEC-2021-0089}{RUSTSEC-2021-0089} & raw-cpuid & Optional Deserialize implementations lacking validation \ \hline \href{https://rustsec.org/advisories/RUSTSEC-2020-0071}{RUSTSEC-2020-0071} & time & Potential segfault in the time crate \ \hline \href{https://rustsec.org/advisories/RUSTSEC-2021-0110}{RUSTSEC-2021-0110} & wasmtime & Multiple Vulnerabilities in Wasmtime \ \hline \end{tabular} \end{center}

Halborn strongly recommends conducting a follow-up assessment of the project either within six months or immediately following any material changes to the codebase, whichever comes first. This approach is crucial for maintaining the project’s integrity and addressing potential vulnerabilities introduced by code modifications.