Casper 2.0 - Casper Association


Prepared by:

Halborn Logo

HALBORN

Last Updated 04/17/2025

Date of Engagement: December 23rd, 2024 - March 14th, 2025

Summary

100% of all REPORTED Findings have been addressed

All findings

14

Critical

4

High

0

Medium

1

Low

5

Informational

4


1. Introduction

Casper Association engaged Halborn to conduct a security assessment of the semi-final version of their reference node for Casper Protocol 2.0, beginning on December 23rd, 2024 and ending on March 14th, 2025. The security assessment was scoped to the repository listed with commit hash, and further details in the Scope section of this report.

Casper is the blockchain platform purpose-built to scale opportunity for everyone. Building toward blockchain’s next frontier, Casper is designed for real-world applications without sacrificing usability, cost, decentralization, or security. It removes the barriers that prevent mainstream blockchain adoption by making blockchain friendly to use, open to the world, and future-proof to support innovations today and tomorrow.

2. Assessment Summary

The team at Halborn assigned one full-time security engineer to verify the security of the Casper Protocol 2.0 release. The security engineer is a blockchain and smart-contract security expert with advanced penetration testing and smart-contract hacking skills, and deep knowledge of multiple blockchain protocols.


The purpose of this assessment is to:

    • Ensure that the functionalities of the execution engine and data access layer operate as intended

    • Identify potential security issues with the execution engine and data access layer


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

    • Use the PenalizedAccount variant instead of Payment when determining whether the balance identifier corresponds to a penalty.

    • Review the logic for setting the balance identifier when the transaction is intended for the Casper V2 VM, as the balance identifier should not be penalized.

    • Ensure that addressable entities purses are preserved during each upgrade.

    • Use the payment code instead of the session code when handling custom payments and update the phase for the ClearRefundPurse refund mode to Payment.

    • Eliminate the maximum allowed threshold check during the removal of associated keys.

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 blockchain assessment. While manual testing is recommended to uncover flaws in logic, process, and implementation, automated testing techniques help enhance the coverage of modules. 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 walkthrough.

    • 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 a local testing environment.

    • Deployment to devnet through casper-client and nctl.


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

Files and Repository
(a) Repository: casper-node
(b) Assessed Commit ID: 18955ae
(c) Items in scope:
  • execution_engine/src/runtime/mod.rs
  • execution_engine/src/runtime/utils.rs
  • execution_engine/src/runtime_context/mod.rs
↓ Expand ↓
Out-of-Scope: Third party dependencies and economic attacks.
Remediation Commit ID:
Out-of-Scope: New features/implementations after the remediation commit IDs.

6. Assessment Summary & Findings Overview

Critical

4

High

0

Medium

1

Low

5

Informational

4

Security analysisRisk levelRemediation Date
Flawed Payment Preprocessing Can Render Valid Transactions Ineligible For Execution, And Vice VersaCriticalSolved - 02/11/2025
Transactions Intended For The V2 Runtime Are Ineligible For ExecutionCriticalSolved - 02/07/2025
Addressable Entities Funds Are Lost Upon Each UpgradeCriticalSolved - 03/12/2025
Transactions Using Custom Payment Will FailCriticalSolved - 01/21/2025
Possibility Of Accounts Being Permanently Linked As Associated KeysMediumSolved - 02/07/2025
Possible Inaccurate Gas ChargesLowSolved - 02/12/2025
Improper Error HandlingLowSolved - 02/07/2025
Incorrect Function NameLowSolved - 02/07/2025
Potential Inaccurate Gas Cost CalculationLowSolved - 02/07/2025
Inconsistent Key Mismatch Issue in Mint and Auction System ContractsLowSolved - 03/11/2025
Unnecessary Mutable BorrowsInformationalSolved - 03/31/2025
Presence Of Typographical ErrorsInformationalSolved - 03/31/2025
Inadequate Variable NamingInformationalSolved - 04/04/2025
Misleading CommentsInformationalSolved - 03/31/2025

7. Findings & Tech Details

7.1 Flawed Payment Preprocessing Can Render Valid Transactions Ineligible For Execution, And Vice Versa

//

Critical

Description

Within the data access layer, the BalanceIdentifier type is used to perform balance inquiries, which could correspond to a refund identifier, payment identifier, accumulation identifier, etc. Each identifier contains the underlying purse information used for payment during transaction execution and reflects the payment preprocessing status. If a transaction requires payment and the payment fails, the balance identifier is marked with a penalized status, rendering the transaction ineligible for further processing.


However, within the BalanceIdentifier implementation, the is_penalty function incorrectly identifies a Payment balance identifier as a penalty, instead of the correct PenalizedAccount.


As a result, the function's integrity is compromised, as it incorrectly classifies a PenalizedAccount identifier as a non-penalty and a Payment identifier as a penalty. Consequently, valid transactions may be rejected, while invalid ones could be processed, particularly within the execute_finalized_block function in node/src/components/contract_runtime/operations.rs. This is because a non-penalized balance identifier indicates that the payment preprocessing requirement within the transaction was successfully met, which is a key requirement for the transaction to be eligible for execution.


The following scenarios, where the balance identifier is set to either PenalizedAccount or Payment, will be affected by this bug when determining whether a transaction's eligibility for execution:

  1. When a transaction uses a standard payment mechanism, but neither the contract nor the transaction initiator will make the payment, the transaction will bypass the payment pre-processing requirement, even though it should not.

  2. When the execution runtime is set to v1 and the initiating account intends to pay but requires a different purse or a custom payment method, the transaction will not be eligible for execution, as the balance identifier will be set to either Payment or PenalizedPayment.

Code Location

Below is the implementation of the BalanceIdentifier::is_penalty function:

/// Is this balance identifier for penalty?
pub fn is_penalty(&self) -> bool {
    matches!(
        self,
        BalanceIdentifier::Payment | BalanceIdentifier::PenalizedPayment
    )
}

Below is a code snippet of the operations::execute_finalized_block function:

let allow_execution = {
    let is_not_penalized = !balance_identifier.is_penalty();
    let sufficient_balance = post_payment_balance_result.is_sufficient(cost);
    let is_supported = chainspec.is_supported(lane_id);
    trace!(%transaction_hash, ?sufficient_balance, ?is_not_penalized, ?is_supported, "payment preprocessing");
    is_not_penalized && sufficient_balance && is_supported
};

if allow_execution {
            ...
} else {
    debug!(%transaction_hash, "not eligible for execution");
}

Proof of Concept

Scenario

Note: The PoC was executed against version 52af64a58a49925dda05c7f606d29f056b5f1784, which includes a fix for another issue reported: transactions using custom payments were previously failing.


The PoC performs the following steps:

  1. Initialize a payment code based on gh_5058_regression.

  2. Verify that Alice's account does not already have Bob's account as an associated key before executing the transaction.

  3. Use the payment code as a custom payment to send a transaction to an existing test session (add_associated_key), which attempts to add Bob's account as a new associated key to Alice's account.

  4. Ensure the transaction completes without errors.

  5. Confirm that Alice's associated keys remain unchanged after executing the add_associated_key session. This demonstrates that the transaction was ineligible for execution, which is why it did not produce any effects.

Implementation

The following is the implementation of the PoC, which has been added to the existing tests in node/src/reactor/main_reactor/tests/transactions.rs:

#[tokio::test]
async fn custom_payment_with_deploy_variant_is_not_eligible_for_execution() {
    let config = SingleTransactionTestCase::default_test_config()
        .with_pricing_handling(PricingHandling::Classic)
        .with_refund_handling(RefundHandling::NoRefund)
        .with_fee_handling(FeeHandling::NoFee);

    let mut test = SingleTransactionTestCase::new(
        ALICE_SECRET_KEY.clone(),
        BOB_SECRET_KEY.clone(),
        CHARLIE_SECRET_KEY.clone(),
        Some(config),
    )
    .await;

    test.fixture
        .run_until_consensus_in_era(ERA_ONE, ONE_MIN)
        .await;

    let base_path = RESOURCES_PATH
        .parent()
        .unwrap()
        .join("target")
        .join("wasm32-unknown-unknown")
        .join("release");

    let payment_amount = U512::from(1_000_000_000u64);

    let txn = {
        let timestamp = Timestamp::now();
        let ttl = TimeDiff::from_seconds(100);
        let gas_price = 1;
        let chain_name = test.chainspec().network_config.name.clone();

        let payment = ExecutableDeployItem::ModuleBytes {
            module_bytes: std::fs::read(base_path.join("gh_5058_regression.wasm"))
                .unwrap()
                .into(),
            args: runtime_args! {
                "amount" => payment_amount,
            },
        };

        let session = ExecutableDeployItem::ModuleBytes {
            module_bytes: std::fs::read(base_path.join("add_associated_key.wasm"))
                .unwrap()
                .into(),
            args: runtime_args! {
                "account" => BOB_PUBLIC_KEY.to_account_hash(),
                "weight" => Weight::new(1u8),
            },
        };

        Transaction::Deploy(Deploy::new_signed(
            timestamp,
            ttl,
            gas_price,
            vec![],
            chain_name.clone(),
            payment,
            session,
            &ALICE_SECRET_KEY,
            Some(ALICE_PUBLIC_KEY.clone()),
        ))
    };

    let acct = get_balance(&mut test.fixture, &ALICE_PUBLIC_KEY, None, true);
    assert!(acct.total_balance().cloned().unwrap() >= payment_amount);

    let mut state_root_hash = *test.fixture.highest_complete_block().state_root_hash();
    let mut entity = get_entity_by_account_hash(
        &mut test.fixture,
        state_root_hash,
        ALICE_PUBLIC_KEY.to_account_hash(),
    );
    assert!(
        entity
            .associated_keys()
            .get(&BOB_PUBLIC_KEY.to_account_hash())
            .is_none(),
        "bob's account is not one of alice's associated keys"
    );

    let (_txn_hash, block_height, exec_result) = test.send_transaction(txn).await;

    assert_eq!(exec_result.error_message(), None);

    state_root_hash = *test
        .fixture
        .get_block_by_height(block_height)
        .state_root_hash();
    entity = get_entity_by_account_hash(
        &mut test.fixture,
        state_root_hash,
        ALICE_PUBLIC_KEY.to_account_hash(),
    );
    assert!(
        entity
            .associated_keys()
            .get(&BOB_PUBLIC_KEY.to_account_hash())
            .is_none(),
        "bob's account is still not one of alice's associated keys"
    );
}

