@@ -595,21 +595,33 @@ pub mod pallet {
595595 ///
596596 #[ pallet:: call_index( 6 ) ]
597597 #[ pallet:: weight( T :: WeightInfo :: realize_yield( ) ) ]
598- #[ transactional]
599598 pub fn realize_yield ( origin : OriginFor < T > ) -> DispatchResult {
600599 let who = ensure_signed ( origin) ?;
600+ Self :: do_realize_yield ( & who) ?;
601+ Ok ( ( ) )
602+ }
603+ }
601604
602- let stake = Stakes :: < T > :: get ( & who) . unwrap_or_default ( ) ;
605+ impl < T : Config > Pallet < T > {
606+ /// Realize `who`'s accrued GIGAHDX yield into their locked stake
607+ /// principal: move `rate × gigahdx − Stakes[who].hdx` HDX from the
608+ /// gigapot into `who`, fold it into `Stakes[who].hdx`, refresh the
609+ /// lock. No-op (returns `0`) when there is no accrued yield or no
610+ /// stake. Shared by the `realize_yield` extrinsic and the liquidation
611+ /// seize path. `#[transactional]` so a mid-way Err rolls back.
612+ #[ transactional]
613+ fn do_realize_yield ( who : & T :: AccountId ) -> Result < Balance , DispatchError > {
614+ let stake = Stakes :: < T > :: get ( who) . unwrap_or_default ( ) ;
603615 let current_value =
604616 Self :: calculate_hdx_amount_given_gigahdx ( stake. gigahdx ) . map_err ( |_| Error :: < T > :: Overflow ) ?;
605617 let accrued = current_value. saturating_sub ( stake. hdx ) ;
606618 if accrued == 0 {
607- return Ok ( ( ) ) ;
619+ return Ok ( 0 ) ;
608620 }
609621
610622 if T :: NativeCurrency :: transfer (
611623 & Self :: gigapot_account_id ( ) ,
612- & who,
624+ who,
613625 accrued,
614626 ExistenceRequirement :: AllowDeath ,
615627 )
@@ -625,20 +637,21 @@ pub mod pallet {
625637 return Err ( Error :: < T > :: GigapotInsufficient . into ( ) ) ;
626638 }
627639
628- Stakes :: < T > :: try_mutate ( & who, |maybe| -> Result < ( ) , Error < T > > {
640+ Stakes :: < T > :: try_mutate ( who, |maybe| -> Result < ( ) , Error < T > > {
629641 let s = maybe. get_or_insert_with ( StakeRecord :: default) ;
630642 s. hdx = s. hdx . checked_add ( accrued) . ok_or ( Error :: < T > :: Overflow ) ?;
631643 Ok ( ( ) )
632644 } ) ?;
633645 TotalLocked :: < T > :: mutate ( |x| * x = x. saturating_add ( accrued) ) ;
634- Self :: refresh_lock ( & who) ?;
646+ Self :: refresh_lock ( who) ?;
635647
636- Self :: deposit_event ( Event :: YieldRealized { who, amount : accrued } ) ;
637- Ok ( ( ) )
648+ Self :: deposit_event ( Event :: YieldRealized {
649+ who : who. clone ( ) ,
650+ amount : accrued,
651+ } ) ;
652+ Ok ( accrued)
638653 }
639- }
640654
641- impl < T : Config > Pallet < T > {
642655 /// Internal helper for `giga_unstake`. Uses `?` freely; the
643656 /// `#[transactional]` attribute wraps the body in its own storage
644657 /// layer so any Err here rolls back partial mutations.
@@ -853,91 +866,6 @@ pub mod pallet {
853866 Stakes :: < T > :: get ( who) . map ( |s| s. gigahdx ) . unwrap_or ( 0 )
854867 }
855868
856- /// `Seize::snapshot_stake` impl.
857- pub fn seize_snapshot ( borrower : & T :: AccountId ) -> Result < ( Balance , Balance ) , DispatchError > {
858- let s = Stakes :: < T > :: get ( borrower) . ok_or ( Error :: < T > :: NoStake ) ?;
859- Ok ( ( s. hdx , s. gigahdx ) )
860- }
861-
862- /// `Seize::pre_seize` impl. Zeroes `Stakes[borrower].gigahdx` so the
863- /// lock-manager precompile reports `locked = 0` and `LockableAToken`
864- /// accepts Aave's internal aToken transfer. Returns the prior value
865- /// for the caller to use when computing the actually-seized amount.
866- pub fn seize_unlock_atoken ( borrower : & T :: AccountId ) -> Result < Balance , DispatchError > {
867- let orig = Stakes :: < T > :: try_mutate ( borrower, |maybe| -> Result < Balance , DispatchError > {
868- let s = maybe. as_mut ( ) . ok_or ( Error :: < T > :: NoStake ) ?;
869- let prev = s. gigahdx ;
870- s. gigahdx = 0 ;
871- Ok ( prev)
872- } ) ?;
873- Ok ( orig)
874- }
875-
876- /// `Seize::finalise_seize` impl. After Aave has moved aToken to the
877- /// liquidator's EVM account, move the matching HDX from borrower to
878- /// `recipient`, restore the borrower's `gigahdx` to the residual
879- /// (`orig - actually_seized`), credit the recipient's stake, refresh
880- /// both ghdxlocks. `TotalLocked` is unchanged — same HDX moved
881- /// between two gigahdx-stakers.
882- #[ transactional]
883- pub fn seize_finalise (
884- borrower : & T :: AccountId ,
885- recipient : & T :: AccountId ,
886- seize_hdx : Balance ,
887- seize_gigahdx : Balance ,
888- residual_borrower_gigahdx : Balance ,
889- ) -> DispatchResult {
890- Stakes :: < T > :: try_mutate ( borrower, |maybe| -> DispatchResult {
891- let s = maybe. as_mut ( ) . ok_or ( Error :: < T > :: NoStake ) ?;
892- s. hdx = s. hdx . checked_sub ( seize_hdx) . ok_or ( Error :: < T > :: Overflow ) ?;
893- // Votes stay intact across a seize; clamp `frozen` so the
894- // `hdx >= frozen` invariant still holds on the residual stake.
895- s. frozen = s. frozen . min ( s. hdx ) ;
896- s. gigahdx = residual_borrower_gigahdx;
897- Ok ( ( ) )
898- } ) ?;
899- // Shrink the borrower's lock *before* withdrawing. The stale
900- // pre-seize ghdxlock (sized to `hdx + unstaking`) would otherwise
901- // block the transfer with `LiquidityRestrictions` for any
902- // borrower whose free balance equals their staked amount.
903- Self :: refresh_lock ( borrower) ?;
904-
905- if !seize_hdx. is_zero ( ) {
906- // Prefer a clean transfer. If the borrower's remaining locks
907- // (e.g. uncleared `pyconvot`, vesting, or any unmanaged lock)
908- // still block the move, fall back to `slash` + `resolve_creating`
909- // — liquidation is top priority and must always land.
910- let new_balance = T :: NativeCurrency :: free_balance ( borrower) . saturating_sub ( seize_hdx) ;
911- let can_transfer =
912- T :: NativeCurrency :: ensure_can_withdraw ( borrower, seize_hdx, WithdrawReasons :: TRANSFER , new_balance)
913- . is_ok ( ) ;
914- if can_transfer {
915- T :: NativeCurrency :: transfer ( borrower, recipient, seize_hdx, ExistenceRequirement :: AllowDeath ) ?;
916- } else {
917- let ( imbalance, remaining) = T :: NativeCurrency :: slash ( borrower, seize_hdx) ;
918- let slashed = imbalance. peek ( ) ;
919- T :: NativeCurrency :: resolve_creating ( recipient, imbalance) ;
920- // Liquidation must move EXACTLY `seize_hdx`. `slash` is
921- // bounded above by request (it never takes more than asked)
922- // but can take less when ED leaves an unslashable remainder;
923- // both `remaining == 0` and `slashed == seize_hdx` are
924- // equivalent expressions of "took exactly what we asked".
925- ensure ! ( remaining. is_zero( ) , Error :: <T >:: SeizeFailed ) ;
926- ensure ! ( slashed == seize_hdx, Error :: <T >:: SeizeFailed ) ;
927- }
928- }
929-
930- Stakes :: < T > :: try_mutate ( recipient, |maybe| -> DispatchResult {
931- let s = maybe. get_or_insert_with ( StakeRecord :: default) ;
932- s. hdx = s. hdx . checked_add ( seize_hdx) . ok_or ( Error :: < T > :: Overflow ) ?;
933- s. gigahdx = s. gigahdx . checked_add ( seize_gigahdx) . ok_or ( Error :: < T > :: Overflow ) ?;
934- Ok ( ( ) )
935- } ) ?;
936-
937- Self :: refresh_lock ( recipient) ?;
938- Ok ( ( ) )
939- }
940-
941869 /// Total HDX backing all stHDX:
942870 /// `TotalLocked + free_balance(gigapot_account_id)`.
943871 pub fn total_staked_hdx ( ) -> Balance {
@@ -988,21 +916,103 @@ pub mod pallet {
988916 }
989917 }
990918
991- impl < T : Config > crate :: traits:: Seize < T :: AccountId > for Pallet < T > {
919+ impl < T : Config > hydradx_traits:: gigahdx:: Seize < T :: AccountId > for Pallet < T > {
920+ fn realize_yield ( borrower : & T :: AccountId ) -> DispatchResult {
921+ Self :: do_realize_yield ( borrower) . map ( |_| ( ) )
922+ }
923+
992924 fn snapshot_stake ( borrower : & T :: AccountId ) -> Result < ( Balance , Balance ) , DispatchError > {
993- Self :: seize_snapshot ( borrower)
925+ let s = Stakes :: < T > :: get ( borrower) . ok_or ( Error :: < T > :: NoStake ) ?;
926+ Ok ( ( s. hdx , s. gigahdx ) )
994927 }
995- fn pre_seize ( borrower : & T :: AccountId ) -> Result < Balance , DispatchError > {
996- Self :: seize_unlock_atoken ( borrower)
928+
929+ fn on_pre_seize ( borrower : & T :: AccountId ) -> Result < Balance , DispatchError > {
930+ Stakes :: < T > :: try_mutate ( borrower, |maybe| -> Result < Balance , DispatchError > {
931+ let s = maybe. as_mut ( ) . ok_or ( Error :: < T > :: NoStake ) ?;
932+ let prev = s. gigahdx ;
933+ s. gigahdx = 0 ;
934+ Ok ( prev)
935+ } )
997936 }
998- fn finalise_seize (
937+
938+ /// `orig_gigahdx` is the borrower's pre-seize aToken balance from
939+ /// `snapshot_stake`; `on_pre_seize` has since zeroed the stored value,
940+ /// so the residual cannot be derived from state here. The `checked_sub`
941+ /// also rejects a `seize_gigahdx` larger than the snapshot.
942+ #[ transactional]
943+ fn on_seize (
999944 borrower : & T :: AccountId ,
1000945 recipient : & T :: AccountId ,
1001946 seize_hdx : Balance ,
1002947 seize_gigahdx : Balance ,
1003- residual_borrower_gigahdx : Balance ,
948+ orig_gigahdx : Balance ,
1004949 ) -> DispatchResult {
1005- Self :: seize_finalise ( borrower, recipient, seize_hdx, seize_gigahdx, residual_borrower_gigahdx)
950+ Stakes :: < T > :: try_mutate ( borrower, |maybe| -> DispatchResult {
951+ let s = maybe. as_mut ( ) . ok_or ( Error :: < T > :: NoStake ) ?;
952+ // `seize_gigahdx` must not exceed the borrower's snapshot — a
953+ // larger ask means the caller mis-computed the pro-rata split.
954+ // Treated as unreachable in production; the debug_assert turns
955+ // it into a hard panic under fuzzing instead of a silent
956+ // `SeizeFailed` revert.
957+ debug_assert ! (
958+ seize_gigahdx <= orig_gigahdx,
959+ "on_seize: seize_gigahdx ({:?}) exceeds orig_gigahdx snapshot ({:?})" ,
960+ seize_gigahdx,
961+ orig_gigahdx,
962+ ) ;
963+ let residual_borrower_gigahdx =
964+ orig_gigahdx. checked_sub ( seize_gigahdx) . ok_or ( Error :: < T > :: SeizeFailed ) ?;
965+ s. hdx = s. hdx . checked_sub ( seize_hdx) . ok_or ( Error :: < T > :: Overflow ) ?;
966+ // Votes stay intact across a seize; clamp `frozen` so the
967+ // `hdx >= frozen` invariant still holds on the residual stake.
968+ s. frozen = s. frozen . min ( s. hdx ) ;
969+ s. gigahdx = residual_borrower_gigahdx;
970+ Ok ( ( ) )
971+ } ) ?;
972+ // Shrink the borrower's lock *before* withdrawing. The stale
973+ // pre-seize ghdxlock (sized to `hdx + unstaking`) would otherwise
974+ // block the transfer with `LiquidityRestrictions` for any
975+ // borrower whose free balance equals their staked amount.
976+ Self :: refresh_lock ( borrower) ?;
977+
978+ if !seize_hdx. is_zero ( ) {
979+ // Prefer a clean transfer. If the borrower's remaining locks
980+ // (e.g. uncleared `pyconvot`, vesting, or any unmanaged lock)
981+ // still block the move, fall back to `slash` + `resolve_creating`
982+ // — liquidation is top priority and must always land.
983+ let new_balance = T :: NativeCurrency :: free_balance ( borrower)
984+ . checked_sub ( seize_hdx)
985+ . ok_or ( Error :: < T > :: SeizeFailed ) ?;
986+ let can_transfer =
987+ T :: NativeCurrency :: ensure_can_withdraw ( borrower, seize_hdx, WithdrawReasons :: TRANSFER , new_balance)
988+ . is_ok ( ) ;
989+ if can_transfer {
990+ T :: NativeCurrency :: transfer ( borrower, recipient, seize_hdx, ExistenceRequirement :: AllowDeath ) ?;
991+ } else {
992+ // `slash` ignores locks (unlike `transfer`), but
993+ // `pallet_balances` refuses to push a non-reapable
994+ // account below ED. Tolerate that ≤ED dust — Aave has
995+ // already moved the collateral aToken by this point, so
996+ // the seize must land. Larger shortfalls keep the
997+ // fail-loud tripwire for a genuinely broken stake/lock
998+ // ledger (the `free >= seize_hdx` staking invariant
999+ // bounds the shortfall to exactly the ED).
1000+ let ( imbalance, remaining) = T :: NativeCurrency :: slash ( borrower, seize_hdx) ;
1001+ let ed = T :: NativeCurrency :: minimum_balance ( ) ;
1002+ ensure ! ( remaining <= ed, Error :: <T >:: SeizeFailed ) ;
1003+ T :: NativeCurrency :: resolve_creating ( recipient, imbalance) ;
1004+ }
1005+ }
1006+
1007+ Stakes :: < T > :: try_mutate ( recipient, |maybe| -> DispatchResult {
1008+ let s = maybe. get_or_insert_with ( StakeRecord :: default) ;
1009+ s. hdx = s. hdx . checked_add ( seize_hdx) . ok_or ( Error :: < T > :: Overflow ) ?;
1010+ s. gigahdx = s. gigahdx . checked_add ( seize_gigahdx) . ok_or ( Error :: < T > :: Overflow ) ?;
1011+ Ok ( ( ) )
1012+ } ) ?;
1013+
1014+ Self :: refresh_lock ( recipient) ?;
1015+ Ok ( ( ) )
10061016 }
10071017 }
10081018}
0 commit comments