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}