Bridge Contracts - Casper Association


Prepared by:

Halborn Logo

HALBORN

Last Updated Unknown date

Date of Engagement: June 17th, 2024 - July 17th, 2024

Summary

100% of all REPORTED Findings have been addressed

All findings

9

Critical

0

High

0

Medium

0

Low

6

Informational

3


1. Introduction

Allbridge engaged Halborn to conduct a security assessment of the Bridge and ERC20 contracts, beginning on June 17th, 2024 and ending on July 17th, 2024. This security assessment was scoped to the smart contracts in the bridge-casper-contract GitHub repository.

Allbridge is a simple and reliable tool for moving assets between different blockchain networks. It contributes to a more connected blockchain world by enabling easy and fast transfers, allowing assets to move quickly, similar to normal transactions on any blockchain. Allbridge also offers the flexibility to choose how to send and receive tokens, whether they are native to the blockchain or wrapped versions.

2. Assessment Summary

The team at Halborn assigned a full-time security engineer to verify the security of the smart contracts. The security 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 assessment is to:

    • Ensure that smart contract functions operate as intended.

    • Identify potential security issues with the smart contracts.


In summary, Halborn identified some improvements to reduce the likelihood and impact of risks, which were acknowledged by the Allbridge team. The main ones are the following: 

    • The status of the token should be checked before performing any operations.

    • The access permission of the constructor entry point should be changed to Groups since a user group is created.

    • The fee rate should be checked to ensure it is lower than 100% or under a maximum threshold to avoid unintended errors.

    • The ERC20 contract should provide a mechanism for upgrades if needed.

    • The lock_id should be validated in conjunction with the transaction sender's address to avoid forged lock requests.

    • Ownership should be transferred in a two-step process to avoid lossing the control of the contract.

3. Test Approach and Methodology

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

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

    • Manual code read and walk through.

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

    • Cross contract call controls.

    • Architecture related logical controls.

    • Scanning of Rust files for vulnerabilities (cargo audit)

    • Integration testing using the Casper Engine Test Support.

    • Tesnet deployment and comprehensive review of transactions through the CPSR live explorer.

4. RISK METHODOLOGY

Every vulnerability and issue observed by Halborn is ranked based on two sets of Metrics and a Severity Coefficient. This system is inspired by the industry standard Common Vulnerability Scoring System.
The two Metric sets are: Exploitability and Impact. Exploitability captures the ease and technical means by which vulnerabilities can be exploited and Impact describes the consequences of a successful exploit.
The Severity Coefficients is designed to further refine the accuracy of the ranking with two factors: Reversibility and Scope. These capture the impact of the vulnerability on the environment as well as the number of users and smart contracts affected.
The final score is a value between 0-10 rounded up to 1 decimal place and 10 corresponding to the highest security risk. This provides an objective and accurate rating of the severity of security vulnerabilities in smart contracts.
The system is designed to assist in identifying and prioritizing vulnerabilities based on their level of risk to address the most critical issues in a timely manner.

4.1 EXPLOITABILITY

Attack Origin (AO):
Captures whether the attack requires compromising a specific account.
Attack Cost (AC):
Captures the cost of exploiting the vulnerability incurred by the attacker relative to sending a single transaction on the relevant blockchain. Includes but is not limited to financial and computational cost.
Attack Complexity (AX):
Describes the conditions beyond the attacker’s control that must exist in order to exploit the vulnerability. Includes but is not limited to macro situation, available third-party liquidity and regulatory challenges.
Metrics:
EXPLOITABILITY METRIC (mem_e)METRIC VALUENUMERICAL VALUE
Attack Origin (AO)Arbitrary (AO:A)
Specific (AO:S)
1
0.2
Attack Cost (AC)Low (AC:L)
Medium (AC:M)
High (AC:H)
1
0.67
0.33
Attack Complexity (AX)Low (AX:L)
Medium (AX:M)
High (AX:H)
1
0.67
0.33
Exploitability EE is calculated using the following formula:

