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
3 changes: 3 additions & 0 deletions plt-deployment-unit/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion plt-deployment-unit/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ crate-type = ["cdylib", "staticlib", "rlib"]
concordium_base = {path = "../concordium-base/rust-src/concordium_base"}
# TODO Remove getrandom as dependency when possible, ideally as part of https://linear.app/concordium/issue/COR-2027
getrandom = { version = "0.2", features = ["custom"]}

thiserror = "2.0"
anyhow = "1.0"
itertools = "0.14.0"
179 changes: 160 additions & 19 deletions plt-deployment-unit/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
use anyhow::anyhow;
use concordium_base::base::{AccountIndex, Energy};
use concordium_base::common::cbor::{
CborSerializationError, SerializationOptions, UnknownMapKeys, cbor_decode_with_options,
cbor_encode,
};
use concordium_base::contracts_common::AccountAddress;
use concordium_base::protocol_level_tokens::RawCbor;
use concordium_base::protocol_level_tokens::{
RawCbor, TokenAmount, TokenModuleInitializationParameters,
};
use concordium_base::transactions::Memo;
use itertools::Itertools;

pub type StateKey = Vec<u8>;
pub type StateValue = Vec<u8>;
Expand All @@ -20,25 +28,25 @@ pub trait HostOperations {
type Account;

/// Lookup the account using an account address.
fn account_by_address(&self, address: AccountAddress) -> Option<Self::Account>;
fn account_by_address(&self, address: &AccountAddress) -> Option<Self::Account>;

/// Lookup the account using an account index.
fn account_by_index(&self, index: AccountIndex) -> Option<Self::Account>;

/// Get the account index for the account.
fn account_index(&self, account: Self::Account) -> AccountIndex;
fn account_index(&self, account: &Self::Account) -> AccountIndex;

/// Get the canonical account address of the account, i.e. the address used as part of the
/// credential deployment and not an alias.
fn account_canonical_address(&self, account: Self::Account) -> AccountAddress;
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) -> TokenRawAmount;

/// 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.
fn touch(&mut self, account: Self::Account) -> bool;
fn touch(&mut self, account: &Self::Account) -> bool;