Below is the implementation of the gh_5058_regression payment code:

#![no_main]
#![no_std]

extern crate alloc;

use casper_contract::{
    contract_api::{account, runtime, system},
    unwrap_or_revert::UnwrapOrRevert,
};

use casper_types::{
    runtime_args, system::handle_payment, ApiError, Phase, RuntimeArgs, URef, U512,
};

const ARG_AMOUNT: &str = "amount";

#[repr(u16)]
enum Error {
    InvalidPhase,
}

impl From<Error> for ApiError {
    fn from(e: Error) -> Self {
        ApiError::User(e as u16)
    }
}

fn get_payment_purse() -> URef {
    runtime::call_contract(
        system::get_handle_payment(),
        handle_payment::METHOD_GET_PAYMENT_PURSE,
        RuntimeArgs::default(),
    )
}

fn set_refund_purse(new_refund_purse: URef) {
    let args = runtime_args! {
        handle_payment::ARG_PURSE => new_refund_purse,
    };

    runtime::call_contract(
        system::get_handle_payment(),
        handle_payment::METHOD_SET_REFUND_PURSE,
        args,
    )
}

#[no_mangle]
pub extern "C" fn call() {
    if runtime::get_phase() != Phase::Payment {
        runtime::revert(Error::InvalidPhase);
    }

    let amount: U512 = runtime::get_named_arg(ARG_AMOUNT);
    let payment_purse = get_payment_purse();
    set_refund_purse(account::get_main_purse());

    // transfer amount from named purse to payment purse, which will be used to pay for execution
    system::transfer_from_purse_to_purse(account::get_main_purse(), payment_purse, amount, None)
        .unwrap_or_revert();
}

Below is the implementation of the add_associated_key session:

#![no_std]
#![no_main]

use casper_contract::{
    contract_api::{account, runtime},
    unwrap_or_revert::UnwrapOrRevert,
};
use casper_types::account::{AccountHash, Weight};

const ARG_ACCOUNT: &str = "account";
const ARG_WEIGHT: &str = "weight";

#[no_mangle]
pub extern "C" fn call() {
    let account: AccountHash = runtime::get_named_arg(ARG_ACCOUNT);
    let weight: Weight = runtime::get_named_arg(ARG_WEIGHT);
    account::add_associated_key(account, weight).unwrap_or_revert();
}

Execution

The following entry was added to the project's Makefile:

.PHONY: test-poc01
test-poc01: resources/local/chainspec.toml build-contracts-rs
	$(LEGACY) $(DISABLE_LOGGING) $(CARGO) test reactor::main_reactor::tests::transactions::custom_payment_with_deploy_variant_is_not_eligible_for_execution --all-features --no-fail-fast $(CARGO_FLAGS) -- --nocapture

  • Command:

make test-poc01

  • Result:





BVSS
Recommendation

It is recommended to use the PenalizedAccount variant instead of Payment when determining whether the balance identifier corresponds to a penalty.

Remediation Comment

SOLVED: The Casper team solved this issue in the specified commit id.

Remediation Hash

7.2 Transactions Intended For The V2 Runtime Are Ineligible For Execution

//

Critical

Description

During payment preprocessing for each transaction, a balance identifier is assigned to distinguish the source purse responsible for handling any required payments and execution gas, while also verifying whether all requirements are met. If an error occurs during this phase, the transaction will be ineligible for execution.


When setting the balance identifier for transactions intended for the V2 runtime, a comment was added indicating that the initiating account will pay using the refund purse. However, the balance identifier is set to a PenalizedAccount, which renders all such transactions ineligible for execution processing.

Code Location

Below are code snippets of the execute_finalized_block function:

} else if is_v2_wasm {
    // if transaction runtime is v2 then the initiating account will pay using
    // the refund purse
    BalanceIdentifier::PenalizedAccount(initiator_addr.account_hash())
}

let allow_execution = {
    let is_not_penalized = !balance_identifier.is_penalty();
    let sufficient_balance = post_payment_balance_result.is_sufficient(cost);
    let is_supported = chainspec.is_supported(lane_id);
    trace!(%transaction_hash, ?sufficient_balance, ?is_not_penalized, ?is_supported, "payment preprocessing");
    is_not_penalized && sufficient_balance && is_supported
};

if allow_execution {
...
} else {
    debug!(%transaction_hash, "not eligible for execution");
}

BVSS
Recommendation

It is recommended to review the logic for setting the balance identifier when the transaction is intended for the Casper V2 VM, as the balance identifier should not be penalized.

Remediation Comment

SOLVED: The Casper team solved this issue by using the initiating account's main purse when the transaction is intended for the v2 runtime.

Remediation Hash

7.3 Addressable Entities Funds Are Lost Upon Each Upgrade

//

Critical

Description

Casper 2.0 introduces a major update with the AddressableEntity type, replacing the separate AccountHash and ContractHash from Casper 1.x. This unified structure allows contracts to directly manage funds and their own keys, offering improved access control. According to the official documentation, the feature won't be activated in Casper 2.0 initially but will be enabled in a future network update, which will be irreversible once implemented.


To ensure compatibility with both the current version and the version after enabling this feature, the logic continuously checks whether the enable_entity flag is enabled and executes the appropriate implementation accordingly. However, the contract upgrade mechanism faces issues when this feature is activated, caused by the following:

The logic in the new_version_entity_parts function is inverted, leading to incorrect handling of whether a new purse should be created during an entity version upgrade.


Currently, the logic works backwards:

  • When requires_purse_creation is true, it indicates the previous version was a Contract and needs to be migrated to an Entity with a new purse.

  • When requires_purse_creation is false, it means the previous version was already an Entity and should retain its existing purse.


However, the current implementation does the opposite:

  • It creates a new purse when requires_purse_creation is false (Entity case).

  • It uses the existing purse when requires_purse_creation is true (Contract case).


As a result, entities receive a new purse with every upgrade, causing them to lose their previous purse and balance.

Code Location

Below is a code snippet of the new_version_entity_parts function:

let (mut previous_entity, requires_purse_creation) =
    self.context.get_contract_entity(previous_entity_key)?;

...

let main_purse = if !requires_purse_creation {
    self.create_purse()?
} else {
    previous_entity.main_purse()
};

Below is the implementation of the get_contract_entity function:

pub(crate) fn get_contract_entity(
        &mut self,
        entity_key: Key,
    ) -> Result<(AddressableEntity, bool), ExecError> {
        let entity_hash = if let Some(entity_hash) = entity_key.into_entity_hash() {
            entity_hash
        } else {
            return Err(ExecError::UnexpectedKeyVariant(entity_key));
        };

        let mut tc = self.tracking_copy.borrow_mut();

        let key = Key::contract_entity_key(entity_hash);
        match tc.read(&key)? {
            Some(StoredValue::AddressableEntity(entity)) => Ok((entity, false)),
            Some(other) => Err(ExecError::TypeMismatch(StoredValueTypeMismatch::new(
                "AddressableEntity".to_string(),
                other.type_name(),
            ))),
            None => match tc.read(&Key::Hash(entity_hash.value()))? {
                Some(StoredValue::Contract(contract)) => Ok((contract.into(), true)),
                Some(other) => Err(ExecError::TypeMismatch(StoredValueTypeMismatch::new(
                    "Contract".to_string(),
                    other.type_name(),
                ))),
                None => Err(TrackingCopyError::KeyNotFound(key).into()),
            },
        }
    }

The new_version_entity_parts function is called by the add_contract_version_by_package function:

let (
    main_purse,
    previous_named_keys,
    action_thresholds,
    associated_keys,
    previous_hash_addr,
) = self.new_version_entity_parts(&package)?;

The add_contract_version_by_package function is called within the add_contract_version function when the enable_entity flag is activated:

fn add_contract_version(
        &mut self,
        package_hash: PackageHash,
        version_ptr: u32,
        entry_points: EntryPoints,
        named_keys: NamedKeys,
        message_topics: BTreeMap<String, MessageTopicOperation>,
        output_ptr: u32,
    ) -> Result<Result<(), ApiError>, ExecError> {
        if self.context.engine_config().enable_entity {
            self.add_contract_version_by_package(
                package_hash,
                version_ptr,
                entry_points,
                named_keys,
                message_topics,
                output_ptr,
            )
        } else {
            self.add_contract_version_by_contract_package(
                package_hash.value(),
                version_ptr,
                entry_points,
                named_keys,
                message_topics,
                output_ptr,
            )
        }
    }

Proof of Concept

Scenario

The PoC performs the following steps:


  1. Implement an upgradeable mock contract that stores the contract as an addressable entity under the deployer's named keys.

  2. Set up test environment with addressable entities

  3. Upgrade protocol version to 2.0.0

  4. Deploy the mock contract and store its initial main purse

  5. Upgrade the mock contract and verify that its main purse was overwritten

Implementation

Below is the implementation of the mock-02 contract:

#![no_std]
#![no_main]

use alloc::string::{String, ToString};
use alloc::{format, vec};
use casper_contract::contract_api::account::get_main_purse;
use casper_contract::contract_api::{account, entity, runtime, storage, system};
use casper_contract::unwrap_or_revert::UnwrapOrRevert;
use casper_types::account::AccountHash;
use casper_types::addressable_entity::EntityKindTag;
use casper_types::{bytesrepr::FromBytes, CLTyped};
use casper_types::{AddressableEntity, Key, Parameter, U256};
use casper_types::{
    ApiError, CLType, EntryPoint, EntryPointType, EntryPoints, NamedKeys, URef, U512,
};

extern crate alloc;

pub const PACKAGE_NAME: &str = "mock_02_contract_package";
pub const ACCESS_UREF: &str = "mock_02_contract_package_access";
pub const VERSION_KEY: &str = "mock_02_contract_version";
pub const CONTRACT_NAME: &str = "mock_02_contract_hash";