E=meE = \prod m_e

4.2 IMPACT

Confidentiality (C):
Measures the impact to the confidentiality of the information resources managed by the contract due to a successfully exploited vulnerability. Confidentiality refers to limiting access to authorized users only.
Integrity (I):
Measures the impact to integrity of a successfully exploited vulnerability. Integrity refers to the trustworthiness and veracity of data stored and/or processed on-chain. Integrity impact directly affecting Deposit or Yield records is excluded.
Availability (A):
Measures the impact to the availability of the impacted component resulting from a successfully exploited vulnerability. This metric refers to smart contract features and functionality, not state. Availability impact directly affecting Deposit or Yield is excluded.
Deposit (D):
Measures the impact to the deposits made to the contract by either users or owners.
Yield (Y):
Measures the impact to the yield generated by the contract for either users or owners.
Metrics:
IMPACT METRIC (mIm_I)METRIC VALUENUMERICAL VALUE
Confidentiality (C)None (I:N)
Low (I:L)
Medium (I:M)
High (I:H)
Critical (I:C)
0
0.25
0.5
0.75
1
Integrity (I)None (I:N)
Low (I:L)
Medium (I:M)
High (I:H)
Critical (I:C)
0
0.25
0.5
0.75
1
Availability (A)None (A:N)
Low (A:L)
Medium (A:M)
High (A:H)
Critical (A:C)
0
0.25
0.5
0.75
1
Deposit (D)None (D:N)
Low (D:L)
Medium (D:M)
High (D:H)
Critical (D:C)
0
0.25
0.5
0.75
1
Yield (Y)None (Y:N)
Low (Y:L)
Medium (Y:M)
High (Y:H)
Critical (Y:C)
0
0.25
0.5
0.75
1
Impact II is calculated using the following formula:

I=max(mI)+mImax(mI)4I = max(m_I) + \frac{\sum{m_I} - max(m_I)}{4}

4.3 SEVERITY COEFFICIENT

Reversibility (R):
Describes the share of the exploited vulnerability effects that can be reversed. For upgradeable contracts, assume the contract private key is available.
Scope (S):
Captures whether a vulnerability in one vulnerable contract impacts resources in other contracts.
Metrics:
SEVERITY COEFFICIENT (CC)COEFFICIENT VALUENUMERICAL VALUE
Reversibility (rr)None (R:N)
Partial (R:P)
Full (R:F)
1
0.5
0.25
Scope (ss)Changed (S:C)
Unchanged (S:U)
1.25
1
Severity Coefficient CC is obtained by the following product:

C=rsC = rs

The Vulnerability Severity Score SS is obtained by:

S=min(10,EIC10)S = min(10, EIC * 10)

The score is rounded up to 1 decimal places.
SeverityScore Value Range
Critical9 - 10
High7 - 8.9
Medium4.5 - 6.9
Low2 - 4.4
Informational0 - 1.9

5. SCOPE

REPOSITORY
(a) Repository: bridge-casper-contract
(c) Items in scope:
  • contracts/bridge
  • contracts/erc20
  • common/bridge_types
↓ Expand ↓
Out-of-Scope: Third party dependencies, economic attacks.
Out-of-Scope: New features/implementations after the remediation commit IDs.

6. Assessment Summary & Findings Overview

Critical

0

High

0

Medium

0

Low

6

Informational

3

Security analysisRisk levelRemediation Date
Unlock operation could be done on disabled tokensLowRisk Accepted
Incorrect access permissions in constructor entry pointLowRisk Accepted
Fee rate not validatedLowRisk Accepted
Lack of contract upgrade capabilityLowRisk Accepted
Potential lock ID forgery leading to DoSLowRisk Accepted
Owneship transfer in one stepLowRisk Accepted
Compliance with Casper Fungible Token Standard (CEP-18)InformationalAcknowledged
Redundant codeInformationalAcknowledged
Missing argument on entry pointInformationalAcknowledged

