From 9988e60ff73e31e8ddbba07c536042415c15b1d0 Mon Sep 17 00:00:00 2001 From: stanly-johnson Date: Wed, 22 Dec 2021 19:06:51 +0000 Subject: [PATCH] [feat] Fee Handler (#161) --- pallets/payment/src/lib.rs | 37 ++++++++++--- pallets/payment/src/mock.rs | 13 +++++ pallets/payment/src/tests.rs | 100 +++++++++++++++++++++++++++++++---- pallets/payment/src/types.rs | 24 ++++++++- runtime/src/lib.rs | 9 ++++ 5 files changed, 164 insertions(+), 19 deletions(-) diff --git a/pallets/payment/src/lib.rs b/pallets/payment/src/lib.rs index 45b6da6a..f7a59765 100644 --- a/pallets/payment/src/lib.rs +++ b/pallets/payment/src/lib.rs @@ -15,7 +15,9 @@ mod types; #[frame_support::pallet] pub mod pallet { - pub use crate::types::{DisputeResolver, PaymentDetail, PaymentHandler, PaymentState}; + pub use crate::types::{ + DisputeResolver, FeeHandler, PaymentDetail, PaymentHandler, PaymentState, + }; use frame_support::{dispatch::DispatchResultWithPostInfo, pallet_prelude::*}; use frame_system::pallet_prelude::*; use orml_traits::{MultiCurrency, MultiReservableCurrency}; @@ -34,7 +36,9 @@ pub mod pallet { type Asset: MultiReservableCurrency; /// Dispute resolution account type DisputeResolver: DisputeResolver; - + /// Fee handler trait + type FeeHandler: FeeHandler; + /// Incentive percentage - amount witheld from sender #[pallet::constant] type IncentivePercentage: Get; } @@ -177,12 +181,15 @@ pub mod pallet { recipient.clone(), |maybe_payment| -> DispatchResult { let incentive_amount = T::IncentivePercentage::get() * amount; + let (fee_recipient, fee_percent) = T::FeeHandler::apply_fees(&from, &recipient); + let fee_amount = fee_percent * amount; let new_payment = Some(PaymentDetail { asset, amount, incentive_amount, state: PaymentState::Created, resolver_account: T::DisputeResolver::get_origin(), + fee_detail: (fee_recipient, fee_amount), }); match maybe_payment { Some(x) => { @@ -193,8 +200,8 @@ pub mod pallet { x.state != PaymentState::Created, Error::::PaymentAlreadyInProcess ); - // reserve the incentive amount from the payment creator - T::Asset::reserve(asset, &from, incentive_amount)?; + // reserve the incentive + fees amount from the payment creator + T::Asset::reserve(asset, &from, incentive_amount + fee_amount)?; // transfer amount to recipient T::Asset::transfer(asset, &from, &recipient, amount)?; // reserved the amount in the recipient account @@ -203,7 +210,7 @@ pub mod pallet { }, None => { // reserve the incentive amount from the payment creator - T::Asset::reserve(asset, &from, incentive_amount)?; + T::Asset::reserve(asset, &from, incentive_amount + fee_amount)?; // transfer amount to recipient T::Asset::transfer(asset, &from, &recipient, amount)?; // reserved the amount in the recipient account @@ -230,10 +237,20 @@ pub mod pallet { // ensure the payment is in created state ensure!(payment.state == Created, Error::::PaymentAlreadyReleased); // unreserve the incentive amount back to the creator - T::Asset::unreserve(payment.asset, &from, payment.incentive_amount); + T::Asset::unreserve( + payment.asset, + &from, + payment.incentive_amount + payment.fee_detail.1, + ); // unreserve the amount to the recipent T::Asset::unreserve(payment.asset, &to, payment.amount); - + // transfer fee amount to marketplace + T::Asset::transfer( + payment.asset, + &from, // fee is paid by payment creator + &payment.fee_detail.0, // account of fee recipient + payment.fee_detail.1, // amount of fee + )?; payment.state = PaymentState::Released; Ok(()) @@ -261,7 +278,11 @@ pub mod pallet { Error::::PaymentAlreadyReleased ); // unreserve the incentive amount from the owner account - T::Asset::unreserve(payment.asset, &from, payment.incentive_amount); + T::Asset::unreserve( + payment.asset, + &from, + payment.incentive_amount + payment.fee_detail.1, + ); T::Asset::unreserve(payment.asset, &to, payment.amount); // transfer amount to creator match T::Asset::transfer(payment.asset, &to, &from, payment.amount) { diff --git a/pallets/payment/src/mock.rs b/pallets/payment/src/mock.rs index e22ce942..9fdb5d47 100644 --- a/pallets/payment/src/mock.rs +++ b/pallets/payment/src/mock.rs @@ -19,6 +19,8 @@ pub const PAYMENT_CREATOR: AccountId = 10; pub const PAYMENT_RECIPENT: AccountId = 11; pub const CURRENCY_ID: u32 = 1u32; pub const RESOLVER_ACCOUNT: AccountId = 12; +pub const FEE_RECIPIENT_ACCOUNT: AccountId = 20; +pub const PAYMENT_RECIPENT_FEE_CHARGED: AccountId = 21; frame_support::construct_runtime!( pub enum Test where @@ -98,6 +100,16 @@ impl crate::types::DisputeResolver for MockDisputeResolver { } } +pub struct MockFeeHandler; +impl crate::types::FeeHandler for MockFeeHandler { + fn apply_fees(_from: &AccountId, to: &AccountId) -> (AccountId, Percent) { + match to { + &PAYMENT_RECIPENT_FEE_CHARGED => (FEE_RECIPIENT_ACCOUNT, Percent::from_percent(10)), + _ => (FEE_RECIPIENT_ACCOUNT, Percent::from_percent(0)), + } + } +} + parameter_types! { pub const IncentivePercentage: Percent = Percent::from_percent(10); } @@ -107,6 +119,7 @@ impl payment::Config for Test { type Asset = Tokens; type DisputeResolver = MockDisputeResolver; type IncentivePercentage = IncentivePercentage; + type FeeHandler = MockFeeHandler; } // Build genesis storage according to the mock runtime. diff --git a/pallets/payment/src/tests.rs b/pallets/payment/src/tests.rs index 68906855..39c2d3e1 100644 --- a/pallets/payment/src/tests.rs +++ b/pallets/payment/src/tests.rs @@ -27,7 +27,8 @@ fn test_create_payment_works() { amount: 20, incentive_amount: 2, state: PaymentState::Created, - resolver_account: RESOLVER_ACCOUNT + resolver_account: RESOLVER_ACCOUNT, + fee_detail: (FEE_RECIPIENT_ACCOUNT, 0) }) ); // the payment amount should be reserved correctly @@ -52,14 +53,15 @@ fn test_create_payment_works() { amount: 20, incentive_amount: 2, state: PaymentState::Created, - resolver_account: RESOLVER_ACCOUNT + resolver_account: RESOLVER_ACCOUNT, + fee_detail: (FEE_RECIPIENT_ACCOUNT, 0) }) ); }); } #[test] -fn test_release_payment_works() { +fn test_cancel_payment_works() { new_test_ext().execute_with(|| { // should be able to create a payment with available balance assert_ok!(Payment::create( @@ -75,7 +77,8 @@ fn test_release_payment_works() { amount: 40, incentive_amount: 4, state: PaymentState::Created, - resolver_account: RESOLVER_ACCOUNT + resolver_account: RESOLVER_ACCOUNT, + fee_detail: (FEE_RECIPIENT_ACCOUNT, 0) }) ); // the payment amount should be reserved @@ -103,7 +106,8 @@ fn test_release_payment_works() { amount: 40, incentive_amount: 4, state: PaymentState::Cancelled, - resolver_account: RESOLVER_ACCOUNT + resolver_account: RESOLVER_ACCOUNT, + fee_detail: (FEE_RECIPIENT_ACCOUNT, 0) }) ); // cannot call cancel again @@ -115,7 +119,7 @@ fn test_release_payment_works() { } #[test] -fn test_cancel_payment_works() { +fn test_release_payment_works() { new_test_ext().execute_with(|| { // should be able to create a payment with available balance assert_ok!(Payment::create( @@ -131,7 +135,8 @@ fn test_cancel_payment_works() { amount: 40, incentive_amount: 4, state: PaymentState::Created, - resolver_account: RESOLVER_ACCOUNT + resolver_account: RESOLVER_ACCOUNT, + fee_detail: (FEE_RECIPIENT_ACCOUNT, 0) }) ); // the payment amount should be reserved @@ -153,7 +158,8 @@ fn test_cancel_payment_works() { amount: 40, incentive_amount: 4, state: PaymentState::Released, - resolver_account: RESOLVER_ACCOUNT + resolver_account: RESOLVER_ACCOUNT, + fee_detail: (FEE_RECIPIENT_ACCOUNT, 0) }) ); // cannot call release again @@ -218,7 +224,8 @@ fn test_set_state_payment_works() { amount: 40, incentive_amount: 4, state: PaymentState::Released, - resolver_account: RESOLVER_ACCOUNT + resolver_account: RESOLVER_ACCOUNT, + fee_detail: (FEE_RECIPIENT_ACCOUNT, 0) }) ); @@ -250,8 +257,81 @@ fn test_set_state_payment_works() { amount: 40, incentive_amount: 4, state: PaymentState::Cancelled, - resolver_account: RESOLVER_ACCOUNT + resolver_account: RESOLVER_ACCOUNT, + fee_detail: (FEE_RECIPIENT_ACCOUNT, 0) + }) + ); + }); +} + +#[test] +fn test_charging_fee_payment_works() { + new_test_ext().execute_with(|| { + // should be able to create a payment with available balance + assert_ok!(Payment::create( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT_FEE_CHARGED, + CURRENCY_ID, + 40, + )); + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT_FEE_CHARGED), + Some(PaymentDetail { + asset: CURRENCY_ID, + amount: 40, + incentive_amount: 4, + state: PaymentState::Created, + resolver_account: RESOLVER_ACCOUNT, + fee_detail: (FEE_RECIPIENT_ACCOUNT, 4) + }) + ); + // the payment amount should be reserved + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 52); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), 0); + + // should succeed for valid payment + assert_ok!(Payment::release(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT_FEE_CHARGED)); + // the payment amount should be transferred + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 56); + assert_eq!(Tokens::total_balance(CURRENCY_ID, &PAYMENT_CREATOR), 56); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), 40); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &FEE_RECIPIENT_ACCOUNT), 4); + assert_eq!(Tokens::total_issuance(CURRENCY_ID), 100); + }); +} + +#[test] +fn test_charging_fee_payment_works_when_canceled() { + new_test_ext().execute_with(|| { + // should be able to create a payment with available balance + assert_ok!(Payment::create( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT_FEE_CHARGED, + CURRENCY_ID, + 40, + )); + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT_FEE_CHARGED), + Some(PaymentDetail { + asset: CURRENCY_ID, + amount: 40, + incentive_amount: 4, + state: PaymentState::Created, + resolver_account: RESOLVER_ACCOUNT, + fee_detail: (FEE_RECIPIENT_ACCOUNT, 4) }) ); + // the payment amount should be reserved + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 52); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), 0); + + // should succeed for valid payment + assert_ok!(Payment::cancel(Origin::signed(PAYMENT_RECIPENT_FEE_CHARGED), PAYMENT_CREATOR)); + // the payment amount should be transferred + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); + assert_eq!(Tokens::total_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), 0); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &FEE_RECIPIENT_ACCOUNT), 0); + assert_eq!(Tokens::total_issuance(CURRENCY_ID), 100); }); } diff --git a/pallets/payment/src/types.rs b/pallets/payment/src/types.rs index c4a3aa4c..05457e2a 100644 --- a/pallets/payment/src/types.rs +++ b/pallets/payment/src/types.rs @@ -1,23 +1,39 @@ #![allow(unused_qualifications)] use parity_scale_codec::{Decode, Encode}; use scale_info::TypeInfo; -use sp_runtime::DispatchResult; +use sp_runtime::{DispatchResult, Percent}; +/* +The PaymentDetail struct stores information about the payment/escrow +A "payment" in virto network is similar to an escrow, it is used to guarantee proof of funds +and can be released once an agreed upon condition has reached between the payment creator +and recipient. The payment lifecycle is tracked using the state field. +*/ #[derive(Encode, Decode, Debug, Clone, PartialEq, Eq, TypeInfo)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct PaymentDetail { + /// type of asset used for payment pub asset: Asset, + /// amount of asset used for payment pub amount: Amount, + /// incentive amount that is credited to creator for resolving pub incentive_amount: Amount, + /// enum to track payment lifecycle [Created, Released, Cancelled, NeedsReview] pub state: PaymentState, + /// account that can settle any disputes created in the payment pub resolver_account: Account, + /// fee charged and recipient account details + pub fee_detail: (Account, Amount), } #[derive(Encode, Decode, Debug, Clone, PartialEq, Eq, TypeInfo)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum PaymentState { + /// Amounts have been reserved and waiting for release/cancel Created, + /// Payment has been completed and amount transferred Released, + /// All funds unreserved and sent to original owners Cancelled, /// A judge needs to review and release manually NeedsReview, @@ -59,3 +75,9 @@ pub trait DisputeResolver { /// Get a DisputeResolver (Judge) account fn get_origin() -> Account; } + +/// Fee Handler trait that defines how to handle marketplace fees to every payment/swap +pub trait FeeHandler { + /// Get the distribution of fees to marketplace participants + fn apply_fees(from: &Account, to: &Account) -> (Account, Percent); +} diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 74aa1172..5cebce18 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -602,6 +602,14 @@ impl virto_payment::DisputeResolver for VirtoDisputeResolver { } } +pub struct VirtoFeeHandler; +impl virto_payment::FeeHandler for VirtoFeeHandler { + fn apply_fees(_from: &AccountId, _to: &AccountId) -> (AccountId, Percent) { + const VIRTO_MARKETPLACE_FEE_PERCENT: Percent = Percent::from_percent(0); + (Sudo::key(), VIRTO_MARKETPLACE_FEE_PERCENT) + } +} + parameter_types! { pub const IncentivePercentage: Percent = Percent::from_percent(10); } @@ -611,6 +619,7 @@ impl virto_payment::Config for Runtime { type Asset = Assets; type DisputeResolver = VirtoDisputeResolver; type IncentivePercentage = IncentivePercentage; + type FeeHandler = VirtoFeeHandler; } parameter_types! {