Skip to content

Commit ffbb3c1

Browse files
review comments
1 parent 634e112 commit ffbb3c1

4 files changed

Lines changed: 72 additions & 39 deletions

File tree

pallets/gigahdx-rewards/src/lib.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@
2828
//! - First `on_remove_vote` for a completed referendum lazily transfers
2929
//! `track_pct × accumulator_balance` into the allocated pot and snapshots
3030
//! the frozen denominator.
31-
//! - Per-user shares are pro-rata against that frozen denominator. The last
32-
//! claimant scoops any remaining dust, draining the pool to exactly zero
33-
//! and triggering storage cleanup.
31+
//! - Per-user shares are pro-rata against that frozen denominator. Once the
32+
//! last counted voter is paid, any rounding dust left in the pool is
33+
//! recycled to the accumulator pot and the pool entry is cleaned up.
3434
//! - `claim_rewards` atomically compounds the user's accumulated HDX back
3535
//! into their gigahdx position.
3636

pallets/gigahdx-rewards/src/types.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,13 @@ pub struct ReferendaReward<TrackId> {
6464
/// Frozen denominator for pro-rata math.
6565
pub total_weighted_votes: u128,
6666
/// Countdown of voters who still hold a `UserVoteRecord` for this
67-
/// referendum. When this reaches zero on a per-user payout, the last
68-
/// claimant scoops `remaining_reward` and the pool entry is deleted.
67+
/// referendum. When this reaches zero on a per-user payout, any
68+
/// `remaining_reward` dust is recycled to the accumulator pot and the
69+
/// pool entry is deleted.
6970
pub voters_remaining: u32,
70-
/// Decremented on each per-user payout. Equals `total_reward` at
71-
/// allocation; drained to exactly zero by the final claimant.
71+
/// Decremented on each per-user payout by the capped share. Equals
72+
/// `total_reward` at allocation; any residual after the final payout is
73+
/// recycled to the accumulator pot.
7274
pub remaining_reward: Balance,
7375
}
7476

pallets/gigahdx-rewards/src/voting_hooks.rs

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,11 +156,38 @@ impl<T: Config> VotingHooks<T::AccountId, ReferendumIndex, Balance> for VotingHo
156156
None
157157
}
158158

159+
// `on_before_vote` / `on_remove_vote` short-circuit at `Stakes[who].hdx == 0`,
160+
// so the conviction-voting `vote` / `remove_vote` benchmarks must make `who`
161+
// a gigahdx staker for their weight to cover this hook's per-vote storage
162+
// writes (tally + `UserVoteRecords` + `freeze`/`unfreeze`). Seed the record
163+
// directly — the hook only reads `.hdx` and mutates `.frozen`, so no
164+
// money-market / lock setup is needed.
165+
//
166+
// The one-time-per-referendum allocation path (`Status::Completed` → pot
167+
// transfer + `record_user_reward`) is not reachable here: the benchmark's
168+
// poll stays Ongoing, so that bounded cost is paid by — and documented on —
169+
// the first post-completion remover rather than charged per vote.
159170
#[cfg(feature = "runtime-benchmarks")]
160-
fn on_vote_worst_case(_who: &T::AccountId) {}
171+
fn on_vote_worst_case(who: &T::AccountId) {
172+
seed_staker_worst_case::<T>(who);
173+
}
161174

162175
#[cfg(feature = "runtime-benchmarks")]
163-
fn on_remove_vote_worst_case(_who: &T::AccountId) {}
176+
fn on_remove_vote_worst_case(who: &T::AccountId) {
177+
seed_staker_worst_case::<T>(who);
178+
}
179+
}
180+
181+
#[cfg(feature = "runtime-benchmarks")]
182+
fn seed_staker_worst_case<T: Config>(who: &T::AccountId) {
183+
pallet_gigahdx::Stakes::<T>::insert(
184+
who,
185+
pallet_gigahdx::StakeRecord {
186+
hdx: 1_000_000_000_000_000,
187+
gigahdx: 1_000_000_000_000_000,
188+
..Default::default()
189+
},
190+
);
164191
}
165192

166193
/// Allocate the pool on first call for a completed referendum, then credit

pallets/gigahdx/src/lib.rs

Lines changed: 34 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)