7. Findings & Tech Details

7.1 Unlock operation could be done on disabled tokens

//

Low

Description

The unlock function allows unlocking the funds that have been locked on the origin chain, transferring or minting the corresponding token on the Casper chain.

However, this function does not verify if the token to be unlocked is already set as disabled in the token_info.token_status variable.

If the lock operation is not disabled on the other side of the bridge, this would allow continuing to work with a disabled token, but without the option of recovering the initial tokens on the origin chain, since the lock/burn operation on this chain does not allow the operation if the token is disabled.

Code Location

Code snippet of unlock function from contracts/bridge/src/bridge_unlock.rs file:

    pub fn unlock(unlock_args: UnlockArgs) -> ContractResult<()> {
        Self::assert_active()?;
        assert_lock_id_version(unlock_args.lock_id.as_u128(), Self::get_version()?)?;

        Self::verify_signature(&unlock_args)?;

        let (token_info, token_address) = get_assert_token_info_by_source(
            &unlock_args.token_source,
            &unlock_args.token_source_address,
        )?;

        let amount_total = from_system_precision(unlock_args.amount, token_info.precision);

        let fee = if runtime::get_caller() == Self::get_unlock_signer()? {
            token_info.min_fee
        } else {
            U256::zero()
        };
BVSS
Recommendation

It is recommended to validate the token_info.token_status parameter at the beginning of the unlock function, before unlocking/mining any tokens.

Remediation Plan

RISK ACCEPTED: The AllBridge team has accepted the risk of this finding by stating that they need the ability to unlock transactions that might have been left incomplete at the time of token deactivation.

7.2 Incorrect access permissions in constructor entry point

//

Low

Description

The endpoint function in the common/bridge_utils/src/utils.rs file is used to create each EntryPoint object during contract deployment. The problem is that this function automatically sets the access permissions of the entry point to Public by default.

In the deploy function, a user group is created to prevent the constructor entry point from being called from an external source. The constructor entry point initializes all the main parameters of the contract, so it should be executed only once.

However, because the endpoint function sets the EntryPointAccess parameter for the constructor entry point to Public, the user group creation becomes ineffective.

Thankfully, this issue is mitigated by the init functions that create the dictionaries during the constructor execution, as they revert execution if the dictionary already exists.

Code Location

Code snippet of endpoint function from common/bridge_utils/src/utils.rs file for EntryPoint object creation:

pub fn endpoint(name: &str, param: Vec<Parameter>) -> EntryPoint {
    EntryPoint::new(
        String::from(name),
        param,
        CLType::Unit,
        EntryPointAccess::Public,
        EntryPointType::Contract,
    )
}

Code snippet of get_entry_points function from contracts/bridge/src/bridge_contract.rs file for constructor entry point creation:

    entry_points.add_entry_point(endpoint(
        "constructor",
        vec![
            Parameter::new("version", u8::cl_type()),
            Parameter::new("role_manager", AccountHash::cl_type()),
            Parameter::new("oracle", Oracle::cl_type()),
            Parameter::new("unlock_signer", AccountHash::cl_type()),
            Parameter::new("fee_collector", AccountHash::cl_type()),
            Parameter::new("base_fee_rate_bp", U256::cl_type()),
        ],
    ));
BVSS
Recommendation

It is recommended to modify the EntryPointAccess parameter of the constructor entry point and set it to Groups.

Alternatively, since the dictionary initialization already prevents a second execution, the user group can be removed from the deploy function, which would save some gas during deployment.

Remediation Plan

RISK ACCEPTED: The AllBridge team has accepted the risk of this finding.

7.3 Fee rate not validated

//

Low

Description

The base_fee_rate_bp parameter is a fee rate used to calculate the amount of fee collected during the lock operation.

As a general best practice in such operations, any rate corresponding to a fee should be checked to ensure it is less than 100% or below a fixed maximum threshold (e.g., 20%).

In this case, the base_fee_rate_bp value is not validated in either the set_base_fee_rate or the init functions. Although any value over 100% would result in the BridgeError::AmountTooSmall error, a 100% value would be allowed, which could leave the amount_to_lock equal to zero, for example.

Code Location

Code snippet of set_base_fee_rate function from contract/bridge/src/bridge_mics.rs file:

    pub fn set_base_fee_rate(base_fee_rate: U256) -> ContractResult<()> {
        Manager::assert_role(Manager::Bridge)?;

        Bridge::set_base_fee_rate_bp(base_fee_rate)
    }

Code snippet of init function from contract/bridge/src/bridge.rs file for contract initialization:

        Self::set_status(true)?;
        Self::set_version(version)?;
        Self::set_oracle(oracle)?;
        Self::set_fee_collector(fee_collector)?;
        Self::set_unlock_signer(unlock_signer)?;
        Self::set_package_hash(package_hash)?;
        Self::set_base_fee_rate_bp(base_fee_rate_bp)?;

        Ok(())
    }

