Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feat] Fee Handler #161

Merged
merged 11 commits into from
Dec 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
stanly-johnson marked this conversation as resolved.
Show resolved Hide resolved
&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)
stanly-johnson marked this conversation as resolved.
Show resolved Hide resolved
})
);
// 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