diff --git a/plt/plt-scheduler/src/block_state.rs b/plt/plt-scheduler/src/block_state.rs index 174218117..bf7487161 100644 --- a/plt/plt-scheduler/src/block_state.rs +++ b/plt/plt-scheduler/src/block_state.rs @@ -29,14 +29,14 @@ impl BlockStateOperations for BlockState { fn get_token_circulating_supply( &self, _token_index: crate::TokenIndex, - ) -> plt_token_module::host_interface::TokenRawAmount { + ) -> plt_token_module::token_kernel_interface::RawTokenAmount { todo!() } fn set_token_circulating_supply( &mut self, _token_index: crate::TokenIndex, - _circulating_supply: plt_token_module::host_interface::TokenRawAmount, + _circulating_supply: plt_token_module::token_kernel_interface::RawTokenAmount, ) { todo!() } diff --git a/plt/plt-scheduler/src/lib.rs b/plt/plt-scheduler/src/lib.rs index acc6a383d..eb5e24222 100644 --- a/plt/plt-scheduler/src/lib.rs +++ b/plt/plt-scheduler/src/lib.rs @@ -1,7 +1,7 @@ use concordium_base::base::{AccountIndex, Energy}; use concordium_base::id::types::AccountAddress; use concordium_base::protocol_level_tokens::TokenId; -use plt_token_module::host_interface::TokenRawAmount; +use plt_token_module::token_kernel_interface::RawTokenAmount; mod block_state; #[cfg(feature = "ffi")] @@ -73,7 +73,7 @@ pub trait BlockStateOperations { /// # Panics /// /// Panics if the token identified by `token_index` does not exist. - fn get_token_circulating_supply(&self, token_index: TokenIndex) -> TokenRawAmount; + fn get_token_circulating_supply(&self, token_index: TokenIndex) -> RawTokenAmount; /// Set the recorded total circulating supply for a protocol-level token. /// @@ -90,7 +90,7 @@ pub trait BlockStateOperations { fn set_token_circulating_supply( &mut self, token_index: TokenIndex, - circulating_supply: TokenRawAmount, + circulating_supply: RawTokenAmount, ); /// Create a new token with the given configuration. The initial state will be empty diff --git a/plt/plt-token-module/src/lib.rs b/plt/plt-token-module/src/lib.rs index 3a02c83bf..2f56aa53a 100644 --- a/plt/plt-token-module/src/lib.rs +++ b/plt/plt-token-module/src/lib.rs @@ -1,2 +1,2 @@ -pub mod host_interface; +pub mod token_kernel_interface; pub mod token_module; diff --git a/plt/plt-token-module/src/host_interface.rs b/plt/plt-token-module/src/token_kernel_interface.rs similarity index 74% rename from plt/plt-token-module/src/host_interface.rs rename to plt/plt-token-module/src/token_kernel_interface.rs index 2d850c70a..b575e5f42 100644 --- a/plt/plt-token-module/src/host_interface.rs +++ b/plt/plt-token-module/src/token_kernel_interface.rs @@ -1,15 +1,19 @@ -//! Host interface for protocol-level tokens. +//! Token kernel interface for protocol-level tokens. The kernel handles all operations affecting token +//! balance and supply and manages the state and events related to balances and supply. + use concordium_base::base::{AccountIndex, Energy}; use concordium_base::contracts_common::AccountAddress; -use concordium_base::protocol_level_tokens::RawCbor; +use concordium_base::protocol_level_tokens::TokenModuleEventType; use concordium_base::transactions::Memo; pub type StateKey = Vec; pub type StateValue = Vec; -pub type TokenEventType = String; -pub type TokenEventDetails = RawCbor; -pub type Parameter = RawCbor; -pub type TokenRawAmount = u64; + +/// Token amount without decimals specified. The token amount represented by +/// this type must always be represented with the number of decimals +/// the token natively has. +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Ord, PartialOrd, Default)] +pub struct RawTokenAmount(pub u64); /// The account has insufficient balance. #[derive(Debug)] @@ -25,10 +29,13 @@ pub struct LockedStateKeyError; #[error("Amount not representable")] pub struct AmountNotRepresentableError; -/// Operations provided by the deployment unit host. -/// -/// This is abstracted in a trait to allow for a testing stub. -pub trait HostOperations { +/// Energy limit for execution reached. +#[derive(Debug, thiserror::Error)] +#[error("Out of energy")] +pub struct OutOfEnergyError; + +/// Queries provided by the token kernel. +pub trait TokenKernelQueries { /// The type for the account object. /// /// The account is guaranteed to exist on chain, when holding an instance of this type. @@ -48,8 +55,20 @@ pub trait HostOperations { fn account_canonical_address(&self, account: &Self::Account) -> AccountAddress; /// Get the token balance of the account. - fn account_balance(&self, account: &Self::Account) -> TokenRawAmount; + fn account_balance(&self, account: &Self::Account) -> RawTokenAmount; + + /// The current token circulation supply. + fn circulating_supply(&self) -> RawTokenAmount; + /// The number of decimals used in the presentation of the token amount. + fn decimals(&self) -> u8; + + /// Lookup a key in the token state. + fn get_token_state(&self, key: StateKey) -> Option; +} + +/// Operations provided by the token kernel. +pub trait TokenKernelOperations: TokenKernelQueries { /// Update the balance of the given account to zero if it didn't have a balance before. /// /// Returns `true` if the balance wasn't present on the given account and `false` otherwise. @@ -67,7 +86,7 @@ pub trait HostOperations { fn mint( &mut self, account: &Self::Account, - amount: TokenRawAmount, + amount: RawTokenAmount, ) -> Result<(), AmountNotRepresentableError>; /// Burn a specified amount from the account. @@ -82,7 +101,7 @@ pub trait HostOperations { fn burn( &mut self, account: &Self::Account, - amount: TokenRawAmount, + amount: RawTokenAmount, ) -> Result<(), InsufficientBalanceError>; /// Transfer a token amount from one account to another, with an optional memo. @@ -98,19 +117,10 @@ pub trait HostOperations { &mut self, from: &Self::Account, to: &Self::Account, - amount: TokenRawAmount, + amount: RawTokenAmount, memo: Option, ) -> Result<(), InsufficientBalanceError>; - /// The current token circulation supply. - fn circulating_supply(&self) -> TokenRawAmount; - - /// The number of decimals used in the presentation of the token amount. - fn decimals(&self) -> u8; - - /// Lookup a key in the token state. - fn get_token_state(&self, key: StateKey) -> Option; - /// Set or clear a value in the token state at the corresponding key. /// /// Returns whether there was an existing entry. @@ -126,15 +136,16 @@ pub trait HostOperations { /// Reduce the available energy for the PLT module execution. /// - /// If the available energy is smaller than the given amount, the containing transaction will - /// abort and the effects of the transaction will be rolled back. + /// If the available energy is smaller than the given amount, an + /// "out of energy" error will be returned, in which case the caller + /// should stop execution and propagate the error upwards. /// The energy is charged in any case (also in case of failure). - fn tick_energy(&mut self, energy: Energy); + fn tick_energy(&mut self, energy: Energy) -> Result<(), OutOfEnergyError>; /// Log a token module event with the specified type and details. /// /// # Events /// /// This will produce a `TokenModuleEvent` in the logs. - fn log_token_event(&mut self, event_type: TokenEventType, event_details: TokenEventDetails); + fn log_token_event(&mut self, event: TokenModuleEventType); } diff --git a/plt/plt-token-module/src/token_module.rs b/plt/plt-token-module/src/token_module.rs index 61fe41445..3cfb39141 100644 --- a/plt/plt-token-module/src/token_module.rs +++ b/plt/plt-token-module/src/token_module.rs @@ -1,17 +1,17 @@ //! Implementation of the protocol-level token module. -use crate::host_interface::*; -use concordium_base::common::cbor::{ - CborSerializationError, SerializationOptions, UnknownMapKeys, cbor_decode_with_options, - cbor_encode, -}; +//! + +use crate::token_kernel_interface::*; +use concordium_base::common::cbor; +use concordium_base::common::cbor::{SerializationOptions, UnknownMapKeys, cbor_encode}; use concordium_base::contracts_common::AccountAddress; use concordium_base::protocol_level_tokens::{ RawCbor, TokenAmount, TokenModuleInitializationParameters, }; -/// Extension trait for `HostOperations` to provide convenience wrappers for +/// Extension trait for `TokenKernelOperations` to provide convenience wrappers for /// module state access and updating. -trait HostOperationsExt: HostOperations { +trait KernelOperationsExt: TokenKernelOperations { /// Set or clear a value in the token module state at the corresponding key. fn set_module_state<'a>( &mut self, @@ -23,7 +23,7 @@ trait HostOperationsExt: HostOperations { } } -impl HostOperationsExt for T {} +impl KernelOperationsExt for T {} /// Little-endian prefix used to distinguish module state keys. const MODULE_STATE_PREFIX: [u8; 2] = 0u16.to_le_bytes(); @@ -40,29 +40,26 @@ fn module_state_key<'a>(key: impl IntoIterator) -> StateKey { /// Represents the reasons why [`initialize_token`] can fail. #[derive(Debug, thiserror::Error)] -pub enum InitError { - #[error("Token initialization parameters could not be deserialized: {0}")] - DeserializationFailure(String), +pub enum TokenInitializationError { + #[error("Invalid token initialization parameters: {0}")] + InvalidInitializationParameters(String), #[error("{0}")] LockedStateKey(#[from] LockedStateKeyError), #[error("The given governance account does not exist: {0}")] GovernanceAccountDoesNotExist(AccountAddress), - #[error("The initial mint amount was not valid: {0}")] - InvalidMintAmount(String), -} - -impl From for InitError { - fn from(value: CborSerializationError) -> Self { - Self::DeserializationFailure(value.to_string()) - } + #[error("The initial mint amount has wrong number of decimals: {0}")] + MintAmountDecimalsMismatch(#[from] TokenAmountDecimalsMismatchError), + #[error("The initial mint amount is not representable: {0}")] + MintAmountNotRepresentable(#[from] AmountNotRepresentableError), } /// Represents the reasons why [`execute_token_update_transaction`] can fail. #[derive(Debug, thiserror::Error)] -pub enum UpdateError {} +pub enum TokenUpdateError {} + /// Represents the reasons why a query to the token module can fail. #[derive(Debug)] -pub enum QueryError {} +pub enum TokenQueryError {} /// The context for a token-holder or token-governance transaction. #[derive(Debug)] @@ -74,23 +71,26 @@ pub struct TransactionContext { } #[derive(Debug, thiserror::Error)] -pub enum TokenAmountError { - #[error("Token amount decimals mismatch: expected {expected}, found {found}")] - DecimalsMismatch { expected: u8, found: u8 }, +#[error("Token amount decimals mismatch: expected {expected}, found {found}")] +pub struct TokenAmountDecimalsMismatchError { + pub expected: u8, + pub found: u8, } +/// Asserts that token amount has the right number of decimals and converts it to a plain +/// integer. fn to_token_raw_amount( amount: TokenAmount, - actual_decimals: u8, -) -> Result { + expected_decimals: u8, +) -> Result { let decimals = amount.decimals(); - if decimals != actual_decimals { - return Err(TokenAmountError::DecimalsMismatch { - expected: actual_decimals, + if decimals != expected_decimals { + return Err(TokenAmountDecimalsMismatchError { + expected: expected_decimals, found: decimals, }); } - Ok(amount.value()) + Ok(RawTokenAmount(amount.value())) } const STATE_KEY_NAME: &[u8] = b"name"; @@ -104,79 +104,91 @@ const STATE_KEY_GOVERNANCE_ACCOUNT: &[u8] = b"governanceAccount"; /// Initialize a PLT by recording the relevant configuration parameters in the state and /// (if necessary) minting the initial supply to the token governance account. pub fn initialize_token( - host: &mut impl HostOperations, - token_parameter: Parameter, -) -> Result<(), InitError> { + host: &mut impl TokenKernelOperations, + initialization_parameters_cbor: RawCbor, +) -> Result<(), TokenInitializationError> { let decode_options = SerializationOptions { unknown_map_keys: UnknownMapKeys::Fail, }; - let parameter: TokenModuleInitializationParameters = - cbor_decode_with_options(token_parameter, decode_options)?; - if !parameter.additional.is_empty() { - return Err(InitError::DeserializationFailure(format!( - "Unknown additional parameters: {}", - parameter - .additional - .keys() - .map(|k| k.as_str()) - .collect::>() - .join(", ") - ))); + let init_params: TokenModuleInitializationParameters = + cbor::cbor_decode_with_options(initialization_parameters_cbor, decode_options).map_err( + |err| { + TokenInitializationError::InvalidInitializationParameters(format!( + "Error decoding token initialization parameters: {}", + err + )) + }, + )?; + if !init_params.additional.is_empty() { + return Err(TokenInitializationError::InvalidInitializationParameters( + format!( + "Unknown additional parameters: {}", + init_params + .additional + .keys() + .map(|k| k.as_str()) + .collect::>() + .join(", ") + ), + )); } - let Some(name) = parameter.name else { - return Err(InitError::DeserializationFailure( + let name = init_params.name.ok_or_else(|| { + TokenInitializationError::InvalidInitializationParameters( "Token name is missing".to_string(), - )); - }; - let Some(metadata) = parameter.metadata else { - return Err(InitError::DeserializationFailure( + ) + })?; + let metadata = init_params.metadata.ok_or_else(|| { + TokenInitializationError::InvalidInitializationParameters( "Token metadata is missing".to_string(), - )); - }; - let Some(governance_account) = parameter.governance_account else { - return Err(InitError::DeserializationFailure( + ) + })?; + let governance_account = init_params.governance_account.ok_or_else(|| { + TokenInitializationError::InvalidInitializationParameters( "Token governance account is missing".to_string(), - )); - }; + ) + })?; host.set_module_state(STATE_KEY_NAME, Some(name.into()))?; - let encoded_metadata = cbor_encode(&metadata)?; + let encoded_metadata = cbor_encode(&metadata).map_err(|err| { + TokenInitializationError::InvalidInitializationParameters(format!( + "Error encoding token metadata: {}", + err + )) + })?; host.set_module_state(STATE_KEY_METADATA, Some(encoded_metadata))?; - if let Some(true) = parameter.allow_list { + if init_params.allow_list == Some(true) { host.set_module_state(STATE_KEY_ALLOW_LIST, Some(vec![]))?; } - if let Some(true) = parameter.deny_list { + if init_params.deny_list == Some(true) { host.set_module_state(STATE_KEY_DENY_LIST, Some(vec![]))?; } - if let Some(true) = parameter.mintable { + if init_params.mintable == Some(true) { host.set_module_state(STATE_KEY_MINTABLE, Some(vec![]))?; } - if let Some(true) = parameter.burnable { + if init_params.burnable == Some(true) { host.set_module_state(STATE_KEY_BURNABLE, Some(vec![]))?; } - let Some(governance_account) = host.account_by_address(&governance_account.address) else { - return Err(InitError::GovernanceAccountDoesNotExist( - governance_account.address, - )); - }; + + let governance_account = host.account_by_address(&governance_account.address).ok_or( + TokenInitializationError::GovernanceAccountDoesNotExist(governance_account.address), + )?; let governance_account_index = host.account_index(&governance_account); host.set_module_state( STATE_KEY_GOVERNANCE_ACCOUNT, Some(governance_account_index.index.to_be_bytes().to_vec()), )?; - if let Some(initial_supply) = parameter.initial_supply { - let mint_amount = to_token_raw_amount(initial_supply, host.decimals()) - .map_err(|e| InitError::InvalidMintAmount(e.to_string()))?; + if let Some(initial_supply) = init_params.initial_supply { + let mint_amount = to_token_raw_amount(initial_supply, host.decimals())?; host.mint(&governance_account, mint_amount) - .map_err(|_| InitError::InvalidMintAmount("Kernel failed to mint".to_string()))?; + .map_err(TokenInitializationError::MintAmountNotRepresentable)?; } Ok(()) } -/// Execute a token update transaction using the [`HostOperations`] implementation on `host` to +/// Execute a token update transaction using the [`TokenKernelOperations`] implementation on `host` to /// update state and produce events. /// /// When resulting in an `Err` signals a rejected operation and all of the calls to -/// [`HostOperations`] must be rolled back y the caller. +/// [`TokenKernelOperations`] must be rolled back y the caller. /// /// The process is as follows: /// @@ -217,29 +229,25 @@ pub fn initialize_token( /// # INVARIANTS: /// /// - Token module state contains a correctly encoded governance account address. -pub fn execute_token_update_transaction( - _host: &mut Host, - _context: TransactionContext, - _token_parameter: Parameter, -) -> Result<(), UpdateError> -where - Host: HostOperations, -{ +pub fn execute_token_update_transaction( + _kernel: &mut Kernel, + _context: TransactionContext, + _token_operations: RawCbor, +) -> Result<(), TokenUpdateError> { todo!() } /// Get the CBOR-encoded representation of the token module state. -pub fn query_token_module_state(_host: &impl HostOperations) -> Result { +pub fn query_token_module_state( + _kernel: &impl TokenKernelQueries, +) -> Result { todo!() } /// Get the CBOR-encoded representation of the token module account state. -pub fn query_account_state( - _host: &Host, - _account: Host::Account, -) -> Result, QueryError> -where - Host: HostOperations, -{ +pub fn query_account_state( + _kernel: &impl TokenKernelQueries, + _account: Kernel::Account, +) -> Result, TokenQueryError> { todo!() } diff --git a/plt/plt-token-module/tests/host_stub.rs b/plt/plt-token-module/tests/kernel_stub.rs similarity index 50% rename from plt/plt-token-module/tests/host_stub.rs rename to plt/plt-token-module/tests/kernel_stub.rs index 4123b6897..753fbb508 100644 --- a/plt/plt-token-module/tests/host_stub.rs +++ b/plt/plt-token-module/tests/kernel_stub.rs @@ -2,25 +2,27 @@ use std::collections::HashMap; use concordium_base::base::{AccountIndex, Energy}; use concordium_base::contracts_common::AccountAddress; +use concordium_base::protocol_level_tokens::TokenModuleEventType; use concordium_base::transactions::Memo; -use plt_token_module::host_interface::{ - AmountNotRepresentableError, HostOperations, InsufficientBalanceError, LockedStateKeyError, - StateKey, StateValue, TokenEventDetails, TokenEventType, +use plt_token_module::token_kernel_interface::{ + AmountNotRepresentableError, InsufficientBalanceError, LockedStateKeyError, RawTokenAmount, + StateKey, StateValue, TokenKernelOperations, TokenKernelQueries, }; -/// The deployment host stub providing an implementation of [`HostOperations`] and methods for +/// Token kernel stub providing an implementation of [`TokenKernelOperations`] and methods for /// configuring the state of the host. -#[derive(Debug, Clone, PartialEq, Eq, Default)] -pub struct HostStub { +#[derive(Debug)] +pub struct KernelStub { /// List of accounts existing. - pub accounts: Vec, + accounts: Vec, /// Token managed state. pub state: HashMap, /// Decimal places in token representation. - pub decimals: u8, + decimals: u8, + next_account_index: AccountIndex, } -/// Internal representation of an Account in [`HostStub`]. +/// Internal representation of an Account in [`KernelStub`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Account { /// The index of the account @@ -28,48 +30,63 @@ pub struct Account { /// The canonical account address of the account. pub address: AccountAddress, /// The token balance of the account. - pub balance: Option, + pub balance: Option, } -impl HostStub { - /// Construct a new `HostStub` with a number of accounts. +impl KernelStub { + /// Create + pub fn new(decimals: u8) -> Self { + Self { + accounts: vec![], + state: Default::default(), + decimals, + next_account_index: AccountIndex { index: 0 }, + } + } + + /// Create an account in the stub. /// /// # Example /// /// ``` - /// let account_address0 = [0u8; 32]; - /// let account_address1 = [1u8; 32]; - /// let host = HostStub::with_accounts([(0, account_address0, None), (1, account_address1, Some(42))]); + /// let mut stub = KernelStub::new(0); + /// let account = stub.create_account(); /// assert!(host.account_by_address(account_address1).is_some(), "Account must exist"); /// ``` - pub fn with_accounts( - accounts: impl IntoIterator)>, - ) -> Self { - let accounts = accounts - .into_iter() - .map(|(index, address, balance)| Account { - index, - address, - balance, - }) - .collect(); + pub fn create_account(&mut self) -> AccountStubIndex { + let index = self.next_account_index; + let mut address = AccountAddress([0u8; 32]); + address.0[..8].copy_from_slice(&index.index.to_be_bytes()); + let account = Account { + index, + address, + balance: None, + }; + let stub_index = AccountStubIndex(self.accounts.len()); + self.accounts.push(account); - Self { - accounts, - state: HashMap::new(), - decimals: 0, - } + self.next_account_index.index += 1; + + stub_index + } + + /// Set account balance in the stub + pub fn set_account_balance(&mut self, account: AccountStubIndex, balance: RawTokenAmount) { + self.accounts + .get_mut(account.0) + .expect("account in stub") + .balance = Some(balance); } } /// Host stub account object. /// -/// When testing it is the index into the list of accounts tracked by the `HostStub`. +/// When testing it is the index into the list of accounts tracked by the `KernelStub`. /// Holding #[derive(Debug, Clone, Copy)] pub struct AccountStubIndex(usize); -impl HostOperations for HostStub { +impl TokenKernelQueries for KernelStub { type Account = AccountStubIndex; fn account_by_address(&self, address: &AccountAddress) -> Option { @@ -101,15 +118,29 @@ impl HostOperations for HostStub { self.accounts[account.0].address } - fn account_balance(&self, account: &Self::Account) -> u64 { - self.accounts[account.0].balance.unwrap_or(0) + fn account_balance(&self, account: &Self::Account) -> RawTokenAmount { + self.accounts[account.0].balance.unwrap_or_default() + } + + fn circulating_supply(&self) -> RawTokenAmount { + todo!() + } + + fn decimals(&self) -> u8 { + self.decimals + } + + fn get_token_state(&self, key: StateKey) -> Option { + self.state.get(&key).cloned() } +} +impl TokenKernelOperations for KernelStub { fn touch(&mut self, account: &Self::Account) -> bool { if self.accounts[account.0].balance.is_some() { false } else { - self.accounts[account.0].balance = Some(0); + self.accounts[account.0].balance = Some(RawTokenAmount::default()); true } } @@ -117,13 +148,13 @@ impl HostOperations for HostStub { fn mint( &mut self, account: &Self::Account, - amount: u64, + amount: RawTokenAmount, ) -> Result<(), AmountNotRepresentableError> { if let Some(balance) = self.accounts[account.0].balance { - if balance > u64::MAX - amount { + if balance > RawTokenAmount(u64::MAX - amount.0) { Err(AmountNotRepresentableError) } else { - self.accounts[account.0].balance = Some(balance + amount); + self.accounts[account.0].balance = Some(RawTokenAmount(balance.0 + amount.0)); Ok(()) } } else { @@ -135,7 +166,7 @@ impl HostOperations for HostStub { fn burn( &mut self, _account: &Self::Account, - _amount: u64, + _amount: RawTokenAmount, ) -> Result<(), InsufficientBalanceError> { todo!() } @@ -144,24 +175,12 @@ impl HostOperations for HostStub { &mut self, _from: &Self::Account, _to: &Self::Account, - _amount: u64, + _amount: RawTokenAmount, _memo: Option, ) -> Result<(), InsufficientBalanceError> { todo!() } - fn circulating_supply(&self) -> u64 { - todo!() - } - - fn decimals(&self) -> u8 { - self.decimals - } - - fn get_token_state(&self, key: StateKey) -> Option { - self.state.get(&key).cloned() - } - fn set_token_state( &mut self, key: StateKey, @@ -178,88 +197,61 @@ impl HostOperations for HostStub { todo!() } - fn log_token_event(&mut self, _event_type: TokenEventType, _event_details: TokenEventDetails) { + fn log_token_event(&mut self, _event: TokenModuleEventType) { todo!() } } -// Tests for the HostStub +// Tests for the kernel stub -pub const TEST_ACCOUNT0: AccountAddress = AccountAddress([0u8; 32]); -pub const TEST_ACCOUNT1: AccountAddress = AccountAddress([1u8; 32]); -pub const TEST_ACCOUNT2: AccountAddress = AccountAddress([2u8; 32]); +const TEST_ACCOUNT2: AccountAddress = AccountAddress([2u8; 32]); +/// Test lookup account address and account from address #[test] -fn test_account_lookup() { - let host = HostStub::with_accounts([ - (0.into(), TEST_ACCOUNT0, None), - (1.into(), TEST_ACCOUNT1, None), - ]); - - let _ = host - .account_by_address(&TEST_ACCOUNT0) - .expect("Account is expected to exist"); - let _ = host - .account_by_address(&TEST_ACCOUNT1) +fn test_account_lookup_address() { + let mut stub = KernelStub::new(0); + let account = stub.create_account(); + + let address = stub.account_canonical_address(&account); + stub.account_by_address(&address) .expect("Account is expected to exist"); assert!( - host.account_by_address(&TEST_ACCOUNT2).is_none(), + stub.account_by_address(&TEST_ACCOUNT2).is_none(), "Account is not expected to exist" ); - // TODO test lookup using alias. +} - let _ = host - .account_by_index(0.into()) - .expect("Account is expected to exist"); - let _ = host - .account_by_index(1.into()) +/// Test lookup account index and account from index +#[test] +fn test_account_lookup_index() { + let mut stub = KernelStub::new(0); + let account = stub.create_account(); + + let index = stub.account_index(&account); + stub.account_by_index(index) .expect("Account is expected to exist"); assert!( - host.account_by_index(2.into()).is_none(), + stub.account_by_index(2.into()).is_none(), "Account is not expected to exist" ); } +/// Test get account balance #[test] fn test_account_balance() { - let host = HostStub::with_accounts([ - (0.into(), TEST_ACCOUNT0, Some(245)), - (1.into(), TEST_ACCOUNT1, None), - ]); - { - let account = host - .account_by_address(&TEST_ACCOUNT0) - .expect("Account is expected to exist"); - let balance = host.account_balance(&account); - assert_eq!(balance, 245); - } - { - let account = host - .account_by_address(&TEST_ACCOUNT1) - .expect("Account is expected to exist"); - let balance = host.account_balance(&account); - assert_eq!(balance, 0); - } + let mut stub = KernelStub::new(0); + let account0 = stub.create_account(); + let account1 = stub.create_account(); + stub.set_account_balance(account0, RawTokenAmount(245)); + + let balance = stub.account_balance(&account0); + assert_eq!(balance, RawTokenAmount(245)); + + let balance = stub.account_balance(&account1); + assert_eq!(balance, RawTokenAmount(0)); } #[test] -fn test_account_canonical_address() { - let host = HostStub::with_accounts([ - (0.into(), TEST_ACCOUNT0, Some(245)), - (1.into(), TEST_ACCOUNT1, None), - ]); - { - let account = host - .account_by_address(&TEST_ACCOUNT0) - .expect("Account is expected to exist"); - let balance = host.account_balance(&account); - assert_eq!(balance, 245); - } - { - let account = host - .account_by_address(&TEST_ACCOUNT1) - .expect("Account is expected to exist"); - let balance = host.account_balance(&account); - assert_eq!(balance, 0); - } +fn test_account_lookup_canonical_address() { + // TODO test lookup using alias. } diff --git a/plt/plt-token-module/tests/token_module.rs b/plt/plt-token-module/tests/token_module.rs deleted file mode 100644 index 43fff6564..000000000 --- a/plt/plt-token-module/tests/token_module.rs +++ /dev/null @@ -1,246 +0,0 @@ -use std::collections::HashMap; - -use assert_matches::assert_matches; -use concordium_base::{ - common::cbor::{cbor_encode, value::Value}, - protocol_level_tokens::{TokenAmount, TokenModuleInitializationParameters}, -}; -use host_stub::{HostStub, TEST_ACCOUNT0, TEST_ACCOUNT1}; -use plt_token_module::token_module::{self, InitError}; - -mod host_stub; - -/// In this example, the parameters are not a valid encoding. -#[test] -fn test_initialize_token_parameters_decode_failiure() { - let mut host = HostStub::with_accounts([ - (0.into(), TEST_ACCOUNT0, None), - (1.into(), TEST_ACCOUNT1, None), - ]); - let res = token_module::initialize_token(&mut host, vec![].into()); - assert_matches!( - res, - Err(InitError::DeserializationFailure(ref e)) - if e == "IO error: failed to fill whole buffer" - ); -} - -/// In this example, a parameter is missing from the required initialization parameters. -#[test] -fn test_initialize_token_parameters_missing() { - let mut host = HostStub::with_accounts([ - (0.into(), TEST_ACCOUNT0, None), - (1.into(), TEST_ACCOUNT1, None), - ]); - let parameters = TokenModuleInitializationParameters { - name: None, - metadata: Some("https://plt.token".to_owned().into()), - governance_account: Some(TEST_ACCOUNT1.into()), - allow_list: Some(true), - deny_list: Some(false), - initial_supply: None, - mintable: Some(true), - burnable: Some(true), - additional: Default::default(), - }; - let encoded_parameters = cbor_encode(¶meters).unwrap().into(); - let res = token_module::initialize_token(&mut host, encoded_parameters); - assert_matches!(res, - Err(InitError::DeserializationFailure(e)) - if e == "Token name is missing" - ); -} - -/// In this example, an unsupported additional parameter is present in the -/// initialization parameters. -#[test] -fn test_initiailize_token_additional_parameter() { - let mut host = HostStub::with_accounts([ - (0.into(), TEST_ACCOUNT0, None), - (1.into(), TEST_ACCOUNT1, None), - ]); - let mut additional = HashMap::with_capacity(1); - additional.insert("_param1".into(), Value::Text("extravalue1".into())); - let parameters = TokenModuleInitializationParameters { - name: Some("Protocol-level token".to_owned()), - metadata: Some("https://plt.token".to_owned().into()), - governance_account: Some(TEST_ACCOUNT1.into()), - allow_list: Some(true), - deny_list: Some(false), - initial_supply: None, - mintable: Some(true), - burnable: Some(true), - additional, - }; - let encoded_parameters = cbor_encode(¶meters).unwrap().into(); - let res = token_module::initialize_token(&mut host, encoded_parameters); - assert_matches!( - res, - Err(InitError::DeserializationFailure(e)) - if e == "Unknown additional parameters: _param1" - ); -} - -/// In this example, minimal parameters are specified to check defaulting -/// behaviour. -#[test] -fn test_initiailize_token_default_values() { - let mut host = HostStub::with_accounts([ - (0.into(), TEST_ACCOUNT0, None), - (1.into(), TEST_ACCOUNT1, None), - ]); - let init_accounts = host.accounts.clone(); - let metadata = "https://plt.token".to_owned().into(); - let encoded_metadata = cbor_encode(&metadata).unwrap(); - let parameters = TokenModuleInitializationParameters { - name: Some("Protocol-level token".to_owned()), - metadata: Some(metadata), - governance_account: Some(TEST_ACCOUNT1.into()), - allow_list: None, - deny_list: None, - initial_supply: None, - mintable: None, - burnable: None, - additional: Default::default(), - }; - let encoded_parameters = cbor_encode(¶meters).unwrap().into(); - token_module::initialize_token(&mut host, encoded_parameters).unwrap(); - assert_eq!(host.accounts, init_accounts); - let mut expected_state = HashMap::with_capacity(3); - expected_state.insert(b"\0\0name".into(), b"Protocol-level token".into()); - expected_state.insert(b"\0\0metadata".into(), encoded_metadata); - expected_state.insert(b"\0\0governanceAccount".into(), 1u64.to_be_bytes().into()); - assert_eq!(host.state, expected_state); -} - -/// In this example, the parameters are valid, no minting. -#[test] -fn test_initiailize_token_valid_1() { - let mut host = HostStub::with_accounts([ - (0.into(), TEST_ACCOUNT0, None), - (1.into(), TEST_ACCOUNT1, None), - ]); - let init_accounts = host.accounts.clone(); - let metadata = "https://plt.token".to_owned().into(); - let encoded_metadata = cbor_encode(&metadata).unwrap(); - let parameters = TokenModuleInitializationParameters { - name: Some("Protocol-level token".to_owned()), - metadata: Some(metadata), - governance_account: Some(TEST_ACCOUNT1.into()), - allow_list: Some(true), - deny_list: Some(false), - initial_supply: None, - mintable: Some(true), - burnable: Some(true), - additional: Default::default(), - }; - let encoded_parameters = cbor_encode(¶meters).unwrap().into(); - token_module::initialize_token(&mut host, encoded_parameters).unwrap(); - assert_eq!(host.accounts, init_accounts); - let mut expected_state = HashMap::with_capacity(3); - expected_state.insert(b"\0\0name".into(), b"Protocol-level token".into()); - expected_state.insert(b"\0\0metadata".into(), encoded_metadata); - expected_state.insert(b"\0\0governanceAccount".into(), 1u64.to_be_bytes().into()); - expected_state.insert(b"\0\0allowList".into(), vec![]); - expected_state.insert(b"\0\0mintable".into(), vec![]); - expected_state.insert(b"\0\0burnable".into(), vec![]); - assert_eq!(host.state, expected_state); -} - -/// In this example, the parameters are valid, with minting. -#[test] -fn test_initiailize_token_valid_2() { - let mut host = HostStub::with_accounts([ - (0.into(), TEST_ACCOUNT0, None), - (1.into(), TEST_ACCOUNT1, None), - ]); - host.decimals = 2; - let mut accounts = host.accounts.clone(); - let metadata = "https://plt.token".to_owned().into(); - let encoded_metadata = cbor_encode(&metadata).unwrap(); - let parameters = TokenModuleInitializationParameters { - name: Some("Protocol-level token".to_owned()), - metadata: Some(metadata), - governance_account: Some(TEST_ACCOUNT1.into()), - allow_list: Some(false), - deny_list: Some(true), - initial_supply: Some(TokenAmount::from_raw(500000, 2)), - mintable: Some(false), - burnable: Some(false), - additional: Default::default(), - }; - let encoded_parameters = cbor_encode(¶meters).unwrap().into(); - token_module::initialize_token(&mut host, encoded_parameters).unwrap(); - for account in accounts.iter_mut() { - if account.index == 1.into() { - account.balance = Some(500000); - } - } - assert_eq!(host.accounts, accounts); - let mut expected_state = HashMap::with_capacity(3); - expected_state.insert(b"\0\0name".into(), b"Protocol-level token".into()); - expected_state.insert(b"\0\0metadata".into(), encoded_metadata); - expected_state.insert(b"\0\0governanceAccount".into(), 1u64.to_be_bytes().into()); - expected_state.insert(b"\0\0denyList".into(), vec![]); - assert_eq!(host.state, expected_state); -} - -/// In this example, the parameters specify an initial supply with higher precision -/// than the token allows. -#[test] -fn test_initiailize_token_excessive_mint_decimals() { - let mut host = HostStub::with_accounts([ - (0.into(), TEST_ACCOUNT0, None), - (1.into(), TEST_ACCOUNT1, None), - ]); - host.decimals = 2; - let metadata = "https://plt.token".to_owned().into(); - let parameters = TokenModuleInitializationParameters { - name: Some("Protocol-level token".to_owned()), - metadata: Some(metadata), - governance_account: Some(TEST_ACCOUNT1.into()), - allow_list: Some(false), - deny_list: Some(false), - initial_supply: Some(TokenAmount::from_raw(500000, 6)), - mintable: Some(false), - burnable: Some(false), - additional: Default::default(), - }; - let encoded_parameters = cbor_encode(¶meters).unwrap().into(); - let res = token_module::initialize_token(&mut host, encoded_parameters); - assert_matches!( - res, - Err(InitError::InvalidMintAmount(e)) - if e == "Token amount decimals mismatch: expected 2, found 6" - ); -} - -/// In this example, the parameters specify an initial supply with less precision -/// than the token allows. -#[test] -fn test_initiailize_token_insufficient_mint_decimals() { - let mut host = HostStub::with_accounts([ - (0.into(), TEST_ACCOUNT0, None), - (1.into(), TEST_ACCOUNT1, None), - ]); - host.decimals = 6; - let metadata = "https://plt.token".to_owned().into(); - let parameters = TokenModuleInitializationParameters { - name: Some("Protocol-level token".to_owned()), - metadata: Some(metadata), - governance_account: Some(TEST_ACCOUNT1.into()), - allow_list: Some(false), - deny_list: Some(false), - initial_supply: Some(TokenAmount::from_raw(500000, 2)), - mintable: Some(false), - burnable: Some(false), - additional: Default::default(), - }; - let encoded_parameters = cbor_encode(¶meters).unwrap().into(); - let res = token_module::initialize_token(&mut host, encoded_parameters); - assert_matches!( - res, - Err(InitError::InvalidMintAmount(e)) - if e == "Token amount decimals mismatch: expected 6, found 2" - ); -} diff --git a/plt/plt-token-module/tests/token_module_initialize.rs b/plt/plt-token-module/tests/token_module_initialize.rs new file mode 100644 index 000000000..fc4cf8e7b --- /dev/null +++ b/plt/plt-token-module/tests/token_module_initialize.rs @@ -0,0 +1,268 @@ +use std::collections::HashMap; + +use assert_matches::assert_matches; +use concordium_base::common::cbor; +use concordium_base::contracts_common::AccountAddress; +use concordium_base::{ + common::cbor::value::Value, + protocol_level_tokens::{TokenAmount, TokenModuleInitializationParameters}, +}; +use kernel_stub::KernelStub; +use plt_token_module::token_kernel_interface::{RawTokenAmount, TokenKernelQueries}; +use plt_token_module::token_module::{ + self, TokenAmountDecimalsMismatchError, TokenInitializationError, +}; + +mod kernel_stub; + +const TEST_ACCOUNT2: AccountAddress = AccountAddress([2u8; 32]); + +/// In this example, the parameters are not a valid encoding. +#[test] +fn test_initialize_token_parameters_decode_failure() { + let mut stub = KernelStub::new(0); + let res = token_module::initialize_token(&mut stub, vec![].into()); + assert_matches!( + &res, + Err(TokenInitializationError::InvalidInitializationParameters(err)) + if err.contains("Error decoding token initialization parameters") + ); +} + +/// In this example, a parameter is missing from the required initialization parameters. +#[test] +fn test_initialize_token_parameters_missing() { + let mut stub = KernelStub::new(0); + let gov_account = stub.create_account(); + let parameters = TokenModuleInitializationParameters { + name: None, + metadata: Some("https://plt.token".to_owned().into()), + governance_account: Some(stub.account_canonical_address(&gov_account).into()), + allow_list: Some(true), + deny_list: Some(false), + initial_supply: None, + mintable: Some(true), + burnable: Some(true), + additional: Default::default(), + }; + let encoded_parameters = cbor::cbor_encode(¶meters).unwrap().into(); + let res = token_module::initialize_token(&mut stub, encoded_parameters); + assert_matches!(res, + Err(TokenInitializationError::InvalidInitializationParameters(err)) + if err == "Token name is missing" + ); +} + +/// In this example, an unsupported additional parameter is present in the +/// initialization parameters. +#[test] +fn test_initialize_token_additional_parameter() { + let mut stub = KernelStub::new(0); + let gov_account = stub.create_account(); + let mut additional = HashMap::with_capacity(1); + additional.insert("_param1".into(), Value::Text("extravalue1".into())); + let parameters = TokenModuleInitializationParameters { + name: Some("Protocol-level token".to_owned()), + metadata: Some("https://plt.token".to_owned().into()), + governance_account: Some(stub.account_canonical_address(&gov_account).into()), + allow_list: Some(true), + deny_list: Some(false), + initial_supply: None, + mintable: Some(true), + burnable: Some(true), + additional, + }; + let encoded_parameters = cbor::cbor_encode(¶meters).unwrap().into(); + let res = token_module::initialize_token(&mut stub, encoded_parameters); + assert_matches!( + res, + Err(TokenInitializationError::InvalidInitializationParameters(err)) + if err == "Unknown additional parameters: _param1" + ); +} + +/// In this example, minimal parameters are specified to check defaulting +/// behaviour. +#[test] +fn test_initialize_token_default_values() { + let mut stub = KernelStub::new(0); + let gov_account = stub.create_account(); + let metadata = "https://plt.token".to_owned().into(); + let encoded_metadata = cbor::cbor_encode(&metadata).unwrap(); + let parameters = TokenModuleInitializationParameters { + name: Some("Protocol-level token".to_owned()), + metadata: Some(metadata), + governance_account: Some(stub.account_canonical_address(&gov_account).into()), + allow_list: None, + deny_list: None, + initial_supply: None, + mintable: None, + burnable: None, + additional: Default::default(), + }; + let encoded_parameters = cbor::cbor_encode(¶meters).unwrap().into(); + token_module::initialize_token(&mut stub, encoded_parameters).unwrap(); + let mut expected_state = HashMap::with_capacity(3); + expected_state.insert(b"\0\0name".into(), b"Protocol-level token".into()); + expected_state.insert(b"\0\0metadata".into(), encoded_metadata); + + expected_state.insert( + b"\0\0governanceAccount".into(), + stub.account_index(&gov_account).index.to_be_bytes().into(), + ); + assert_eq!(stub.state, expected_state); +} + +/// In this example, the parameters are valid, no minting. +#[test] +fn test_initialize_token_no_minting() { + let mut stub = KernelStub::new(0); + let gov_account = stub.create_account(); + let metadata = "https://plt.token".to_owned().into(); + let encoded_metadata = cbor::cbor_encode(&metadata).unwrap(); + let parameters = TokenModuleInitializationParameters { + name: Some("Protocol-level token".to_owned()), + metadata: Some(metadata), + governance_account: Some(stub.account_canonical_address(&gov_account).into()), + allow_list: Some(true), + deny_list: Some(false), + initial_supply: None, + mintable: Some(true), + burnable: Some(true), + additional: Default::default(), + }; + let encoded_parameters = cbor::cbor_encode(¶meters).unwrap().into(); + token_module::initialize_token(&mut stub, encoded_parameters).unwrap(); + let mut expected_state = HashMap::with_capacity(3); + expected_state.insert(b"\0\0name".into(), b"Protocol-level token".into()); + expected_state.insert(b"\0\0metadata".into(), encoded_metadata); + expected_state.insert( + b"\0\0governanceAccount".into(), + stub.account_index(&gov_account).index.to_be_bytes().into(), + ); + expected_state.insert(b"\0\0allowList".into(), vec![]); + expected_state.insert(b"\0\0mintable".into(), vec![]); + expected_state.insert(b"\0\0burnable".into(), vec![]); + assert_eq!(stub.state, expected_state); +} + +/// In this example, the parameters are valid, with minting. +#[test] +fn test_initialize_token_valid_2() { + let mut stub = KernelStub::new(2); + let gov_account = stub.create_account(); + let metadata = "https://plt.token".to_owned().into(); + let encoded_metadata = cbor::cbor_encode(&metadata).unwrap(); + let parameters = TokenModuleInitializationParameters { + name: Some("Protocol-level token".to_owned()), + metadata: Some(metadata), + governance_account: Some(stub.account_canonical_address(&gov_account).into()), + allow_list: Some(false), + deny_list: Some(true), + initial_supply: Some(TokenAmount::from_raw(500000, 2)), + mintable: Some(false), + burnable: Some(false), + additional: Default::default(), + }; + let encoded_parameters = cbor::cbor_encode(¶meters).unwrap().into(); + token_module::initialize_token(&mut stub, encoded_parameters).unwrap(); + assert_eq!(stub.account_balance(&gov_account), RawTokenAmount(500000)); + let mut expected_state = HashMap::with_capacity(3); + expected_state.insert(b"\0\0name".into(), b"Protocol-level token".into()); + expected_state.insert(b"\0\0metadata".into(), encoded_metadata); + expected_state.insert( + b"\0\0governanceAccount".into(), + stub.account_index(&gov_account).index.to_be_bytes().into(), + ); + expected_state.insert(b"\0\0denyList".into(), vec![]); + assert_eq!(stub.state, expected_state); +} + +/// In this example, the parameters specify an initial supply with higher precision +/// than the token allows. +#[test] +fn test_initialize_token_excessive_mint_decimals() { + let mut stub = KernelStub::new(2); + let gov_account = stub.create_account(); + let metadata = "https://plt.token".to_owned().into(); + let parameters = TokenModuleInitializationParameters { + name: Some("Protocol-level token".to_owned()), + metadata: Some(metadata), + governance_account: Some(stub.account_canonical_address(&gov_account).into()), + allow_list: Some(false), + deny_list: Some(false), + initial_supply: Some(TokenAmount::from_raw(500000, 6)), + mintable: Some(false), + burnable: Some(false), + additional: Default::default(), + }; + let encoded_parameters = cbor::cbor_encode(¶meters).unwrap().into(); + let res = token_module::initialize_token(&mut stub, encoded_parameters); + assert_matches!( + res, + Err(TokenInitializationError::MintAmountDecimalsMismatch( + TokenAmountDecimalsMismatchError { + expected: 2, + found: 6 + } + )) + ); +} + +/// In this example, the parameters specify an initial supply with less precision +/// than the token allows. +#[test] +fn test_initialize_token_insufficient_mint_decimals() { + let mut stub = KernelStub::new(6); + let gov_account = stub.create_account(); + let metadata = "https://plt.token".to_owned().into(); + let parameters = TokenModuleInitializationParameters { + name: Some("Protocol-level token".to_owned()), + metadata: Some(metadata), + governance_account: Some(stub.account_canonical_address(&gov_account).into()), + allow_list: Some(false), + deny_list: Some(false), + initial_supply: Some(TokenAmount::from_raw(500000, 2)), + mintable: Some(false), + burnable: Some(false), + additional: Default::default(), + }; + let encoded_parameters = cbor::cbor_encode(¶meters).unwrap().into(); + let res = token_module::initialize_token(&mut stub, encoded_parameters); + assert_matches!( + res, + Err(TokenInitializationError::MintAmountDecimalsMismatch( + TokenAmountDecimalsMismatchError { + expected: 6, + found: 2 + } + )) + ); +} + +/// In this example, the parameters specify an initial supply with less precision +/// than the token allows. +#[test] +fn test_initialize_token_non_existing_governance_account() { + let mut stub = KernelStub::new(0); + let metadata = "https://plt.token".to_owned().into(); + let parameters = TokenModuleInitializationParameters { + name: Some("Protocol-level token".to_owned()), + metadata: Some(metadata), + governance_account: Some(TEST_ACCOUNT2.into()), + allow_list: Some(false), + deny_list: Some(false), + initial_supply: Some(TokenAmount::from_raw(500000, 2)), + mintable: Some(false), + burnable: Some(false), + additional: Default::default(), + }; + let encoded_parameters = cbor::cbor_encode(¶meters).unwrap().into(); + let res = token_module::initialize_token(&mut stub, encoded_parameters); + assert_matches!( + res, + Err(TokenInitializationError::GovernanceAccountDoesNotExist( + TEST_ACCOUNT2 + )) + ); +}