BVSS
Recommendation

It is recommended to validate the base_rate_fee_bp parameter before storing it, checking that it is less than 100% or another type of maximum threshold (also less than 100%).

Remediation Plan

RISK ACCEPTED: The AllBridge team has accepted the risk of this finding.

7.4 Lack of contract upgrade capability

//

Low

Description

The current ERC20 token contract lacks a mechanism for future upgrades, which is essential for adapting to changes and improvements.

Casper supports contract versioning, allowing developers to deploy new versions of contracts while maintaining compatibility with existing states. This feature is crucial for the long-term evolution and security of smart contracts, enabling seamless updates and continuous improvement without disrupting existing functionality.

BVSS
Recommendation

It is recommended to implement an upgrade function to enable the contract to be updated to new versions, ensuring ongoing maintenance and flexibility for future enhancements.

Remediation Plan

RISK ACCEPTED: The AllBridge team has accepted the risk of this finding.

7.5 Potential lock ID forgery leading to DoS

//

Low

Description

The lock function allows a user to create a lock on the Casper blockchain by sending the LockArgs parameters and some funds.

The only checks performed by the contract are to validate if the lock_id has already been registered and if the funds are sufficient to cover the fee. If a malicious transaction is made before the legitimate one with the same lock_id and an amount sufficient to cover the fees, the legitimate lock would be rejected because the lock_id already exists, resulting in a DoS attack.

The sender of the lock transaction is not verified at any point. Although this is a very unlikely scenario because the lock_id is supposed to be a random identifier, it is still possible.

Code Location

Code snippet of _create_lock function from contract/bridge/src/validator.rs file:

        let locks_dict = Locks::instance()?;

        assert_lock_id_version(new_lock.lock_id.as_u128(), Self::get_version()?)?;

        assert(
            !locks_dict.has(&new_lock.lock_id),
            BridgeError::LockAlreadyExists,
        )?;

        let sender = runtime::get_caller();

        let lock = Lock {
            sender,
            recipient: new_lock.recipient,
            amount: to_system_precision(amount_to_lock, token_info.precision),
            destination: new_lock.destination,
            token_source: token_info.token_source,
            token_source_address: token_info.token_source_address,
        };

BVSS
Recommendation

It is recommended to store the lock_id in conjunction with the sender's address as the key in the Locks dictionary to prevent any possibility of lock record falsification.

Remediation Plan

RISK ACCEPTED: The AllBridge team has accepted the risk of this finding.

7.6 Owneship transfer in one step

//

Low

Description

The transfer_ownership function from ERC20 contract transfers ownership in one step.

In case of an error in the minter input, the contract would be locked in another account without access.

Code Location

