@@ -18,7 +18,7 @@ pub mod pallet {
1818 use frame_support:: traits:: tokens:: Preservation ;
1919 use frame_support:: PalletId ;
2020 use frame_system:: pallet_prelude:: * ;
21- use hydradx_traits:: fee_processor:: { Convert , FeeReceiver } ;
21+ use hydradx_traits:: fee_processor:: { Convert , FeeDestination , FeeReceiver } ;
2222 use sp_runtime:: helpers_128bit:: multiply_by_rational_with_rounding;
2323 use sp_runtime:: traits:: AccountIdConversion ;
2424 use sp_runtime:: { Permill , Rounding , Saturating } ;
@@ -71,6 +71,14 @@ pub mod pallet {
7171 #[ pallet:: getter( fn pending_conversions) ]
7272 pub type PendingConversions < T : Config > = CountedStorageMap < _ , Blake2_128Concat , T :: AssetId , ( ) , OptionQuery > ;
7373
74+ /// HDX held in the pot for a `hold_until_ed` receiver whose account is still
75+ /// below the existential deposit. The HDX physically lives in
76+ /// `pot_account_id()`; this map only earmarks how much of it belongs to each
77+ /// destination. Flushed to the destination once `balance + held + slice ≥ ED`.
78+ #[ pallet:: storage]
79+ #[ pallet:: getter( fn held_fees) ]
80+ pub type HeldFees < T : Config > = StorageMap < _ , Blake2_128Concat , T :: AccountId , Balance , ValueQuery > ;
81+
7482 #[ pallet:: event]
7583 #[ pallet:: generate_deposit( pub ( super ) fn deposit_event) ]
7684 pub enum Event < T : Config > {
@@ -208,6 +216,7 @@ pub mod pallet {
208216 } ;
209217
210218 let pot = Self :: pot_account_id ( ) ;
219+ Self :: ensure_pot_exists ( ) ;
211220 let mut total_taken: Balance = 0 ;
212221
213222 // Raw-asset receivers: each reports how much of its slice it actually wants
@@ -257,6 +266,7 @@ pub mod pallet {
257266 ensure ! ( asset_id != T :: HdxAssetId :: get( ) , Error :: <T >:: AlreadyHdx ) ;
258267
259268 let pot = Self :: pot_account_id ( ) ;
269+ Self :: ensure_pot_exists ( ) ;
260270 let balance = T :: Currency :: balance ( asset_id, & pot) ;
261271
262272 let hdx_received = T :: Convert :: convert ( pot. clone ( ) , asset_id, T :: HdxAssetId :: get ( ) , balance) ?;
@@ -276,36 +286,78 @@ pub mod pallet {
276286 Ok ( ( ) )
277287 }
278288
289+ /// Give the pot a provider reference if it has none, so it survives at a
290+ /// zero balance between trades. Without this a fresh-chain pot is created
291+ /// and reaped within a single distribution, surfacing as
292+ /// `Token::FundsUnavailable`. Idempotent: a no-op once the pot holds a
293+ /// balance or has already been provided.
294+ fn ensure_pot_exists ( ) {
295+ let pot = Self :: pot_account_id ( ) ;
296+ if frame_system:: Pallet :: < T > :: providers ( & pot) == 0 {
297+ let _ = frame_system:: Pallet :: < T > :: inc_providers ( & pot) ;
298+ }
299+ }
300+
279301 /// Sum of percentages of the HDX-target (non-raw) receivers.
280- fn convert_percentage ( destinations : & [ ( T :: AccountId , Permill , bool ) ] ) -> Permill {
302+ fn convert_percentage ( destinations : & [ FeeDestination < T :: AccountId > ] ) -> Permill {
281303 destinations
282304 . iter ( )
283- . filter ( |( _ , _ , accepts_raw ) | !accepts_raw)
284- . fold ( Permill :: zero ( ) , |acc, ( _ , pct , _ ) | acc. saturating_add ( * pct ) )
305+ . filter ( |d | !d . accepts_raw )
306+ . fold ( Permill :: zero ( ) , |acc, d | acc. saturating_add ( d . percentage ) )
285307 }
286308
287309 /// Distribute `total` HDX among the HDX-target `destinations` proportionally to
288310 /// each destination's percentage relative to `total_pct`. Raw-asset receivers are
289311 /// skipped — they were already paid in the original asset.
312+ ///
313+ /// `total` is already sitting in `source` (the pot). For a `hold_until_ed`
314+ /// destination whose account would still be below ED after receiving its
315+ /// slice, the slice is left in the pot and tracked in `HeldFees` instead of
316+ /// transferred — avoiding a `Token::BelowMinimum` revert. The buffer is
317+ /// flushed (held + new slice) the moment `balance + held + slice ≥ ED`.
290318 fn distribute_proportionally (
291319 source : & T :: AccountId ,
292320 total : Balance ,
293- destinations : Vec < ( T :: AccountId , Permill , bool ) > ,
321+ destinations : Vec < FeeDestination < T :: AccountId > > ,
294322 total_pct : Permill ,
295323 ) -> DispatchResult {
296324 if total == 0 || total_pct. is_zero ( ) {
297325 return Ok ( ( ) ) ;
298326 }
327+ let hdx = T :: HdxAssetId :: get ( ) ;
328+ let ed = T :: Currency :: minimum_balance ( hdx) ;
299329 let denom = total_pct. deconstruct ( ) as u128 ;
300- for ( dest, pct, accepts_raw) in destinations {
330+ for FeeDestination {
331+ account,
332+ percentage,
333+ accepts_raw,
334+ hold_until_ed,
335+ } in destinations
336+ {
301337 if accepts_raw {
302338 continue ;
303339 }
304- let numer = pct . deconstruct ( ) as u128 ;
305- let amount = multiply_by_rational_with_rounding ( total, numer, denom, Rounding :: Down )
340+ let numer = percentage . deconstruct ( ) as u128 ;
341+ let slice = multiply_by_rational_with_rounding ( total, numer, denom, Rounding :: Down )
306342 . ok_or ( Error :: < T > :: Arithmetic ) ?;
307- if amount > 0 {
308- T :: Currency :: transfer ( T :: HdxAssetId :: get ( ) , source, & dest, amount, Preservation :: Expendable ) ?;
343+ if slice == 0 {
344+ continue ;
345+ }
346+
347+ if hold_until_ed {
348+ // `slice` is already in the pot; flush the accumulated buffer
349+ // only if the account would then reach ED, else keep holding.
350+ let held = HeldFees :: < T > :: get ( & account) ;
351+ let pending = held. saturating_add ( slice) ;
352+ let balance = T :: Currency :: balance ( hdx, & account) ;
353+ if balance. saturating_add ( pending) >= ed {
354+ T :: Currency :: transfer ( hdx, source, & account, pending, Preservation :: Expendable ) ?;
355+ HeldFees :: < T > :: remove ( & account) ;
356+ } else {
357+ HeldFees :: < T > :: insert ( & account, pending) ;
358+ }
359+ } else {
360+ T :: Currency :: transfer ( hdx, source, & account, slice, Preservation :: Expendable ) ?;
309361 }
310362 }
311363 Ok ( ( ) )
0 commit comments