pallet_proxy_bonding/
lib.rs

1// Polimec Blockchain – https://www.polimec.org/
2// Copyright (C) Polimec 2022. All rights reserved.
3
4// The Polimec Blockchain is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// The Polimec Blockchain is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with this program.  If not, see <https://www.gnu.org/licenses/>.
16
17//! # Proxy Bonding Pallet
18//!
19//! A FRAME pallet that facilitates token bonding operations with fee management capabilities. This pallet allows users to bond tokens from a configurable account (we call Treasury) while paying fees in various assets.
20//! This pallet is intended to be used as an alternative to a direct bonding mechanism. In this way, the user does not need to own or hold the tokens, but can still participate in various activities by paying a fee.
21//!
22//! ## Overview
23//!
24//! The Bonding Pallet provides functionality to:
25//! - Bond treasury tokens on behalf of users
26//! - Pay a bonding fee in different assets (e.g., DOT)
27//! - Set the bond release to either immediate refund or time-locked release
28//!
29//! ## Features
30//!
31//! ### Token Bonding
32//! - Bond tokens from a treasury account into sub-accounts
33//! - Support for existential deposit management
34//! - Hold-based bonding mechanism using runtime-defined hold reasons
35//!
36//! ### Fee Management
37//! - Accept fees in configurable assets (e.g., DOT)
38//! - Calculate fees based on bond amount and current token prices
39//! - Support both fee refunds and fee transfers to recipients
40//! - Percentage-based fee calculation in USD terms
41//!
42//! ### Release Mechanisms
43//! Two types of release mechanisms are supported:
44//! - Immediate refund: Bonds can be immediately returned to treasury, and fees await refunding to users.
45//! - Time-locked release: Bonds are locked until a specific block number, and fees can be sent to the configured fee recipient.
46
47//! ## Extrinsics
48//! - [transfer_bonds_back_to_treasury](crate::pallet::Pallet::transfer_bonds_back_to_treasury): Transfer bonded tokens back to the treasury when release conditions are met.
49//! - [transfer_fees_to_recipient](crate::pallet::Pallet::transfer_fees_to_recipient): Transfer collected fees to the designated fee recipient.
50//!
51//! ## Public Functions
52//! - [`calculate_fee`](crate::pallet::Pallet::calculate_fee): Calculate the fee amount in the specified fee asset based on the bond amount.
53//! - [`get_bonding_account`](crate::pallet::Pallet::get_bonding_account): Get the sub-account used for bonding based on a u32.
54//! - [`bond_on_behalf_of`](crate::pallet::Pallet::bond_on_behalf_of): Bond tokens from the treasury into a sub-account on behalf of a user.
55//! - [`set_release_type`](crate::pallet::Pallet::set_release_type): Set the release type for a given derivation path and hold reason.
56//! - [`refund_fee`](crate::pallet::Pallet::refund_fee): Refund the fee to the specified account (only if the release is set to `Refunded`).
57//!
58
59//!
60//! ### Example Configuration (Similar on how it's configured on the Polimec Runtime)
61//!
62//! ```rust,compile_fail
63//! parameter_types! {
64//! 	// Fee is defined as 1.5% of the USD Amount. Since fee is applied to the PLMC amount, and that is always 5 times
65//! 	// less than the usd_amount (multiplier of 5), we multiply the 1.5 by 5 to get 7.5%
66//! 	pub FeePercentage: Perbill = Perbill::from_rational(75u32, 1000u32);
67//! 	pub FeeRecipient: AccountId =  AccountId::from(hex_literal::hex!("3ea952b5fa77f4c67698e79fe2d023a764a41aae409a83991b7a7bdd9b74ab56"));
68//! 	pub RootId: PalletId = PalletId(*b"treasury");
69//! }
70//!
71//! impl pallet_proxy_bonding::Config for Runtime {
72//! 	type BondingToken = Balances; // The Balances pallet is used for the bonding token
73//! 	type BondingTokenDecimals = ConstU8<10>; // The PLMC token has 10 decimals
74//! 	type BondingTokenId = ConstU32<X>; // TODO: Replace with a proper number and explanation.
75//! 	type FeePercentage = FeePercentage; // The fee kept by the treasury
76//! 	type FeeRecipient = FeeRecipient; // THe account that receives the fee
77//! 	type FeeToken = ForeignAssets; // The Asset pallet is used for the fee token
78//! 	type Id = PalletId; // The ID type used for the ... account
79//! 	type PriceProvider = OraclePriceProvider<AssetId, Price, Oracle>; // The Oracle pallet is used for the price provider
80//! 	type RootId = TreasuryId; // The treasury account ID
81//! 	type Treasury = TreasuryAccount; // The treasury account
82//! 	type UsdDecimals = ConstU8<X>; // TODO: Replace with a proper number and explanation.
83//! 	type RuntimeEvent = RuntimeEvent;
84//! 	type RuntimeHoldReason = RuntimeHoldReason;
85//! }
86//! ```
87
88
89//! ## Example integration
90//!
91//! The Proxy Bonding Pallet work seamlessly with the Funding Pallet to handle OTM (One-Token-Model) participation modes in project funding. Here's how the integration works:
92//!
93//! ### Funding Pallet Flow
94//! 1. When a user contributes to a project using OTM mode:
95//! - The Funding Pallet calls `bond_on_behalf_of` with:
96//! - Project ID as the derivation path
97//! - User's account
98//! - PLMC bond amount
99//! - Funding asset ID
100//! - Participation hold reason
101//!
102//! 2. During project settlement phase:
103//! - For successful projects:
104//! - An OTM release type is set with a time-lock based on the multiplier
105//! - Bonds remain locked until the vesting duration completes
106//! - For failed projects:
107//! - Release type is set to `Refunded`
108//! - Allows immediate return of bonds to treasury
109//! - Enables fee refunds to participants
110//!
111//! ### Key Integration
112//! ```rust,compile_fail
113//! // In Funding Pallet
114//! pub fn bond_plmc_with_mode(
115//! 	who: &T::AccountId,
116//! 	project_id: ProjectId,
117//! 	amount: Balance,
118//! 	mode: ParticipationMode,
119//! 	asset: AcceptedFundingAsset,
120//! ) -> DispatchResult {
121//! 	match mode {
122//! 		ParticipationMode::OTM => pallet_proxy_bonding::Pallet::<T>::bond_on_behalf_of(
123//! 			project_id,
124//! 			who.clone(),
125//! 			amount,
126//! 			asset.id(),
127//! 			HoldReason::Participation.into(),
128//! 		),
129//! 		ParticipationMode::Classic(_) => // ... other handling
130//! 	}
131//! }
132//! ```
133//!
134//! ### Settlement Process
135//! The settlement process determines the release conditions for bonded tokens:
136//! - Success: Tokens remain locked with a time-based release schedule
137//! - Failure: Tokens are marked for immediate return to treasury with fee refunds
138//!
139//! ## License
140//!
141//! License: GPL-3.0
142
143#![cfg_attr(not(feature = "std"), no_std)]
144// Needed due to empty sections raising the warning
145
146
147#![allow(unreachable_patterns)]
148pub use pallet::*;
149
150mod functions;
151
152#[cfg(test)]
153mod mock;
154#[cfg(test)]
155mod tests;
156
157#[frame_support::pallet]
158pub mod pallet {
159	use frame_support::{
160		pallet_prelude::{Weight, *},
161		traits::{
162			fungible,
163			fungible::{Mutate, MutateHold},
164			fungibles,
165			fungibles::{Inspect as FungiblesInspect, Mutate as FungiblesMutate},
166			tokens::{Precision, Preservation},
167		},
168	};
169	use frame_system::pallet_prelude::*;
170	use polimec_common::ProvideAssetPrice;
171	use sp_runtime::{Perbill, TypeId};
172
173	pub type AssetId = u32;
174	pub type BalanceOf<T> = <<T as Config>::BondingToken as fungible::Inspect<AccountIdOf<T>>>::Balance;
175	pub type AccountIdOf<T> = <T as frame_system::Config>::AccountId;
176	pub type HoldReasonOf<T> = <<T as Config>::BondingToken as fungible::InspectHold<AccountIdOf<T>>>::Reason;
177	pub type PriceProviderOf<T> = <T as Config>::PriceProvider;
178
179	/// Configure the pallet by specifying the parameters and types on which it depends.
180	#[pallet::config]
181	pub trait Config: frame_system::Config {
182		/// Because this pallet emits events, it depends on the runtime's definition of an event.
183		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
184
185		/// The overarching hold reason generated by `construct_runtime`. This is used for the bonding.
186		type RuntimeHoldReason: IsType<HoldReasonOf<Self>> + Parameter + MaxEncodedLen;
187
188		/// The pallet giving access to the bonding token
189		type BondingToken: fungible::Inspect<Self::AccountId>
190			+ fungible::Mutate<Self::AccountId>
191			+ fungible::MutateHold<Self::AccountId>;
192
193		/// The number of decimals one unit of the bonding token has. Used to calculate decimal aware prices.
194		#[pallet::constant]
195		type BondingTokenDecimals: Get<u8>;
196
197		/// The number of decimals one unit of USD has. Used to calculate decimal aware prices. USD is not a real asset, but a reference point.
198		#[pallet::constant]
199		type UsdDecimals: Get<u8>;
200
201		/// The id of the bonding token. Used to get the price of the bonding token.
202		#[pallet::constant]
203		type BondingTokenId: Get<AssetId>;
204
205		/// The pallet giving access to fee-paying assets, like USDT
206		type FeeToken: fungibles::Inspect<Self::AccountId, Balance = BalanceOf<Self>, AssetId = AssetId>
207			+ fungibles::Mutate<Self::AccountId, Balance = BalanceOf<Self>, AssetId = AssetId>
208			+ fungibles::metadata::Inspect<Self::AccountId, Balance = BalanceOf<Self>, AssetId = AssetId>;
209
210		/// The percentage of the bonded amount in USD that will be taken as a fee in the fee asset.
211		#[pallet::constant]
212		type FeePercentage: Get<Perbill>;
213
214		/// Method to get the price of an asset like USDT or PLMC. Likely to come from an oracle
215		type PriceProvider: ProvideAssetPrice<AssetId = u32>;
216
217		/// The account holding the tokens to be bonded. Normally the treasury
218		#[pallet::constant]
219		type Treasury: Get<Self::AccountId>;
220
221		/// The account receiving the fees
222		#[pallet::constant]
223		type FeeRecipient: Get<Self::AccountId>;
224
225		/// The id type that can generate sub-accounts
226		type Id: Encode + Decode + TypeId;
227
228		/// The root id used to derive sub-accounts. These sub-accounts will be used to bond the tokens
229		type RootId: Get<Self::Id>;
230	}
231
232	#[pallet::pallet]
233	pub struct Pallet<T>(_);
234
235	#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)]
236	pub enum ReleaseType<BlockNumber> {
237		/// The bonded tokens are immediately sent back to the treasury, and fees await refunding
238		Refunded,
239		/// The bonded tokens are locked until the block number, and the fees can be immediately sent to the [fee recipient](Config::FeeRecipient)
240		Locked(BlockNumber),
241	}
242
243	/// Maps at which block can we release the bonds of a sub-account
244	#[pallet::storage]
245	pub type Releases<T: Config> = StorageDoubleMap<
246		_,
247		Blake2_128Concat,
248		u32,
249		Blake2_128Concat,
250		T::RuntimeHoldReason,
251		ReleaseType<BlockNumberFor<T>>,
252	>;
253
254	#[pallet::event]
255	#[pallet::generate_deposit(fn deposit_event)]
256	pub enum Event<T: Config> {
257		BondsTransferredBackToTreasury { bond_amount: BalanceOf<T> },
258		FeesTransferredToFeeRecipient { fee_asset: AssetId, fee_amount: BalanceOf<T> },
259	}
260
261	#[pallet::error]
262	pub enum Error<T> {
263		/// The release type for the given derivation path / hold reason is not set
264		ReleaseTypeNotSet,
265		/// Tried to unlock the native tokens and send them back to the treasury, but the release is configured for a later block.
266		TooEarlyToUnlock,
267		/// The release type for the given derivation path / hold reason is set to `Refunded`, which disallows sending fees to the recipient
268		FeeToRecipientDisallowed,
269		/// The release type for the given derivation path / hold reason is set to `Locked`, which disallows refunding fees
270		FeeRefundDisallowed,
271		/// The price of a fee asset or the native token could not be retrieved
272		PriceNotAvailable,
273	}
274
275	#[pallet::hooks]
276	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {}
277
278	#[pallet::call]
279	impl<T: Config> Pallet<T> {
280		/// Transfer bonded tokens back to the treasury if conditions are met.
281		///
282		/// # Description
283		/// This extrinsic allows transferring bonded tokens back to the treasury account when either:
284		/// - The release block number has been reached for time-locked bonds
285		/// - Or immediately if the release type is set to `Refunded`
286		/// 
287		/// The function will release all tokens held under the specified hold reason and transfer them,
288		/// including the existential deposit, back to the treasury account.
289		/// If sub-account has all the tokens unbonded, it will transfer everything including ED back to the treasury
290		///
291		/// # Parameters
292		/// * `origin` - The origin of the call. Must be signed. Can be anyone.
293		/// * `derivation_path` - The derivation path used to calculate the bonding sub-account
294		/// * `hold_reason` - The reason for which the tokens were held
295		///
296		/// # Errors
297		/// * [`Error::ReleaseTypeNotSet`] - If no release type is configured for the given derivation path and hold reason
298		/// * [`Error::TooEarlyToUnlock`] - If the current block is before the configured release block for locked bonds
299		///
300		/// # Events
301		/// * [`Event::BondsTransferredBackToTreasury`] - When tokens are successfully transferred back to treasury
302		///
303		/// ```
304		#[pallet::call_index(0)]
305		#[pallet::weight(Weight::zero())]
306		pub fn transfer_bonds_back_to_treasury(
307			origin: OriginFor<T>,
308			derivation_path: u32,
309			hold_reason: T::RuntimeHoldReason,
310		) -> DispatchResult {
311			let _caller = ensure_signed(origin)?;
312
313			let treasury = T::Treasury::get();
314			let bonding_account = Self::get_bonding_account(derivation_path);
315			let now = frame_system::Pallet::<T>::block_number();
316
317			let release_block =
318				match Releases::<T>::get(derivation_path, hold_reason.clone()).ok_or(Error::<T>::ReleaseTypeNotSet)? {
319					ReleaseType::Locked(release_block) => release_block,
320					ReleaseType::Refunded => now,
321				};
322
323			ensure!(release_block <= now, Error::<T>::TooEarlyToUnlock);
324
325			let transfer_to_treasury_amount =
326				T::BondingToken::release_all(&hold_reason.into(), &bonding_account, Precision::BestEffort)?;
327
328			T::BondingToken::transfer(
329				&bonding_account,
330				&treasury,
331				transfer_to_treasury_amount,
332				Preservation::Expendable,
333			)?;
334
335			Self::deposit_event(Event::BondsTransferredBackToTreasury { bond_amount: transfer_to_treasury_amount });
336
337			Ok(())
338		}
339
340		/// Transfer collected fees to the designated fee recipient.
341		///
342		/// # Description
343		/// This extrinsic transfers all collected fees in the specified fee asset from the bonding 
344		/// sub-account to the configured fee recipient. This operation is only allowed when the 
345		/// release type is set to `Locked`, indicating that the bonds are being held legitimately
346		/// rather than awaiting refund.
347		///
348		/// # Parameters
349		/// * `origin` - The origin of the call. Must be signed. Can be anyone.
350		/// * `derivation_path` - The derivation path used to calculate the bonding sub-account
351		/// * `hold_reason` - The reason for which the tokens were held
352		/// * `fee_asset` - The asset ID of the fee token to transfer
353		///
354		/// # Errors
355		/// * [`Error::ReleaseTypeNotSet`] - If no release type is configured for the given derivation path and hold reason
356		/// * [`Error::FeeToRecipientDisallowed`] - If the release type is set to `Refunded`, which means fees should be refunded instead
357		///
358		/// # Events
359		/// * [`Event::FeesTransferredToFeeRecipient`] - When fees are successfully transferred to the recipient
360		///
361		/// ```
362		#[pallet::call_index(1)]
363		#[pallet::weight(Weight::zero())]
364		pub fn transfer_fees_to_recipient(
365			origin: OriginFor<T>,
366			derivation_path: u32,
367			hold_reason: T::RuntimeHoldReason,
368			fee_asset: AssetId,
369		) -> DispatchResult {
370			let _caller = ensure_signed(origin)?;
371			let fee_recipient = T::FeeRecipient::get();
372			let bonding_account = Self::get_bonding_account(derivation_path);
373			let release_type = Releases::<T>::get(derivation_path, hold_reason).ok_or(Error::<T>::ReleaseTypeNotSet)?;
374			ensure!(release_type != ReleaseType::Refunded, Error::<T>::FeeToRecipientDisallowed);
375
376			let fees_balance = T::FeeToken::balance(fee_asset, &bonding_account);
377			T::FeeToken::transfer(fee_asset, &bonding_account, &fee_recipient, fees_balance, Preservation::Expendable)?;
378
379			Self::deposit_event(Event::FeesTransferredToFeeRecipient { fee_asset, fee_amount: fees_balance });
380
381			Ok(())
382		}
383	}
384}