@@ -128,6 +128,16 @@ pub mod pallet {
128128
129129 pub const STORAGE_VERSION : StorageVersion = StorageVersion :: new ( 1 ) ;
130130
131+ /// Defensive tripwire bound for `realize_yield`. Aggregate solvency
132+ /// guarantees the gigapot covers all accrued yield; a *per-account*
133+ /// `realize_yield` can fall a few atomic units short purely from
134+ /// cross-user floor-rounding (one staker's clamped negative residual
135+ /// nudging another's rate up). Anything beyond this many atomic units is
136+ /// an accounting bug, not rounding — `debug_assert` panics so tests and
137+ /// fuzzing surface it; release still returns `GigapotInsufficient`.
138+ /// 1 µHDX ≫ any realistic rounding accumulation, ≪ any real shortfall.
139+ const MAX_GIGAPOT_ROUNDING_SHORTFALL : Balance = 1_000_000 ;
140+
131141 #[ pallet:: pallet]
132142 #[ pallet:: storage_version( STORAGE_VERSION ) ]
133143 pub struct Pallet < T > ( _ ) ;
@@ -260,6 +270,13 @@ pub mod pallet {
260270 hdx_unlocked : Balance ,
261271 gigahdx_received : Balance ,
262272 } ,
273+ /// Accrued yield was moved from the gigapot into the caller's locked
274+ /// stake principal. `amount` is the HDX transferred and added to
275+ /// `Stakes[who].hdx`; `gigahdx` and the exchange rate are unchanged.
276+ YieldRealized {
277+ who : T :: AccountId ,
278+ amount : Balance ,
279+ } ,
263280 }
264281
265282 #[ pallet:: error]
@@ -308,6 +325,9 @@ pub mod pallet {
308325 /// Some HDX is currently frozen (e.g. backing an active reward-eligible
309326 /// vote in `pallet-gigahdx-rewards`); release the freeze first.
310327 StakeFrozen ,
328+ /// The gigapot lacks the HDX to cover the caller's accrued yield.
329+ /// Only reachable in a drained/floored state, not normal operation.
330+ GigapotInsufficient ,
311331 }
312332
313333 #[ pallet:: call]
@@ -558,6 +578,60 @@ pub mod pallet {
558578 } ) ;
559579 Ok ( ( ) )
560580 }
581+
582+ /// Realize the caller's accrued yield into their locked stake principal.
583+ ///
584+ /// Moves the HDX value the caller's GIGAHDX has gained since it was last
585+ /// reconciled (`rate × gigahdx − Stakes[who].hdx`) from the gigapot into
586+ /// the caller's account, folds it into `Stakes[who].hdx`, and refreshes
587+ /// the lock. GIGAHDX balance and the exchange rate are unchanged. A
588+ /// caller with no accrued yield (or no stake) is a successful no-op.
589+ ///
590+ /// Emits `YieldRealized` event when there was yield to realize.
591+ ///
592+ #[ pallet:: call_index( 6 ) ]
593+ #[ pallet:: weight( T :: WeightInfo :: realize_yield( ) ) ]
594+ #[ transactional]
595+ pub fn realize_yield ( origin : OriginFor < T > ) -> DispatchResult {
596+ let who = ensure_signed ( origin) ?;
597+
598+ let stake = Stakes :: < T > :: get ( & who) . unwrap_or_default ( ) ;
599+ let current_value =
600+ Self :: calculate_hdx_amount_given_gigahdx ( stake. gigahdx ) . map_err ( |_| Error :: < T > :: Overflow ) ?;
601+ let accrued = current_value. saturating_sub ( stake. hdx ) ;
602+ if accrued == 0 {
603+ return Ok ( ( ) ) ;
604+ }
605+
606+ if T :: NativeCurrency :: transfer (
607+ & Self :: gigapot_account_id ( ) ,
608+ & who,
609+ accrued,
610+ ExistenceRequirement :: AllowDeath ,
611+ )
612+ . is_err ( )
613+ {
614+ let gigapot = T :: NativeCurrency :: free_balance ( & Self :: gigapot_account_id ( ) ) ;
615+ let shortfall = accrued. saturating_sub ( gigapot) ;
616+ debug_assert ! (
617+ shortfall <= MAX_GIGAPOT_ROUNDING_SHORTFALL ,
618+ "realize_yield: gigapot short by {shortfall} (accrued {accrued}, gigapot {gigapot}) \
619+ — exceeds rounding tolerance, indicates an accounting bug"
620+ ) ;
621+ return Err ( Error :: < T > :: GigapotInsufficient . into ( ) ) ;
622+ }
623+
624+ Stakes :: < T > :: try_mutate ( & who, |maybe| -> Result < ( ) , Error < T > > {
625+ let s = maybe. get_or_insert_with ( StakeRecord :: default) ;
626+ s. hdx = s. hdx . checked_add ( accrued) . ok_or ( Error :: < T > :: Overflow ) ?;
627+ Ok ( ( ) )
628+ } ) ?;
629+ TotalLocked :: < T > :: mutate ( |x| * x = x. saturating_add ( accrued) ) ;
630+ Self :: refresh_lock ( & who) ?;
631+
632+ Self :: deposit_event ( Event :: YieldRealized { who, amount : accrued } ) ;
633+ Ok ( ( ) )
634+ }
561635 }
562636
563637 impl < T : Config > Pallet < T > {
0 commit comments