diff --git a/Cargo.lock b/Cargo.lock index 3f1349eb4..42f715a5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "Inflector" @@ -822,7 +822,7 @@ dependencies = [ "cfg-if", "libc", "miniz_oxide", - "object 0.36.0", + "object 0.36.4", "rustc-demangle", ] @@ -7195,9 +7195,9 @@ dependencies = [ [[package]] name = "object" -version = "0.36.0" +version = "0.36.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "576dfe1fc8f9df304abb159d767a29d0476f7750fbf8aa7ad07816004a207434" +checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a" dependencies = [ "memchr", ] diff --git a/pop-api/examples/fungibles/README.md b/pop-api/examples/fungibles/README.md new file mode 100644 index 000000000..df0b108c6 --- /dev/null +++ b/pop-api/examples/fungibles/README.md @@ -0,0 +1,32 @@ +# PSP22 Example Contract with Pop Fungibles API + +This contract is an example implementation of the [PSP22 standard](https://github.com/inkdevhub/standards/blob/master/PSPs/psp-22.md), utilizing the Pop API Fungibles module. + +- This contract provides core functionalities to manage a PSP22 standard token that exists as an asset on the Pop Network, rather than being confined solely to the contract. +- Only the contract owner has a permission to call specific methods. + +This design choice is due to the fact that `pop-api` invokes runtime calls with the contract itself as the `origin`. For examples, methods like `transfer` and `transfer_from` will operate on the contract's own account balance, not on the balance of the caller. + +To learn more about Pop API and how Pop API works under the hood: [Read here](/pop-api/README.md) + +## Security risks prevention + +To prevent potential misuse of contract methods by malicious actors—such as using `approve` to grant unauthorized transfer permissions—the contract restricts access exclusively to the `owner` (the account that instantiated the contract). + +```rs +self.ensure_owner()?; +``` + +Only the `owner` is permitted to call these methods, ensuring the contract’s balance remains secure. The owner of the contract can be updated by an existing `owner` by calling the method `transfer_ownership`. + +## What can be improved? + +- Instead of restricting ownership to a single `owner`, the contract could be designed to accommodate multiple owners. Therefore, the `transfer_ownership` is also need to be updated to support this feature. + +## Use cases + +This contract can be used in multiple different real world cases such as: + +- **Use case for a governance token within a [DAO (Decentralized Autonomous Organization)](https://www.investopedia.com/tech/what-dao/)**: The DAO contract performs cross-contract calls to this PSP22 example contract to create a new token, with the DAO authority as the token’s owner. This allows the DAO authority to manage governance tokens for its members by performing actions such as mint and burn. + +- **Staking Rewards Program**: This contract can be used to issue rewards in a staking program, where users lock tokens for network security or liquidity. The staking contract calls this PSP22 contract to mint and burn tokens, allowing the staking authority to manage rewards and ensure distribution only to eligible participants. diff --git a/pop-api/examples/fungibles/lib.rs b/pop-api/examples/fungibles/lib.rs index 50a3e3a84..eaaeac344 100644 --- a/pop-api/examples/fungibles/lib.rs +++ b/pop-api/examples/fungibles/lib.rs @@ -1,8 +1,11 @@ #![cfg_attr(not(feature = "std"), no_std, no_main)] -use ink::prelude::{string::String, vec::Vec}; +use ink::{ + prelude::{string::String, vec::Vec}, + storage::Mapping, +}; use pop_api::{ - primitives::TokenId, + primitives::{AccountId, TokenId}, v0::fungibles::{ self as api, events::{Approval, Created, Transfer}, @@ -21,20 +24,24 @@ mod fungibles { #[ink(storage)] pub struct Fungible { id: TokenId, + owner: Option, } impl Fungible { - /// Instantiate the contract and wrap around an existing token. + /// Instantiate the contract and wrap an existing token. /// /// # Parameters - /// * - `token` - The token. + /// * - `id` - The token. #[ink(constructor, payable)] pub fn existing(id: TokenId) -> Result { // Make sure token exists. if !api::token_exists(id).unwrap_or_default() { return Err(Psp22Error::Custom(String::from("Token does not exist"))); } - Ok(Self { id }) + let mut instance = Self { id, owner: None }; + let contract_id = instance.env().account_id(); + instance.owner = Some(instance.env().caller()); + Ok(instance) } /// Instantiate the contract and create a new token. The token identifier will be stored @@ -47,14 +54,34 @@ mod fungibles { // inactive balances from bloating the blockchain state and slowing down the network. #[ink(constructor, payable)] pub fn new(id: TokenId, min_balance: Balance) -> Result { - let instance = Self { id }; + let mut instance = Self { id, owner: None }; let contract_id = instance.env().account_id(); + instance.owner = Some(instance.env().caller()); api::create(id, contract_id, min_balance).map_err(Psp22Error::from)?; instance .env() .emit_event(Created { id, creator: contract_id, admin: contract_id }); Ok(instance) } + + /// Check if a caller is an owner of the contract. + fn ensure_owner(&self) -> Result<(), Psp22Error> { + if self.owner != Some(self.env().caller()) { + return Err(Psp22Error::Custom(String::from("Not an owner"))); + } + Ok(()) + } + + /// Transfer an ownership of the contract to another accoun. + /// + /// # Parameters + /// - `owner` - New owner account. + #[ink(message)] + pub fn transfer_ownership(&mut self, owner: AccountId) -> Result<(), Psp22Error> { + self.ensure_owner()?; + self.owner = Some(owner); + Ok(()) + } } impl Psp22 for Fungible { @@ -83,8 +110,8 @@ mod fungibles { api::allowance(self.id, owner, spender).unwrap_or_default() } - /// Transfers `value` amount of tokens from the caller's account to account `to` - /// with additional `data` in unspecified format. + /// Transfers `value` amount of tokens from the contract to account `to` with + /// additional `data` in unspecified format. /// /// # Parameters /// - `to` - The recipient account. @@ -97,18 +124,20 @@ mod fungibles { value: Balance, _data: Vec, ) -> Result<(), Psp22Error> { - let caller = self.env().caller(); - // No-op if the caller and `to` is the same address or `value` is zero. - if caller == to || value == 0 { + self.ensure_owner()?; + let contract = self.env().account_id(); + + // No-op if the contract and `to` is the same address or `value` is zero. + if contract == to || value == 0 { return Ok(()); } api::transfer(self.id, to, value).map_err(Psp22Error::from)?; - self.env().emit_event(Transfer { from: Some(caller), to: Some(to), value }); + self.env().emit_event(Transfer { from: Some(contract), to: Some(to), value }); Ok(()) } /// Transfers `value` tokens on behalf of `from` to the account `to` - /// with additional `data` in unspecified format. + /// with additional `data` in unspecified format. Contract must be pre-approved by `from`. /// /// # Parameters /// - `from` - The account from which the token balance will be withdrawn. @@ -123,25 +152,27 @@ mod fungibles { value: Balance, _data: Vec, ) -> Result<(), Psp22Error> { - let caller = self.env().caller(); + self.ensure_owner()?; + let contract = self.env().account_id(); + // No-op if `from` and `to` is the same address or `value` is zero. if from == to || value == 0 { return Ok(()); } - // If `from` and the caller are different addresses, a successful transfer results - // in decreased allowance by `from` to the caller and an `Approval` event with + // If `from` and the contract are different addresses, a successful transfer results + // in decreased allowance by `from` to the contract and an `Approval` event with // the new allowance amount is emitted. api::transfer_from(self.id, from, to, value).map_err(Psp22Error::from)?; - self.env().emit_event(Transfer { from: Some(caller), to: Some(to), value }); + self.env().emit_event(Transfer { from: Some(contract), to: Some(to), value }); self.env().emit_event(Approval { owner: from, - spender: caller, - value: self.allowance(from, caller), + spender: contract, + value: self.allowance(from, contract), }); Ok(()) } - /// Approves `spender` to spend `value` amount of tokens on behalf of the caller. + /// Approves `spender` to spend `value` amount of tokens on behalf of the contract. /// /// Successive calls of this method overwrite previous values. /// @@ -150,13 +181,15 @@ mod fungibles { /// - `value` - The number of tokens to approve. #[ink(message)] fn approve(&mut self, spender: AccountId, value: Balance) -> Result<(), Psp22Error> { - let caller = self.env().caller(); - // No-op if the caller and `spender` is the same address. - if caller == spender { + self.ensure_owner()?; + let contract = self.env().account_id(); + + // No-op if the contract and `spender` is the same address. + if contract == spender { return Ok(()); } api::approve(self.id, spender, value).map_err(Psp22Error::from)?; - self.env().emit_event(Approval { owner: caller, spender, value }); + self.env().emit_event(Approval { owner: contract, spender, value }); Ok(()) } @@ -171,14 +204,16 @@ mod fungibles { spender: AccountId, value: Balance, ) -> Result<(), Psp22Error> { - let caller = self.env().caller(); - // No-op if the caller and `spender` is the same address or `value` is zero. - if caller == spender || value == 0 { + self.ensure_owner()?; + let contract = self.env().account_id(); + + // No-op if the contract and `spender` is the same address or `value` is zero. + if contract == spender || value == 0 { return Ok(()); } api::increase_allowance(self.id, spender, value).map_err(Psp22Error::from)?; - let allowance = self.allowance(caller, spender); - self.env().emit_event(Approval { owner: caller, spender, value: allowance }); + let allowance = self.allowance(contract, spender); + self.env().emit_event(Approval { owner: contract, spender, value: allowance }); Ok(()) } @@ -193,14 +228,16 @@ mod fungibles { spender: AccountId, value: Balance, ) -> Result<(), Psp22Error> { - let caller = self.env().caller(); - // No-op if the caller and `spender` is the same address or `value` is zero. - if caller == spender || value == 0 { + self.ensure_owner()?; + let contract = self.env().account_id(); + + // No-op if the contract and `spender` is the same address or `value` is zero. + if contract == spender || value == 0 { return Ok(()); } api::decrease_allowance(self.id, spender, value).map_err(Psp22Error::from)?; - let value = self.allowance(caller, spender); - self.env().emit_event(Approval { owner: caller, spender, value }); + let value = self.allowance(contract, spender); + self.env().emit_event(Approval { owner: contract, spender, value }); Ok(()) } } @@ -238,6 +275,7 @@ mod fungibles { /// - `value` - The number of tokens to mint. #[ink(message)] fn mint(&mut self, account: AccountId, value: Balance) -> Result<(), Psp22Error> { + self.ensure_owner()?; // No-op if `value` is zero. if value == 0 { return Ok(()); @@ -256,6 +294,7 @@ mod fungibles { /// - `value` - The number of tokens to destroy. #[ink(message)] fn burn(&mut self, account: AccountId, value: Balance) -> Result<(), Psp22Error> { + self.ensure_owner()?; // No-op if `value` is zero. if value == 0 { return Ok(()); diff --git a/pop-api/examples/fungibles/tests.rs b/pop-api/examples/fungibles/tests.rs index ad429bf64..788adc1c7 100644 --- a/pop-api/examples/fungibles/tests.rs +++ b/pop-api/examples/fungibles/tests.rs @@ -57,12 +57,15 @@ drink::impl_sandbox!(Pop, Runtime, ALICE); // Deployment and constructor method tests. +fn deploy_with_default(session: &mut Session) -> Result { + deploy(session, "new", vec![TOKEN.to_string(), MIN_BALANCE.to_string()]) +} + #[drink::test(sandbox = Pop)] fn new_constructor_works(mut session: Session) { let _ = env_logger::try_init(); // Deploy a new contract. - let contract = - deploy(&mut session, "new", vec![TOKEN.to_string(), MIN_BALANCE.to_string()]).unwrap(); + let contract = deploy_with_default(&mut session).unwrap(); // Token exists after the deployment. assert!(session.sandbox().asset_exists(&TOKEN)); // Successfully emit event. @@ -117,7 +120,7 @@ fn existing_constructor_fails_with_non_existing_token(&mut session: Session) { fn total_supply_works(mut session: Session) { let _ = env_logger::try_init(); // Deploy a new contract. - assert_ok!(deploy(&mut session, "new", vec![TOKEN.to_string(), MIN_BALANCE.to_string()],)); + assert_ok!(deploy_with_default(&mut session)); // No tokens in circulation. assert_eq!(total_supply(&mut session), 0); assert_eq!(total_supply(&mut session), session.sandbox().total_supply(&TOKEN)); @@ -131,7 +134,7 @@ fn total_supply_works(mut session: Session) { fn balance_of_works(mut session: Session) { let _ = env_logger::try_init(); // Deploy a new contract. - assert_ok!(deploy(&mut session, "new", vec![TOKEN.to_string(), MIN_BALANCE.to_string()])); + assert_ok!(deploy_with_default(&mut session)); // No tokens in circulation. assert_eq!(balance_of(&mut session, ALICE), 0); assert_eq!(balance_of(&mut session, ALICE), session.sandbox().balance_of(&TOKEN, &ALICE)); @@ -145,8 +148,7 @@ fn balance_of_works(mut session: Session) { fn allowance_works(mut session: Session) { let _ = env_logger::try_init(); // Deploy a new contract. - let contract = - deploy(&mut session, "new", vec![TOKEN.to_string(), MIN_BALANCE.to_string()]).unwrap(); + let contract = deploy_with_default(&mut session).unwrap(); // No tokens in circulation. assert_eq!(allowance(&mut session, contract.clone(), ALICE), 0); // Tokens in circulation. @@ -158,10 +160,12 @@ fn allowance_works(mut session: Session) { fn transfer_fails_with_no_account() { let _ = env_logger::try_init(); // Deploy a new contract. - let contract = - deploy(&mut session, "new", vec![TOKEN.to_string(), MIN_BALANCE.to_string()]).unwrap(); - session.set_actor(contract.clone()); + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); // `pallet-assets` returns `NoAccount` error. + assert_ok!(session + .sandbox() + .approve(&TOKEN, &contract.clone(), &contract.clone(), AMOUNT * 2)); assert_err!(transfer(&mut session, ALICE, AMOUNT), Error::Module(Assets(NoAccount))); } @@ -169,9 +173,8 @@ fn transfer_fails_with_no_account() { fn transfer_noop_works(&mut session: Session) { let _ = env_logger::try_init(); // Deploy a new contract. - let contract = - deploy(&mut session, "new", vec![TOKEN.to_string(), MIN_BALANCE.to_string()]).unwrap(); - session.set_actor(contract.clone()); + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); // No-op if `value` is zero, returns success and no events are emitted. assert_ok!(transfer(&mut session, ALICE, 0)); assert_eq!(last_contract_event(&session), None); @@ -184,12 +187,14 @@ fn transfer_noop_works(&mut session: Session) { fn transfer_fails_with_insufficient_balance() { let _ = env_logger::try_init(); // Deploy a new contract. - let contract = - deploy(&mut session, "new", vec![TOKEN.to_string(), MIN_BALANCE.to_string()]).unwrap(); - session.set_actor(contract.clone()); + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); // Mint tokens. assert_ok!(session.sandbox().mint_into(&TOKEN, &contract.clone(), AMOUNT)); // Failed with `InsufficientBalance`. + assert_ok!(session + .sandbox() + .approve(&TOKEN, &contract.clone(), &contract.clone(), AMOUNT * 2)); assert_eq!(transfer(&mut session, BOB, AMOUNT + 1), Err(Psp22Error::InsufficientBalance)); } @@ -197,9 +202,8 @@ fn transfer_fails_with_insufficient_balance() { fn transfer_fails_with_token_not_live() { let _ = env_logger::try_init(); // Deploy a new contract. - let contract = - deploy(&mut session, "new", vec![TOKEN.to_string(), MIN_BALANCE.to_string()]).unwrap(); - session.set_actor(contract.clone()); + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); // Mint tokens. assert_ok!(session.sandbox().mint_into(&TOKEN, &contract.clone(), AMOUNT)); // Token is not live, i.e. frozen or being destroyed. @@ -212,13 +216,13 @@ fn transfer_fails_with_token_not_live() { fn transfer_works(mut session: Session) { let _ = env_logger::try_init(); // Deploy a new contract. - let contract = - deploy(&mut session, "new", vec![TOKEN.to_string(), MIN_BALANCE.to_string()]).unwrap(); - session.set_actor(contract.clone()); + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); let value = AMOUNT / 4; // Mint tokens. assert_ok!(session.sandbox().mint_into(&TOKEN, &contract.clone(), AMOUNT)); // Successfully transfer. + assert_ok!(session.sandbox().approve(&TOKEN, &contract.clone(), &contract.clone(), AMOUNT)); assert_ok!(transfer(&mut session, BOB, value)); assert_eq!(session.sandbox().balance_of(&TOKEN, &contract), AMOUNT - value); assert_eq!(session.sandbox().balance_of(&TOKEN, &BOB), value); @@ -239,9 +243,8 @@ fn transfer_works(mut session: Session) { fn transfer_from_noop_works() { let _ = env_logger::try_init(); // Deploy a new contract. - let contract = - deploy(&mut session, "new", vec![TOKEN.to_string(), MIN_BALANCE.to_string()]).unwrap(); - session.set_actor(contract.clone()); + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); // No-op if `value` is zero, returns success and no events are emitted. assert_ok!(transfer_from(&mut session, ALICE, BOB, 0)); assert_eq!(last_contract_event(&session), None); @@ -254,9 +257,8 @@ fn transfer_from_noop_works() { fn transfer_from_fails_with_insufficient_balance() { let _ = env_logger::try_init(); // Deploy a new contract. - let contract = - deploy(&mut session, "new", vec![TOKEN.to_string(), MIN_BALANCE.to_string()]).unwrap(); - session.set_actor(contract.clone()); + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); // Mint tokens and approve. assert_ok!(session.sandbox().mint_into(&TOKEN, &ALICE, AMOUNT)); assert_ok!(session.sandbox().approve(&TOKEN, &ALICE, &contract.clone(), AMOUNT * 2)); @@ -271,9 +273,8 @@ fn transfer_from_fails_with_insufficient_balance() { fn transfer_from_fails_with_insufficient_allowance() { let _ = env_logger::try_init(); // Deploy a new contract. - let contract = - deploy(&mut session, "new", vec![TOKEN.to_string(), MIN_BALANCE.to_string()]).unwrap(); - session.set_actor(contract.clone()); + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); // Mint tokens and approve. assert_ok!(session.sandbox().mint_into(&TOKEN, &ALICE, AMOUNT)); assert_ok!(session.sandbox().approve(&TOKEN, &ALICE, &contract.clone(), AMOUNT)); @@ -288,9 +289,8 @@ fn transfer_from_fails_with_insufficient_allowance() { fn transfer_from_fails_with_token_not_live() { let _ = env_logger::try_init(); // Deploy a new contract. - let contract = - deploy(&mut session, "new", vec![TOKEN.to_string(), MIN_BALANCE.to_string()]).unwrap(); - session.set_actor(contract.clone()); + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); // Mint tokens and approve. assert_ok!(session.sandbox().mint_into(&TOKEN, &ALICE, AMOUNT)); assert_ok!(session.sandbox().approve(&TOKEN, &ALICE, &contract.clone(), AMOUNT)); @@ -307,9 +307,8 @@ fn transfer_from_fails_with_token_not_live() { fn transfer_from_works(mut session: Session) { let _ = env_logger::try_init(); // Deploy a new contract. - let contract = - deploy(&mut session, "new", vec![TOKEN.to_string(), MIN_BALANCE.to_string()]).unwrap(); - session.set_actor(contract.clone()); + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); let value = AMOUNT / 2; // Mint tokens and approve. assert_ok!(session.sandbox().mint_into(&TOKEN, &ALICE, AMOUNT)); @@ -336,9 +335,8 @@ fn transfer_from_works(mut session: Session) { fn approve_noop_works(mut session: Session) { let _ = env_logger::try_init(); // Deploy a new contract. - let contract = - deploy(&mut session, "new", vec![TOKEN.to_string(), MIN_BALANCE.to_string()]).unwrap(); - session.set_actor(contract.clone()); + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); // No-op if the caller and `spender` is the same address, returns success and no events are // emitted. assert_ok!(approve(&mut session, contract.clone(), AMOUNT)); @@ -349,9 +347,8 @@ fn approve_noop_works(mut session: Session) { fn approve_fails_with_token_not_live(mut session: Session) { let _ = env_logger::try_init(); // Deploy a new contract. - let contract = - deploy(&mut session, "new", vec![TOKEN.to_string(), MIN_BALANCE.to_string()]).unwrap(); - session.set_actor(contract.clone()); + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); // Token is not live, i.e. frozen or being destroyed. assert_ok!(session.sandbox().start_destroy(&TOKEN)); // `pallet-assets` returns `AssetNotLive` error. @@ -362,21 +359,20 @@ fn approve_fails_with_token_not_live(mut session: Session) { fn approve_works(mut session: Session) { let _ = env_logger::try_init(); // Deploy a new contract. - let contract = - deploy(&mut session, "new", vec![TOKEN.to_string(), MIN_BALANCE.to_string()]).unwrap(); - session.set_actor(contract.clone()); + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); let value = AMOUNT / 2; // Mint tokens. assert_ok!(session.sandbox().mint_into(&TOKEN, &contract.clone(), AMOUNT)); // Successfully approve. - assert_ok!(approve(&mut session, ALICE, value)); - assert_eq!(session.sandbox().allowance(&TOKEN, &contract, &ALICE), value); + assert_ok!(approve(&mut session, BOB, value)); + assert_eq!(session.sandbox().allowance(&TOKEN, &contract, &BOB), value); // Successfully emit event. assert_eq!( last_contract_event(&session).unwrap(), Approval { owner: account_id_from_slice(&contract), - spender: account_id_from_slice(&ALICE), + spender: account_id_from_slice(&BOB), value, } .encode() @@ -402,9 +398,8 @@ fn approve_works(mut session: Session) { fn increase_allowance_noop_works(mut session: Session) { let _ = env_logger::try_init(); // Deploy a new contract. - let contract = - deploy(&mut session, "new", vec![TOKEN.to_string(), MIN_BALANCE.to_string()]).unwrap(); - session.set_actor(contract.clone()); + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); // No-op if the caller and `spender` is the same address, returns success and no events are // emitted. assert_ok!(increase_allowance(&mut session, contract.clone(), AMOUNT)); @@ -418,9 +413,8 @@ fn increase_allowance_noop_works(mut session: Session) { fn increase_allowance_fails_with_token_not_live(mut session: Session) { let _ = env_logger::try_init(); // Deploy a new contract. - let contract = - deploy(&mut session, "new", vec![TOKEN.to_string(), MIN_BALANCE.to_string()]).unwrap(); - session.set_actor(contract.clone()); + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); // Token is not live, i.e. frozen or being destroyed. assert_ok!(session.sandbox().start_destroy(&TOKEN)); // `pallet-assets` returns `AssetNotLive` error. @@ -434,9 +428,8 @@ fn increase_allowance_fails_with_token_not_live(mut session: Session) { fn increase_allowance_works(mut session: Session) { let _ = env_logger::try_init(); // Deploy a new contract. - let contract = - deploy(&mut session, "new", vec![TOKEN.to_string(), MIN_BALANCE.to_string()]).unwrap(); - session.set_actor(contract.clone()); + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); let value = AMOUNT / 2; // Mint tokens and approve. assert_ok!(session.sandbox().mint_into(&TOKEN, &contract.clone(), AMOUNT)); @@ -475,9 +468,8 @@ fn increase_allowance_works(mut session: Session) { fn decrease_allowance_noop_works(mut session: Session) { let _ = env_logger::try_init(); // Deploy a new contract. - let contract = - deploy(&mut session, "new", vec![TOKEN.to_string(), MIN_BALANCE.to_string()]).unwrap(); - session.set_actor(contract.clone()); + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); // No-op if the caller and `spender` is the same address, returns success and no events are // emitted. assert_ok!(decrease_allowance(&mut session, contract.clone(), AMOUNT)); @@ -488,9 +480,8 @@ fn decrease_allowance_noop_works(mut session: Session) { fn decrease_allowance_fails_with_insufficient_allowance(mut session: Session) { let _ = env_logger::try_init(); // Deploy a new contract. - let contract = - deploy(&mut session, "new", vec![TOKEN.to_string(), MIN_BALANCE.to_string()]).unwrap(); - session.set_actor(contract.clone()); + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); // Failed with `InsufficientAllowance`. assert_eq!( decrease_allowance(&mut session, ALICE, AMOUNT), @@ -502,9 +493,8 @@ fn decrease_allowance_fails_with_insufficient_allowance(mut session: Session) { fn decrease_allowance_fails_with_token_not_live(mut session: Session) { let _ = env_logger::try_init(); // Deploy a new contract. - let contract = - deploy(&mut session, "new", vec![TOKEN.to_string(), MIN_BALANCE.to_string()]).unwrap(); - session.set_actor(contract.clone()); + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); // Mint tokens and approve. assert_ok!(session.sandbox().mint_into(&TOKEN, &contract.clone(), AMOUNT)); assert_ok!(session.sandbox().approve(&TOKEN, &contract.clone(), &ALICE, AMOUNT)); @@ -521,9 +511,8 @@ fn decrease_allowance_fails_with_token_not_live(mut session: Session) { fn decrease_allowance_works(mut session: Session) { let _ = env_logger::try_init(); // Deploy a new contract. - let contract = - deploy(&mut session, "new", vec![TOKEN.to_string(), MIN_BALANCE.to_string()]).unwrap(); - session.set_actor(contract.clone()); + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); let value = 1; // Mint tokens and approve. assert_ok!(session.sandbox().mint_into(&TOKEN, &contract.clone(), AMOUNT)); @@ -564,8 +553,7 @@ fn decrease_allowance_works(mut session: Session) { fn token_metadata(mut session: Session) { let _ = env_logger::try_init(); // Deploy a new contract. - let contract = - deploy(&mut session, "new", vec![TOKEN.to_string(), MIN_BALANCE.to_string()]).unwrap(); + let contract = deploy_with_default(&mut session).unwrap(); session.set_actor(contract.clone()); let name: String = String::from("Paseo Token"); let symbol: String = String::from("PAS"); @@ -597,16 +585,15 @@ fn mint_fails_with_no_permission(mut session: Session) { assert_ok!(session.sandbox().create(&(TOKEN + 1), &BOB, MIN_BALANCE)); assert_ok!(deploy(&mut session, "existing", vec![(TOKEN + 1).to_string()])); // `pallet-assets` returns `NoPermission` error. - assert_err!(mint(&mut session, BOB, AMOUNT), Error::Module(Assets(NoPermission))); + assert_err!(mint(&mut session, ALICE, AMOUNT), Error::Module(Assets(NoPermission))); } #[drink::test(sandbox = Pop)] fn mint_noop_works(mut session: Session) { let _ = env_logger::try_init(); // Deploy a new contract. - let contract = - deploy(&mut session, "new", vec![TOKEN.to_string(), MIN_BALANCE.to_string()]).unwrap(); - session.set_actor(contract.clone()); + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); // No-op if minted value is zero, returns success and no events are emitted. assert_ok!(mint(&mut session, ALICE, 0)); assert_eq!(last_contract_event(&session), None); @@ -616,9 +603,8 @@ fn mint_noop_works(mut session: Session) { fn mint_fails_with_arithmetic_overflow(mut session: Session) { let _ = env_logger::try_init(); // Deploy a new contract. - let contract = - deploy(&mut session, "new", vec![TOKEN.to_string(), MIN_BALANCE.to_string()]).unwrap(); - session.set_actor(contract.clone()); + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); assert_ok!(mint(&mut session, ALICE, AMOUNT)); // Total supply increased by `value` exceeds maximal value of `u128` type. assert_err!(mint(&mut session, ALICE, u128::MAX), Error::Raw(Arithmetic(Overflow))); @@ -628,9 +614,8 @@ fn mint_fails_with_arithmetic_overflow(mut session: Session) { fn mint_fails_with_token_not_live(mut session: Session) { let _ = env_logger::try_init(); // Deploy a new contract. - let contract = - deploy(&mut session, "new", vec![TOKEN.to_string(), MIN_BALANCE.to_string()]).unwrap(); - session.set_actor(contract.clone()); + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); // Token is not live, i.e. frozen or being destroyed. assert_ok!(session.sandbox().start_destroy(&TOKEN)); // `pallet-assets` returns `AssetNotLive` error. @@ -641,9 +626,8 @@ fn mint_fails_with_token_not_live(mut session: Session) { fn mint_works(mut session: Session) { let _ = env_logger::try_init(); // Deploy a new contract. - let contract = - deploy(&mut session, "new", vec![TOKEN.to_string(), MIN_BALANCE.to_string()]).unwrap(); - session.set_actor(contract.clone()); + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); let value = AMOUNT; // Successfully mint tokens. assert_ok!(mint(&mut session, ALICE, value)); @@ -673,9 +657,8 @@ fn burn_fails_with_no_permission(mut session: Session) { fn burn_noop_works(mut session: Session) { let _ = env_logger::try_init(); // Deploy a new contract. - let contract = - deploy(&mut session, "new", vec![TOKEN.to_string(), MIN_BALANCE.to_string()]).unwrap(); - session.set_actor(contract.clone()); + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); // No-op if burned value is zero, returns success and no events are emitted. assert_ok!(burn(&mut session, ALICE, 0)); assert_eq!(last_contract_event(&session), None); @@ -685,9 +668,8 @@ fn burn_noop_works(mut session: Session) { fn burn_fails_with_insufficient_balance(mut session: Session) { let _ = env_logger::try_init(); // Deploy a new contract. - let contract = - deploy(&mut session, "new", vec![TOKEN.to_string(), MIN_BALANCE.to_string()]).unwrap(); - session.set_actor(contract.clone()); + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); // Failed with `InsufficientBalance`. assert_eq!(burn(&mut session, ALICE, AMOUNT), Err(Psp22Error::InsufficientBalance)); } @@ -696,9 +678,8 @@ fn burn_fails_with_insufficient_balance(mut session: Session) { fn burn_fails_with_token_not_live(mut session: Session) { let _ = env_logger::try_init(); // Deploy a new contract. - let contract = - deploy(&mut session, "new", vec![TOKEN.to_string(), MIN_BALANCE.to_string()]).unwrap(); - session.set_actor(contract.clone()); + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); assert_ok!(session.sandbox().mint_into(&TOKEN, &ALICE, AMOUNT)); // Token is not live, i.e. frozen or being destroyed. assert_ok!(session.sandbox().start_destroy(&TOKEN)); @@ -710,9 +691,8 @@ fn burn_fails_with_token_not_live(mut session: Session) { fn burn_works(mut session: Session) { let _ = env_logger::try_init(); // Deploy a new contract. - let contract = - deploy(&mut session, "new", vec![TOKEN.to_string(), MIN_BALANCE.to_string()]).unwrap(); - session.set_actor(contract.clone()); + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); let value = 1; // Mint tokens. assert_ok!(session.sandbox().mint_into(&TOKEN, &ALICE, AMOUNT)); diff --git a/pop-api/src/lib.rs b/pop-api/src/lib.rs index 546106e5c..3607a70df 100644 --- a/pop-api/src/lib.rs +++ b/pop-api/src/lib.rs @@ -63,6 +63,9 @@ mod constants { // Error. pub(crate) const DECODING_FAILED: u32 = 255; + // Runtime Errors. + pub(crate) const MODULE_ERROR: u8 = 3; + // Function IDs. pub(crate) const DISPATCH: u8 = 0; pub(crate) const READ_STATE: u8 = 1; diff --git a/pop-api/src/v0/fungibles/errors.rs b/pop-api/src/v0/fungibles/errors.rs index 8ce287585..3080a2c4c 100644 --- a/pop-api/src/v0/fungibles/errors.rs +++ b/pop-api/src/v0/fungibles/errors.rs @@ -94,8 +94,8 @@ pub enum Psp22Error { impl From for u32 { fn from(value: Psp22Error) -> u32 { match value { - Psp22Error::InsufficientBalance => u32::from_le_bytes([3, ASSETS, 0, 0]), - Psp22Error::InsufficientAllowance => u32::from_le_bytes([3, ASSETS, 10, 0]), + Psp22Error::InsufficientBalance => u32::from_le_bytes([MODULE_ERROR, ASSETS, 0, 0]), + Psp22Error::InsufficientAllowance => u32::from_le_bytes([MODULE_ERROR, ASSETS, 10, 0]), Psp22Error::Custom(value) => value.parse::().expect("Failed to parse"), _ => unimplemented!("Variant is not supported"), } @@ -108,9 +108,9 @@ impl From for Psp22Error { let encoded = value.0.to_le_bytes(); match encoded { // BalanceLow. - [3, ASSETS, 0, _] => Psp22Error::InsufficientBalance, + [MODULE_ERROR, ASSETS, 0, _] => Psp22Error::InsufficientBalance, // Unapproved. - [3, ASSETS, 10, _] => Psp22Error::InsufficientAllowance, + [MODULE_ERROR, ASSETS, 10, _] => Psp22Error::InsufficientAllowance, // Custom error with status code. _ => Psp22Error::Custom(value.0.to_string()), } diff --git a/pop-api/src/v0/fungibles/mod.rs b/pop-api/src/v0/fungibles/mod.rs index 6fc1e9e5a..1e1479820 100644 --- a/pop-api/src/v0/fungibles/mod.rs +++ b/pop-api/src/v0/fungibles/mod.rs @@ -15,7 +15,7 @@ pub use metadata::*; pub use traits::*; use crate::{ - constants::{ASSETS, BALANCES, FUNGIBLES}, + constants::{ASSETS, BALANCES, FUNGIBLES, MODULE_ERROR}, primitives::{AccountId, Balance, TokenId}, ChainExtensionMethodApi, Result, StatusCode, };