Skip to content

Commit

Permalink
[feat] Fee Handler (#161)
Browse files Browse the repository at this point in the history
  • Loading branch information
stanly-johnson authored Dec 22, 2021
1 parent b50d02d commit 9988e60
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 19 deletions.
37 changes: 29 additions & 8 deletions pallets/payment/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -34,7 +36,9 @@ pub mod pallet {
type Asset: MultiReservableCurrency<Self::AccountId>;
/// Dispute resolution account
type DisputeResolver: DisputeResolver<Self::AccountId>;

/// Fee handler trait
type FeeHandler: FeeHandler<Self::AccountId>;
/// Incentive percentage - amount witheld from sender
#[pallet::constant]
type IncentivePercentage: Get<Percent>;
}
Expand Down Expand Up @@ -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) => {
Expand All @@ -193,8 +200,8 @@ pub mod pallet {
x.state != PaymentState::Created,
Error::<T>::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
Expand All @@ -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
Expand All @@ -230,10 +237,20 @@ pub mod pallet {
// ensure the payment is in created state
ensure!(payment.state == Created, Error::<T>::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(())
Expand Down Expand Up @@ -261,7 +278,11 @@ pub mod pallet {
Error::<T>::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) {
Expand Down
13 changes: 13 additions & 0 deletions pallets/payment/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -98,6 +100,16 @@ impl crate::types::DisputeResolver<AccountId> for MockDisputeResolver {
}
}

pub struct MockFeeHandler;
impl crate::types::FeeHandler<AccountId> 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);
}
Expand All @@ -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.
Expand Down
100 changes: 90 additions & 10 deletions pallets/payment/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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(
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
})
);

Expand Down Expand Up @@ -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::<Test>::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::<Test>::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);
});
}
24 changes: 23 additions & 1 deletion pallets/payment/src/types.rs
Original file line number Diff line number Diff line change
@@ -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<Asset, Amount, Account> {
/// 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,
Expand Down Expand Up @@ -59,3 +75,9 @@ pub trait DisputeResolver<Account> {
/// 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<Account> {
/// Get the distribution of fees to marketplace participants
fn apply_fees(from: &Account, to: &Account) -> (Account, Percent);
}
9 changes: 9 additions & 0 deletions runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,14 @@ impl virto_payment::DisputeResolver<AccountId> for VirtoDisputeResolver {
}
}

pub struct VirtoFeeHandler;
impl virto_payment::FeeHandler<AccountId> 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);
}
Expand All @@ -611,6 +619,7 @@ impl virto_payment::Config for Runtime {
type Asset = Assets;
type DisputeResolver = VirtoDisputeResolver;
type IncentivePercentage = IncentivePercentage;
type FeeHandler = VirtoFeeHandler;
}

parameter_types! {
Expand Down

0 comments on commit 9988e60

Please sign in to comment.