3333
3434pub use pallet:: * ;
3535
36+ pub mod traits;
37+
3638#[ cfg( test) ]
3739mod tests;
3840
@@ -60,6 +62,7 @@ impl<AccountId> BenchmarkHelper<AccountId> for () {
6062
6163#[ frame_support:: pallet]
6264pub mod pallet {
65+ pub use crate :: traits:: ExternalClaims ;
6366 pub use crate :: weights:: WeightInfo ;
6467 use codec:: { Decode , DecodeWithMemTracking , Encode , MaxEncodedLen } ;
6568 use frame_support:: pallet_prelude:: * ;
@@ -69,7 +72,7 @@ pub mod pallet {
6972 use frame_support:: traits:: fungibles:: Mutate as FungiblesMutate ;
7073 use frame_support:: traits:: tokens:: { Fortitude , Precision , Preservation } ;
7174 use frame_support:: traits:: {
72- fungible , fungibles, Currency , ExistenceRequirement , LockIdentifier , LockableCurrency , WithdrawReasons ,
75+ fungibles, Currency , ExistenceRequirement , LockIdentifier , LockableCurrency , WithdrawReasons ,
7376 } ;
7477 use frame_support:: { transactional, PalletId } ;
7578 use frame_system:: pallet_prelude:: * ;
@@ -121,11 +124,7 @@ pub mod pallet {
121124
122125 #[ pallet:: config]
123126 pub trait Config : frame_system:: Config < RuntimeEvent : From < Event < Self > > > {
124- /// Native (HDX) lockable currency. The `fungible::Inspect` bound is
125- /// required so `giga_stake` can use `reducible_balance` (free balance
126- /// minus transfer-blocking locks) instead of raw `free_balance`.
127- type NativeCurrency : LockableCurrency < Self :: AccountId , Balance = Balance , Moment = BlockNumberFor < Self > >
128- + fungible:: Inspect < Self :: AccountId , Balance = Balance > ;
127+ type NativeCurrency : LockableCurrency < Self :: AccountId , Balance = Balance , Moment = BlockNumberFor < Self > > ;
129128
130129 /// Multi-asset register that holds stHDX (and any other registered
131130 /// fungible). Only this pallet mints / burns stHDX through it.
@@ -163,6 +162,12 @@ pub mod pallet {
163162 #[ pallet:: constant]
164163 type MaxPendingUnstakes : Get < u32 > ;
165164
165+ /// Inspector returning the sum of non-overlapping HDX claims on the
166+ /// caller. Any non-zero value blocks `giga_stake` admission — the
167+ /// strict policy rejects stakes whenever the account carries a lock
168+ /// the runtime has not whitelisted for overlap (e.g. `pyconvot`).
169+ type ExternalClaims : crate :: traits:: ExternalClaims < Self :: AccountId > ;
170+
166171 type WeightInfo : WeightInfo ;
167172
168173 /// Benchmark helper for setting up state that can't be created from
@@ -237,10 +242,15 @@ pub mod pallet {
237242 pub enum Error < T > {
238243 /// Stake amount is below `Config::MinStake`.
239244 BelowMinStake ,
240- /// Caller does not have enough unlocked HDX to back this stake (the
241- /// admission check uses `reducible_balance`, which subtracts every
242- /// transfer-blocking lock — including this pallet's own lock).
245+ /// Caller doesn't have enough unencumbered HDX to back the stake
246+ /// after subtracting their existing gigahdx commitment.
243247 InsufficientFreeBalance ,
248+ /// Caller holds a non-overlapping lock (legacy staking, vesting, …)
249+ /// reported by `Config::ExternalClaims`. Strict policy: gigahdx
250+ /// admission requires the caller to have no claims on their HDX
251+ /// other than those the runtime explicitly allows to coexist
252+ /// (e.g. `pyconvot`). Release the conflicting lock before staking.
253+ BlockedByExternalLock ,
244254 /// Unstake amount exceeds the caller's `Stakes.gigahdx`.
245255 InsufficientStake ,
246256 /// Caller has no active stake record.
@@ -289,10 +299,12 @@ pub mod pallet {
289299 /// money market (may differ from the requested mint amount by rounding).
290300 ///
291301 /// Fails with `BelowMinStake` if `amount < Config::MinStake`, with
292- /// `InsufficientFreeBalance` if the caller's `reducible_balance` does not cover
293- /// `amount`, with `StHdxMintFailed` if stHDX minting fails (asset not registered,
294- /// max issuance hit), or with `MoneyMarketSupplyFailed` if the AAVE `Pool.supply`
295- /// call reverts.
302+ /// `BlockedByExternalLock` if `Config::ExternalClaims::on(caller) > 0`
303+ /// (the caller holds a non-allowed lock — strict policy rejects
304+ /// stake admission entirely), with `InsufficientFreeBalance` if
305+ /// `free_balance − own_gigahdx_commitment < amount`, with
306+ /// `StHdxMintFailed` if stHDX minting fails, or with
307+ /// `MoneyMarketSupplyFailed` if the AAVE `Pool.supply` call reverts.
296308 ///
297309 /// Parameters:
298310 /// - `amount`: HDX amount to stake. Must be at least `Config::MinStake`.
@@ -305,15 +317,19 @@ pub mod pallet {
305317 let who = ensure_signed ( origin) ?;
306318 ensure ! ( amount >= T :: MinStake :: get( ) , Error :: <T >:: BelowMinStake ) ;
307319
308- // Use `reducible_balance` so the check respects every transfer-blocking
309- // lock — including this pallet's own combined `LockId` lock (active
310- // stake + pending unstake) and any unrelated conviction/vesting locks.
311- let usable = <T :: NativeCurrency as fungible:: Inspect < T :: AccountId > >:: reducible_balance (
312- & who,
313- Preservation :: Expendable ,
314- Fortitude :: Polite ,
315- ) ;
316- ensure ! ( usable >= amount, Error :: <T >:: InsufficientFreeBalance ) ;
320+ // Strict policy: refuse if the caller carries any lock the runtime
321+ // does not whitelist for overlap. Lock-layering via `max()` would
322+ // otherwise let the same HDX back both a gigahdx stake and another
323+ // pallet's claim after a single transfer of the unlocked portion.
324+ ensure ! ( T :: ExternalClaims :: on( & who) == 0 , Error :: <T >:: BlockedByExternalLock ) ;
325+
326+ // Own commitment (active + pending unstakes) still has to fit
327+ // under free_balance — re-staking under the same `ghdxlock` must
328+ // not exceed what the user actually owns.
329+ let stake = Stakes :: < T > :: get ( & who) . unwrap_or_default ( ) ;
330+ let own_claim = stake. hdx . saturating_add ( stake. unstaking ) ;
331+ let stakeable = T :: NativeCurrency :: free_balance ( & who) . saturating_sub ( own_claim) ;
332+ ensure ! ( stakeable >= amount, Error :: <T >:: InsufficientFreeBalance ) ;
317333
318334 Self :: do_stake ( & who, amount) ?;
319335 Ok ( ( ) )
@@ -613,9 +629,12 @@ pub mod pallet {
613629 /// `Staked`.
614630 ///
615631 /// Caller invariant: `amount` HDX must already be in `who`'s free
616- /// balance. This helper does not enforce the `MinStake` floor — callers
617- /// requiring that check (e.g. the `giga_stake` extrinsic) must perform
618- /// it before invoking `do_stake`.
632+ /// balance. This helper performs **no admission control** — neither
633+ /// the `MinStake` floor nor the `ExternalClaims`/headroom checks that
634+ /// `giga_stake` applies. It is intended for trusted internal callers
635+ /// (e.g. `cancel_unstake` rearranging already-locked HDX, or
636+ /// `pallet-gigahdx-rewards` compounding accrued rewards). Untrusted
637+ /// callers must replicate the `giga_stake` checks before invoking.
619638 #[ transactional]
620639 pub fn do_stake ( who : & T :: AccountId , amount : Balance ) -> Result < Balance , DispatchError > {
621640 ensure ! ( amount > 0 , Error :: <T >:: ZeroAmount ) ;
0 commit comments