pub fn install_contract() {
    let entry_points = EntryPoints::new();

    let hash_key_name = format!("{}", PACKAGE_NAME);

    let (contract_hash, contract_version) = storage::new_contract(
        entry_points,
        Some(NamedKeys::new()),
        Some(hash_key_name.clone()),
        Some(format!("{}", ACCESS_UREF)),
        None,
    );

    let contract_hash_key =
        Key::addressable_entity_key(EntityKindTag::SmartContract, contract_hash.into());

    runtime::put_key(&format!("{}", CONTRACT_NAME), contract_hash_key);
    runtime::put_key(
        &format!("{}", VERSION_KEY),
        storage::new_uref(contract_version).into(),
    );
}

pub fn upgrade() {
    let entry_points = EntryPoints::new();

    let contract_package_hash = runtime::get_key(&format!("{}", PACKAGE_NAME))
        .unwrap()
        .into_package_hash()
        .unwrap();

    let previous_contract_hash = runtime::get_key(&format!("{}", CONTRACT_NAME))
        .unwrap()
        .into_entity_hash()
        .unwrap();

    let (contract_hash, contract_version) = storage::add_contract_version(
        contract_package_hash.into(),
        entry_points,
        NamedKeys::new(),
        Default::default(),
    );
    storage::disable_contract_version(contract_package_hash.into(), previous_contract_hash.into())
        .unwrap_or_revert_with(ApiError::PermissionDenied);
    runtime::put_key(
        &format!("{}", CONTRACT_NAME),
        Key::addressable_entity_key(EntityKindTag::SmartContract, contract_hash.into()),
    );
    runtime::put_key(
        &format!("{}", VERSION_KEY),
        storage::new_uref(contract_version).into(),
    );
}

#[no_mangle]
pub extern "C" fn call() {
    match runtime::get_key(&format!("{}", ACCESS_UREF)) {
        Some(_) => {
            upgrade();
        }
        None => {
            install_contract();
        }
    }
}

The following is the implementation of the PoC, which has been added to the existing tests in execution_engine_testing/tests/src/test/upgrade.rs:

fn get_mock_02_entity(builder: &mut LmdbWasmTestBuilder) -> EntityWithNamedKeys {
    let account = builder
        .get_entity_with_named_keys_by_account_hash(*DEFAULT_ACCOUNT_ADDR)
        .expect("should get account");

    let entity_hash = account
        .named_keys()
        .get("mock_02_contract_hash")
        .expect("must have mock_02_contract_hash")
        .into_entity_hash()
        .unwrap();

    builder
        .get_entity_with_named_keys_by_entity_hash(entity_hash)
        .expect("must have entity")
}

fn upgrade_protocol_version(builder: &mut LmdbWasmTestBuilder, old_protocol_version: ProtocolVersion) {
    let mut upgrade_request = UpgradeRequestBuilder::new()
        .with_current_protocol_version(old_protocol_version)
        .with_new_protocol_version(ProtocolVersion::from_parts(2, 0, 0))
        .with_activation_point(EraId::new(1))
        .with_enable_addressable_entity(true)
        .build();

    builder
        .upgrade(&mut upgrade_request)
        .expect_upgrade_success();
}

fn deploy_mock_02_contract(builder: &mut LmdbWasmTestBuilder) {
    let exec_request = ExecuteRequestBuilder::standard(
        *DEFAULT_ACCOUNT_ADDR,
        "mock-02.wasm",
        RuntimeArgs::default(),
    )
    .build();
    builder.exec(exec_request).expect_success().commit();
}

#[test]
fn test_main_purse_overwrite_on_entity_upgrade() {
    // Set up builder with addressable entities enabled
    let (mut builder, lmdb_fixture_state, _temp_dir) =
        lmdb_fixture::builder_from_global_state_fixture_with_enable_ae(
            lmdb_fixture::RELEASE_1_5_8,
            true,
        );

    // Upgrade protocol version to 2.0.0
    upgrade_protocol_version(&mut builder, lmdb_fixture_state.genesis_protocol_version());

    // Verify account is 1.x format
    let account_as_1x = builder
        .query(None, Key::Account(*DEFAULT_ACCOUNT_ADDR), &[])
        .expect("must have stored value")
        .as_account()
        .is_some();
    assert!(account_as_1x);

    // Install initial contract version
    deploy_mock_02_contract(&mut builder);

    // Get initial contract entity and purse
    let entity = get_mock_02_entity(&mut builder);
    let initial_purse = entity.main_purse();

    // Upgrade contract
    deploy_mock_02_contract(&mut builder);

    // Get upgraded contract entity and verify purse changed
    let upgraded_entity = get_mock_02_entity(&mut builder);

    assert_ne!(initial_purse, upgraded_entity.main_purse(), "purse should be different");
}

Execution

The following entry was added to the project's Makefile:

