Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions pallets/subtensor/src/coinbase/run_coinbase.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,11 +187,6 @@ impl<T: Config> Pallet<T> {
let mut alpha_in: BTreeMap<NetUid, U96F32> = BTreeMap::new();
let mut alpha_out: BTreeMap<NetUid, U96F32> = BTreeMap::new();
let mut excess_tao: BTreeMap<NetUid, U96F32> = BTreeMap::new();
let tao_block_emission: U96F32 = U96F32::saturating_from_num(
Self::calculate_block_emission()
.unwrap_or(TaoBalance::ZERO)
.to_u64(),
);

// Only calculate for subnets that we are emitting to.
for (&netuid_i, &tao_emission_i) in subnet_emissions.iter() {
Expand All @@ -210,7 +205,14 @@ impl<T: Config> Pallet<T> {
let alpha_out_i: U96F32 = alpha_emission_i;
let mut alpha_in_i: U96F32 = tao_emission_i.safe_div_or(price_i, U96F32::from_num(0.0));

let alpha_injection_cap: U96F32 = alpha_emission_i.min(tao_block_emission);
// Cap alpha injection by the subnet's root proportion of its alpha emission.
// root_proportion = tao_weight / (tao_weight + alpha_issuance), so as a subnet
// ages its alpha issuance grows, root_proportion shrinks, and the injection cap
// falls. The TAO emission that can no longer be injected as liquidity becomes
// excess TAO and is routed into chain buys instead. This is what transitions
// older subnets from liquidity injection to chain buys over time.
let root_proportion_i: U96F32 = Self::root_proportion(netuid_i);
let alpha_injection_cap: U96F32 = root_proportion_i.saturating_mul(alpha_emission_i);
if alpha_in_i > alpha_injection_cap {
alpha_in_i = alpha_injection_cap;
tao_in_i = alpha_in_i.saturating_mul(price_i);
Expand Down
12 changes: 7 additions & 5 deletions pallets/subtensor/src/coinbase/subnet_emissions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -329,14 +329,16 @@ impl<T: Config> Pallet<T> {
offset_flows
}

// Combines ema price method and tao flow method linearly over FlowHalfLife blocks
// Price-based emission shares: each subnet's share is its EMA price normalized
// by the sum of EMA prices. Emit-disabled subnets are zeroed and their share
// redistributed to enabled subnets in `get_subnet_block_emissions`, so the
// effective emission is e_i = p_i / sum(p_j) over emit-enabled subnets.
pub(crate) fn get_shares(subnets_to_emit_to: &[NetUid]) -> BTreeMap<NetUid, U64F64> {
Self::get_shares_flow(subnets_to_emit_to)
// Self::get_shares_price_ema(subnets_to_emit_to)
Self::get_shares_price_ema(subnets_to_emit_to)
}

// DEPRECATED: Implementation of shares that uses EMA prices will be gradually deprecated
#[allow(dead_code)]
// Implementation of shares that uses subnet EMA prices (SubnetMovingPrice),
// not the active/spot alpha price.
fn get_shares_price_ema(subnets_to_emit_to: &[NetUid]) -> BTreeMap<NetUid, U64F64> {
// Get sum of alpha moving prices
let total_moving_prices = subnets_to_emit_to
Expand Down
8 changes: 8 additions & 0 deletions pallets/subtensor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ pub const MAX_SUBNET_CLAIMS: usize = 5;

pub const MAX_ROOT_CLAIM_THRESHOLD: u64 = 10_000_000;

/// Account flag bit that opts into receiving locked alpha transfers.
pub const ACCOUNT_FLAGS_ACCEPT_LOCKED_ALPHA: u128 = 1u128 << 0;

#[allow(deprecated)]
#[deny(missing_docs)]
#[import_section(errors::errors)]
Expand Down Expand Up @@ -1186,6 +1189,11 @@ pub mod pallet {
pub type Owner<T: Config> =
StorageMap<_, Blake2_128Concat, T::AccountId, T::AccountId, ValueQuery, DefaultAccount<T>>;

/// MAP ( coldkey ) --> flags | Account-level flags. Defaults to zero.
#[pallet::storage]
pub type AccountFlags<T: Config> =
StorageMap<_, Blake2_128Concat, T::AccountId, u128, ValueQuery>;

/// MAP ( hot ) --> take | Returns the hotkey delegation take. And signals that this key is open for delegation
#[pallet::storage]
pub type Delegates<T: Config> =
Expand Down
25 changes: 25 additions & 0 deletions pallets/subtensor/src/macros/dispatches.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2593,5 +2593,30 @@ mod dispatches {
let coldkey = ensure_signed(origin)?;
Self::do_set_perpetual_lock(&coldkey, netuid, enabled)
}
/// Sets or clears whether the caller rejects incoming locked alpha.
///
/// Coldkeys reject locked alpha by default. Passing `false` opts the
/// caller into receiving locked alpha from stake transfers or coldkey
/// swaps.
#[pallet::call_index(139)]
#[pallet::weight((
<T as frame_system::Config>::DbWeight::get().reads_writes(1, 1),
DispatchClass::Normal,
Pays::Yes
))]
pub fn set_reject_locked_alpha(origin: OriginFor<T>, enabled: bool) -> DispatchResult {
let coldkey = ensure_signed(origin)?;
AccountFlags::<T>::mutate_exists(&coldkey, |maybe_flags| {
let mut flags = maybe_flags.unwrap_or_default();
if enabled {
flags &= !crate::ACCOUNT_FLAGS_ACCEPT_LOCKED_ALPHA;
} else {
flags |= crate::ACCOUNT_FLAGS_ACCEPT_LOCKED_ALPHA;
}
*maybe_flags = if flags == 0 { None } else { Some(flags) };
});
Self::deposit_event(Event::RejectLockedAlphaUpdated { coldkey, enabled });
Ok(())
}
}
}
2 changes: 2 additions & 0 deletions pallets/subtensor/src/macros/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -305,5 +305,7 @@ mod errors {
CannotUseSystemAccount,
/// Trying to unlock more than locked
UnlockAmountTooHigh,
/// The destination coldkey rejects incoming locked alpha.
AccountRejectsLockedAlpha,
}
}
8 changes: 8 additions & 0 deletions pallets/subtensor/src/macros/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -631,5 +631,13 @@ mod events {
/// Whether this coldkey's locks are now perpetual.
enabled: bool,
},

