Skip to content
Open
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
13 changes: 13 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ fc-traits-nonfungibles-helpers = { path = "./traits/nonfungibles-helpers", defau
fc-traits-payments = { path = "./traits/payments", default-features = false }
fc-pallet-listings = { path = "./pallets/listings", default-features = false }
fc-pallet-payments = { path = "./pallets/payments", default-features = false }
fc-pallet-fees = { path = "./pallets/fees", default-features = false }
fc-pallet-referenda-tracks = { path = "./pallets/referenda-tracks", default-features = false }
mock-helpers = { path = "./mock-helpers", default-features = false }

Expand Down
41 changes: 41 additions & 0 deletions pallets/fees/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
[package]
authors.workspace = true
edition.workspace = true
license.workspace = true
name = "fc-pallet-fees"
repository.workspace = true
version = "0.1.0"

[dependencies]
codec.workspace = true
frame.workspace = true
scale-info.workspace = true
sp-runtime.workspace = true

[dev-dependencies]
pallet-assets.workspace = true
pallet-balances.workspace = true
sp-io.workspace = true

[features]
default = ["std"]
runtime-benchmarks = [
"frame/runtime-benchmarks",
"pallet-assets/runtime-benchmarks",
"pallet-balances/runtime-benchmarks",
"sp-runtime/runtime-benchmarks",
]
std = [
"codec/std",
"frame/std",
"pallet-assets/std",
"pallet-balances/std",
"scale-info/std",
"sp-io/std",
"sp-runtime/std",
]
try-runtime = [
"frame/try-runtime",
"pallet-assets/try-runtime",
"pallet-balances/try-runtime",
]
141 changes: 141 additions & 0 deletions pallets/fees/src/adapter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
use core::marker::PhantomData;
use frame::deps::frame_support::traits::{
fungibles::{self, Dust},
tokens::{DepositConsequence, Fortitude, Preservation, Provenance, WithdrawConsequence},
};
use sp_runtime::{traits::Zero, DispatchError, Saturating};

use crate::{
types::{AssetIdOf, BalanceOf},
Config, Event, Pallet,
};

/// A fungibles adapter that charges community and protocol fees on transfers.
///
/// Wraps the inner `T::Assets` implementation. All read operations are delegated;
/// `transfer` is intercepted to charge configured fees on top.
pub struct WithFees<T>(PhantomData<T>);

// ---------------------------------------------------------------------------
// fungibles::Inspect — pure delegation
// ---------------------------------------------------------------------------
impl<T: Config> fungibles::Inspect<T::AccountId> for WithFees<T> {
type AssetId = AssetIdOf<T>;
type Balance = BalanceOf<T>;

fn total_issuance(asset: AssetIdOf<T>) -> BalanceOf<T> {
T::Assets::total_issuance(asset)
}

fn active_issuance(asset: AssetIdOf<T>) -> BalanceOf<T> {
T::Assets::active_issuance(asset)
}

fn minimum_balance(asset: AssetIdOf<T>) -> BalanceOf<T> {
T::Assets::minimum_balance(asset)
}

fn total_balance(asset: AssetIdOf<T>, who: &T::AccountId) -> BalanceOf<T> {
T::Assets::total_balance(asset, who)
}

fn balance(asset: AssetIdOf<T>, who: &T::AccountId) -> BalanceOf<T> {
T::Assets::balance(asset, who)
}

fn reducible_balance(
asset: AssetIdOf<T>,
who: &T::AccountId,
preservation: Preservation,
force: Fortitude,
) -> BalanceOf<T> {
T::Assets::reducible_balance(asset, who, preservation, force)
}

fn can_deposit(
asset: AssetIdOf<T>,
who: &T::AccountId,
amount: BalanceOf<T>,
provenance: Provenance,
) -> DepositConsequence {
T::Assets::can_deposit(asset, who, amount, provenance)
}

fn can_withdraw(
asset: AssetIdOf<T>,
who: &T::AccountId,
amount: BalanceOf<T>,
) -> WithdrawConsequence<BalanceOf<T>> {
T::Assets::can_withdraw(asset, who, amount)
}

fn asset_exists(asset: AssetIdOf<T>) -> bool {
T::Assets::asset_exists(asset)
}
}

// ---------------------------------------------------------------------------
// fungibles::Unbalanced — pure delegation
// ---------------------------------------------------------------------------
impl<T: Config> fungibles::Unbalanced<T::AccountId> for WithFees<T> {
fn handle_dust(dust: Dust<T::AccountId, Self>) {
T::Assets::handle_dust(Dust(dust.0, dust.1));
}

fn write_balance(
asset: AssetIdOf<T>,
who: &T::AccountId,
amount: BalanceOf<T>,
) -> Result<Option<BalanceOf<T>>, DispatchError> {
T::Assets::write_balance(asset, who, amount)
}

fn set_total_issuance(asset: AssetIdOf<T>, amount: BalanceOf<T>) {
T::Assets::set_total_issuance(asset, amount)
}
}

// ---------------------------------------------------------------------------
// fungibles::Mutate — intercept `transfer` to charge fees on top
// ---------------------------------------------------------------------------
impl<T: Config> fungibles::Mutate<T::AccountId> for WithFees<T>
where
T::AccountId: Eq,
{
fn transfer(
asset: AssetIdOf<T>,
source: &T::AccountId,
dest: &T::AccountId,
amount: BalanceOf<T>,
preservation: Preservation,
) -> Result<BalanceOf<T>, DispatchError> {
let fees = Pallet::<T>::calculate_fees(asset.clone(), source, amount);

// Charge fees on top — each fee goes from source to the fee beneficiary.
// Uses the inner implementation directly to avoid recursion.
for (beneficiary, fee_amount) in &fees {
T::Assets::transfer(
asset.clone(),
source,
beneficiary,
*fee_amount,
Preservation::Preserve,
)?;
}

let total_fees: BalanceOf<T> = fees
.iter()
.map(|(_, a)| *a)
.fold(BalanceOf::<T>::zero(), |acc, x| acc.saturating_add(x));

if !total_fees.is_zero() {
Pallet::<T>::deposit_event(Event::FeesCharged {
who: source.clone(),
total_fees,
});
}

// Execute the original transfer
T::Assets::transfer(asset, source, dest, amount, preservation)
}
}
146 changes: 146 additions & 0 deletions pallets/fees/src/extensions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
use super::*;
use core::marker::PhantomData;

use codec::{Decode, DecodeWithMemTracking, Encode};
use frame::deps::{
frame_support::{
dispatch::DispatchInfo,
traits::{fungibles, tokens::Preservation},
},
frame_system,
};
use scale_info::TypeInfo;
use sp_runtime::{
traits::{
DispatchInfoOf, DispatchOriginOf, Implication, PostDispatchInfoOf, TransactionExtension,
ValidateResult,
},
transaction_validity::{InvalidTransaction, TransactionSource, ValidTransaction},
};

use crate::types::{AssetIdOf, BalanceOf};

/// Transaction extension that charges community and protocol fees
/// on direct pallet-assets calls detected by `T::CallInspector`.
///
/// Fees are charged in `prepare` (before the call executes) so
/// the sender's balance is reduced before the transfer runs.
#[derive(Encode, Decode, DecodeWithMemTracking, Clone, Eq, PartialEq, TypeInfo)]
#[scale_info(skip_type_params(T))]
pub struct ChargeFees<T: Config>(#[codec(skip)] PhantomData<T>);

impl<T: Config> Default for ChargeFees<T> {
fn default() -> Self {
Self(PhantomData)
}
}

impl<T: Config> core::fmt::Debug for ChargeFees<T> {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
write!(f, "ChargeFees")
}
}

impl<T> TransactionExtension<T::RuntimeCall> for ChargeFees<T>
where
T: Config + Send + Sync,
T::RuntimeCall: Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>,
{
const IDENTIFIER: &'static str = "ChargeFees";
type Implicit = ();
type Val = Option<(AssetIdOf<T>, BalanceOf<T>, T::AccountId)>;
type Pre = ();

fn weight(&self, _: &T::RuntimeCall) -> Weight {
// Reads: ProtocolFees (1), CommunityFees (1), asset balance (1), community detection
// Writes: per-fee transfer (up to MaxProtocolFees + MaxCommunityFees)
Weight::from_parts(15_000_000, 0)
}

fn validate(
&self,
origin: DispatchOriginOf<T::RuntimeCall>,
call: &T::RuntimeCall,
_info: &DispatchInfoOf<T::RuntimeCall>,
_len: usize,
_self_implicit: Self::Implicit,
_inherited_implication: &impl Implication,
_source: TransactionSource,
) -> ValidateResult<Self::Val, T::RuntimeCall> {
// Only inspect signed transactions
let who = match origin.clone().into() {
Ok(frame_system::RawOrigin::Signed(ref who)) => who.clone(),
_ => return Ok((ValidTransaction::default(), None, origin)),
};

// Check if the call is an asset operation
let Some((asset, amount)) = T::CallInspector::extract_asset_transfer(call) else {
return Ok((ValidTransaction::default(), None, origin));
};

// Calculate fees and verify sender can afford transfer + fees
let fees = Pallet::<T>::calculate_fees(asset.clone(), &who, amount);
let total_fees = fees
.iter()
.map(|(_, a)| *a)
.fold(BalanceOf::<T>::zero(), |a, b| a.saturating_add(b));

if !total_fees.is_zero() {
let balance =
<T::Assets as fungibles::Inspect<T::AccountId>>::balance(asset.clone(), &who);
if balance < amount.saturating_add(total_fees) {
return Err(InvalidTransaction::Payment.into());
}
}

Ok((
ValidTransaction::default(),
Some((asset, amount, who)),
origin,
))
}

fn prepare(
self,
val: Self::Val,
_origin: &DispatchOriginOf<T::RuntimeCall>,
_call: &T::RuntimeCall,
_info: &DispatchInfoOf<T::RuntimeCall>,
_len: usize,
) -> Result<Self::Pre, sp_runtime::transaction_validity::TransactionValidityError> {
let Some((asset, amount, who)) = val else {
return Ok(());
};

let fees = Pallet::<T>::calculate_fees(asset.clone(), &who, amount);
let mut total_fees = BalanceOf::<T>::zero();

for (beneficiary, fee_amount) in fees {
<T::Assets as fungibles::Mutate<T::AccountId>>::transfer(
asset.clone(),
&who,
&beneficiary,
fee_amount,
Preservation::Preserve,
)
.map_err(|_| InvalidTransaction::Payment)?;
total_fees = total_fees.saturating_add(fee_amount);
}

if !total_fees.is_zero() {
Pallet::<T>::deposit_event(Event::FeesCharged { who, total_fees });
}

Ok(())
}

fn post_dispatch_details(
_pre: Self::Pre,
_info: &DispatchInfoOf<T::RuntimeCall>,
_post_info: &PostDispatchInfoOf<T::RuntimeCall>,
_len: usize,
_result: &DispatchResult,
) -> Result<Weight, sp_runtime::transaction_validity::TransactionValidityError> {
Ok(Weight::zero())
}
}
Loading
Loading