Code snippet of transfer_ownership function from contracts/erc20/src/lib.rs file:

    /// Transfers ownership of the contract to a new account (`minter`).
    /// Can only be called by the current minter.
    pub fn transfer_ownership(&mut self, minter: Address) {
        self.assert_minter();

        let minter_uref = *self.minter_uref.get_or_init(minter::minter_uref);
        minter::write_minter_to(minter_uref, minter)
    }

BVSS
Recommendation

It is recommended to perform this action in two steps: one to establish the new owner's address (propose_new_owner) and one to accept it (accept_ownership).

The latter function can only be executed by the new owner.

Remediation Plan

RISK ACCEPTED: The AllBridge team has accepted the risk of this finding.


7.7 Compliance with Casper Fungible Token Standard (CEP-18)

//

Informational

Description

The current ERC20 contract used for fungible tokens it is based on the erc20-guide-extraction repository from Casper Network GitHub, and it has not been updated for 2 years.

To ensure enhanced security and maintenance, it is recommended to transition to the Casper CEP-18 standard, which is actively maintained and audited by the Casper community, and serves as the standard for fungible tokens in Casper.

BVSS
Recommendation

It is recommended to transition to the Casper CEP-18 standard, which serves as the standard for fungible tokens in Casper.

This update will align the token implementation with the latest industry standards and best practices.

Remediation Plan

ACKNOWLEDGED: The AllBridge team has acknowledged this finding.

7.8 Redundant code

//

Informational

Description

The create_unlock function contains code that is already present in the unlock function, leading to redundancy and increasing gas usage in transactions and deployment.

Code Location

Code snippet of create_unlock function from contract/bridge/src/validator.rs file:

    pub fn create_unlock(unlock_args: &UnlockArgs) -> ContractResult<()> {
        assert_lock_id_version(unlock_args.lock_id.as_u128(), Self::get_version()?)?;
        let unlock_id = compose_unlock_id(&unlock_args.lock_source, unlock_args.lock_id.as_u128());

        assert_unlock_not_exists(&unlock_id)?;

        Unlocks::instance()?.set(unlock_id);

        Ok(())
    }