/// A coldkey's reject locked alpha account flag was updated.
RejectLockedAlphaUpdated {
/// The coldkey whose flag changed.
coldkey: T::AccountId,
/// Whether this coldkey rejects incoming locked alpha.
enabled: bool,
},
}
}
55 changes: 49 additions & 6 deletions pallets/subtensor/src/staking/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,29 @@ impl ConvictionModel {
}

impl<T: Config> Pallet<T> {
pub fn account_rejects_locked_alpha(coldkey: &T::AccountId) -> bool {
AccountFlags::<T>::get(coldkey) & crate::ACCOUNT_FLAGS_ACCEPT_LOCKED_ALPHA != 1
}

pub fn ensure_can_receive_locked_alpha(
coldkey: &T::AccountId,
amount: AlphaBalance,
) -> DispatchResult {
let rejects_locked_alpha = Self::account_rejects_locked_alpha(coldkey);
Self::ensure_can_receive_locked_alpha_with_flag(rejects_locked_alpha, amount)
}

fn ensure_can_receive_locked_alpha_with_flag(
rejects_locked_alpha: bool,
amount: AlphaBalance,
) -> DispatchResult {
if amount.is_zero() {
return Ok(());
}
ensure!(!rejects_locked_alpha, Error::<T>::AccountRejectsLockedAlpha);
Ok(())
}

pub fn insert_lock_state(
coldkey: &T::AccountId,
netuid: NetUid,
Expand Down Expand Up @@ -1331,32 +1354,51 @@ impl<T: Config> Pallet<T> {
Self::ensure_no_active_locks(new_coldkey)?;

let mut locks_to_transfer: Vec<(NetUid, T::AccountId, LockState)> = Vec::new();
let now = Self::get_current_block_as_u64();
let unlock_rate = UnlockRate::<T>::get();
let maturity_rate = MaturityRate::<T>::get();
let new_coldkey_rejects_locked_alpha = Self::account_rejects_locked_alpha(new_coldkey);
let decaying_locks_to_transfer: Vec<(NetUid, bool)> =
DecayingLock::<T>::iter_prefix(old_coldkey).collect();

// Gather locks for old coldkey
for ((netuid, hotkey), lock) in Lock::<T>::iter_prefix((old_coldkey,)) {
locks_to_transfer.push((netuid, hotkey, lock));
}

// Remove locks for old coldkey and insert for new
for (netuid, decaying) in decaying_locks_to_transfer.iter() {
DecayingLock::<T>::insert(new_coldkey, *netuid, *decaying);
Comment on lines +1369 to +1370

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[LOW] Validate locked-alpha receipt before copying decaying flags

swap_coldkey_locks copies DecayingLock entries to new_coldkey before the new AccountRejectsLockedAlpha check runs. If old_coldkey has a decaying/perpetual flag and a non-zero rolled lock, a direct helper failure can leave the destination DecayingLock mutated even though the function returns an error. The public extrinsic path appears to rely on storage-layer rollback, but the helper's own no-mutation-on-error invariant and test do not cover these flags, and a stale flag can affect the mode of later locks for that coldkey/netuid. Build rolled_locks_to_transfer and run all rejection checks first, then insert the DecayingLock entries in the commit phase with the lock moves.

}
Comment on lines +1369 to +1371

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[LOW] Validate locked-alpha receipt before copying decaying flags

swap_coldkey_locks still copies DecayingLock rows to new_coldkey before the locked-alpha rejection check runs. If this helper is called outside the public extrinsic's transactional storage layer, an AccountRejectsLockedAlpha error can leave destination decaying flags behind even though no locks moved. Build and validate rolled_locks_to_transfer first, then copy the decaying flags in the mutation phase after validation succeeds; add a helper-level failure assertion that DecayingLock is absent on the destination.


let mut rolled_locks_to_transfer: Vec<(NetUid, T::AccountId, LockState, bool)> = Vec::new();
for (netuid, hotkey, lock) in locks_to_transfer {
let now = Self::get_current_block_as_u64();
let unlock_rate = UnlockRate::<T>::get();
let maturity_rate = MaturityRate::<T>::get();
let perpetual_lock = decaying_locks_to_transfer
.iter()
.any(|(decaying_netuid, decaying)| *decaying_netuid == netuid && !*decaying);
let old_lock = ConvictionModel::roll_forward_lock(
lock,
now,
unlock_rate,
maturity_rate,
Self::is_subnet_owner_hotkey(netuid, &hotkey),
Self::is_perpetual_lock(old_coldkey, netuid),
perpetual_lock,
);
Self::ensure_can_receive_locked_alpha_with_flag(
new_coldkey_rejects_locked_alpha,
old_lock.locked_mass,
)?;
rolled_locks_to_transfer.push((netuid, hotkey, old_lock, perpetual_lock));
}

// Remove locks for old coldkey and insert for new
for (netuid, hotkey, old_lock, perpetual_lock) in rolled_locks_to_transfer {
let new_lock = ConvictionModel::roll_forward_lock(
old_lock.clone(),
now,
unlock_rate,
maturity_rate,
Self::is_subnet_owner_hotkey(netuid, &hotkey),
Self::is_perpetual_lock(new_coldkey, netuid),
perpetual_lock,
);
Lock::<T>::remove((old_coldkey.clone(), netuid, hotkey.clone()));
Self::reduce_aggregate_lock(
Expand Down Expand Up @@ -1780,6 +1822,7 @@ impl<T: Config> Pallet<T> {
.conviction
.saturating_add(conviction_transfer);
}
Self::ensure_can_receive_locked_alpha(destination_coldkey, locked_transfer)?;

source_lock = ConvictionModel::roll_forward_lock(
source_lock,
Expand Down
14 changes: 9 additions & 5 deletions pallets/subtensor/src/tests/claim_root.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1139,11 +1139,15 @@ fn test_claim_root_coinbase_distribution() {
run_to_block(2);

let alpha_issuance = SubtensorModule::get_alpha_issuance(netuid);
// We went two blocks so we should have 2x the alpha emissions
assert_eq!(
initial_alpha_issuance + alpha_emissions.saturating_mul(2.into()),
alpha_issuance
);
// Net issuance grows by the block alpha emission (alpha_out) plus the
// root-proportion-capped alpha injection. Chain buys move alpha between the
// pool reserve and outstanding supply without changing net issuance, and with
// this subnet's small root proportion the injection is well under a second
// full emission.
let issuance_growth =
u64::from(alpha_issuance).saturating_sub(u64::from(initial_alpha_issuance));
assert!(issuance_growth >= u64::from(alpha_emissions));
assert!(issuance_growth < u64::from(alpha_emissions.saturating_mul(2.into())));

let root_prop = initial_tao as f64 / (u64::from(alpha_issuance) + initial_tao) as f64;
let root_validators_share = 0.5f64;
Expand Down
Loading
Loading