/// Mint a specified amount and deposit it in the account.
///
Expand All @@ -51,7 +59,7 @@ pub trait HostOperations {
/// - [`AmountNotRepresentableError`] The total supply would exceed the representable amount.
fn mint(
&mut self,
account: Self::Account,
account: &Self::Account,
amount: TokenRawAmount,
) -> Result<(), AmountNotRepresentableError>;

Expand All @@ -66,7 +74,7 @@ pub trait HostOperations {
/// - [`InsufficientBalanceError`] The sender has insufficient balance.
fn burn(
&mut self,
account: Self::Account,
account: &Self::Account,
amount: TokenRawAmount,
) -> Result<(), InsufficientBalanceError>;

Expand All @@ -81,8 +89,8 @@ pub trait HostOperations {
/// - [`InsufficientBalanceError`] The sender has insufficient balance.
fn transfer(
&mut self,
from: Self::Account,
to: Self::Account,
from: &Self::Account,
to: &Self::Account,
amount: TokenRawAmount,
memo: Option<Memo>,
) -> Result<(), InsufficientBalanceError>;
Expand Down Expand Up @@ -124,24 +132,72 @@ pub trait HostOperations {
fn log_token_event(&mut self, event_type: TokenEventType, event_details: TokenEventDetails);
}

trait HostOperationsExt: HostOperations {
/// Set or clear a value in the token module state at the corresponding key.
fn set_module_state<'a>(
&mut self,
key: impl IntoIterator<Item = &'a u8>,
value: Option<StateValue>,
) -> Result<(), LockedStateKeyError> {
self.set_token_state(module_state_key(key), value)?;
Ok(())
}
}

impl<T: HostOperations> HostOperationsExt for T {}

/// Little-endian prefix used to distinguish module state keys.
const MODULE_STATE_PREFIX: u16 = 0;

/// Construct a [`StateKey`] for a module key. This prefixes the key to
/// distinguish it from other keys.
fn module_state_key<'a>(key: impl IntoIterator<Item = &'a u8>) -> StateKey {
let iter = key.into_iter();
let mut module_key = Vec::with_capacity(2 + iter.size_hint().0);
module_key.extend_from_slice(&MODULE_STATE_PREFIX.to_le_bytes());
module_key.extend(iter);
module_key
}

/// The account has insufficient balance.
#[derive(Debug)]
pub struct InsufficientBalanceError;

/// Update to state key failed because the key was locked by an iterator.
#[derive(Debug)]
#[derive(Debug, thiserror::Error)]
#[error("State key is locked")]
pub struct LockedStateKeyError;

/// Mint exceed the representable amount.
#[derive(Debug)]
#[derive(Debug, thiserror::Error)]
#[error("Amount not representable")]
pub struct AmountNotRepresentableError;

/// Represents the reasons why [`initialize_token`] can fail.
#[derive(Debug)]
pub enum InitError {}
#[derive(Debug, thiserror::Error)]
pub enum InitError {
#[error("Token initialization parameters could not be deserialized: {0}")]
DeserializationFailure(anyhow::Error),
#[error("{0}")]
LockedStateKey(#[from] LockedStateKeyError),
#[error("The given governance account does not exist: {0}")]
GovernanceAccountDoesNotExist(AccountAddress),
#[error("Invalid mint amount: {0}")]
InvalidMintAmount(anyhow::Error),
}

impl From<CborSerializationError> for InitError {
fn from(value: CborSerializationError) -> Self {
Self::DeserializationFailure(value.into())
}
}

/// Represents the reasons why [`execute_token_update_transaction`] can fail.
#[derive(Debug)]
pub enum UpdateError {}
#[derive(Debug, thiserror::Error)]
pub enum UpdateError {
#[error("")]
DeserializationFailure(anyhow::Error),
}
/// Represents the reasons why a query to the token module can fail.
#[derive(Debug)]
pub enum QueryError {}
Expand All @@ -155,13 +211,98 @@ pub struct TransactionContext<Account> {
pub sender_address: AccountAddress,
}

#[derive(Debug, thiserror::Error)]
pub enum TokenAmountError {
#[error("Token amount decimals mismatch: expected {expected}, found {found}")]
DecimalsMismatch { expected: u8, found: u8 },
}

fn to_token_raw_amount(
amount: TokenAmount,
actual_decimals: u8,
) -> Result<TokenRawAmount, TokenAmountError> {
let decimals = amount.decimals();
if decimals != actual_decimals {
return Err(TokenAmountError::DecimalsMismatch {
expected: actual_decimals,
found: decimals,
});
}
Ok(amount.value())
}

const STATE_KEY_NAME: &[u8] = b"name";
const STATE_KEY_METADATA: &[u8] = b"metadata";
const STATE_KEY_ALLOW_LIST: &[u8] = b"allowList";
const STATE_KEY_DENY_LIST: &[u8] = b"denyList";
const STATE_KEY_MINTABLE: &[u8] = b"mintable";
const STATE_KEY_BURNABLE: &[u8] = b"burnable";
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,
host: &mut impl HostOperations,
token_parameter: Parameter,
) -> Result<(), InitError> {
todo!()
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(anyhow!(
"Unknown additional parameters: {}",
parameter.additional.keys().join(", ")
)));
}
let Some(name) = parameter.name else {
return Err(InitError::DeserializationFailure(anyhow!(
"Token name is missing"
)));
};
let Some(metadata) = parameter.metadata else {
return Err(InitError::DeserializationFailure(anyhow!(
"Token metadata is missing"
)));
};
let Some(governance_account) = parameter.governance_account else {
return Err(InitError::DeserializationFailure(anyhow!(
"Token governance account is missing"
)));
};
host.set_module_state(STATE_KEY_NAME, Some(name.into()))?;
let encoded_metadata = cbor_encode(&metadata)?;
host.set_module_state(STATE_KEY_METADATA, Some(encoded_metadata))?;
if let Some(true) = parameter.allow_list {
host.set_module_state(STATE_KEY_ALLOW_LIST, Some(vec![]))?;
}
if let Some(true) = parameter.deny_list {
host.set_module_state(STATE_KEY_DENY_LIST, Some(vec![]))?;
}
if let Some(true) = parameter.mintable {
host.set_module_state(STATE_KEY_MINTABLE, Some(vec![]))?;
}
if let Some(true) = parameter.burnable {
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_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.into()))?;
host.mint(&governance_account, mint_amount)
.map_err(|_| InitError::InvalidMintAmount(anyhow!("Kernel failed to mint")))?;
}
Ok(())
}

/// Execute a token update transaction using the [`HostOperations`] implementation on `host` to
Expand Down
Loading
Loading