diff --git a/concordium-base b/concordium-base index 3b735e101..d0590c6d6 160000 --- a/concordium-base +++ b/concordium-base @@ -1 +1 @@ -Subproject commit 3b735e1012411208ec20d7bb2b2d224239a21150 +Subproject commit d0590c6d67232d50342461075267b280c134daf2 diff --git a/plt-deployment-unit/Cargo.lock b/plt-deployment-unit/Cargo.lock index 989f338b8..556f09f48 100644 --- a/plt-deployment-unit/Cargo.lock +++ b/plt-deployment-unit/Cargo.lock @@ -164,6 +164,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "assert_matches" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" + [[package]] name = "autocfg" version = "1.5.0" @@ -184,9 +190,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" +checksum = "0e050f626429857a27ddccb31e0aca21356bfa709c04041aefddac081a8f068a" [[package]] name = "bitvec" @@ -211,9 +217,9 @@ dependencies = [ [[package]] name = "borsh" -version = "1.5.7" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" +checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" dependencies = [ "borsh-derive", "cfg_aliases", @@ -221,15 +227,15 @@ dependencies = [ [[package]] name = "borsh-derive" -version = "1.5.7" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdd1d3c0c2f5833f22386f252fe8ed005c7f59fdcddeef025c01b4c3b9fd9ac3" +checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c" dependencies = [ "once_cell", "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -278,15 +284,15 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" [[package]] name = "cc" -version = "1.2.43" +version = "1.2.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "739eb0f94557554b3ca9a86d2d37bebd49c5e6d0c1d2bda35ba5bdac830befc2" +checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" dependencies = [ "find-msvc-tools", "shlex", @@ -360,7 +366,7 @@ version = "4.1.0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -415,7 +421,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -487,9 +493,9 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "typenum", @@ -521,7 +527,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -555,7 +561,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -569,7 +575,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -580,7 +586,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core 0.20.11", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -591,7 +597,7 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core 0.21.3", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -635,7 +641,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -710,9 +716,9 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "find-msvc-tools" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" [[package]] name = "fnv" @@ -728,9 +734,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "generic-array" -version = "0.14.9" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", @@ -795,9 +801,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "hex" @@ -848,12 +854,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.12.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", - "hashbrown 0.16.0", + "hashbrown 0.16.1", "serde", "serde_core", ] @@ -884,9 +890,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "js-sys" -version = "0.3.82" +version = "0.3.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" dependencies = [ "once_cell", "wasm-bindgen", @@ -909,15 +915,15 @@ checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" [[package]] name = "libc" -version = "0.2.177" +version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "memchr" @@ -1046,8 +1052,12 @@ dependencies = [ name = "plt-deployment-unit" version = "0.1.0" dependencies = [ + "anyhow", + "assert_matches", "concordium_base", "getrandom", + "itertools 0.14.0", + "thiserror 2.0.17", ] [[package]] @@ -1105,9 +1115,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.41" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] @@ -1185,7 +1195,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -1277,9 +1287,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.0.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" +checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289" dependencies = [ "dyn-clone", "ref-cast", @@ -1326,7 +1336,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -1344,17 +1354,17 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.15.1" +version = "3.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa66c845eee442168b2c8134fec70ac50dc20e760769c8ba0ad1319ca1959b04" +checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" dependencies = [ "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.12.0", + "indexmap 2.12.1", "schemars 0.9.0", - "schemars 1.0.4", + "schemars 1.1.0", "serde_core", "serde_json", "serde_with_macros", @@ -1363,14 +1373,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.15.1" +version = "3.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91a903660542fced4e99881aa481bdbaec1634568ee02e0b8bd57c64cb38955" +checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" dependencies = [ "darling 0.21.3", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -1450,9 +1460,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.108" +version = "2.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" dependencies = [ "proc-macro2", "quote", @@ -1491,7 +1501,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -1502,7 +1512,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -1562,11 +1572,11 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.23.7" +version = "0.23.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" +checksum = "5d7cbc3b4b49633d57a0509303158ca50de80ae32c265093b24c414705807832" dependencies = [ - "indexmap 2.12.0", + "indexmap 2.12.1", "toml_datetime", "toml_parser", "winnow", @@ -1589,9 +1599,9 @@ checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "unicode-ident" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-segmentation" @@ -1601,9 +1611,9 @@ checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "uuid" -version = "1.18.1" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" dependencies = [ "js-sys", "wasm-bindgen", @@ -1623,9 +1633,9 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasm-bindgen" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" dependencies = [ "cfg-if", "once_cell", @@ -1636,9 +1646,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1646,22 +1656,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" dependencies = [ "unicode-ident", ] @@ -1687,7 +1697,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -1698,7 +1708,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -1727,9 +1737,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.7.13" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" dependencies = [ "memchr", ] @@ -1745,22 +1755,22 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.27" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.27" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -1780,5 +1790,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] diff --git a/plt-deployment-unit/Cargo.toml b/plt-deployment-unit/Cargo.toml index db6fcbe86..8d04538ad 100644 --- a/plt-deployment-unit/Cargo.toml +++ b/plt-deployment-unit/Cargo.toml @@ -10,4 +10,9 @@ 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" +[dev-dependencies] +assert_matches = "1.5" \ No newline at end of file diff --git a/plt-deployment-unit/src/host_interface.rs b/plt-deployment-unit/src/host_interface.rs new file mode 100644 index 000000000..2d850c70a --- /dev/null +++ b/plt-deployment-unit/src/host_interface.rs @@ -0,0 +1,140 @@ +//! Host interface for protocol-level tokens. +use concordium_base::base::{AccountIndex, Energy}; +use concordium_base::contracts_common::AccountAddress; +use concordium_base::protocol_level_tokens::RawCbor; +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; + +/// 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, thiserror::Error)] +#[error("State key is locked")] +pub struct LockedStateKeyError; + +/// Mint exceed the representable amount. +#[derive(Debug, thiserror::Error)] +#[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 { + /// The type for the account object. + /// + /// The account is guaranteed to exist on chain, when holding an instance of this type. + type Account; + + /// Lookup the account using an account address. + fn account_by_address(&self, address: &AccountAddress) -> Option; + + /// Lookup the account using an account index. + fn account_by_index(&self, index: AccountIndex) -> Option; + + /// Get the account index for the account. + 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; + + /// Get the token balance of the account. + 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; + + /// Mint a specified amount and deposit it in the account. + /// + /// # Events + /// + /// This will produce a `TokenMintEvent` in the logs. + /// + /// # Errors + /// + /// - [`AmountNotRepresentableError`] The total supply would exceed the representable amount. + fn mint( + &mut self, + account: &Self::Account, + amount: TokenRawAmount, + ) -> Result<(), AmountNotRepresentableError>; + + /// Burn a specified amount from the account. + /// + /// # Events + /// + /// This will produce a `TokenBurnEvent` in the logs. + /// + /// # Errors + /// + /// - [`InsufficientBalanceError`] The sender has insufficient balance. + fn burn( + &mut self, + account: &Self::Account, + amount: TokenRawAmount, + ) -> Result<(), InsufficientBalanceError>; + + /// Transfer a token amount from one account to another, with an optional memo. + /// + /// # Events + /// + /// This will produce a `TokenTransferEvent` in the logs. + /// + /// # Errors + /// + /// - [`InsufficientBalanceError`] The sender has insufficient balance. + fn transfer( + &mut self, + from: &Self::Account, + to: &Self::Account, + amount: TokenRawAmount, + 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. + /// + /// # Errors + /// + /// - [`LockedStateKeyError`] if the update failed because the key was locked by an iterator. + fn set_token_state( + &mut self, + key: StateKey, + value: Option, + ) -> Result; + + /// 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. + /// The energy is charged in any case (also in case of failure). + fn tick_energy(&mut self, energy: Energy); + + /// 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); +} diff --git a/plt-deployment-unit/src/lib.rs b/plt-deployment-unit/src/lib.rs index cfa165a27..3a02c83bf 100644 --- a/plt-deployment-unit/src/lib.rs +++ b/plt-deployment-unit/src/lib.rs @@ -1,237 +1,2 @@ -use concordium_base::base::{AccountIndex, Energy}; -use concordium_base::contracts_common::AccountAddress; -use concordium_base::protocol_level_tokens::RawCbor; -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; - -/// Operations provided by the deployment unit host. -/// -/// This is abstracted in a trait to allow for a testing stub. -pub trait HostOperations { - /// The type for the account object. - /// - /// The account is guaranteed to exist on chain, when holding an instance of this type. - type Account; - - /// Lookup the account using an account address. - fn account_by_address(&self, address: AccountAddress) -> Option; - - /// Lookup the account using an account index. - fn account_by_index(&self, index: AccountIndex) -> Option; - - /// Get the account index for the account. - 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; - - /// Get the token balance of the account. - 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; - - /// Mint a specified amount and deposit it in the account. - /// - /// # Events - /// - /// This will produce a `TokenMintEvent` in the logs. - /// - /// # Errors - /// - /// - [`AmountNotRepresentableError`] The total supply would exceed the representable amount. - fn mint( - &mut self, - account: Self::Account, - amount: TokenRawAmount, - ) -> Result<(), AmountNotRepresentableError>; - - /// Burn a specified amount from the account. - /// - /// # Events - /// - /// This will produce a `TokenBurnEvent` in the logs. - /// - /// # Errors - /// - /// - [`InsufficientBalanceError`] The sender has insufficient balance. - fn burn( - &mut self, - account: Self::Account, - amount: TokenRawAmount, - ) -> Result<(), InsufficientBalanceError>; - - /// Transfer a token amount from one account to another, with an optional memo. - /// - /// # Events - /// - /// This will produce a `TokenTransferEvent` in the logs. - /// - /// # Errors - /// - /// - [`InsufficientBalanceError`] The sender has insufficient balance. - fn transfer( - &mut self, - from: Self::Account, - to: Self::Account, - amount: TokenRawAmount, - 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. - /// - /// # Errors - /// - /// - [`LockedStateKeyError`] if the update failed because the key was locked by an iterator. - fn set_token_state( - &mut self, - key: StateKey, - value: Option, - ) -> Result; - - /// 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. - /// The energy is charged in any case (also in case of failure). - fn tick_energy(&mut self, energy: Energy); - - /// 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); -} - -/// 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)] -pub struct LockedStateKeyError; - -/// Mint exceed the representable amount. -#[derive(Debug)] -pub struct AmountNotRepresentableError; - -/// Represents the reasons why [`initialize_token`] can fail. -#[derive(Debug)] -pub enum InitError {} -/// Represents the reasons why [`execute_token_update_transaction`] can fail. -#[derive(Debug)] -pub enum UpdateError {} -/// Represents the reasons why a query to the token module can fail. -#[derive(Debug)] -pub enum QueryError {} - -/// The context for a token-holder or token-governance transaction. -#[derive(Debug)] -pub struct TransactionContext { - /// The sender account object. - pub sender: Account, - /// The sender account address. This is the account alias that is used by the transaction itself. - pub sender_address: AccountAddress, -} - -/// 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> { - todo!() -} - -/// Execute a token update transaction using the [`HostOperations`] 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. -/// -/// The process is as follows: -/// -/// - Decode the transaction CBOR parameter. -/// - Check that amounts are within the representable range. -/// - For each transfer operation: -/// -/// - Check that the module is not paused. -/// - Check that the recipient is valid. -/// - Check allowList/denyList restrictions. -/// - Transfer the amount from the sender to the recipient, if the sender's balance is -/// sufficient. -/// -/// - For each list update operation: -/// -/// - Check that the governance account is the sender. -/// - Check that the module configuration allows the list operation. -/// - Check that the account to add/remove exists on-chain. -/// -/// - For each mint operation: -/// -/// - Check that the governance account is the sender. -/// - Check that the module is not paused. -/// - Check that the module configuration allows minting. -/// - Check that the minting process was successful. -/// -/// - For each burn operation: -/// -/// - Check that the governance account is the sender. -/// - Check that the module is not paused. -/// - Check that the module configuration allows burning. -/// - Check that the burning process was successful. -/// -/// - For each pause/unpause operation: -/// -/// - Check that the governance account is the sender. -/// -/// # 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, -{ - todo!() -} - -/// Get the CBOR-encoded representation of the token module state. -pub fn query_token_module_state(_host: &impl HostOperations) -> 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, -{ - todo!() -} +pub mod host_interface; +pub mod token_module; diff --git a/plt-deployment-unit/src/token_module.rs b/plt-deployment-unit/src/token_module.rs new file mode 100644 index 000000000..00c2b6471 --- /dev/null +++ b/plt-deployment-unit/src/token_module.rs @@ -0,0 +1,242 @@ +//! Implementation of the protocol-level token module.car +use crate::host_interface::*; +use anyhow::anyhow; +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, TokenAmount, TokenModuleInitializationParameters, +}; +use itertools::Itertools; + +/// Extension trait for `HostOperations` to provide convenience wrappers for +/// module state access and updating. +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, + value: Option, + ) -> Result<(), LockedStateKeyError> { + self.set_token_state(module_state_key(key), value)?; + Ok(()) + } +} + +impl HostOperationsExt for T {} + +/// Little-endian prefix used to distinguish module state keys. +const MODULE_STATE_PREFIX: [u8; 2] = 0u16.to_le_bytes(); + +/// 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) -> StateKey { + let iter = key.into_iter(); + let mut module_key = Vec::with_capacity(MODULE_STATE_PREFIX.len() + iter.size_hint().0); + module_key.extend_from_slice(&MODULE_STATE_PREFIX); + module_key.extend(iter); + module_key +} + +/// 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(anyhow::Error), + #[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(anyhow::Error), +} + +impl From for InitError { + fn from(value: CborSerializationError) -> Self { + Self::DeserializationFailure(value.into()) + } +} + +/// Represents the reasons why [`execute_token_update_transaction`] can fail. +#[derive(Debug, thiserror::Error)] +pub enum UpdateError {} +/// Represents the reasons why a query to the token module can fail. +#[derive(Debug)] +pub enum QueryError {} + +/// The context for a token-holder or token-governance transaction. +#[derive(Debug)] +pub struct TransactionContext { + /// The sender account object. + pub sender: Account, + /// The sender account address. This is the account alias that is used by the transaction itself. + 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 { + 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, +) -> Result<(), InitError> { + 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 +/// 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. +/// +/// The process is as follows: +/// +/// - Decode the transaction CBOR parameter. +/// - Check that amounts are within the representable range. +/// - For each transfer operation: +/// +/// - Check that the module is not paused. +/// - Check that the recipient is valid. +/// - Check allowList/denyList restrictions. +/// - Transfer the amount from the sender to the recipient, if the sender's balance is +/// sufficient. +/// +/// - For each list update operation: +/// +/// - Check that the governance account is the sender. +/// - Check that the module configuration allows the list operation. +/// - Check that the account to add/remove exists on-chain. +/// +/// - For each mint operation: +/// +/// - Check that the governance account is the sender. +/// - Check that the module is not paused. +/// - Check that the module configuration allows minting. +/// - Check that the minting process was successful. +/// +/// - For each burn operation: +/// +/// - Check that the governance account is the sender. +/// - Check that the module is not paused. +/// - Check that the module configuration allows burning. +/// - Check that the burning process was successful. +/// +/// - For each pause/unpause operation: +/// +/// - Check that the governance account is the sender. +/// +/// # 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, +{ + todo!() +} + +/// Get the CBOR-encoded representation of the token module state. +pub fn query_token_module_state(_host: &impl HostOperations) -> 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, +{ + todo!() +} diff --git a/plt-deployment-unit/tests/host_stub.rs b/plt-deployment-unit/tests/host_stub.rs index d09c1fda2..a896932d1 100644 --- a/plt-deployment-unit/tests/host_stub.rs +++ b/plt-deployment-unit/tests/host_stub.rs @@ -1,7 +1,9 @@ +use std::collections::HashMap; + use concordium_base::base::{AccountIndex, Energy}; use concordium_base::contracts_common::AccountAddress; use concordium_base::transactions::Memo; -use plt_deployment_unit::{ +use plt_deployment_unit::host_interface::{ AmountNotRepresentableError, HostOperations, InsufficientBalanceError, LockedStateKeyError, StateKey, StateValue, TokenEventDetails, TokenEventType, }; @@ -11,18 +13,22 @@ use plt_deployment_unit::{ #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct HostStub { /// List of accounts existing. - accounts: Vec, + pub accounts: Vec, + /// Token managed state. + pub state: HashMap, + /// Decimal places in token representation. + pub decimals: u8, } /// Internal representation of an Account in [`HostStub`]. #[derive(Debug, Clone, PartialEq, Eq)] -struct Account { +pub struct Account { /// The index of the account - index: AccountIndex, + pub index: AccountIndex, /// The canonical account address of the account. - address: AccountAddress, + pub address: AccountAddress, /// The token balance of the account. - balance: Option, + pub balance: Option, } impl HostStub { @@ -48,7 +54,11 @@ impl HostStub { }) .collect(); - Self { accounts } + Self { + accounts, + state: HashMap::new(), + decimals: 0, + } } } @@ -62,10 +72,10 @@ pub struct AccountStubIndex(usize); impl HostOperations for HostStub { type Account = AccountStubIndex; - fn account_by_address(&self, address: AccountAddress) -> Option { + fn account_by_address(&self, address: &AccountAddress) -> Option { self.accounts.iter().enumerate().find_map(|(i, account)| { // TODO resolve an account alias as well here. - if account.address == address { + if account.address == *address { Some(AccountStubIndex(i)) } else { None @@ -83,19 +93,19 @@ impl HostOperations for HostStub { }) } - fn account_index(&self, account: Self::Account) -> AccountIndex { + fn account_index(&self, account: &Self::Account) -> AccountIndex { self.accounts[account.0].index } - fn account_canonical_address(&self, account: Self::Account) -> AccountAddress { + fn account_canonical_address(&self, account: &Self::Account) -> AccountAddress { self.accounts[account.0].address } - fn account_balance(&self, account: Self::Account) -> u64 { + fn account_balance(&self, account: &Self::Account) -> u64 { self.accounts[account.0].balance.unwrap_or(0) } - fn touch(&mut self, account: Self::Account) -> bool { + fn touch(&mut self, account: &Self::Account) -> bool { if self.accounts[account.0].balance.is_some() { false } else { @@ -106,15 +116,25 @@ impl HostOperations for HostStub { fn mint( &mut self, - _account: Self::Account, - _amount: u64, + account: &Self::Account, + amount: u64, ) -> Result<(), AmountNotRepresentableError> { - todo!() + if let Some(balance) = self.accounts[account.0].balance { + if balance > u64::MAX - amount { + Err(AmountNotRepresentableError) + } else { + self.accounts[account.0].balance = Some(balance + amount); + Ok(()) + } + } else { + self.accounts[account.0].balance = Some(amount); + Ok(()) + } } fn burn( &mut self, - _account: Self::Account, + _account: &Self::Account, _amount: u64, ) -> Result<(), InsufficientBalanceError> { todo!() @@ -122,8 +142,8 @@ impl HostOperations for HostStub { fn transfer( &mut self, - _from: Self::Account, - _to: Self::Account, + _from: &Self::Account, + _to: &Self::Account, _amount: u64, _memo: Option, ) -> Result<(), InsufficientBalanceError> { @@ -135,19 +155,23 @@ impl HostOperations for HostStub { } fn decimals(&self) -> u8 { - todo!() + self.decimals } - fn get_token_state(&self, _key: StateKey) -> Option { - todo!() + fn get_token_state(&self, key: StateKey) -> Option { + self.state.get(&key).cloned() } fn set_token_state( &mut self, - _key: StateKey, - _value: Option, + key: StateKey, + value: Option, ) -> Result { - todo!() + let res = match value { + None => self.state.remove(&key).is_some(), + Some(value) => self.state.insert(key, value).is_some(), + }; + Ok(res) } fn tick_energy(&mut self, _energy: Energy) { @@ -161,9 +185,9 @@ impl HostOperations for HostStub { // Tests for the HostStub -const TEST_ACCOUNT0: AccountAddress = AccountAddress([0u8; 32]); -const TEST_ACCOUNT1: AccountAddress = AccountAddress([1u8; 32]); -const TEST_ACCOUNT2: AccountAddress = AccountAddress([2u8; 32]); +pub const TEST_ACCOUNT0: AccountAddress = AccountAddress([0u8; 32]); +pub const TEST_ACCOUNT1: AccountAddress = AccountAddress([1u8; 32]); +pub const TEST_ACCOUNT2: AccountAddress = AccountAddress([2u8; 32]); #[test] fn test_account_lookup() { @@ -173,13 +197,13 @@ fn test_account_lookup() { ]); let _ = host - .account_by_address(TEST_ACCOUNT0) + .account_by_address(&TEST_ACCOUNT0) .expect("Account is expected to exist"); let _ = host - .account_by_address(TEST_ACCOUNT1) + .account_by_address(&TEST_ACCOUNT1) .expect("Account is expected to exist"); assert!( - host.account_by_address(TEST_ACCOUNT2).is_none(), + host.account_by_address(&TEST_ACCOUNT2).is_none(), "Account is not expected to exist" ); // TODO test lookup using alias. @@ -204,16 +228,16 @@ fn test_account_balance() { ]); { let account = host - .account_by_address(TEST_ACCOUNT0) + .account_by_address(&TEST_ACCOUNT0) .expect("Account is expected to exist"); - let balance = host.account_balance(account); + let balance = host.account_balance(&account); assert_eq!(balance, 245); } { let account = host - .account_by_address(TEST_ACCOUNT1) + .account_by_address(&TEST_ACCOUNT1) .expect("Account is expected to exist"); - let balance = host.account_balance(account); + let balance = host.account_balance(&account); assert_eq!(balance, 0); } } @@ -226,16 +250,16 @@ fn test_account_canonical_address() { ]); { let account = host - .account_by_address(TEST_ACCOUNT0) + .account_by_address(&TEST_ACCOUNT0) .expect("Account is expected to exist"); - let balance = host.account_balance(account); + let balance = host.account_balance(&account); assert_eq!(balance, 245); } { let account = host - .account_by_address(TEST_ACCOUNT1) + .account_by_address(&TEST_ACCOUNT1) .expect("Account is expected to exist"); - let balance = host.account_balance(account); + let balance = host.account_balance(&account); assert_eq!(balance, 0); } } diff --git a/plt-deployment-unit/tests/token_module.rs b/plt-deployment-unit/tests/token_module.rs new file mode 100644 index 000000000..5fe31b2aa --- /dev/null +++ b/plt-deployment-unit/tests/token_module.rs @@ -0,0 +1,246 @@ +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_deployment_unit::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.to_string() == "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.to_string() == "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.to_string() == "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.to_string() == "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.to_string() == "Token amount decimals mismatch: expected 6, found 2" + ); +} diff --git a/plt-scheduler/Cargo.lock b/plt-scheduler/Cargo.lock index cad28c04d..d36537929 100644 --- a/plt-scheduler/Cargo.lock +++ b/plt-scheduler/Cargo.lock @@ -1067,8 +1067,11 @@ dependencies = [ name = "plt-deployment-unit" version = "0.1.0" dependencies = [ + "anyhow", "concordium_base", "getrandom", + "itertools 0.14.0", + "thiserror 2.0.17", ] [[package]] diff --git a/plt-scheduler/src/lib.rs b/plt-scheduler/src/lib.rs index d993d1842..757493055 100644 --- a/plt-scheduler/src/lib.rs +++ b/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_deployment_unit::TokenRawAmount; +use plt_deployment_unit::host_interface::TokenRawAmount; // Placeholder types to be defined or replaced with types from other crates.