Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 8 additions & 3 deletions contracts/contract/src/allowances.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
28 changes: 7 additions & 21 deletions contracts/contract/src/balances.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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()
}
Expand Down
24 changes: 12 additions & 12 deletions contracts/contract/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
};

Expand Down Expand Up @@ -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::<Key>(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),
Expand All @@ -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::<Key>(ARG_SPENDER).key_as_account_or_package();
let owner: Key = runtime::get_named_arg::<Key>(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),
Expand All @@ -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::<Key>(ARG_SPENDER).key_as_account_or_package();
if spender == caller {
revert(Cep18Error::CannotTargetSelfUser);
}
Expand All @@ -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::<Key>(ARG_SPENDER).key_as_account_or_package();
if spender == caller {
revert(Cep18Error::CannotTargetSelfUser);
}
Expand All @@ -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::<Key>(ARG_SPENDER).key_as_account_or_package();
if spender == caller {
revert(Cep18Error::CannotTargetSelfUser);
}
Expand All @@ -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::<Key>(ARG_RECIPIENT).key_as_account_or_package();
if caller == recipient {
revert(Cep18Error::CannotTargetSelfUser);
}
Expand All @@ -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::<Key>(ARG_RECIPIENT).key_as_account_or_package();
let owner: Key = runtime::get_named_arg::<Key>(ARG_OWNER).key_as_account_or_package();
if owner == recipient {
revert(Cep18Error::CannotTargetSelfUser);
}
Expand Down Expand Up @@ -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::<Key>(ARG_OWNER).key_as_account_or_package();
let amount: U256 = runtime::get_named_arg(ARG_AMOUNT);

let new_balance = {
Expand Down Expand Up @@ -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::<Key>(ARG_OWNER).key_as_account_or_package();
let caller = get_immediate_caller();
if owner != caller {
revert(Cep18Error::InvalidBurnTarget);
Expand Down
114 changes: 105 additions & 9 deletions contracts/contract/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,41 +18,120 @@ 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()
.to_t::<Option<AccountHash>>()
.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::<Option<ContractPackageHash>>()
.to_t::<Option<PackageHash>>()
.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::<Option<EntityAddr>>()
.to_t::<Option<ContractPackageHash>>()
.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)
}
}

Expand Down Expand Up @@ -126,7 +205,24 @@ pub fn get_optional_named_arg_with_user_errors<T: FromBytes>(
}
}

pub fn make_dictionary_item_key<T: CLTyped + ToBytes, V: CLTyped + ToBytes>(
/// 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<T: CLTyped + ToBytes, V: CLTyped + ToBytes>(
key: &T,
value: &V,
) -> String {
Expand Down
3 changes: 3 additions & 0 deletions tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
8 changes: 4 additions & 4 deletions tests/src/migration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand All @@ -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
Expand Down
Loading
Loading