diff --git a/Cargo.lock b/Cargo.lock index 531d7d1c..29947d01 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1588,7 +1588,7 @@ dependencies = [ [[package]] name = "encointer-primitives" -version = "14.3.0" +version = "14.4.0" dependencies = [ "approx", "bs58 0.5.1", @@ -4492,7 +4492,7 @@ dependencies = [ [[package]] name = "pallet-encointer-balances" -version = "14.1.0" +version = "14.2.0" dependencies = [ "approx", "encointer-primitives", @@ -4670,7 +4670,7 @@ dependencies = [ [[package]] name = "pallet-encointer-democracy" -version = "14.3.2" +version = "14.4.0" dependencies = [ "approx", "encointer-primitives", @@ -4763,7 +4763,7 @@ dependencies = [ [[package]] name = "pallet-encointer-treasuries" -version = "14.3.0" +version = "14.4.1" dependencies = [ "approx", "encointer-primitives", @@ -4771,9 +4771,12 @@ dependencies = [ "frame-support", "frame-system", "log", + "pallet-encointer-balances", "pallet-encointer-communities", "pallet-encointer-reputation-commitments", + "pallet-timestamp", "parity-scale-codec", + "rstest", "scale-info", "sp-core", "sp-io", diff --git a/Cargo.toml b/Cargo.toml index 5f901f82..05f5531b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,15 +33,15 @@ members = [ # local pin encointer-ceremonies-assignment = { path = "ceremonies/assignment", default-features = false, version = "14.1.0" } encointer-meetup-validation = { path = "ceremonies/meetup-validation", default-features = false, version = "14.1.0" } -encointer-primitives = { path = "primitives", default-features = false, features = ["serde_derive"], version = "14.3.0" } +encointer-primitives = { path = "primitives", default-features = false, features = ["serde_derive"], version = "14.4.0" } encointer-rpc = { path = "rpc", version = "14.1.0" } ep-core = { path = "primitives/core", default-features = false, version = "14.0.0" } -pallet-encointer-balances = { path = "balances", default-features = false, version = "14.1.0" } +pallet-encointer-balances = { path = "balances", default-features = false, version = "14.2.0" } pallet-encointer-ceremonies = { path = "ceremonies", default-features = false, version = "14.1.0" } pallet-encointer-communities = { path = "communities", default-features = false, version = "14.1.0" } pallet-encointer-reputation-commitments = { path = "reputation-commitments", default-features = false, version = "14.1.0" } pallet-encointer-scheduler = { path = "scheduler", default-features = false, version = "14.1.0" } -pallet-encointer-treasuries = { path = "treasuries", default-features = false, version = "14.3.0" } +pallet-encointer-treasuries = { path = "treasuries", default-features = false, version = "14.4.0" } test-utils = { path = "test-utils" } # rpc apis encointer-balances-tx-payment-rpc-runtime-api = { path = "balances-tx-payment/rpc/runtime-api", version = "14.1.0" } diff --git a/balances/Cargo.toml b/balances/Cargo.toml index 89740cbd..8db0b551 100644 --- a/balances/Cargo.toml +++ b/balances/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pallet-encointer-balances" -version = "14.1.0" +version = "14.2.0" authors = ["Encointer Association "] edition = "2021" description = "Balances pallet for the Encointer blockchain runtime" diff --git a/balances/src/lib.rs b/balances/src/lib.rs index 9e087734..35601922 100644 --- a/balances/src/lib.rs +++ b/balances/src/lib.rs @@ -96,7 +96,7 @@ pub mod pallet { amount: BalanceType, ) -> DispatchResultWithPostInfo { let from = ensure_signed(origin)?; - Self::do_transfer(community_id, from, dest, amount)?; + Self::do_transfer(community_id, &from, &dest, amount)?; Ok(().into()) } @@ -122,7 +122,7 @@ pub mod pallet { ) -> DispatchResultWithPostInfo { let from = ensure_signed(origin)?; let amount = Self::balance(cid, &from); - Self::do_transfer(cid, from, dest, amount)?; + Self::do_transfer(cid, &from, &dest, amount)?; Ok(().into()) } } @@ -134,8 +134,8 @@ pub mod pallet { /// /// [CC]: 1 Unit of Community Currency /// NI: Nominal Income. Unit = [CC] - /// FCF: Fee Conversion Factor. Unit = [1/ KKSM] <- Kilo-KSM to be able to adjust fee factor - /// in both ways. CB: Balance in Community Currency [CC] + /// FCF: Fee Conversion Factor. Unit = [1/ KKSM] <- Kilo-KSM to be able to adjust fee + /// factor in both ways. CB: Balance in Community Currency [CC] /// /// The following equation should hold for fee design: /// KSM * FCF * NI = CB -> FCF = CB / (NI * KSM) @@ -164,6 +164,8 @@ pub mod pallet { Transferred(CommunityIdentifier, T::AccountId, T::AccountId, BalanceType), /// Token issuance success `[community_id, beneficiary, amount]` Issued(CommunityIdentifier, T::AccountId, BalanceType), + /// Token burn success `[community_id, who, amount]` + Burned(CommunityIdentifier, T::AccountId, BalanceType), /// fee conversion factor updated successfully FeeConversionFactorUpdated(FeeConversionFactorType), } @@ -272,47 +274,47 @@ impl Pallet { pub fn do_transfer( cid: CommunityIdentifier, - source: T::AccountId, - dest: T::AccountId, + source: &T::AccountId, + dest: &T::AccountId, amount: BalanceType, ) -> Result { // Early exist if no-op. if amount == 0u128 { - Self::deposit_event(Event::Transferred(cid, source, dest, amount)); + Self::deposit_event(Event::Transferred(cid, source.clone(), dest.clone(), amount)); return Ok(amount); } - ensure!(Balance::::contains_key(cid, &source), Error::::NoAccount); + ensure!(Balance::::contains_key(cid, source), Error::::NoAccount); - let mut entry_from = Self::balance_entry_updated(cid, &source); + let mut entry_from = Self::balance_entry_updated(cid, source); ensure!(entry_from.principal >= amount, Error::::BalanceTooLow); if source == dest { - >::insert(cid, &source, entry_from); + >::insert(cid, source, entry_from); return Ok(amount); } - if !Balance::::contains_key(cid, &dest) { + if !Balance::::contains_key(cid, dest) { ensure!(amount > T::ExistentialDeposit::get(), Error::::ExistentialDeposit); - Self::new_account(&dest)?; + Self::new_account(dest)?; Self::deposit_event(Event::Endowed { cid, who: dest.clone(), balance: amount }); } - let mut entry_to = Self::balance_entry_updated(cid, &dest); + let mut entry_to = Self::balance_entry_updated(cid, dest); entry_from.principal = entry_from.principal.saturating_sub(amount); entry_to.principal = entry_to.principal.saturating_add(amount); - >::insert(cid, &source, entry_from); - >::insert(cid, &dest, entry_to); + >::insert(cid, source, entry_from); + >::insert(cid, dest, entry_to); - Self::deposit_event(Event::Transferred(cid, source.clone(), dest, amount)); + Self::deposit_event(Event::Transferred(cid, source.clone(), dest.clone(), amount)); // remove account if it falls beloe existential deposit - entry_from = Self::balance_entry_updated(cid, &source); + entry_from = Self::balance_entry_updated(cid, source); if entry_from.principal < T::ExistentialDeposit::get() { - Self::remove_account(cid, &source)?; + Self::remove_account(cid, source)?; } Ok(amount) @@ -360,6 +362,7 @@ impl Pallet { >::insert(community_id, entry_tot); >::insert(community_id, who, entry_who); + Self::deposit_event(Event::Burned(community_id, who.clone(), amount)); Ok(()) } diff --git a/democracy/Cargo.toml b/democracy/Cargo.toml index 25a0f74b..3e98041b 100644 --- a/democracy/Cargo.toml +++ b/democracy/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pallet-encointer-democracy" -version = "14.3.2" +version = "14.4.0" authors = ["Encointer Association "] edition = "2021" description = "Democracy pallet for the Encointer blockchain runtime" diff --git a/democracy/src/lib.rs b/democracy/src/lib.rs index 6a71a0f7..a028236c 100644 --- a/democracy/src/lib.rs +++ b/democracy/src/lib.rs @@ -128,7 +128,7 @@ pub mod pallet { }, ProposalSubmitted { proposal_id: ProposalIdType, - proposal_action: ProposalAction>, + proposal_action: ProposalAction, T::Moment>, }, VotePlaced { proposal_id: ProposalIdType, @@ -247,7 +247,7 @@ pub mod pallet { )] pub fn submit_proposal( origin: OriginFor, - proposal_action: ProposalAction>, + proposal_action: ProposalAction, T::Moment>, ) -> DispatchResultWithPostInfo { if Self::enactment_queue(proposal_action.clone().get_identifier()).is_some() { return Err(Error::::ProposalWaitingForEnactment.into()); @@ -497,7 +497,7 @@ pub mod pallet { pub fn get_electorate( start_cindex: CeremonyIndexType, - proposal_action: ProposalAction>, + proposal_action: ProposalAction, T::Moment>, ) -> Result> { let voting_cindexes = Self::voting_cindexes(start_cindex)?; @@ -595,7 +595,10 @@ pub mod pallet { }); }, ProposalAction::SpendNative(maybe_cid, ref beneficiary, amount) => { - TreasuriesPallet::::do_spend_native(maybe_cid, beneficiary.clone(), amount)?; + TreasuriesPallet::::do_spend_native(maybe_cid, beneficiary, amount)?; + }, + ProposalAction::IssueSwapNativeOption(cid, ref owner, swap_option) => { + TreasuriesPallet::::do_issue_swap_native_option(cid, owner, swap_option)?; }, }; diff --git a/democracy/src/tests.rs b/democracy/src/tests.rs index df4ec5a4..66c2a0c8 100644 --- a/democracy/src/tests.rs +++ b/democracy/src/tests.rs @@ -22,7 +22,7 @@ use crate::mock::{ EncointerTreasuries, Timestamp, }; use encointer_primitives::{ - balances::Demurrage, + balances::{BalanceType, Demurrage}, ceremonies::{InactivityTimeoutType, Reputation}, common::{FromStr, PalletString}, communities::{ @@ -30,6 +30,7 @@ use encointer_primitives::{ NominalIncome as NominalIncomeType, }, democracy::{ProposalAction, ProposalActionIdentifier, ProposalState, Tally, Vote}, + treasuries::SwapNativeOption, }; use frame_support::{ assert_err, assert_ok, @@ -1056,7 +1057,43 @@ fn enact_spend_native_works() { assert_eq!(Balances::free_balance(&beneficiary), amount); }); } +#[test] +fn enact_issue_swap_native_option_works() { + new_test_ext().execute_with(|| { + System::set_block_number(System::block_number() + 1); // this is needed to assert events + let beneficiary = AccountId::from(AccountKeyring::Alice); + let native_allowance: BalanceOf = 100_000_000; + let rate = Some(BalanceType::from_num(1.5)); + let cid = CommunityIdentifier::default(); + let swap_option: SwapNativeOption = SwapNativeOption { + cid, + native_allowance, + rate, + do_burn: true, + valid_from: None, + valid_until: None, + }; + + let alice = alice(); + let proposal_action = + ProposalAction::IssueSwapNativeOption(cid, beneficiary.clone(), swap_option); + assert_ok!(EncointerDemocracy::submit_proposal( + RuntimeOrigin::signed(alice.clone()), + proposal_action.clone() + )); + + // directly inject the proposal into the enactment queue + EnactmentQueue::::insert(proposal_action.clone().get_identifier(), 1); + + run_to_next_phase(); + // first assigning phase after proposal lifetime ended + + assert_eq!(EncointerDemocracy::proposals(1).unwrap().state, ProposalState::Enacted); + assert_eq!(EncointerDemocracy::enactment_queue(proposal_action.get_identifier()), None); + assert_eq!(EncointerTreasuries::swap_native_options(cid, beneficiary), Some(swap_option)); + }); +} #[test] fn enactment_error_fires_event() { new_test_ext().execute_with(|| { diff --git a/primitives/Cargo.toml b/primitives/Cargo.toml index f668b579..3fce043a 100644 --- a/primitives/Cargo.toml +++ b/primitives/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "encointer-primitives" -version = "14.3.0" +version = "14.4.0" authors = ["Encointer Association "] edition = "2021" description = "Primitives for the Encointer blockchain runtime" diff --git a/primitives/src/democracy.rs b/primitives/src/democracy.rs index cea1deb4..00f99af7 100644 --- a/primitives/src/democracy.rs +++ b/primitives/src/democracy.rs @@ -9,7 +9,10 @@ use crate::{ use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; -use crate::{ceremonies::ReputationCountType, common::PalletString, scheduler::CeremonyIndexType}; +use crate::{ + ceremonies::ReputationCountType, common::PalletString, scheduler::CeremonyIndexType, + treasuries::SwapNativeOption, +}; #[cfg(feature = "serde_derive")] use serde::{Deserialize, Serialize}; use sp_core::RuntimeDebug; @@ -49,7 +52,7 @@ pub enum ProposalAccessPolicy { #[derive(Encode, Decode, RuntimeDebug, Clone, PartialEq, Eq, TypeInfo, MaxEncodedLen)] #[cfg_attr(feature = "serde_derive", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde_derive", serde(rename_all = "camelCase"))] -pub enum ProposalAction { +pub enum ProposalAction { AddLocation(CommunityIdentifier, Location), RemoveLocation(CommunityIdentifier, Location), UpdateCommunityMetadata(CommunityIdentifier, CommunityMetadataType), @@ -58,6 +61,7 @@ pub enum ProposalAction { SetInactivityTimeout(InactivityTimeoutType), Petition(Option, PalletString), SpendNative(Option, AccountId, Balance), + IssueSwapNativeOption(CommunityIdentifier, AccountId, SwapNativeOption), } #[derive(Encode, Decode, RuntimeDebug, Clone, Copy, PartialEq, Eq, TypeInfo, MaxEncodedLen)] @@ -72,9 +76,10 @@ pub enum ProposalActionIdentifier { SetInactivityTimeout, Petition(Option), SpendNative(Option), + IssueSwapNativeOption(CommunityIdentifier), } -impl ProposalAction { +impl ProposalAction { pub fn get_access_policy(&self) -> ProposalAccessPolicy { match self { ProposalAction::AddLocation(cid, _) => ProposalAccessPolicy::Community(*cid), @@ -88,6 +93,7 @@ impl ProposalAction { ProposalAction::Petition(None, _) => ProposalAccessPolicy::Global, ProposalAction::SpendNative(Some(cid), ..) => ProposalAccessPolicy::Community(*cid), ProposalAction::SpendNative(None, ..) => ProposalAccessPolicy::Global, + ProposalAction::IssueSwapNativeOption(cid, ..) => ProposalAccessPolicy::Community(*cid), } } @@ -108,6 +114,8 @@ impl ProposalAction { ProposalActionIdentifier::Petition(*maybe_cid), ProposalAction::SpendNative(maybe_cid, ..) => ProposalActionIdentifier::SpendNative(*maybe_cid), + ProposalAction::IssueSwapNativeOption(cid, ..) => + ProposalActionIdentifier::IssueSwapNativeOption(*cid), } } @@ -122,6 +130,7 @@ impl ProposalAction { ProposalAction::SetInactivityTimeout(_) => true, ProposalAction::Petition(_, _) => false, ProposalAction::SpendNative(_, _, _) => false, + ProposalAction::IssueSwapNativeOption(..) => false, } } } @@ -153,7 +162,7 @@ impl ProposalState { pub struct Proposal { pub start: Moment, pub start_cindex: CeremonyIndexType, - pub action: ProposalAction, + pub action: ProposalAction, pub state: ProposalState, pub electorate_size: ReputationCountType, } diff --git a/primitives/src/lib.rs b/primitives/src/lib.rs index 687c38c9..57f06a5e 100644 --- a/primitives/src/lib.rs +++ b/primitives/src/lib.rs @@ -27,6 +27,7 @@ pub mod faucet; pub mod reputation_commitments; pub mod scheduler; pub mod storage; +pub mod treasuries; pub mod vouches; pub use ep_core::*; diff --git a/primitives/src/treasuries.rs b/primitives/src/treasuries.rs new file mode 100644 index 00000000..223dc103 --- /dev/null +++ b/primitives/src/treasuries.rs @@ -0,0 +1,31 @@ +use crate::{balances::BalanceType, communities::CommunityIdentifier}; +use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; +use scale_info::TypeInfo; +#[cfg(feature = "serde_derive")] +use serde::{Deserialize, Serialize}; +use sp_core::RuntimeDebug; + +#[derive( + Encode, Decode, Default, RuntimeDebug, Clone, Copy, PartialEq, Eq, TypeInfo, MaxEncodedLen, +)] +#[cfg_attr(feature = "serde_derive", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde_derive", serde(rename_all = "camelCase"))] +/// specifies an amount of native tokens which the owner of this option can receive from the +/// community treasury in return for community currency before the expiry date +pub struct SwapNativeOption { + /// specifies community currency to be swapped for native tokens out of its community + /// treasury + pub cid: CommunityIdentifier, + /// the total amount of native tokens which can be swapped with this option + pub native_allowance: NativeBalance, + /// the exchange rate. How many units of community currency will you pay to get one + /// native token (not applying decimals)? Leave as None if the rate is derived on the spot by + /// either an oracle or an auction + pub rate: Option, + /// if true, cc will be burned. If false, cc will be put into community treasury + pub do_burn: bool, + /// first time of validity for this option + pub valid_from: Option, + /// the latest time of validity for this option + pub valid_until: Option, +} diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 69d82979..1dc106a6 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "1.77.0" # align with https://github.com/polkadot-fellows/runtimes/blob/7157d41176bebf128aa2e29e72ed184844446b19/.github/env#L1 +channel = "1.81.0" # align with https://github.com/polkadot-fellows/runtimes/blob/main/.github/env#L1 profile = "default" # include rustfmt, clippy targets = ["wasm32-unknown-unknown"] diff --git a/test-utils/src/lib.rs b/test-utils/src/lib.rs index 14085912..aafac1b2 100644 --- a/test-utils/src/lib.rs +++ b/test-utils/src/lib.rs @@ -216,6 +216,7 @@ macro_rules! impl_encointer_treasuries { type RuntimeEvent = RuntimeEvent; type Currency = pallet_balances::Pallet; type PalletId = TreasuriesPalletId; + type WeightInfo = (); } }; } diff --git a/treasuries/Cargo.toml b/treasuries/Cargo.toml index 5d663e90..42e7c2df 100644 --- a/treasuries/Cargo.toml +++ b/treasuries/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pallet-encointer-treasuries" -version = "14.3.0" +version = "14.4.1" authors = ["Encointer Association "] edition = "2021" description = "Treasuries pallet for the Encointer blockchain runtime" @@ -16,6 +16,7 @@ scale-info = { workspace = true } # local deps encointer-primitives = { workspace = true } +pallet-encointer-balances = { workspace = true } pallet-encointer-communities = { workspace = true } pallet-encointer-reputation-commitments = { workspace = true } @@ -23,12 +24,14 @@ pallet-encointer-reputation-commitments = { workspace = true } frame-benchmarking = { workspace = true, optional = true } frame-support = { workspace = true } frame-system = { workspace = true } +pallet-timestamp = { workspace = true } sp-core = { workspace = true } sp-runtime = { workspace = true } sp-std = { workspace = true } [dev-dependencies] approx = { workspace = true } +rstest = { workspace = true } sp-io = { workspace = true, features = ["std"] } test-utils = { workspace = true } @@ -40,8 +43,10 @@ std = [ "frame-support/std", "frame-system/std", "log/std", + "pallet-encointer-balances/std", "pallet-encointer-communities/std", "pallet-encointer-reputation-commitments/std", + "pallet-timestamp/std", "parity-scale-codec/std", "scale-info/std", "sp-core/std", @@ -53,11 +58,15 @@ runtime-benchmarks = [ "frame-benchmarking", "frame-support/runtime-benchmarks", "frame-system/runtime-benchmarks", + "pallet-encointer-balances/runtime-benchmarks", "pallet-encointer-communities/runtime-benchmarks", "pallet-encointer-reputation-commitments/runtime-benchmarks", + "pallet-timestamp/runtime-benchmarks", ] try-runtime = [ "frame-system/try-runtime", "pallet-encointer-communities/try-runtime", + "pallet-encointer-balances/try-runtime", "pallet-encointer-reputation-commitments/try-runtime", + "pallet-timestamp/try-runtime", ] diff --git a/treasuries/src/benchmarking.rs b/treasuries/src/benchmarking.rs new file mode 100644 index 00000000..f07caa39 --- /dev/null +++ b/treasuries/src/benchmarking.rs @@ -0,0 +1,37 @@ +use crate::*; +use encointer_primitives::treasuries::SwapNativeOption; +use frame_benchmarking::{account, benchmarks, impl_benchmark_test_suite}; +use frame_system::RawOrigin; +use sp_runtime::SaturatedConversion; + +benchmarks! { + where_clause { + where + H256: From<::Hash>, + } + swap_native { + let cid = CommunityIdentifier::default(); + let alice: T::AccountId = account("alice", 1, 1); + let treasury = Pallet::::get_community_treasury_account_unchecked(Some(cid)); + ::Currency::make_free_balance_be(&treasury, 200_000_000u64.saturated_into()); + pallet_encointer_balances::Pallet::::issue(cid, &alice, BalanceType::from_num(12i32)).unwrap(); + let swap_option: SwapNativeOption, T::Moment> = SwapNativeOption { + cid, + native_allowance: 100_000_000u64.saturated_into(), + rate: Some(BalanceType::from_num(0.000_000_2)), + do_burn: false, + valid_from: None, + valid_until: None, + }; + Pallet::::do_issue_swap_native_option( + cid, + &alice, + swap_option + ).unwrap(); + } : _(RawOrigin::Signed(alice.clone()), cid, 50_000_000u64.saturated_into()) + verify { + assert_eq!(::Currency::free_balance(&alice), 50_000_000u64.saturated_into()); + } +} + +impl_benchmark_test_suite!(Pallet, crate::mock::new_test_ext(), crate::mock::TestRuntime); diff --git a/treasuries/src/lib.rs b/treasuries/src/lib.rs index ab37b37a..1019ad1c 100644 --- a/treasuries/src/lib.rs +++ b/treasuries/src/lib.rs @@ -17,25 +17,29 @@ #![cfg_attr(not(feature = "std"), no_std)] use core::marker::PhantomData; -use encointer_primitives::communities::CommunityIdentifier; +use encointer_primitives::{balances::BalanceType, communities::CommunityIdentifier}; use frame_support::{ traits::{Currency, ExistenceRequirement::KeepAlive, Get}, PalletId, }; +use frame_system::ensure_signed; use log::info; use parity_scale_codec::Decode; use sp_core::H256; use sp_runtime::traits::Hash; - // Logger target const LOG: &str = "encointer"; +pub use crate::weights::WeightInfo; pub use pallet::*; +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; #[cfg(test)] mod mock; #[cfg(test)] mod tests; +mod weights; pub type BalanceOf = <::Currency as Currency<::AccountId>>::Balance; @@ -43,25 +47,102 @@ pub type BalanceOf = #[frame_support::pallet] pub mod pallet { use super::*; + use encointer_primitives::treasuries::SwapNativeOption; use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::OriginFor; #[pallet::pallet] pub struct Pallet(PhantomData); #[pallet::config] - pub trait Config: frame_system::Config { + pub trait Config: + frame_system::Config + pallet_encointer_balances::Config + pallet_timestamp::Config + { type RuntimeEvent: From> + IsType<::RuntimeEvent>; type Currency: Currency; /// The treasuries' pallet id, used for deriving sovereign account IDs per community. #[pallet::constant] type PalletId: Get; + + // /// the maximum fraction of available treasury funds a single swap can claim + // /// defined as divisor: 2 means half of the available funds can be swapped + // #[pallet::constant] + // type MaxFractionPerSwap: Get; + // + // /// the minimum period an account has to wait between two swaps + // #[pallet::constant] + // type SwapCooldownPeriod: Get; + + type WeightInfo: WeightInfo; } + #[pallet::storage] + #[pallet::getter(fn swap_native_options)] + pub type SwapNativeOptions = StorageDoubleMap< + _, + Blake2_128Concat, + CommunityIdentifier, + Blake2_128Concat, + T::AccountId, + SwapNativeOption, T::Moment>, + OptionQuery, + >; + #[pallet::call] + impl Pallet + where + sp_core::H256: From<::Hash>, + { + /// swap native tokens for community currency subject to an existing swap option for the + /// sender account. + #[pallet::call_index(0)] + #[pallet::weight((::WeightInfo::swap_native(), DispatchClass::Normal, Pays::Yes))] + pub fn swap_native( + origin: OriginFor, + cid: CommunityIdentifier, + desired_native_amount: BalanceOf, + ) -> DispatchResultWithPostInfo { + let sender = ensure_signed(origin)?; + let swap_option = + Self::swap_native_options(cid, &sender).ok_or(>::NoValidSwapOption)?; + ensure!( + swap_option.native_allowance >= desired_native_amount, + Error::::InsufficientAllowance + ); + let treasury_account = Self::get_community_treasury_account_unchecked(Some(cid)); + ensure!( + T::Currency::free_balance(&treasury_account) - T::Currency::minimum_balance() >= + desired_native_amount, + Error::::InsufficientNativeFunds + ); + let rate = swap_option.rate.ok_or(Error::::SwapRateNotDefined)?; + let cc_amount = BalanceType::from_num::( + desired_native_amount.try_into().or(Err(Error::::SwapOverflow))?, + ) + .checked_mul(rate) + .ok_or(Error::::SwapOverflow)?; + if swap_option.do_burn { + >::burn(cid, &sender, cc_amount)?; + } else { + >::do_transfer( + cid, + &sender, + &treasury_account, + cc_amount, + )?; + } + let new_swap_option = SwapNativeOption { + native_allowance: swap_option.native_allowance - desired_native_amount, + ..swap_option + }; + >::insert(cid, &sender, new_swap_option); + Self::do_spend_native(Some(cid), &sender, desired_native_amount)?; + Ok(().into()) + } + } impl Pallet where sp_core::H256: From<::Hash>, - T::AccountId: AsRef<[u8; 32]>, { pub fn get_community_treasury_account_unchecked( maybecid: Option, @@ -76,13 +157,28 @@ pub mod pallet { pub fn do_spend_native( maybecid: Option, - beneficiary: T::AccountId, + beneficiary: &T::AccountId, amount: BalanceOf, ) -> DispatchResultWithPostInfo { let treasury = Self::get_community_treasury_account_unchecked(maybecid); - T::Currency::transfer(&treasury, &beneficiary, amount, KeepAlive)?; + T::Currency::transfer(&treasury, beneficiary, amount, KeepAlive)?; info!(target: LOG, "treasury spent native: {:?}, {:?} to {:?}", maybecid, amount, beneficiary); - Self::deposit_event(Event::SpentNative { treasury, beneficiary, amount }); + Self::deposit_event(Event::SpentNative { + treasury, + beneficiary: beneficiary.clone(), + amount, + }); + Ok(().into()) + } + + /// store a swap option possibly replacing any previously existing option + pub fn do_issue_swap_native_option( + cid: CommunityIdentifier, + who: &T::AccountId, + option: SwapNativeOption, T::Moment>, + ) -> DispatchResultWithPostInfo { + SwapNativeOptions::::insert(cid, who, option); + Self::deposit_event(Event::GrantedSwapNativeOption { cid, who: who.clone() }); Ok(().into()) } } @@ -91,6 +187,24 @@ pub mod pallet { #[pallet::generate_deposit(pub (super) fn deposit_event)] pub enum Event { /// treasury spent native tokens from community `cid` to `beneficiary` amounting `amount` - SpentNative { treasury: T::AccountId, beneficiary: T::AccountId, amount: BalanceOf }, + SpentNative { + treasury: T::AccountId, + beneficiary: T::AccountId, + amount: BalanceOf, + }, + GrantedSwapNativeOption { + cid: CommunityIdentifier, + who: T::AccountId, + }, + } + + #[pallet::error] + pub enum Error { + /// no valid swap option. Either no option at all or insufficient allowance + NoValidSwapOption, + SwapRateNotDefined, + SwapOverflow, + InsufficientNativeFunds, + InsufficientAllowance, } } diff --git a/treasuries/src/mock.rs b/treasuries/src/mock.rs index 9c8f9159..0da03549 100644 --- a/treasuries/src/mock.rs +++ b/treasuries/src/mock.rs @@ -49,6 +49,7 @@ impl dut::Config for TestRuntime { type RuntimeEvent = RuntimeEvent; type Currency = pallet_balances::Pallet; type PalletId = TreasuriesPalletId; + type WeightInfo = (); } // boilerplate diff --git a/treasuries/src/tests.rs b/treasuries/src/tests.rs index 5b85736a..fb1c2b7b 100644 --- a/treasuries/src/tests.rs +++ b/treasuries/src/tests.rs @@ -17,9 +17,12 @@ //! Unit tests for the encointer_treasuries module. use super::*; -use crate::mock::{Balances, EncointerTreasuries, System}; -use frame_support::assert_ok; +use crate::mock::{Balances, EncointerBalances, EncointerTreasuries, RuntimeOrigin, System}; +use approx::assert_abs_diff_eq; +use encointer_primitives::treasuries::SwapNativeOption; +use frame_support::{assert_err, assert_ok}; use mock::{new_test_ext, TestRuntime}; +use rstest::rstest; use sp_core::crypto::Ss58Codec; use std::str::FromStr; use test_utils::{helpers::*, *}; @@ -38,7 +41,7 @@ fn treasury_spending_works() { let treasury = EncointerTreasuries::get_community_treasury_account_unchecked(Some(cid)); Balances::make_free_balance_be(&treasury, 500_000_000); - assert_ok!(EncointerTreasuries::do_spend_native(Some(cid), beneficiary.clone(), amount)); + assert_ok!(EncointerTreasuries::do_spend_native(Some(cid), &beneficiary, amount)); assert_eq!(Balances::free_balance(&treasury), 400_000_000); assert_eq!(Balances::free_balance(&beneficiary), amount); assert_eq!( @@ -47,7 +50,6 @@ fn treasury_spending_works() { ); }); } - #[test] fn treasury_getter_works() { new_test_ext().execute_with(|| { @@ -72,3 +74,222 @@ fn treasury_getter_works() { ) }); } +#[rstest(burn, case(false), case(true))] +fn swap_native_partial_works(burn: bool) { + new_test_ext().execute_with(|| { + System::set_block_number(System::block_number() + 1); // this is needed to assert events + let beneficiary = AccountId::from(AccountKeyring::Alice); + let native_allowance: BalanceOf = 110_000_000; + let rate_float = 0.000_000_2; + let rate = Some(BalanceType::from_num(rate_float)); + let cid = CommunityIdentifier::default(); + let swap_option: SwapNativeOption = SwapNativeOption { + cid, + native_allowance, + rate, + do_burn: burn, + valid_from: None, + valid_until: None, + }; + + let treasury = EncointerTreasuries::get_community_treasury_account_unchecked(Some(cid)); + Balances::make_free_balance_be(&treasury, 500_000_000); + EncointerBalances::issue(cid, &beneficiary, BalanceType::from_num(100)).unwrap(); + + assert_ok!(EncointerTreasuries::do_issue_swap_native_option( + cid, + &beneficiary, + swap_option + )); + assert_eq!(EncointerTreasuries::swap_native_options(cid, &beneficiary), Some(swap_option)); + + let swap_native_amount = 50_000_000; + assert_ok!(EncointerTreasuries::swap_native( + RuntimeOrigin::signed(beneficiary.clone()), + cid, + swap_native_amount + )); + + assert_eq!(Balances::free_balance(&treasury), 450_000_000); + assert_eq!(Balances::free_balance(&beneficiary), swap_native_amount); + assert_abs_diff_eq!( + EncointerBalances::balance(cid, &beneficiary).to_num::(), + 100.0 - f64::from(u32::try_from(swap_native_amount).unwrap()) * rate_float, + epsilon = 0.0001 + ); + // remaining allowance must decrease + assert_eq!( + EncointerTreasuries::swap_native_options(cid, &beneficiary) + .unwrap() + .native_allowance, + 60_000_000 + ); + assert!(event_deposited::( + Event::::SpentNative { + treasury: treasury.clone(), + beneficiary: beneficiary.clone(), + amount: swap_native_amount + } + .into() + )); + if burn { + assert!(event_deposited::( + pallet_encointer_balances::Event::::Burned( + cid, + beneficiary.clone(), + BalanceType::from_num(swap_native_amount) * rate.unwrap() + ) + .into() + )); + } else { + assert!(event_deposited::( + pallet_encointer_balances::Event::::Transferred( + cid, + beneficiary.clone(), + treasury.clone(), + BalanceType::from_num(swap_native_amount) * rate.unwrap() + ) + .into() + )); + } + }); +} +#[test] +fn swap_native_without_option_fails() { + new_test_ext().execute_with(|| { + System::set_block_number(System::block_number() + 1); // this is needed to assert events + let beneficiary = AccountId::from(AccountKeyring::Alice); + let cid = CommunityIdentifier::default(); + let swap_native_amount = 50_000_000; + assert_err!( + EncointerTreasuries::swap_native( + RuntimeOrigin::signed(beneficiary.clone()), + cid, + swap_native_amount + ), + Error::::NoValidSwapOption + ); + }); +} +#[rstest(burn, case(false), case(true))] +fn swap_native_insufficient_cc_fails(burn: bool) { + new_test_ext().execute_with(|| { + System::set_block_number(System::block_number() + 1); // this is needed to assert events + let beneficiary = AccountId::from(AccountKeyring::Alice); + let native_allowance: BalanceOf = 100_000_000; + let rate_float = 0.000_000_2; + let rate = Some(BalanceType::from_num(rate_float)); + let cid = CommunityIdentifier::default(); + let swap_option: SwapNativeOption = SwapNativeOption { + cid, + native_allowance, + rate, + do_burn: burn, + valid_from: None, + valid_until: None, + }; + + let treasury = EncointerTreasuries::get_community_treasury_account_unchecked(Some(cid)); + Balances::make_free_balance_be(&treasury, 51_000_000); + EncointerBalances::issue(cid, &beneficiary, BalanceType::from_num(1)).unwrap(); + + assert_ok!(EncointerTreasuries::do_issue_swap_native_option( + cid, + &beneficiary, + swap_option + )); + assert_eq!(EncointerTreasuries::swap_native_options(cid, &beneficiary), Some(swap_option)); + + let swap_native_amount = 50_000_000; + assert_err!( + EncointerTreasuries::swap_native( + RuntimeOrigin::signed(beneficiary.clone()), + cid, + swap_native_amount + ), + pallet_encointer_balances::Error::::BalanceTooLow + ); + }); +} + +#[rstest(burn, case(false), case(true))] +fn swap_native_insufficient_treasury_funds_fails(burn: bool) { + new_test_ext().execute_with(|| { + System::set_block_number(System::block_number() + 1); // this is needed to assert events + let beneficiary = AccountId::from(AccountKeyring::Alice); + let native_allowance: BalanceOf = 100_000_000; + let rate_float = 0.000_000_2; + let rate = Some(BalanceType::from_num(rate_float)); + let cid = CommunityIdentifier::default(); + let swap_option: SwapNativeOption = SwapNativeOption { + cid, + native_allowance, + rate, + do_burn: burn, + valid_from: None, + valid_until: None, + }; + + let treasury = EncointerTreasuries::get_community_treasury_account_unchecked(Some(cid)); + Balances::make_free_balance_be(&treasury, 49_000_000); + EncointerBalances::issue(cid, &beneficiary, BalanceType::from_num(1)).unwrap(); + + assert_ok!(EncointerTreasuries::do_issue_swap_native_option( + cid, + &beneficiary, + swap_option + )); + assert_eq!(EncointerTreasuries::swap_native_options(cid, &beneficiary), Some(swap_option)); + + let swap_native_amount = 50_000_000; + assert_err!( + EncointerTreasuries::swap_native( + RuntimeOrigin::signed(beneficiary.clone()), + cid, + swap_native_amount + ), + Error::::InsufficientNativeFunds + ); + }); +} + +#[rstest(burn, case(false), case(true))] +fn swap_native_insufficient_allowance_fails(burn: bool) { + new_test_ext().execute_with(|| { + System::set_block_number(System::block_number() + 1); // this is needed to assert events + let beneficiary = AccountId::from(AccountKeyring::Alice); + let native_allowance: BalanceOf = 49_000_000; + let rate_float = 0.000_000_2; + let rate = Some(BalanceType::from_num(rate_float)); + let cid = CommunityIdentifier::default(); + let swap_option: SwapNativeOption = SwapNativeOption { + cid, + native_allowance, + rate, + do_burn: burn, + valid_from: None, + valid_until: None, + }; + + let treasury = EncointerTreasuries::get_community_treasury_account_unchecked(Some(cid)); + Balances::make_free_balance_be(&treasury, 51_000_000); + EncointerBalances::issue(cid, &beneficiary, BalanceType::from_num(1)).unwrap(); + + assert_ok!(EncointerTreasuries::do_issue_swap_native_option( + cid, + &beneficiary, + swap_option + )); + assert_eq!(EncointerTreasuries::swap_native_options(cid, &beneficiary), Some(swap_option)); + + let swap_native_amount = 50_000_000; + assert_err!( + EncointerTreasuries::swap_native( + RuntimeOrigin::signed(beneficiary.clone()), + cid, + swap_native_amount + ), + Error::::InsufficientAllowance + ); + }); +} diff --git a/treasuries/src/weights.rs b/treasuries/src/weights.rs new file mode 100644 index 00000000..e51e7ef4 --- /dev/null +++ b/treasuries/src/weights.rs @@ -0,0 +1,30 @@ +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{ + traits::Get, + weights::{constants::RocksDbWeight, Weight}, +}; +use sp_std::marker::PhantomData; + +pub trait WeightInfo { + fn swap_native() -> Weight; +} + +pub struct EncointerWeight(PhantomData); +impl WeightInfo for EncointerWeight { + fn swap_native() -> Weight { + Weight::from_parts(55_100_000, 0) + .saturating_add(T::DbWeight::get().reads(10)) + .saturating_add(T::DbWeight::get().writes(3)) + } +} + +// For tests +impl WeightInfo for () { + fn swap_native() -> Weight { + Weight::from_parts(5_100_000, 0) + .saturating_add(RocksDbWeight::get().reads(10)) + .saturating_add(RocksDbWeight::get().writes(3)) + } +}