Prepared by:
HALBORN
Last Updated 04/17/2025
Date of Engagement: December 23rd, 2024 - March 14th, 2025
100% of all REPORTED Findings have been addressed
All findings
14
Critical
4
High
0
Medium
1
Low
5
Informational
4
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.
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.
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
.
EXPLOITABILITY METRIC () | METRIC VALUE | NUMERICAL 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 |
IMPACT METRIC () | METRIC VALUE | NUMERICAL 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 |
SEVERITY COEFFICIENT () | COEFFICIENT VALUE | NUMERICAL VALUE |
---|---|---|
Reversibility () | None (R:N) Partial (R:P) Full (R:F) | 1 0.5 0.25 |
Scope () | Changed (S:C) Unchanged (S:U) | 1.25 1 |
Severity | Score Value Range |
---|---|
Critical | 9 - 10 |
High | 7 - 8.9 |
Medium | 4.5 - 6.9 |
Low | 2 - 4.4 |
Informational | 0 - 1.9 |
Critical
4
High
0
Medium
1
Low
5
Informational
4
Security analysis | Risk level | Remediation Date |
---|---|---|
Flawed Payment Preprocessing Can Render Valid Transactions Ineligible For Execution, And Vice Versa | Critical | Solved - 02/11/2025 |
Transactions Intended For The V2 Runtime Are Ineligible For Execution | Critical | Solved - 02/07/2025 |
Addressable Entities Funds Are Lost Upon Each Upgrade | Critical | Solved - 03/12/2025 |
Transactions Using Custom Payment Will Fail | Critical | Solved - 01/21/2025 |
Possibility Of Accounts Being Permanently Linked As Associated Keys | Medium | Solved - 02/07/2025 |
Possible Inaccurate Gas Charges | Low | Solved - 02/12/2025 |
Improper Error Handling | Low | Solved - 02/07/2025 |
Incorrect Function Name | Low | Solved - 02/07/2025 |
Potential Inaccurate Gas Cost Calculation | Low | Solved - 02/07/2025 |
Inconsistent Key Mismatch Issue in Mint and Auction System Contracts | Low | Solved - 03/11/2025 |
Unnecessary Mutable Borrows | Informational | Solved - 03/31/2025 |
Presence Of Typographical Errors | Informational | Solved - 03/31/2025 |
Inadequate Variable Naming | Informational | Solved - 04/04/2025 |
Misleading Comments | Informational | Solved - 03/31/2025 |
//
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:
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.
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
.
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");
}
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:
Initialize a payment code based on gh_5058_regression
.
Verify that Alice's account does not already have Bob's account as an associated key before executing the transaction.
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.
Ensure the transaction completes without errors.
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.
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();
}
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:
It is recommended to use the PenalizedAccount
variant instead of Payment
when determining whether the balance identifier corresponds to a penalty.
SOLVED: The Casper team solved this issue in the specified commit id.
//
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.
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");
}
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.
SOLVED: The Casper team solved this issue by using the initiating account's main purse when the transaction is intended for the v2 runtime.
//
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.
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,
)
}
}
The PoC performs the following steps:
Implement an upgradeable mock contract that stores the contract as an addressable entity under the deployer's named keys.
Set up test environment with addressable entities
Upgrade protocol version to 2.0.0
Deploy the mock contract and store its initial main purse
Upgrade the mock contract and verify that its main purse was overwritten
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");
}
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:
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()
};
SOLVED: The Casper team solved this issue in the specified commit id.
//
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.
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)
}
The PoC performs the following steps:
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.
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.
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.
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();
}
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.
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
.
SOLVED: The Casper team solved this issue in the specified commit id.
//
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.
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)?;
}
It is recommended to eliminate the maximum allowed threshold check during the removal of associated keys.
SOLVED: The Casper team solved this issue in the specified commit id.
//
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.
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(())
}
}
}
It is recommended to use the charge_system_contract_call
function for gas charges related to system contracts to avoid misleading charges.
SOLVED: The Casper team solved this issue in the specified commit id.
//
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.
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");
}
}
}
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.
SOLVED: The Casper team solved this issue in the specified commit id.
//
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
.
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 { .. })
}
}
It is recommended to rename the is_some
function to is_success
to ensure consistency and eliminate any potential ambiguity.
SOLVED: The Casper team solved this issue in the specified commit id.
//
Each host function in Casper incurs a gas cost, which is determined by the sum of two factors:
Base cost: The fee charged to the user for invoking the host function.
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.
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] }
It is recommended to use the correct host function cost to prevent potential errors in gas cost calculations.
SOLVED: The Casper team solved this issue in the specified commit id.
//
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.
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,
))
}
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.
SOLVED: The Casper team solved this issue in the specified commit id.
//
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.
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(()))
}
It is recommended to replace the unnecessary mutable borrows with immutable borrows.
SOLVED: The Casper team solved this issue in the specified commit id.
//
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
It is recommended to fix the aforementioned typographical error as follows:
undelgation -> undelegation
unforgable -> unforgeable
pointt -> point
SOLVED: The Casper team solved this issue in the following commit IDs:
//
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.
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)?;
It is recommended to rename the aforementioned variables to more appropriate names that better align with their intended functionality.
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
.
//
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.
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)? {
It is recommended to correct the misleading comments for accuracy.
SOLVED: The Casper team solved this issue in the following commit IDs:
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 |
---|---|---|
openssl 0.10.69 | ssl::select_next_proto use after free | |
protobuf 2.28.0 | Crash due to uncontrolled recursion in protobuf crate | |
ansi_term 0.12.1 | ansi_term is Unmaintained | |
atty 0.2.14 |
| |
derivative 2.2.0 |
| |
mach 0.3.2 | mach is unmaintained | |
paste 1.0.15 | paste - no longer maintained | |
proc-macro-error 1.0.4 | proc-macro-error is unmaintained | |
wee_alloc 0.4.5 | wee_alloc is Unmaintained | |
rmp-serde 0.14.4 |
|
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.
// Download the full report
Casper 2.0
* Use Google Chrome for best results
** Check "Background Graphics" in the print settings if needed