Code snippet of unlock function from contract/bridge/src/bridge_unlock.rs file:

    pub fn unlock(unlock_args: UnlockArgs) -> ContractResult<()> {
        Self::assert_active()?;
        assert_lock_id_version(unlock_args.lock_id.as_u128(), Self::get_version()?)?;

        Self::verify_signature(&unlock_args)?;

        let (token_info, token_address) = get_assert_token_info_by_source(
            &unlock_args.token_source,
            &unlock_args.token_source_address,
        )?;

        let amount_total = from_system_precision(unlock_args.amount, token_info.precision);

        let fee = if runtime::get_caller() == Self::get_unlock_signer()? {
            token_info.min_fee
        } else {
            U256::zero()
        };

        let unlock_id = compose_unlock_id(&unlock_args.lock_source, unlock_args.lock_id.as_u128());
        assert(
            !Unlocks::instance()?.has(unlock_id),
            BridgeError::AlreadyReceived,
        )?;
BVSS
Recommendation

To optimize gas consumption, it is recommended to incorporate the line Unlocks::instance()?.set(unlock_id); within the unlock function and to remove the create_unlock one.

Remediation Plan

ACKNOWLEDGED: The AllBridge team has acknowledged this finding.

7.9 Missing argument on entry point

//

Informational

Description

The constructor EntryPoint object is not properly created because the corresponding function takes the input argument contract_package_hash, which is not declared in the EntryPoint input parameters.

Code Location

Code snippet of get_entry_points function from contract/bridge/bin/bringe_contract.rs file:

    entry_points.add_entry_point(endpoint(
        "constructor",
        vec![
            Parameter::new("version", u8::cl_type()),
            Parameter::new("role_manager", AccountHash::cl_type()),
            Parameter::new("oracle", Oracle::cl_type()),
            Parameter::new("unlock_signer", AccountHash::cl_type()),
            Parameter::new("fee_collector", AccountHash::cl_type()),
            Parameter::new("base_fee_rate_bp", U256::cl_type()),
        ],
    ));

Code snippet of constructor function from contract/bridge/bin/bridge_contract.rs file:

fn constructor() {
    Bridge::init(
        runtime::get_named_arg("version"),
        runtime::get_named_arg("role_manager"),
        runtime::get_named_arg("oracle"),
        runtime::get_named_arg("unlock_signer"),
        runtime::get_named_arg("fee_collector"),
        runtime::get_named_arg("contract_package_hash"),
        runtime::get_named_arg("base_fee_rate_bp"),
    )
    .unwrap_or_revert();
}
BVSS
Recommendation

It is recommended to rewrite the declaration of the EntryPoint object to include the contract_package_hash variable. However, this is not critical as calls to entry points work with extra parameters in addition to those declared.

Remediation Plan

ACKNOWLEDGED: The AllBridge team has acknowledged this finding.

8. Automated Testing

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

ID

Package

Description

RUSTSEC-2024-0344

curve25519-dalek

Version: 3.2.1

Timing variability

Upgrade to >=4.1.3

RUSTSEC-2022-0093

ed25519-dalek

Version: 1.0.1

Double Public Key Signing Function Oracle Attack
Upgrade to >=2

RUSTSEC-2024-0332

h2

Version: 0.3.18

Degradation of service in h2 servers with CONTINUATION Flood

Upgrade to ^0.3.26 OR >=0.4.4

RUSTSEC-2024-0003

h2

Version: 0.3.18

Resource exhaustion vulnerability in h2 may lead to Denial of Service (DoS)

Upgrade to ^0.3.24 OR >=0.4.2

RUSTSEC-2024-0019

mio

Version: 0.8.6

Tokens for named pipes may be delivered after deregistration

Upgrade to >=0.8.11

RUSTSEC-2023-0044

openssl

Version: 0.10.52

openssl buffer over-read

Upgrade to >=0.10.55

RUSTSEC-2020-0071

time

Version: 0.1.45

Potential segfault in the time crate

Upgrade to >=0.2.23

RUSTSEC-2023-0065

tungstenite

Version: 0.18.0

Tungstenite allows remote attackers to cause a denial of service

Upgrade to >=0.20.1

RUSTSEC-2021-0139

ansi_term

Version: 0.12.1

Unmaintained

RUSTSEC-2021-0141

dotenv

Version: 0.15.0

Unmaintained

RUSTSEC-2022-0081

json

Version: 0.12.4

Unmaintained

RUSTSEC-2022-0001

lmdb

Version: 0.8.0

Unmaintained

RUSTSEC-2020-0168

mach

Version: 0.3.2

Unmaintained

RUSTSEC-2022-0061

parity-wasm

Version: 0.41.0

Unmaintained

RUSTSEC-2022-0054

wee_alloc

Version: 0.4.5

Unmaintained

RUSTSEC-2021-0145

atty

Version: 0.2.14

Potential unaligned read

RUSTSEC-2022-0041

crossbeam-utils

Version: 0.7.2

Unsoundness of AtomicCell<*64> arithmetics on 32-bit targets that support Atomic*64

RUSTSEC-2023-0045

memoffset

Version: 0.5.6

memoffset allows reading uninitialized memory

RUSTSEC-2023-0072

openssl

Version: 0.10.52

openssl X509StoreRef::objects is unsound

RUSTSEC-2022-0092

rmp-serde

Version: 0.14.4

rmp-serde Raw and RawRef unsound

RUSTSEC-2022-0070

secp256k1

Version: 0.20.3

Unsound API in secp256k1 allows use-after-free and invalid deallocation from safe code

-

bumpalo

Version: 3.12.1

Yanked

-

hermit-abi

Version: 0.3.1

Yanked

-

rustix

Version: 0.37.14

Yanked

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.

© Halborn 2025. All rights reserved.