@@ -119,6 +119,16 @@ pub mod pallet {
119119 pub unstaking_count : u16 ,
120120 }
121121
122+ impl StakeRecord {
123+ /// True when the record carries no state and can be reaped. Shared by
124+ /// `unlock` and `unfreeze` so their reap predicates stay in lockstep —
125+ /// dropping a record while any field is non-zero (e.g. residual
126+ /// `unstaking`) would orphan `PendingUnstakes` entries or its lock.
127+ fn is_empty ( & self ) -> bool {
128+ self . hdx == 0 && self . gigahdx == 0 && self . frozen == 0 && self . unstaking == 0 && self . unstaking_count == 0
129+ }
130+ }
131+
122132 /// One pending-unstake position. Keyed by the originating block; cooldown
123133 /// expiry is `block + Config::CooldownPeriod`.
124134 #[ derive( Encode , Decode , DecodeWithMemTracking , TypeInfo , MaxEncodedLen , Clone , PartialEq , Eq , Debug ) ]
@@ -359,22 +369,7 @@ pub mod pallet {
359369 #[ pallet:: weight( T :: WeightInfo :: giga_stake( ) . saturating_add( T :: MoneyMarket :: supply_weight( ) ) ) ]
360370 pub fn giga_stake ( origin : OriginFor < T > , amount : Balance ) -> DispatchResult {
361371 let who = ensure_signed ( origin) ?;
362- ensure ! ( amount >= T :: MinStake :: get( ) , Error :: <T >:: BelowMinStake ) ;
363-
364- // Strict policy: refuse if the caller carries any lock the runtime
365- // does not whitelist for overlap. Lock-layering via `max()` would
366- // otherwise let the same HDX back both a gigahdx stake and another
367- // pallet's claim after a single transfer of the unlocked portion.
368- ensure ! ( T :: ExternalClaims :: on( & who) == 0 , Error :: <T >:: BlockedByExternalLock ) ;
369-
370- // Own commitment (active + pending unstakes) still has to fit
371- // under free_balance — re-staking under the same `ghdxlock` must
372- // not exceed what the user actually owns.
373- let stake = Stakes :: < T > :: get ( & who) . unwrap_or_default ( ) ;
374- let own_claim = stake. hdx . saturating_add ( stake. unstaking ) ;
375- let stakeable = T :: NativeCurrency :: free_balance ( & who) . saturating_sub ( own_claim) ;
376- ensure ! ( stakeable >= amount, Error :: <T >:: InsufficientFreeBalance ) ;
377-
372+ Self :: ensure_stakeable ( & who, amount) ?;
378373 Self :: do_stake ( & who, amount) ?;
379374 Ok ( ( ) )
380375 }
@@ -477,7 +472,7 @@ pub mod pallet {
477472 if let Some ( s) = maybe. as_mut ( ) {
478473 s. unstaking = s. unstaking . saturating_sub ( entry. amount ) ;
479474 s. unstaking_count = s. unstaking_count . saturating_sub ( 1 ) ;
480- if s. hdx == 0 && s . gigahdx == 0 && s . frozen == 0 && s . unstaking_count == 0 {
475+ if s. is_empty ( ) {
481476 * maybe = None ;
482477 }
483478 }
@@ -562,13 +557,9 @@ pub mod pallet {
562557
563558 let hdx_unlocked = T :: LegacyStaking :: force_unstake ( & who) ?;
564559
565- ensure ! ( hdx_unlocked >= T :: MinStake :: get( ) , Error :: <T >:: BelowMinStake ) ;
566- ensure ! ( T :: ExternalClaims :: on( & who) == 0 , Error :: <T >:: BlockedByExternalLock ) ;
567-
568- let stake = Stakes :: < T > :: get ( & who) . unwrap_or_default ( ) ;
569- let own_claim = stake. hdx . saturating_add ( stake. unstaking ) ;
570- let stakeable = T :: NativeCurrency :: free_balance ( & who) . saturating_sub ( own_claim) ;
571- ensure ! ( stakeable >= hdx_unlocked, Error :: <T >:: InsufficientFreeBalance ) ;
560+ // Admission runs *after* `force_unstake` so the legacy lock is no
561+ // longer counted against `ExternalClaims`.
562+ Self :: ensure_stakeable ( & who, hdx_unlocked) ?;
572563
573564 let gigahdx_received = Self :: do_stake ( & who, hdx_unlocked) ?;
574565 Self :: deposit_event ( Event :: MigratedFromLegacy {
@@ -773,18 +764,31 @@ pub mod pallet {
773764 Stakes :: < T > :: mutate_exists ( who, |maybe| {
774765 if let Some ( stake) = maybe. as_mut ( ) {
775766 stake. frozen = stake. frozen . saturating_sub ( delta) ;
776- if stake. hdx == 0
777- && stake. gigahdx == 0
778- && stake. frozen == 0
779- && stake. unstaking == 0
780- && stake. unstaking_count == 0
781- {
767+ if stake. is_empty ( ) {
782768 * maybe = None ;
783769 }
784770 }
785771 } ) ;
786772 }
787773
774+ /// Admission gate shared by `giga_stake` and `migrate`.
775+ ///
776+ /// Enforces the `MinStake` floor, the strict no-overlapping-lock policy
777+ /// (lock-layering via `max()` would otherwise let the same HDX back both
778+ /// a gigahdx stake and another pallet's claim after a single transfer of
779+ /// the unlocked portion), and that the caller's own commitment (active +
780+ /// pending unstakes) plus `amount` still fits under their free balance.
781+ fn ensure_stakeable ( who : & T :: AccountId , amount : Balance ) -> DispatchResult {
782+ ensure ! ( amount >= T :: MinStake :: get( ) , Error :: <T >:: BelowMinStake ) ;
783+ ensure ! ( T :: ExternalClaims :: on( who) == 0 , Error :: <T >:: BlockedByExternalLock ) ;
784+
785+ let stake = Stakes :: < T > :: get ( who) . unwrap_or_default ( ) ;
786+ let own_claim = stake. hdx . saturating_add ( stake. unstaking ) ;
787+ let stakeable = T :: NativeCurrency :: free_balance ( who) . saturating_sub ( own_claim) ;
788+ ensure ! ( stakeable >= amount, Error :: <T >:: InsufficientFreeBalance ) ;
789+ Ok ( ( ) )
790+ }
791+
788792 /// Computes the stHDX amount at the current rate, mints it into `who`,
789793 /// supplies it to the money market, credits the resulting aToken amount
790794 /// to `Stakes[who]`, locks `amount` HDX under `Config::LockId`, and emits
0 commit comments