diff --git a/Makefile b/Makefile index 873a316..71d1176 100644 --- a/Makefile +++ b/Makefile @@ -35,6 +35,7 @@ native-test: setup-test test: setup-test cargo test -p tests --lib + cargo test -p tests --lib --features test-enable-addressable-entity clippy: cargo +$(PINNED_TOOLCHAIN) clippy --release -p cep18 --bins --target wasm32-unknown-unknown $(CARGO_BUILD_FLAGS) -- -D warnings diff --git a/contracts/contract/src/allowances.rs b/contracts/contract/src/allowances.rs index e5fd80c..ec9dd83 100644 --- a/contracts/contract/src/allowances.rs +++ b/contracts/contract/src/allowances.rs @@ -2,19 +2,24 @@ use crate::{ constants::DICT_ALLOWANCES, utils::{ - get_dictionary_value_from_key, make_dictionary_item_key, set_dictionary_value_for_key, + get_dictionary_value_from_key, key_as_account_or_package, make_dictionary_item_key_value, + set_dictionary_value_for_key, }, }; use casper_types::{Key, U256}; /// Writes an allowance for owner and spender for a specific amount. pub fn write_allowance_to(owner: Key, spender: Key, amount: U256) { - let dictionary_item_key = make_dictionary_item_key(&owner, &spender); + let owner = key_as_account_or_package(owner); + let spender = key_as_account_or_package(spender); + let dictionary_item_key = make_dictionary_item_key_value(&owner, &spender); set_dictionary_value_for_key(DICT_ALLOWANCES, &dictionary_item_key, &amount) } /// Reads an allowance for a owner and spender pub fn read_allowance_from(owner: Key, spender: Key) -> U256 { - let dictionary_item_key = make_dictionary_item_key(&owner, &spender); + let owner = key_as_account_or_package(owner); + let spender = key_as_account_or_package(spender); + let dictionary_item_key = make_dictionary_item_key_value(&owner, &spender); get_dictionary_value_from_key(DICT_ALLOWANCES, &dictionary_item_key).unwrap_or_default() } diff --git a/contracts/contract/src/balances.rs b/contracts/contract/src/balances.rs index 6829a94..2550bd5 100644 --- a/contracts/contract/src/balances.rs +++ b/contracts/contract/src/balances.rs @@ -2,31 +2,16 @@ use crate::{ constants::DICT_BALANCES, error::Cep18Error, - utils::{base64_encode, get_dictionary_value_from_key, set_dictionary_value_for_key}, + utils::{ + get_dictionary_value_from_key, make_dictionary_item_key, set_dictionary_value_for_key, + key_as_account_or_package, + }, }; -use alloc::string::String; -use casper_contract::unwrap_or_revert::UnwrapOrRevert; -use casper_types::{bytesrepr::ToBytes, Key, U256}; - -/// Creates a dictionary item key for a dictionary item, by base64 encoding the Key argument -/// since stringified Keys are too long to be used as dictionary keys. -#[inline] -/// ! TODO GR check hex::encode with utils::make_dictionary_item_key or else -fn make_dictionary_item_key(owner: Key) -> String { - let preimage = owner - .to_bytes() - .unwrap_or_revert_with(Cep18Error::FailedToConvertBytes); - // NOTE: As for now dictionary item keys are limited to 64 characters only. Instead of using - // hashing (which will effectively hash a hash) we'll use base64. Preimage is 33 bytes for - // both used Key variants, and approximated base64-encoded length will be 4 * (33 / 3) ~ 44 - // characters. - // Even if the preimage increased in size we still have extra space but even in case of much - // larger preimage we can switch to base85 which has ratio of 4:5. - base64_encode(preimage) -} +use casper_types::{Key, U256}; /// Writes token balance of a specified account into a dictionary. pub fn write_balance_to(address: Key, amount: U256) { + let address = key_as_account_or_package(address); let dictionary_item_key = make_dictionary_item_key(address); set_dictionary_value_for_key(DICT_BALANCES, &dictionary_item_key, &amount) } @@ -35,6 +20,7 @@ pub fn write_balance_to(address: Key, amount: U256) { /// /// If a given account does not have balances in the system, then a 0 is returned. pub fn read_balance_from(address: Key) -> U256 { + let address = key_as_account_or_package(address); let dictionary_item_key = make_dictionary_item_key(address); get_dictionary_value_from_key(DICT_BALANCES, &dictionary_item_key).unwrap_or_default() } diff --git a/contracts/contract/src/main.rs b/contracts/contract/src/main.rs index 7546d42..3c7e856 100644 --- a/contracts/contract/src/main.rs +++ b/contracts/contract/src/main.rs @@ -43,7 +43,7 @@ use cep18::{ utils::{ base64_encode, get_contract_version_key, get_immediate_caller, get_optional_named_arg_with_user_errors, get_stored_value, get_uref_with_user_errors, - write_total_supply_to, + write_total_supply_to, UpsertTransform, }, }; @@ -81,7 +81,7 @@ pub extern "C" fn total_supply() { #[no_mangle] pub extern "C" fn balance_of() { - let address: Key = runtime::get_named_arg(ARG_ADDRESS); + let address: Key = runtime::get_named_arg::(ARG_ADDRESS).key_as_account_or_package(); let balance = read_balance_from(address); runtime::ret( CLValue::from_t(balance).unwrap_or_revert_with(Cep18Error::FailedToReturnEntryPointResult), @@ -90,8 +90,8 @@ pub extern "C" fn balance_of() { #[no_mangle] pub extern "C" fn allowance() { - let spender: Key = runtime::get_named_arg(ARG_SPENDER); - let owner: Key = runtime::get_named_arg(ARG_OWNER); + let spender: Key = runtime::get_named_arg::(ARG_SPENDER).key_as_account_or_package(); + let owner: Key = runtime::get_named_arg::(ARG_OWNER).key_as_account_or_package(); let val: U256 = read_allowance_from(owner, spender); runtime::ret( CLValue::from_t(val).unwrap_or_revert_with(Cep18Error::FailedToReturnEntryPointResult), @@ -101,7 +101,7 @@ pub extern "C" fn allowance() { #[no_mangle] pub extern "C" fn approve() { let caller = get_immediate_caller(); - let spender: Key = runtime::get_named_arg(ARG_SPENDER); + let spender: Key = runtime::get_named_arg::(ARG_SPENDER).key_as_account_or_package(); if spender == caller { revert(Cep18Error::CannotTargetSelfUser); } @@ -117,7 +117,7 @@ pub extern "C" fn approve() { #[no_mangle] pub extern "C" fn decrease_allowance() { let caller = get_immediate_caller(); - let spender: Key = runtime::get_named_arg(ARG_SPENDER); + let spender: Key = runtime::get_named_arg::(ARG_SPENDER).key_as_account_or_package(); if spender == caller { revert(Cep18Error::CannotTargetSelfUser); } @@ -136,7 +136,7 @@ pub extern "C" fn decrease_allowance() { #[no_mangle] pub extern "C" fn increase_allowance() { let caller = get_immediate_caller(); - let spender: Key = runtime::get_named_arg(ARG_SPENDER); + let spender: Key = runtime::get_named_arg::(ARG_SPENDER).key_as_account_or_package(); if spender == caller { revert(Cep18Error::CannotTargetSelfUser); } @@ -155,7 +155,7 @@ pub extern "C" fn increase_allowance() { #[no_mangle] pub extern "C" fn transfer() { let caller = get_immediate_caller(); - let recipient: Key = runtime::get_named_arg(ARG_RECIPIENT); + let recipient: Key = runtime::get_named_arg::(ARG_RECIPIENT).key_as_account_or_package(); if caller == recipient { revert(Cep18Error::CannotTargetSelfUser); } @@ -171,8 +171,8 @@ pub extern "C" fn transfer() { #[no_mangle] pub extern "C" fn transfer_from() { let caller = get_immediate_caller(); - let recipient: Key = runtime::get_named_arg(ARG_RECIPIENT); - let owner: Key = runtime::get_named_arg(ARG_OWNER); + let recipient: Key = runtime::get_named_arg::(ARG_RECIPIENT).key_as_account_or_package(); + let owner: Key = runtime::get_named_arg::(ARG_OWNER).key_as_account_or_package(); if owner == recipient { revert(Cep18Error::CannotTargetSelfUser); } @@ -204,7 +204,7 @@ pub extern "C" fn mint() { sec_check(vec![SecurityBadge::Admin, SecurityBadge::Minter]); - let owner: Key = runtime::get_named_arg(ARG_OWNER); + let owner: Key = runtime::get_named_arg::(ARG_OWNER).key_as_account_or_package(); let amount: U256 = runtime::get_named_arg(ARG_AMOUNT); let new_balance = { @@ -236,7 +236,7 @@ pub extern "C" fn burn() { revert(Cep18Error::MintBurnDisabled); } - let owner: Key = runtime::get_named_arg(ARG_OWNER); + let owner: Key = runtime::get_named_arg::(ARG_OWNER).key_as_account_or_package(); let caller = get_immediate_caller(); if owner != caller { revert(Cep18Error::InvalidBurnTarget); diff --git a/contracts/contract/src/utils.rs b/contracts/contract/src/utils.rs index ec19bbc..43726aa 100644 --- a/contracts/contract/src/utils.rs +++ b/contracts/contract/src/utils.rs @@ -18,19 +18,34 @@ use casper_types::{ api_error, bytesrepr::{self, FromBytes, ToBytes}, contracts::{ContractPackageHash, ContractVersionKey}, - ApiError, CLTyped, EntityAddr, Key, URef, U256, + ApiError, CLTyped, Key, PackageHash, URef, U256, }; use core::convert::TryInto; +/// Retrieves the immediate caller of the current contract execution context as a [`Key`]. +/// +/// This function abstracts over the different kinds of entities that may invoke a contract: +/// legacy accounts, legacy contract packages, or new entities. +/// +/// # Behavior +/// +/// * **ACCOUNT (legacy or new entity account)** Returns a `Key::Account` wrapping the +/// `AccountHash`. +/// * **CONTRACT (legacy contract package)** Returns a `Key::Hash` wrapping the +/// `ContractPackageHash`. +/// * **ENTITY (new entity)** Returns a `Key::Hash` wrapping the `PackageHash`. +/// * **Other / unexpected kinds** Reverts with [`Cep18Error::InvalidContext`]. pub fn get_immediate_caller() -> Key { const ACCOUNT: u8 = 0; + const PACKAGE: u8 = 1; const CONTRACT_PACKAGE: u8 = 2; const ENTITY: u8 = 3; const CONTRACT: u8 = 4; let caller_info = casper_get_immediate_caller().unwrap_or_revert(); - match caller_info.kind() { + let caller = match caller_info.kind() { + // Legacy or new entity account returns AccountHash ACCOUNT => caller_info .get_field_by_index(ACCOUNT) .unwrap() @@ -38,21 +53,85 @@ pub fn get_immediate_caller() -> Key { .unwrap_or_revert() .unwrap_or_revert_with(Cep18Error::InvalidContext) .into(), - CONTRACT => caller_info - .get_field_by_index(CONTRACT_PACKAGE) + // New entity returns PackageHash + ENTITY => caller_info + .get_field_by_index(PACKAGE) .unwrap() - .to_t::>() + .to_t::>() .unwrap_or_revert() .unwrap_or_revert_with(Cep18Error::InvalidContext) .into(), - ENTITY => caller_info - .get_field_by_index(ENTITY) + // Legacy returns ContractPackageHash + CONTRACT => caller_info + .get_field_by_index(CONTRACT_PACKAGE) .unwrap() - .to_t::>() + .to_t::>() .unwrap_or_revert() .unwrap_or_revert_with(Cep18Error::InvalidContext) .into(), _ => revert(Cep18Error::InvalidContext), + }; + + // Transform the caller Key to a legacy-compatible form (Account or Hash) for consistent + // on-chain usage. + // ⚠️ Strongly recommended: apply `key_as_account_or_package()` to any `Key` retrieved from + // named arguments before comparing with the caller. This ensures consistent normalization + // between user input and immediate caller, preventing mismatches. + key_as_account_or_package(caller) +} + +/// Converts a new-style [`Key`] returned by [`get_immediate_caller`] into a legacy-compatible +/// [`Key`]. +/// +/// This function ensures backward compatibility with legacy CEP-18 expectations, +/// where callers were identified only as `Account` or `Hash`. +/// +/// # Behavior +/// +/// * **`Key::AddressableEntity`** +/// - If the entity is an **account**, returns a legacy [`Key::Account`]. +/// - If the entity is not an account (e.g., smart contract or system entity), returns a legacy +/// [`Key::Hash`]. This case should not normally occur in CEP-18, where contracts calling are +/// expected to be identified as contract packages, but it is handled for consistency. +/// * **`Key::SmartContract`** Converts directly into a legacy [`Key::Hash`] using the +/// [`PackageHash`]. +/// * **Other legacy keys** (`Key::Account`, `Key::Hash`) Returned unchanged. +/// +/// # Notes +/// +/// - This function is mostly used in CEP-18 context where contract logic needs to interact with +/// storage or access controls (balances, allowances, etc.). +pub fn key_as_account_or_package(key: Key) -> Key { + match key { + Key::AddressableEntity(entity_addr) => { + if entity_addr.is_account() { + let account_hash = AccountHash::new(entity_addr.value()); + Key::Account(account_hash) + } else { + // This case should in theory never happen, since caller returned by + // `get_immediate_caller` is expected to be either an Account or a + // Package. We keep consistency by returning a legacy Key::Hash + // instead of reverting here. + Key::Hash(entity_addr.value()) + } + } + // Manage PackageHash from `get_immediate_caller` ENTITY case + Key::SmartContract(package_addr) => Key::Hash(package_addr), + // Legacy cases Account + ContractPackageHash from `get_immediate_caller` ACCOUNT + CONTRACT + // cases + legacy => legacy, + } +} + +pub trait UpsertTransform: Sized { + fn key_as_account_or_package(self) -> Self { + self + } +} + +impl UpsertTransform for Key { + fn key_as_account_or_package(self) -> Self { + key_as_account_or_package(self) } } @@ -126,7 +205,24 @@ pub fn get_optional_named_arg_with_user_errors( } } -pub fn make_dictionary_item_key( +/// Creates a dictionary item key for a dictionary item, by base64 encoding the Key argument +/// since stringified Keys are too long to be used as dictionary keys. +#[inline] +pub fn make_dictionary_item_key(owner: Key) -> String { + let preimage = owner + .to_bytes() + .unwrap_or_revert_with(Cep18Error::FailedToConvertBytes); + // NOTE: As for now dictionary item keys are limited to 64 characters only. Instead of using + // hashing (which will effectively hash a hash) we'll use base64. Preimage is 33 bytes for + // both used Key variants, and approximated base64-encoded length will be 4 * (33 / 3) ~ 44 + // characters. + // Even if the preimage increased in size we still have extra space but even in case of much + // larger preimage we can switch to base85 which has ratio of 4:5. + base64_encode(preimage) +} + +#[inline] +pub fn make_dictionary_item_key_value( key: &T, value: &V, ) -> String { diff --git a/tests/Cargo.toml b/tests/Cargo.toml index f1ab98b..0097477 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -23,3 +23,6 @@ casper-fixtures = { git = "https://github.com/casper-network/casper-fixtures", b casper-engine-test-support = { version = "8.1.0", default-features = false } casper-execution-engine = { version = "8.1.0", default-features = false } casper-event-standard.workspace = true + +[features] +test-enable-addressable-entity = [] diff --git a/tests/src/migration.rs b/tests/src/migration.rs index a63e6b6..ebbb75d 100644 --- a/tests/src/migration.rs +++ b/tests/src/migration.rs @@ -15,7 +15,9 @@ use crate::utility::{ AMOUNT_1, CEP18_CONTRACT_WASM, CEP18_TEST_CONTRACT_WASM, CEP18_TEST_TOKEN_CONTRACT_NAME, CEP18_TEST_TOKEN_CONTRACT_VERSION, TOKEN_NAME, }, - installer_request_builders::{cep18_check_balance_of, get_test_account}, + installer_request_builders::{ + cep18_check_balance_of, get_enable_addressable_entity, get_test_account, + }, message_handlers::{message_summary, message_topic}, support::query_stored_value, }; @@ -34,10 +36,8 @@ pub fn upgrade_v1_5_6_fixture_to_v2_0_0_ee( let mut upgrade_config = UpgradeRequestBuilder::new() .with_current_protocol_version(lmdb_fixture_state.genesis_protocol_version()) .with_new_protocol_version(ProtocolVersion::V2_0_0) - // TODO GR - // .with_migrate_legacy_accounts(true) - // .with_migrate_legacy_contracts(true) .with_activation_point(EraId::new(1)) + .with_enable_addressable_entity(get_enable_addressable_entity()) .build(); builder diff --git a/tests/src/utility/installer_request_builders.rs b/tests/src/utility/installer_request_builders.rs index 137c99c..da04790 100644 --- a/tests/src/utility/installer_request_builders.rs +++ b/tests/src/utility/installer_request_builders.rs @@ -6,8 +6,8 @@ use crate::utility::constants::{ AMOUNT_ALLOWANCE_1, AMOUNT_ALLOWANCE_2, AMOUNT_TRANSFER_1, AMOUNT_TRANSFER_2, }; use casper_engine_test_support::{ - utils::create_run_genesis_request, ExecuteRequest, ExecuteRequestBuilder, LmdbWasmTestBuilder, - DEFAULT_ACCOUNTS, DEFAULT_ACCOUNT_ADDR, + utils::create_run_genesis_request_with_chainspec_config, ChainspecConfig, ExecuteRequest, + ExecuteRequestBuilder, LmdbWasmTestBuilder, DEFAULT_ACCOUNTS, DEFAULT_ACCOUNT_ADDR, }; use casper_types::{ account::AccountHash, bytesrepr::FromBytes, runtime_args, AddressableEntityHash, CLTyped, @@ -26,6 +26,10 @@ use cep18_test_contract::constants::{ ENTRY_POINT_TRANSFER_AS_STORED_CONTRACT, RESULT_KEY, }; +pub(crate) fn get_enable_addressable_entity() -> bool { + cfg!(feature = "test-enable-addressable-entity") +} + /// Converts hash addr of Account into Hash, and Hash into Account /// /// This is useful for making sure CEP18 library respects different variants of Key when storing @@ -58,14 +62,20 @@ pub(crate) fn setup() -> (LmdbWasmTestBuilder, TestContext) { ARG_SYMBOL => TOKEN_SYMBOL, ARG_DECIMALS => TOKEN_DECIMALS, ARG_TOTAL_SUPPLY => U256::from(TOKEN_TOTAL_SUPPLY), - ARG_EVENTS_MODE => EventsMode::Native as u8 + ARG_EVENTS_MODE => EventsMode::Native as u8, }) } pub(crate) fn setup_with_args(install_args: RuntimeArgs) -> (LmdbWasmTestBuilder, TestContext) { - let mut builder = LmdbWasmTestBuilder::default(); + let chainspec = + ChainspecConfig::default().with_enable_addressable_entity(get_enable_addressable_entity()); + + let mut builder = LmdbWasmTestBuilder::new_temporary_with_config(chainspec.clone()); builder - .run_genesis(create_run_genesis_request(DEFAULT_ACCOUNTS.to_vec())) + .run_genesis(create_run_genesis_request_with_chainspec_config( + DEFAULT_ACCOUNTS.to_vec(), + chainspec, + )) .commit(); let install_request_1 = @@ -229,7 +239,7 @@ pub(crate) fn cep18_check_allowance_of( .and_then(|key| key.into_package_hash()) .expect("should have test contract hash"); - let check_balance_args = runtime_args! { + let check_allowance_args = runtime_args! { ARG_TOKEN_CONTRACT => Key::Hash(cep18_contract_hash.value()), ARG_OWNER => owner, ARG_SPENDER => spender, @@ -239,7 +249,7 @@ pub(crate) fn cep18_check_allowance_of( cep18_test_contract_package, None, ENTRY_POINT_CHECK_ALLOWANCE_OF, - check_balance_args, + check_allowance_args, ) .build(); builder.exec(exec_request).expect_success().commit();