setup-pocs:
	cd pocs/mocks/mock-01 && RUSTFLAGS="-C target-cpu=mvp" cargo +nightly build --release --target wasm32-unknown-unknown -Z build-std=std,panic_abort 
	cd pocs/mocks/payment-code-01 && RUSTFLAGS="-C target-cpu=mvp" cargo +nightly build --release --target wasm32-unknown-unknown -Z build-std=std,panic_abort 
	cd pocs/mocks/mock-02 && RUSTFLAGS="-C target-cpu=mvp" cargo +nightly build --release --target wasm32-unknown-unknown -Z build-std=std,panic_abort 
	wasm-strip target/wasm32-unknown-unknown/release/mock-01.wasm
	wasm-strip target/wasm32-unknown-unknown/release/payment-code-01.wasm
	wasm-strip target/wasm32-unknown-unknown/release/mock-02.wasm
	mkdir -p pocs/tests/wasm
	cp ./target/wasm32-unknown-unknown/release/*.wasm pocs/tests/wasm

  • Commands:

make setup-pocs
cargo test --package casper-engine-tests --lib -- test::upgrade::test_main_purse_overwrite_on_entity_upgrade --exact --show-output

  • Result:




BVSS
Recommendation

It is recommended to fix the inverted check as follows:

let main_purse = if requires_purse_creation {
    self.create_purse()?
} else {
    previous_entity.main_purse()
};

Remediation Comment

SOLVED: The Casper team solved this issue in the specified commit id.

Remediation Hash

7.4 Transactions Using Custom Payment Will Fail

//

Critical

Description

Casper provides two methods for handling execution fee payments. The standard payment method relies on the system handle payment contract, deducting funds from the caller's main purse. The custom payment method, on the other hand, allows the caller to define a custom payment contract with specific logic, such as initiating payment from a secondary purse.


However, a bug was identified in the custom payment handling for the Transaction::Deploy variant, where it incorrectly used the executable deploy item from the session code instead of the payment code. This prevents the payment code from executing, causing the refund purse to default to the transaction initiator's main purse, similar to the standard payment method.


Additionally, although the refund_purse key is set during the Payment phase (which uses the handle payment contract's context), its clearance occurs in the FinalizePayment phase (which uses the system's context). As a result, attempting to remove this key leads to a failure with the error: HandlePayment error: Unable to remove named key.

Code Location

Below is a code snippet of the execute_finalized_block function:

// the initiating account will pay, but wants to do so with a different purse or
// in a custom way. If anything goes wrong, penalize the sender, do not execute
let custom_payment_gas_limit =
    Gas::new(chainspec.transaction_config.native_transfer_minimum_motes * 5);
let session_input_data = transaction.to_session_input_data();

Below is a code snippet of the to_session_input_data function:

pub fn to_session_input_data(&self) -> SessionInputData {
    let initiator_addr = self.initiator_addr();
    let is_standard_payment = self.is_standard_payment();
    match self {
        MetaTransaction::Deploy(meta_deploy) => {
            let deploy = meta_deploy.deploy();
            let data = SessionDataDeploy::new(
                deploy.hash(),
                deploy.session(),
                initiator_addr,
                self.signers().clone(),
                is_standard_payment,
            );
            SessionInputData::DeploySessionData { data }
        }

Below is the implementation of the phase function, which shows that the phase corresponding to the ClearRefundPurse mode is the FinalizePayment phase, whereas the SetRefundPurse mode corresponds to the Payment phase:

pub fn phase(&self) -> Phase {
    match self {
        HandleRefundMode::ClearRefundPurse
        | HandleRefundMode::Burn { .. }
        | HandleRefundMode::Refund { .. }
        | HandleRefundMode::CustomHold { .. }
        | HandleRefundMode::RefundAmount { .. } => Phase::FinalizePayment,
        HandleRefundMode::SetRefundPurse { .. } => Phase::Payment,
    }
}

Below is a code snippet of the execute_finalized_block function, where the handle_refund function processes a refund request with the mode set to ClearRefundPurse:

// clear refund purse if it was set
if refund_purse_active {
    // if refunds are turned on we initialize the refund purse to the initiator's main
    // purse before doing any processing. NOTE: when executed, custom payment logic
    // has the option to call set_refund_purse on the handle payment contract to set
    // up a different refund purse, if desired.
    let handle_refund_request = HandleRefundRequest::new(
        native_runtime_config.clone(),
        state_root_hash,
        protocol_version,
        transaction_hash,
        HandleRefundMode::ClearRefundPurse,
    );
    let handle_refund_result = scratch_state.handle_refund(handle_refund_request);

Below are code snippets of the handle_refund function, implemented in the storage/src/global_state/state/mod.rs file, which illustrates that the refund_purse key will be removed from the FinalizePayment phase's context:

let mut runtime = match phase {
    Phase::FinalizePayment => {
        // this runtime uses the system's context
        match RuntimeNative::new_system_runtime(
            config,
            protocol_version,
            id,
            address_generator,
            Rc::clone(&tc),
            phase,
        ) {
            Ok(rt) => rt,
            Err(tce) => {
                return HandleRefundResult::Failure(tce);
            }
        }
    }
    Phase::Payment => {
        // this runtime uses the handle payment contract's context
        match RuntimeNative::new_system_contract_runtime(
            config,
            protocol_version,
            id,
            address_generator,
            Rc::clone(&tc),
            phase,
            HANDLE_PAYMENT,
        ) {
            Ok(rt) => rt,
            Err(tce) => {
                return HandleRefundResult::Failure(tce);
            }
        }
    }
    Phase::System | Phase::Session => return HandleRefundResult::InvalidPhase,
};
HandleRefundMode::ClearRefundPurse => match runtime.clear_refund_purse() {
    Ok(_) => Ok(None),
    Err(hpe) => Err(TrackingCopyError::SystemContract(
        system::Error::HandlePayment(hpe),
    )),
},

Below is the implementation of the clear_refund_purse function:

/// Clear refund purse.
fn clear_refund_purse(&mut self) -> Result<(), Error> {
    if self.get_caller() != PublicKey::System.to_account_hash() {
        error!("invalid caller to clear refund purse");
        return Err(Error::InvalidCaller);
    }

    self.remove_key(REFUND_PURSE_KEY)
}

Proof of Concept

Scenario

The PoC performs the following steps:


  1. Smart Contract Implementation
    Create a smart contract with an increment_counter entry point. This entry point increments a counter by one which is stored under the contract's named keys.

  2. Payment Code Implementation

    Create a payment code that verifies it is executed within the payment execution phase, sets the refund_purse key to the caller's main purse, and transfers a specified amount of motes from the caller’s main purse to the handle payment contract's payment purse.

  3. Contract Deployment using NCTL
    Deploy the mock contract using NCTL. We used the Make Docker image corresponding to version v2-rc5, which is the same version being audited.

Implementation

Below is the implementation of the mock-01 contract:

#![no_std]
#![no_main]

use alloc::string::{String, ToString};
use alloc::{format, vec};
use casper_contract::contract_api::{account, runtime, storage, system};
use casper_contract::unwrap_or_revert::UnwrapOrRevert;
use casper_types::account::AccountHash;
use casper_types::{Key, Parameter, U256};
use casper_types::{bytesrepr::FromBytes, CLTyped};
use casper_types::{
    ApiError, CLType, EntryPoint, EntryPointType, EntryPoints, NamedKeys, URef, U512,
};

extern crate alloc;

mod entry_points {
    use casper_types::EntryPointAccess;

    use super::*;

    /// Returns the `counter_inc` entry point.
    pub fn counter_inc() -> EntryPoint {
        EntryPoint::new(
            String::from("counter_inc"),
            vec![],
            CLType::Unit,
            EntryPointAccess::Public,
            EntryPointType::Called,
            casper_types::EntryPointPayment::Caller,
        )
    }
}

#[no_mangle]
pub extern "C" fn counter_inc() {
    let uref: URef = runtime::get_key("count")
        .unwrap_or_revert_with(ApiError::MissingKey)
        .into_uref()
        .unwrap_or_revert_with(ApiError::UnexpectedKeyVariant);
    storage::add(uref, 1); // Increment the count by 1.
}

#[no_mangle]
pub extern "C" fn call() {
    let mut named_keys = NamedKeys::new();
    
    named_keys.insert(String::from("count"), storage::new_uref(0_i32).into());

    let mut entry_points = EntryPoints::new();
    entry_points.add_entry_point(entry_points::counter_inc());

    let hash_key_name = format!("mock_01_contract_package");

    let (contract_hash, contract_version) = storage::new_contract(
        entry_points,
        Some(named_keys),
        Some(hash_key_name.clone()),
        Some(format!("mock_01_contract_package_access")),
        None,
    );

    // Store contract_hash and contract_version under the keys mock_01_contract_hash and mock_01_contract_version
    runtime::put_key(&format!("mock_01_contract_hash"), contract_hash.into());
    runtime::put_key(
        &format!("mock_01_contract_version"),
        storage::new_uref(contract_version).into(),
    );
}

Below is the payment code implementation:

#![no_std]
#![no_main]

use casper_contract::{
    contract_api::{account, runtime, system},
    unwrap_or_revert::UnwrapOrRevert,
};
use casper_types::{
    runtime_args, system::handle_payment, ApiError, Phase, RuntimeArgs, URef, U512,
};

const ARG_AMOUNT: &str = "amount";

#[repr(u16)]
enum Error {
    InvalidPhase,
}

impl From<Error> for ApiError {
    fn from(e: Error) -> Self {
        ApiError::User(e as u16)
    }
}

fn get_payment_purse() -> URef {
    runtime::call_contract(
        system::get_handle_payment(),
        handle_payment::METHOD_GET_PAYMENT_PURSE,
        RuntimeArgs::default(),
    )
}

fn set_refund_purse(new_refund_purse: URef) {
    let args = runtime_args! {
        handle_payment::ARG_PURSE => new_refund_purse,
    };

    runtime::call_contract(
        system::get_handle_payment(),
        handle_payment::METHOD_SET_REFUND_PURSE,
        args,
    )
}

#[no_mangle]
pub extern "C" fn call() {
    if runtime::get_phase() != Phase::Payment {
        runtime::revert(Error::InvalidPhase);
    }

    let amount: U512 = runtime::get_named_arg(ARG_AMOUNT);

    let payment_purse = get_payment_purse();
    set_refund_purse(account::get_main_purse());

    // transfer amount from named purse to payment purse, which will be used to pay for execution
    system::transfer_from_purse_to_purse(account::get_main_purse(), payment_purse, amount, None)
        .unwrap_or_revert();
}

Execution

NCTL Setup:

  • Connect to the Docker container

    docker run --rm -it --name mynctl -d -p 11101:11101 -p 14101:14101 -p 18101:18101 makesoftware/casper-nctl:v200-rc5
    docker exec -it mynctl bash

  • Store the caller's public key. Additionally, save the caller's private key to the file system for future use, as the WASM file is not included in the Docker container. Both keys can be selected from one of the predefined users located in casper-nctl/assets/net-1/users.


  • Below are the caller's account details:

    casper@6ef97dc35e11:~/casper-nctl$ casper-client get-account --public-key assets/net-1/users/user-1/public_key_hex --node-address http://localhost:11101
    {
      "jsonrpc": "2.0",
      "id": 4130839899720884417,
      "result": {
        "api_version": "2.0.0",
        "account": {
          "account_hash": "account-hash-884a9a83105dfe1c74961836db32eacf0d4cb4c7e85fa1a09e21708b058e0e0d",
          "named_keys": [],
          "main_purse": "uref-b6ae19d8323e66d85d3b268f30b082a8636e248ecb91c3a401eea6559f77e907-007",
          "associated_keys": [
            {
              "account_hash": "account-hash-884a9a83105dfe1c74961836db32eacf0d4cb4c7e85fa1a09e21708b058e0e0d",
              "weight": 1
            }
          ],
          "action_thresholds": {
            "deployment": 1,
            "key_management": 1
          }
        },
        "merkle_proof": "[2296 hex chars]"
      }
    }

mock-01 contract deployment:

Execution script:

casper-client put-deploy \
--node-address http://localhost:11101 \
--chain-name casper-net-1 \
--secret-key PATH_TO_CALLER_SECRET_KEY \
--payment-amount 500000000000 \
--session-path pocs/tests/wasm/mock-01.wasm \
--session-entry-point call

In our case, the path to the caller's secret key is: $HOME/client_keys/nctl/caller_secret_key.pem

The execution result can be verified by running the following command:

casper-client get-deploy --node-address http://localhost:11101 --id STRING <DEPLOY_HASH>

Below is a screenshot showing the actual execution steps:

Below is the result of the execution:

response Success for rpc-id STRING info_get_deploy is not valid because Some(Error("missing field `execution_results`", line: 0, column: 0)): {
    "jsonrpc": "2.0",
    "result": {
        "api_version": "2.0.0",
        "deploy": {
            "hash": "106c6ce829e76066f9af2962453c337d45d37cb98763e22956a15856606ec38d",
            "header": {
                "account": "01498126216b96652e040e081f69cdfbf679c5c42b3adbbb7ea240872c57011bc5",
                "timestamp": "2025-01-27T15:46:52.692Z",
                "ttl": "30m",
                "gas_price": 1,
                "body_hash": "2ae10688c9953cb7881ee67f7b945beaf322bbdd037bc7e7691ebb6583c6dc57",
                "dependencies": [],
                "chain_name": "casper-net-1"
            },
            "payment": {
                "ModuleBytes": {
                    "module_bytes": "",
                    "args": [
                        [
                            "amount",
                            {
                                "cl_type": "U512",
                                "bytes": "050088526a74",
                                "parsed": "500000000000"
                            }
                        ]
                    ]
                }
            },
            "session": {
                "ModuleBytes": {
                    "module_bytes": "LARGE DATA BYTES",
                    "args": []
                }
            },
            "approvals": [
                {
                    "signer": "01498126216b96652e040e081f69cdfbf679c5c42b3adbbb7ea240872c57011bc5",
                    "signature": "01b62053cfe4047fbb17ed292c8b60da93b6cf110549b5ac76d24cd3184d3c7d2a1efafc90676e87139946101086a137e7f209b3c2710e597f92909050daa83b0f"
                }
            ]
        },
        "execution_info": {
            "block_hash": "d7ea2ab0b465db59e706257c2d7ddfdbe4f30da9fbc384c318edeba2f843d0ac",
            "block_height": 1389,
            "execution_result": {
                "Version2": {
                    "initiator": {
                        "PublicKey": "01498126216b96652e040e081f69cdfbf679c5c42b3adbbb7ea240872c57011bc5"
                    },
                    "error_message": null,
                    "limit": "500000000000",
                    "consumed": "39290334934",
                    "cost": "500000000000",
                    "transfers": [],
                    "size_estimate": 57609,
                    "effects": [
                        {
                            "key": "balance-hold-01b6ae19d8323e66d85d3b268f30b082a8636e248ecb91c3a401eea6559f77e907f3ba71a894010000",
                            "kind": {
                                "Write": {
                                    "CLValue": {
                                        "cl_type": "U512",
                                        "bytes": "050088526a74",
                                        "parsed": "500000000000"
                                    }
                                }
                            }
                        },
                        {
                            "key": "uref-343c619075a0544899d95ec661af3a0667d0c3a0906a13f5e05c2d58bf541c7b-000",
                            "kind": {
                                "Write": {
                                    "CLValue": {
                                        "cl_type": "I32",
                                        "bytes": "00000000",
                                        "parsed": 0
                                    }
                                }
                            }
                        },
                        {
                            "key": "uref-b07faaf91b5a67f0aa6a64f329dacdc7381ff67432f446727326f3c67b9ccacc-000",
                            "kind": {
                                "Write": {
                                    "CLValue": {
                                        "cl_type": "Unit",
                                        "bytes": "",
                                        "parsed": null
                                    }
                                }
                            }
                        },
                        {
                            "key": "hash-d76c399c7411dbd2fe3872f422386e3f455ee3ca313a864878c14ad96e003bb9",
                            "kind": {
                                "Write": {
                                    "ContractPackage": {
                                        "access_key": "uref-b07faaf91b5a67f0aa6a64f329dacdc7381ff67432f446727326f3c67b9ccacc-007",
                                        "versions": [],
                                        "disabled_versions": [],
                                        "groups": [],
                                        "lock_status": "Unlocked"
                                    }
                                }
                            }
                        },
                        {
                            "key": "account-hash-884a9a83105dfe1c74961836db32eacf0d4cb4c7e85fa1a09e21708b058e0e0d",
                            "kind": {
                                "AddKeys": [
                                    {
                                        "name": "mock_01_contract_package",
                                        "key": "hash-d76c399c7411dbd2fe3872f422386e3f455ee3ca313a864878c14ad96e003bb9"
                                    }
                                ]
                            }
                        },
                        {
                            "key": "account-hash-884a9a83105dfe1c74961836db32eacf0d4cb4c7e85fa1a09e21708b058e0e0d",
                            "kind": {
                                "AddKeys": [
                                    {
                                        "name": "mock_01_contract_package_access",
                                        "key": "uref-b07faaf91b5a67f0aa6a64f329dacdc7381ff67432f446727326f3c67b9ccacc-007"
                                    }
                                ]
                            }
                        },
                        {
                            "key": "hash-d76c399c7411dbd2fe3872f422386e3f455ee3ca313a864878c14ad96e003bb9",
                            "kind": "Identity"
                        },
                        {
                            "key": "hash-185b1e26e70547cdfdd2409b10c104b141d43a9d489542fcd4c2b0eca9b7a0d0",
                            "kind": {
                                "Write": {
                                    "ContractWasm": {
                                        "bytes": "LARGE DATA BYTES"
                                    }
                                }
                            }
                        },
                        {
                            "key": "hash-26784926919ea2aca89eb6895e85813349fa03f5fdbfd6d0921fdcd2eab08493",
                            "kind": {
                                "Write": {
                                    "Contract": {
                                        "contract_package_hash": "contract-package-d76c399c7411dbd2fe3872f422386e3f455ee3ca313a864878c14ad96e003bb9",
                                        "contract_wasm_hash": "contract-wasm-185b1e26e70547cdfdd2409b10c104b141d43a9d489542fcd4c2b0eca9b7a0d0",
                                        "named_keys": [
                                            {
                                                "name": "count",
                                                "key": "uref-343c619075a0544899d95ec661af3a0667d0c3a0906a13f5e05c2d58bf541c7b-007"
                                            }
                                        ],
                                        "entry_points": [
                                            {
                                                "name": "counter_inc",
                                                "entry_point": {
                                                    "name": "counter_inc",
                                                    "args": [],
                                                    "ret": "Unit",
                                                    "access": "Public",
                                                    "entry_point_type": "Called"
                                                }
                                            }
                                        ],
                                        "protocol_version": "2.0.0"
                                    }
                                }
                            }
                        },
                        {
                            "key": "hash-d76c399c7411dbd2fe3872f422386e3f455ee3ca313a864878c14ad96e003bb9",
                            "kind": {
                                "Write": {
                                    "ContractPackage": {
                                        "access_key": "uref-b07faaf91b5a67f0aa6a64f329dacdc7381ff67432f446727326f3c67b9ccacc-007",
                                        "versions": [
                                            {
                                                "protocol_version_major": 2,
                                                "contract_version": 1,
                                                "contract_hash": "contract-26784926919ea2aca89eb6895e85813349fa03f5fdbfd6d0921fdcd2eab08493"
                                            }
                                        ],
                                        "disabled_versions": [],
                                        "groups": [],
                                        "lock_status": "Unlocked"
                                    }
                                }
                            }
                        },
                        {
                            "key": "account-hash-884a9a83105dfe1c74961836db32eacf0d4cb4c7e85fa1a09e21708b058e0e0d",
                            "kind": {
                                "AddKeys": [
                                    {
                                        "name": "mock_01_contract_hash",
                                        "key": "hash-26784926919ea2aca89eb6895e85813349fa03f5fdbfd6d0921fdcd2eab08493"
                                    }
                                ]
                            }
                        },
                        {
                            "key": "uref-b8443e85734a0991515e7d299218b786b222fa6fcf7e56967198131671565a6e-000",
                            "kind": {
                                "Write": {
                                    "CLValue": {
                                        "cl_type": "U32",
                                        "bytes": "01000000",
                                        "parsed": 1
                                    }
                                }
                            }
                        },
                        {
                            "key": "account-hash-884a9a83105dfe1c74961836db32eacf0d4cb4c7e85fa1a09e21708b058e0e0d",
                            "kind": {
                                "AddKeys": [
                                    {
                                        "name": "mock_01_contract_version",
                                        "key": "uref-b8443e85734a0991515e7d299218b786b222fa6fcf7e56967198131671565a6e-007"
                                    }
                                ]
                            }
                        },
                        {
                            "key": "balance-hold-01b6ae19d8323e66d85d3b268f30b082a8636e248ecb91c3a401eea6559f77e907f3ba71a894010000",
                            "kind": {
                                "Prune": "balance-hold-01b6ae19d8323e66d85d3b268f30b082a8636e248ecb91c3a401eea6559f77e907f3ba71a894010000"
                            }
                        },
                        {
                            "key": "balance-hold-00b6ae19d8323e66d85d3b268f30b082a8636e248ecb91c3a401eea6559f77e907f3ba71a894010000",
                            "kind": {
                                "Write": {
                                    "CLValue": {
                                        "cl_type": "U512",
                                        "bytes": "050088526a74",
                                        "parsed": "500000000000"
                                    }
                                }
                            }
                        },
                        {
                            "key": "bid-addr-01e59e4795bdf8f25e6c1dc976e312c06098c5a2c1bf1a8c4bbb1c711a60cf624b",
                            "kind": "Identity"
                        },
                        {
                            "key": "bid-addr-04e59e4795bdf8f25e6c1dc976e312c06098c5a2c1bf1a8c4bbb1c711a60cf624b7f00000000000000",
                            "kind": {
                                "Write": {
                                    "BidKind": {
                                        "Credit": {
                                            "validator_public_key": "0192c9c2fe85072475bb0994999aca8f8d7af2f145b0516deafbed1a001377d47f",
                                            "era_id": 127,
                                            "amount": "500000000000"
                                        }
                                    }
                                }
                            }
                        }
                    ]
                }
            }
        }
    },
    "id": "STRING"
}

Caller account status following contract deployment:


  • Command:

casper-client get-account --public-key PATH_TO_CALLER_PUBKEY_HEX --node-address http://localhost:11101

  • Result:

{
  "jsonrpc": "2.0",
  "id": -2549450563895035630,
  "result": {
    "api_version": "2.0.0",
    "account": {
      "account_hash": "account-hash-884a9a83105dfe1c74961836db32eacf0d4cb4c7e85fa1a09e21708b058e0e0d",
      "named_keys": [
        {
          "name": "mock_01_contract_hash",
          "key": "hash-26784926919ea2aca89eb6895e85813349fa03f5fdbfd6d0921fdcd2eab08493"
        },
        {
          "name": "mock_01_contract_package",
          "key": "hash-d76c399c7411dbd2fe3872f422386e3f455ee3ca313a864878c14ad96e003bb9"
        },
        {
          "name": "mock_01_contract_package_access",
          "key": "uref-b07faaf91b5a67f0aa6a64f329dacdc7381ff67432f446727326f3c67b9ccacc-007"
        },
        {
          "name": "mock_01_contract_version",
          "key": "uref-b8443e85734a0991515e7d299218b786b222fa6fcf7e56967198131671565a6e-007"
        }
      ],
      "main_purse": "uref-b6ae19d8323e66d85d3b268f30b082a8636e248ecb91c3a401eea6559f77e907-007",
      "associated_keys": [
        {
          "account_hash": "account-hash-884a9a83105dfe1c74961836db32eacf0d4cb4c7e85fa1a09e21708b058e0e0d",
          "weight": 1
        }
      ],
      "action_thresholds": {
        "deployment": 1,
        "key_management": 1
      }
    },
    "merkle_proof": "[2864 hex chars]"
  }
}

counter_inc entry point call:


  • Command:

casper-client put-deploy \
--node-address http://localhost:11101 \
--chain-name casper-net-1 \
--secret-key $HOME/client_keys/nctl/caller_secret_key.pem \
--session-name "mock_01_contract_hash" \
--session-entry-point "counter_inc" \
--payment-path pocs/tests/wasm/payment-code-01.wasm \
--payment-arg "amount:u512='1000000000000'"

Notice that the payment amount is set to 1000 CSPR, which is deliberately high to ensure there are no issues with covering the execution cost.


  • Deploy hash:

{
  "jsonrpc": "2.0",
  "id": 8689483874901916344,
  "result": {
    "api_version": "2.0.0",
    "deploy_hash": "618f2ff32bde94af4ec2b3db66ad2beb2b6497a2aa65c80563c631f153e445a4"
  }
}

counter_inc entry point call result:

  • Command:

casper-client get-deploy --node-address http://localhost:11101 --id STRING 7343d24f0afbb684a7d407f4b886beef838090c3b7a4972d2193fe59b5a836bf

  • Result:

response Success for rpc-id STRING info_get_deploy is not valid because Some(Error("missing field `execution_results`", line: 0, column: 0)): {
    "jsonrpc": "2.0",
    "result": {
        "api_version": "2.0.0",
        "deploy": {
            "hash": "618f2ff32bde94af4ec2b3db66ad2beb2b6497a2aa65c80563c631f153e445a4",
            "header": {
                "account": "01498126216b96652e040e081f69cdfbf679c5c42b3adbbb7ea240872c57011bc5",
                "timestamp": "2025-01-27T15:51:35.468Z",
                "ttl": "30m",
                "gas_price": 1,
                "body_hash": "29bc3603a3b88e7baff68f1a581d32fcd6867bfc89c55c0942195ecb7cdd6b48",
                "dependencies": [],
                "chain_name": "casper-net-1"
            },
            "payment": {
                "ModuleBytes": {
                    "module_bytes": "LARGE BYTES",
                    "args": [
                        [
                            "amount",
                            {
                                "cl_type": "U512",
                                "bytes": "050010a5d4e8",
                                "parsed": "1000000000000"
                            }
                        ]
                    ]
                }
            },
            "session": {
                "StoredContractByName": {
                    "name": "mock_01_contract_hash",
                    "entry_point": "counter_inc",
                    "args": []
                }
            },
            "approvals": [
                {
                    "signer": "01498126216b96652e040e081f69cdfbf679c5c42b3adbbb7ea240872c57011bc5",
                    "signature": "018a6bc45414ee645b9d9f28c2f3bb650362f3180ce0cc41baa25442a11cfdd1870e429fc4c6544b6d492d2f2fc5e2d02b36e379551e26b333e66946a623089000"
                }
            ]
        },
        "execution_info": {
            "block_hash": "21f7413b6e03ec98d0b0151f2c01b4e4e8885426b523d7c35f1801eaa05b3d65",
            "block_height": 1463,
            "execution_result": {
                "Version2": {
                    "initiator": {
                        "PublicKey": "01498126216b96652e040e081f69cdfbf679c5c42b3adbbb7ea240872c57011bc5"
                    },
                    "error_message": "HandlePayment error: Unable to remove named key",
                    "limit": "1000000000000",
                    "consumed": "13992650",
                    "cost": "1000000000000",
                    "transfers": [],
                    "size_estimate": 22849,
                    "effects": [
                        {
                            "key": "balance-hold-00b6ae19d8323e66d85d3b268f30b082a8636e248ecb91c3a401eea6559f77e907f3ba71a894010000",
                            "kind": "Identity"
                        },
                        {
                            "key": "hash-448d2fdc3527b3948c3b1a1f30fe45e259d7599671adfb9da3eb56f7f1f4d78e",
                            "kind": {
                                "AddKeys": [
                                    {
                                        "name": "refund_purse",
                                        "key": "uref-b6ae19d8323e66d85d3b268f30b082a8636e248ecb91c3a401eea6559f77e907-007"
                                    }
                                ]
                            }
                        },
                        {
                            "key": "hash-26784926919ea2aca89eb6895e85813349fa03f5fdbfd6d0921fdcd2eab08493",
                            "kind": "Identity"
                        },
                        {
                            "key": "hash-d76c399c7411dbd2fe3872f422386e3f455ee3ca313a864878c14ad96e003bb9",
                            "kind": "Identity"
                        },
                        {
                            "key": "hash-185b1e26e70547cdfdd2409b10c104b141d43a9d489542fcd4c2b0eca9b7a0d0",
                            "kind": "Identity"
                        },
                        {
                            "key": "uref-343c619075a0544899d95ec661af3a0667d0c3a0906a13f5e05c2d58bf541c7b-000",
                            "kind": {
                                "AddInt32": 1
                            }
                        },
                        {
                            "key": "balance-hold-00b6ae19d8323e66d85d3b268f30b082a8636e248ecb91c3a401eea6559f77e907f3ba71a894010000",
                            "kind": "Identity"
                        },
                        {
                            "key": "balance-ef63dbc74749b41e7d8b33de2b3955f91360dd16638c694437f6fa364d99541c",
                            "kind": {
                                "Write": {
                                    "CLValue": {
                                        "cl_type": "U512",
                                        "bytes": "00",
                                        "parsed": "0"
                                    }
                                }
                            }
                        },
                        {
                            "key": "balance-b6ae19d8323e66d85d3b268f30b082a8636e248ecb91c3a401eea6559f77e907",
                            "kind": {
                                "AddUInt512": "0"
                            }
                        },
                        {
                            "key": "balance-hold-00b6ae19d8323e66d85d3b268f30b082a8636e248ecb91c3a401eea6559f77e907900d76a894010000",
                            "kind": {
                                "Write": {
                                    "CLValue": {
                                        "cl_type": "U512",
                                        "bytes": "050010a5d4e8",
                                        "parsed": "1000000000000"
                                    }
                                }
                            }
                        },
                        {
                            "key": "bid-addr-01ba2e288aa7a484870448cbb9f979d963bace4db04dffc64e89f0551fd19138d7",
                            "kind": "Identity"
                        },
                        {
                            "key": "bid-addr-04ba2e288aa7a484870448cbb9f979d963bace4db04dffc64e89f0551fd19138d78500000000000000",
                            "kind": {
                                "Write": {
                                    "BidKind": {
                                        "Credit": {
                                            "validator_public_key": "01f4ff21e49f4ee0929c424f7e8ab38f5d675bec568d3c5db3b4e033e55d503858",
                                            "era_id": 133,
                                            "amount": "1000000000000"
                                        }
                                    }
                                }
                            }
                        }
                    ]
                }
            }
        }
    },
    "id": "STRING"
}

The counter_inc transaction failed with the HandlePayment error: Unable to remove named key error, which validates our analysis.

BVSS
Recommendation

It is recommended to use the payment code instead of the session code when handling custom payments. Additionally, consider updating the phase for the ClearRefundPurse refund mode to Payment.

Remediation Comment

SOLVED: The Casper team solved this issue in the specified commit id.

Remediation Hash

7.5 Possibility Of Accounts Being Permanently Linked As Associated Keys

//

Medium

Description

In Casper, addressable entities can associate multiple key pairs through a signature scheme to authorize transactions. Each associated key has a weight, and the combined weight must meet the transaction threshold. A transaction requires signatures from the associated keys, with the sum of their weights meeting or exceeding the threshold. If the transaction involves key management actions, the weight sum of the authorizing keys must also meet the key management threshold.


However, when removing an associated key from an account or entity, an invalid check was identified that enforces the maximum key limit, similar to the check for adding associated keys. This should not apply, as removal operations inherently decrease the associated key count.


As a result, if the associated keys count reaches the maximum allowed threshold, it will not be possible to remove an associated key. This also prevents adding new keys, leaving the account or entity with its current associated keys, with the only option being to update their weights in the future.

Code Location

Below is a code snippet of the remove_associated_key function:

if self.engine_config.enable_entity {
    // Get the current entity record
    let entity = {
        let mut entity: AddressableEntity = self.read_gs_typed(&context_key)?;
        // enforce max keys limit
        if entity.associated_keys().len()
            >= (self.engine_config.max_associated_keys() as usize)
        {
            return Err(ExecError::AddKeyFailure(AddKeyFailure::MaxKeysLimit));
        }

        // Exit early in case of error without updating global state
        entity
            .remove_associated_key(account_hash)
            .map_err(ExecError::from)?;
        entity
    };

    self.metered_write_gs_unsafe(
        context_key,
        self.addressable_entity_to_validated_value(entity)?,
    )?;
} else {
    // Take an account out of the global state
    let account = {
        let mut account: Account = self.read_gs_typed(&context_key)?;

        if account.associated_keys().len()
            >= (self.engine_config.max_associated_keys() as usize)
        {
            return Err(ExecError::AddKeyFailure(AddKeyFailure::MaxKeysLimit));
        }

        // Exit early in case of error without updating global state
        account
            .remove_associated_key(account_hash)
            .map_err(ExecError::from)?;
        account
    };

    let account_value = self.account_to_validated_value(account)?;

    self.metered_write_gs_unsafe(context_key, account_value)?;
}

BVSS
Recommendation

It is recommended to eliminate the maximum allowed threshold check during the removal of associated keys.

Remediation Comment

SOLVED: The Casper team solved this issue in the specified commit id.

Remediation Hash

7.6 Possible Inaccurate Gas Charges

//

Low

Description

In Casper, there are three primary system contracts: Mint, Auction, and Handle Payment. Most system contract calls charge gas through the charge_system_contract_call function, which helps prevent misleading gas charges when one system contract invokes another.

However, some auction functions (get_era_validators, slash, distribute, and read_era_id) use the charge_gas function instead, resulting in gas charges being applied regardless of the call context.


Code Location

Below is the implementation of the charge_system_contract_call function:

pub(crate) fn charge_system_contract_call<T>(&mut self, amount: T) -> Result<(), ExecError>
where
    T: Into<Gas>,
{
    if self.is_system_immediate_caller()? || self.host_function_flag.is_in_host_function_scope() {
        return Ok(());
    }

    self.context.charge_system_contract_call(amount)
}

Below is the implementation of the context.charge_system_contract_call function:

/// Charges gas for using a host system contract's entrypoint.
pub(crate) fn charge_system_contract_call<T>(&mut self, call_cost: T) -> Result<(), ExecError>
where
    T: Into<Gas>,
{
    let amount: Gas = call_cost.into();
    self.charge_gas(amount)
}

Below is the implementation of the charge_gas function:

pub(crate) fn charge_gas(&mut self, gas: Gas) -> Result<(), ExecError> {
    let prev = self.gas_counter();
    let gas_limit = self.gas_limit();
    // gas charge overflow protection
    match prev.checked_add(gas) {
        None => {
            self.set_gas_counter(gas_limit);
            Err(ExecError::GasLimit)
        }
        Some(val) if val > gas_limit => {
            self.set_gas_counter(gas_limit);
            Err(ExecError::GasLimit)
        }
        Some(val) => {
            self.set_gas_counter(val);
            Ok(())
        }
    }
}

BVSS
Recommendation

It is recommended to use the charge_system_contract_call function for gas charges related to system contracts to avoid misleading charges.

Remediation Comment

SOLVED: The Casper team solved this issue in the specified commit id.

Remediation Hash

7.7 Improper Error Handling

//

Low

Description

Although the appropriate errors are defined within the auction errors, it has been observed that some errors thrown do not accurately reflect the issues encountered. Specifically, the following discrepancies were identified:

  • While reading the mint contract hash via get_mint_hash within both the mint and reduce_total_supply functions, the returned error is incorrectly set to Error::MintReward which is defined as "Failed to mint reward tokens".

  • When a failure occurs during the call to the mint contract's mint_read_base_round_reward function, the returned error is incorrectly set to Error::MissingValue which is defined as "Value under an uref does not exist. This means the installer contract didn't work properly".

  • When a failure occurs during the call to the mint contract's mint_reduce_total_supply function, the returned error is incorrectly set to Error::MintReward which is defined as "Failed to mint reward tokens".


Additionally, the following misleading error messages were found in the execution engine's runtime context:

  • Both is_addable and is_writeable functions panic on failure with the error message: "is_readable: entity_key is unexpected key variant" instead of using the correct function name in the message.


As a result, these errors could mislead users and cause confusion in understanding the true cause of the transaction failure.

Code Location

Below is the implementation of the read_base_round_reward function:

fn read_base_round_reward(&mut self) -> Result<U512, Error> {
    let mint_hash = self.get_mint_hash().map_err(|exec_error| {
        <Option<Error>>::from(exec_error).unwrap_or(Error::MissingValue)
    })?;
    self.mint_read_base_round_reward(mint_hash)
        .map_err(|exec_error| <Option<Error>>::from(exec_error).unwrap_or(Error::MissingValue))
}

Below is the implementation of the mint function:

fn mint(&mut self, amount: U512) -> Result<URef, Error> {
    let mint_hash = self
        .get_mint_hash()
        .map_err(|exec_error| <Option<Error>>::from(exec_error).unwrap_or(Error::MintReward))?;
    self.mint_mint(mint_hash, amount)
        .map_err(|exec_error| <Option<Error>>::from(exec_error).unwrap_or(Error::MintReward))
}

Below is the implementation of the reduce_total_supply function:

fn reduce_total_supply(&mut self, amount: U512) -> Result<(), Error> {
    let mint_hash = self
        .get_mint_hash()
        .map_err(|exec_error| <Option<Error>>::from(exec_error).unwrap_or(Error::MintReward))?;
    self.mint_reduce_total_supply(mint_hash, amount)
        .map_err(|exec_error| <Option<Error>>::from(exec_error).unwrap_or(Error::MintReward))
}

Below is a code snippet of the auction-related errors defined in types/src/system/auction/error.rs:


/// Value under an uref does not exist. This means the installer contract didn't work properly.
/// ```
/// # use casper_types::system::auction::Error;
/// assert_eq!(2, Error::MissingValue as u8);
/// ```
MissingValue = 2,

...

/// Failed to mint reward tokens.
/// ```
/// # use casper_types::system::auction::Error;
/// assert_eq!(24, Error::MintReward as u8);
/// ```
MintReward = 24,

Below is the implementation of the is_addable function:

pub fn is_addable(&self, key: &Key) -> bool {
    match self.context_key_to_entity_addr() {
        Ok(entity_addr) => key.is_addable(&entity_addr),
        Err(error) => {
            error!(?error, "entity_key is unexpected key variant");
            panic!("is_readable: entity_key is unexpected key variant");
        }
    }
}

Below is the implementation of the is_writeable function:

pub fn is_writeable(&self, key: &Key) -> bool {
    match self.context_key_to_entity_addr() {
        Ok(entity_addr) => key.is_writeable(&entity_addr),
        Err(error) => {
            error!(?error, "entity_key is unexpected key variant");
            panic!("is_readable: entity_key is unexpected key variant");
        }
    }
}

BVSS
Recommendation

It is recommended to address the aforementioned errors as follows:

  • If an error occurs while reading the mint contract hash via get_mint_hash, the appropriate error should be Error::MissingValue.

  • If the call to the mint contract's mint_read_base_round_reward function fails, the proper error should be Error::MintReward.

  • If the call to the mint contract's mint_reduce_total_supply function fails, the appropriate error to return should be Error::MintReduceTotalSupply.

  • Both is_addable and is_writeable functions should include the correct function name in their panic messages.


Remediation Comment

SOLVED: The Casper team solved this issue in the specified commit id.

Remediation Hash

7.8 Incorrect Function Name

//

Low

Description

Within the data access layer, several data types have corresponding request and result types to query data and track the status of the request. In nearly all of these types, the result includes an is_success method to indicate whether the request was successful. However, the EntryPointExistsResult type defines this method as is_some instead. This can be misleading, as the is_some function is typically associated with Option types, where it returns true if the value is Some(data_type) and false if the value is None.

Code Location

Below is the implementation of the EntryPointExistsResult::is_some function:

impl EntryPointExistsResult {
    /// Returns `true` if the result is `Success`.
    pub fn is_some(self) -> bool {
        matches!(self, Self::Success { .. })
    }
}

BVSS
Recommendation

It is recommended to rename the is_some function to is_success to ensure consistency and eliminate any potential ambiguity.

Remediation Comment

SOLVED: The Casper team solved this issue in the specified commit id.

Remediation Hash

7.9 Potential Inaccurate Gas Cost Calculation

//

Low

Description

Each host function in Casper incurs a gas cost, which is determined by the sum of two factors:

  1. Base cost: The fee charged to the user for invoking the host function.

  2. Argument cost: The value of each argument, weighted by its corresponding weight.


However, it was found that during the gas cost calculation for the add_contract_version_with_message_topics function, the cost was mistakenly set using the self.charge_host_function_call function, assigning the cost of add_package_version instead.


While both functions currently share the same cost, as defined in the chainspec configuration, this creates a potential risk for incorrect gas cost calculation if their costs diverge in the future.

Code Location

Below is a code snippet of the invoke_index function:

FunctionIndex::AddContractVersionWithMessageTopics => {
    ...
    self.charge_host_function_call(
        &host_function_costs.add_package_version,
        [
            contract_package_hash_ptr,
            contract_package_hash_size,
            version_ptr,
            entry_points_ptr,
            entry_points_size,
            named_keys_ptr,
            named_keys_size,
            message_topics_ptr,
            message_topics_size,
            output_ptr,
            output_size,
        ],
    )?;

Below is the implementation of the charge_host_function_call function:

/// Calculate gas cost for a host function
fn charge_host_function_call<T>(
    &mut self,
    host_function: &HostFunction<T>,
    weights: T,
) -> Result<(), Trap>
where
    T: AsRef<[HostFunctionCost]> + Copy,
{
    let cost = host_function
        .calculate_gas_cost(weights)
        .ok_or(ExecError::GasLimit)?; // Overflowing gas calculation means gas limit was exceeded
    self.gas(cost)?;
    Ok(())
}

Below is the implementation of the calculate_gas_cost function:

/// Calculate gas cost for a host function
pub fn calculate_gas_cost(&self, weights: T) -> Option<Gas> {
    let mut gas = Gas::new(self.cost);
    for (argument, weight) in self.arguments.as_ref().iter().zip(weights.as_ref()) {
        let lhs = Gas::new(*argument);
        let rhs = Gas::new(*weight);
        let product = lhs.checked_mul(rhs)?;
        gas = gas.checked_add(product)?;
    }
    Some(gas)
}

Below is a code snippet from the production chainspec configuration, located at resources/production/chainspec.toml:

add_contract_version_with_message_topics = { cost = 200, arguments = [0, 0, 0, 0, 120_000, 0, 0, 0, 30_000, 0, 0] }
add_package_version = { cost = 200, arguments = [0, 0, 0, 0, 120_000, 0, 0, 0, 30_000, 0, 0] }

BVSS
Recommendation

It is recommended to use the correct host function cost to prevent potential errors in gas cost calculations.

Remediation Comment

SOLVED: The Casper team solved this issue in the specified commit id.

Remediation Hash

7.10 Inconsistent Key Mismatch Issue in Mint and Auction System Contracts

//

Low

Description

An inconsistent behavior was identified in the context key creation during system contract calls. Specifically, for call_host_handle_payment, the context key is correctly assigned based on the enable_entity flag—Key::AddressableEntity when enabled, and Key::Hash otherwise. However, for call_host_mint and call_host_auction, the context key is always set to Key::AddressableEntity, regardless of the flag.


This also applied when reading system entity keys, as they were consistently provided as addressable entity keys, regardless of the enable_entity flag. The current implementation works because the incorrectly set keys matched when recording transfers and auction info at a specific era ID.

Code Location

Below is a code snippet of the call_host_mint function:

let mint_hash = self.context.get_system_contract(MINT)?;
let mint_addr = EntityAddr::new_system(mint_hash.value());

let mint_named_keys = self
    .context
    .state()
    .borrow_mut()
    .get_named_keys(mint_addr)?;

let mut named_keys = mint_named_keys;

let runtime_context = self.context.new_from_self(
    mint_addr.into(),
    EntryPointType::Called,
    &mut named_keys,
    access_rights,
    runtime_args.to_owned(),
);

Below is a code snippet of the call_host_auction function:

let entity_hash = self.context.get_system_contract(AUCTION)?;
let auction_key = Key::addressable_entity_key(EntityKindTag::System, entity_hash);

let auction_named_keys = self
    .context
    .state()
    .borrow_mut()
    .get_named_keys(EntityAddr::System(entity_hash.value()))?;

let mut named_keys = auction_named_keys;

let runtime_context = self.context.new_from_self(
    auction_key,
    EntryPointType::Called,
    &mut named_keys,
    access_rights,
    runtime_args.to_owned(),
);

Below is a code snippet of the record_transfer function:

if self.context.get_context_key() != self.context.get_system_entity_key(MINT)? {
    return Err(ExecError::InvalidContext);
}

Below is a code snippet of the record_era_info function:

if self.context.get_context_key() != self.context.get_system_entity_key(AUCTION)? {
    return Err(ExecError::InvalidContext);
}

Below is a code snippet of the get_system_entity_key function:

pub(crate) fn get_system_entity_key(&self, name: &str) -> Result<Key, ExecError> {
    let system_entity_hash = self.get_system_contract(name)?;
    Ok(Key::addressable_entity_key(
        EntityKindTag::System,
        system_entity_hash,
    ))
}

BVSS
Recommendation

Ensure consistent handling of context keys for mint and auction system contract calls, as well as system entity keys, in accordance with the enable_entity flag.

Remediation Comment

SOLVED: The Casper team solved this issue in the specified commit id.

Remediation Hash

7.11 Unnecessary Mutable Borrows

//

Informational

Description

Using mutable borrows when not necessary can lead to unnecessary complexity and potential data conflicts, so it's best to reserve them for situations where mutation is truly required.


However, unnecessary mutable borrows were identified in the remove_contract_user_group function:

  • The package's mutable group definitions were borrowed when only read, not mutated.

  • Similarly, when the enable_entity flag is disabled, the contract package's mutable group definitions were borrowed without any mutation.

Code Location

Below is the implementation of the remove_contract_user_group function:

fn remove_contract_user_group(
        &mut self,
        package_key: PackageHash,
        label: Group,
    ) -> Result<Result<(), ApiError>, ExecError> {
        if self.context.engine_config().enable_entity {
            let mut package: Package = self.context.get_validated_package(package_key)?;

            let group_to_remove = Group::new(label);
            let groups = package.groups_mut();

            // Ensure group exists in groups
            if !groups.contains(&group_to_remove) {
                return Ok(Err(addressable_entity::Error::GroupDoesNotExist.into()));
            }

            // Remove group if it is not referenced by at least one entry_point in active versions.
            let versions = package.versions();
            for entity_hash in versions.contract_hashes() {
                let entry_points = {
                    self.context
                        .get_casper_vm_v1_entry_point(Key::contract_entity_key(*entity_hash))?
                };
                for entry_point in entry_points.take_entry_points() {
                    match entry_point.access() {
                        EntryPointAccess::Public | EntryPointAccess::Template => {
                            continue;
                        }
                        EntryPointAccess::Groups(groups) => {
                            if groups.contains(&group_to_remove) {
                                return Ok(Err(addressable_entity::Error::GroupInUse.into()));
                            }
                        }
                    }
                }
            }

            if !package.remove_group(&group_to_remove) {
                return Ok(Err(addressable_entity::Error::GroupInUse.into()));
            }

            // Write updated package to the global state
            self.context.metered_write_gs_unsafe(package_key, package)?;
        } else {
            let mut contract_package = self
                .context
                .get_validated_contract_package(package_key.value())?;

            let group_to_remove = Group::new(label);
            let groups = contract_package.groups_mut();

            // Ensure group exists in groups
            if !groups.contains(&group_to_remove) {
                return Ok(Err(addressable_entity::Error::GroupDoesNotExist.into()));
            }

            // Remove group if it is not referenced by at least one entry_point in active versions.
            for (_version, contract_hash) in contract_package.versions().iter() {
                let entry_points = {
                    self.context
                        .get_casper_vm_v1_entry_point(Key::contract_entity_key(
                            AddressableEntityHash::new(contract_hash.value()),
                        ))?
                };
                for entry_point in entry_points.take_entry_points() {
                    match entry_point.access() {
                        EntryPointAccess::Public | EntryPointAccess::Template => {
                            continue;
                        }
                        EntryPointAccess::Groups(groups) => {
                            if groups.contains(&group_to_remove) {
                                return Ok(Err(addressable_entity::Error::GroupInUse.into()));
                            }
                        }
                    }
                }
            }

            if !contract_package.remove_group(&group_to_remove) {
                return Ok(Err(addressable_entity::Error::GroupInUse.into()));
            }

            // Write updated package to the global state
            self.context.metered_write_gs_unsafe(
                ContractPackageHash::new(package_key.value()),
                contract_package,
            )?;
        }
        Ok(Ok(()))
    }

BVSS
Recommendation

It is recommended to replace the unnecessary mutable borrows with immutable borrows.

Remediation Comment

SOLVED: The Casper team solved this issue in the specified commit id.

Remediation Hash

7.12 Presence Of Typographical Errors

//

Informational

Description

The following typographical errors were identified:


storage/src/data_access_layer/forced_undelegate.rs
L81: /// Forced undelgation succeeded.

execution_engine/src/runtime/mod.rs
L364: // before reaching this pointt.
L2926: /// Generates new unforgable reference and adds it to the context's

BVSS
Recommendation

It is recommended to fix the aforementioned typographical error as follows:

  • undelgation -> undelegation

  • unforgable -> unforgeable

  • pointt -> point

Remediation Comment

SOLVED: The Casper team solved this issue in the following commit IDs:


Remediation Hash

7.13 Inadequate Variable Naming

//

Informational

Description

Choosing an appropriate variable name that aligns with its functionality improves code readability, maintainability, and reduces the risk of errors.


However, the following instances of inadequate naming were identified:

  • The get_caller function is documented as 'Writes caller (deploy) account public key to dest_ptr in the Wasm memory', but the parameter is incorrectly named output_size instead, which is not an appropriate name for a memory offset.

  • In the call_host_auction function, the argument named public_key is used as account_hash, which can be confusing since its type is a public key.

Code Location

Below is a code snippet of the get_caller function:

/// Writes caller (deploy) account public key to dest_ptr in the Wasm
/// memory.
fn get_caller(&mut self, output_size: u32) -> Result<Result<(), ApiError>, Trap> {

Below is a code snippet of the call_host_auction function:

let account_hash = Self::get_named_argument(runtime_args, auction::ARG_PUBLIC_KEY)?;

BVSS
Recommendation

It is recommended to rename the aforementioned variables to more appropriate names that better align with their intended functionality.

Remediation Comment

SOLVED: The Casper team solved this issue in the following commit IDs:

The references to dest_ptr and output_size within the get_caller function have been renamed to output_size_ptr.

Remediation Hash

7.14 Misleading Comments

//

Informational

Description

A step request executes auction code, slashes validators, evicts validators and distributes rewards. Each executed step request produces a step result, which indicates the outcome of the execution.


While documentation and comments in the codebase are crucial for clarifying key aspects and helping others understand the developer's intentions, an inaccurate comment was found in the storage/src/data_access_layer/step.rs file. The comment incorrectly describes the Failure variant of the StepResult enum as 'Failed to upgrade protocol', when it actually indicates a failure in step execution.


Additionally, the metered_add_gs_unsafe function is documented as "This method performs full validation of the key to be written", but it actually assumes the key is already validated and performs no validation itself.


The is_system_contract function takes a hash address which is of type HashAddr but is documented as "Checks if a [`Key`] is a system contract."


The invocation of the auction contract's slash function within the call_host_function is accompanied by the following comment: "// Type: fn slash(validator_account_hashes: &[AccountHash]) -> Result<(), ExecError>". However, the actual argument accepted by the slash function is as follows: validator_public_keys: Vec<PublicKey>.

Similarly, the invocation of the distribute function was accompanied by the following comment: // Type: fn distribute(reward_factors: BTreeMap<PublicKey, u64>) -> Result<()>, when in fact, the correct parameter should be rewards: BTreeMap<PublicKey, Vec<U512>>.


The transfer_to_new_account function is documented as "Creates a new account at a given public key", yet it actually accepts an account hash instead of a public key.


In the transfer_from_purse_to_account_hash function, the following comment was included: '// Look up the account at the given public key's address', even though an account hash was provided instead of a public key.

Code Location

Below is the definition of the StepResult enum:

/// Outcome of running step process.
#[derive(Debug)]
pub enum StepResult {
    /// Global state root not found.
    RootNotFound,
    /// Step process ran successfully.
    Success {
        /// State hash after step outcome is committed to the global state.
        post_state_hash: Digest,
        /// Effects of the step process.
        effects: Effects,
    },
    /// Failed to upgrade protocol.
    Failure(StepError),
}

Below is the definition of the metered_add_gs_unsafe function:

/// Adds data to a global state key and charges for bytes stored.
///
/// This method performs full validation of the key to be written.
pub(crate) fn metered_add_gs_unsafe(
    &mut self,
    key: Key,
    value: StoredValue,
) -> Result<(), ExecError> {
    let value_bytes_count = value.serialized_length();
    self.charge_gas_storage(value_bytes_count)?;

    match self.tracking_copy.borrow_mut().add(key, value) {
        Err(storage_error) => Err(storage_error.into()),
        Ok(AddResult::Success) => Ok(()),
        Ok(AddResult::KeyNotFound(key)) => Err(ExecError::KeyNotFound(key)),
        Ok(AddResult::TypeMismatch(type_mismatch)) => {
            Err(ExecError::TypeMismatch(type_mismatch))
        }
        Ok(AddResult::Serialization(error)) => Err(ExecError::BytesRepr(error)),
        Ok(AddResult::Transform(error)) => Err(ExecError::Transform(error)),
    }
}

Below is implementation of the is_system_contract function:

/// Checks if a [`Key`] is a system contract.
fn is_system_contract(&self, hash_addr: HashAddr) -> Result<bool, ExecError> {
    self.context.is_system_addressable_entity(&hash_addr)
}

Below are code snippets of the call_host_auction function:

// Type: `fn slash(validator_account_hashes: &[AccountHash]) -> Result<(), ExecError>`
Type: `fn distribute(reward_factors: BTreeMap<PublicKey, u64>) -> Result<(),

Below is a code snippet of the transfer_to_new_account function:

/// Creates a new account at a given public key, transferring a given amount
    /// of motes from the given source purse to the new account's purse.
    fn transfer_to_new_account(
        &mut self,
        source: URef,
        target: AccountHash,
        amount: U512,
        id: Option<u64>,
    ) -> Result<TransferResult, ExecError> {

Below is a code snippet of the transfer_from_purse_to_account_hash function:

fn transfer_from_purse_to_account_hash(
        &mut self,
        source: URef,
        target: AccountHash,
        amount: U512,
        id: Option<u64>,
    ) -> Result<TransferResult, ExecError> {
        let _scoped_host_function_flag = self.host_function_flag.enter_host_function_scope();
        let target_key = Key::Account(target);

        // Look up the account at the given public key's address
        match self.context.read_gs(&target_key)? {

BVSS
Recommendation

It is recommended to correct the misleading comments for accuracy.

Remediation Comment
Remediation Hash

8. Automated Testing

Static Analysis Report

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

Short Description

RUSTSEC-2025-0004

openssl 0.10.69

ssl::select_next_proto use after free

RUSTSEC-2024-0437

protobuf 2.28.0

Crash due to uncontrolled recursion in protobuf crate

RUSTSEC-2021-0139

ansi_term 0.12.1

ansi_term is Unmaintained

RUSTSEC-2024-0375

atty 0.2.14

atty is unmaintained

RUSTSEC-2024-0388

derivative 2.2.0

derivative is unmaintained; consider using an alternative

RUSTSEC-2020-0168

mach 0.3.2

mach is unmaintained

RUSTSEC-2024-0436

paste 1.0.15

paste - no longer maintained

RUSTSEC-2024-0370

proc-macro-error 1.0.4

proc-macro-error is unmaintained

RUSTSEC-2022-0054

wee_alloc 0.4.5

wee_alloc is Unmaintained

RUSTSEC-2022-0092

rmp-serde 0.14.4

rmp-serde Raw and RawRef unsound


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.