//! Gamma: a subnet's service credit, minted by burning its alpha and valued in US dollars. //! //! Gamma is held in [`GammaDeposits`], one per `(netuid, hotkey)`, owned by a coldkey and //! spent by its spender: the spender of the subnet owner's service, otherwise the subnet owner //! (lease beneficiary while its lease owns the subnet), when the deposit opens, unless the //! coldkey sets another. [`ColdkeyGammaHotkeys`] indexes a //! coldkey's deposits per subnet so coldkey swaps move them in a bounded walk. use sp_runtime::ArithmeticError; use substrate_fixed::types::U64F64; use subtensor_runtime_common::{AlphaBalance, GammaBalance, NetUid, TaoBalance}; use subtensor_swap_interface::SwapHandler; use super::*; impl Pallet { /// Requires an existing subnet with its subtoken enabled, and gamma enabled both /// network-wide and on the subnet. pub(crate) fn ensure_gamma_mintable(netuid: NetUid) -> DispatchResult { ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); Self::ensure_subtoken_enabled(netuid)?; ensure!( GammaEnabled::::get() && SubnetGammaEnabled::::get(netuid), Error::::GammaDisabled ); Ok(()) } /// The signer's own deposit at `hotkey` on an existing subnet. fn own_gamma_deposit( netuid: NetUid, hotkey: &T::AccountId, coldkey: &T::AccountId, ) -> Result, DispatchError> { ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); let deposit = GammaDeposits::::get(netuid, hotkey).ok_or(Error::::GammaDepositDoesNotExist)?; ensure!( deposit.coldkey == *coldkey, Error::::NonAssociatedColdKey ); Ok(deposit) } /// Opens a deposit only for the hotkey's associated coldkey, so another /// account cannot reserve a customer's API key by minting first. fn insert_gamma_deposit( netuid: NetUid, hotkey: &T::AccountId, deposit: GammaDeposit, ) -> DispatchResult { ensure!( Owner::::try_get(hotkey).is_ok_and(|owner| owner == deposit.coldkey), Error::::NonAssociatedColdKey ); ColdkeyGammaHotkeys::::try_mutate(netuid, &deposit.coldkey, |hotkeys| { hotkeys .try_push(hotkey.clone()) .map_err(|_| Error::::TooManyGammaDeposits) })?; GammaDeposits::::insert(netuid, hotkey, deposit); Ok(()) } /// Closes a deposit and drops its hotkey from its coldkey's list. fn remove_gamma_deposit(netuid: NetUid, hotkey: &T::AccountId, coldkey: &T::AccountId) { GammaDeposits::::remove(netuid, hotkey); ColdkeyGammaHotkeys::::mutate_exists(netuid, coldkey, |hotkeys| { if let Some(list) = hotkeys { list.retain(|listed| listed != hotkey); if list.is_empty() { *hotkeys = None; } } }); } /// The spender a deposit opens with: the spender of the service of the subnet owner's /// hotkey while the owner owns it, otherwise the beneficiary of a subnet its lease still /// owns, otherwise the subnet owner. pub(crate) fn gamma_default_spender(netuid: NetUid) -> T::AccountId { let owner = SubnetOwner::::get(netuid); let hotkey = SubnetOwnerHotkey::::get(netuid); if Owner::::get(&hotkey) == owner && let Some(service) = Services::::get(netuid, &hotkey) { return service.spender; } SubnetUidToLeaseId::::get(netuid) .and_then(SubnetLeases::::get) .filter(|lease| lease.coldkey == owner) .map_or(owner, |lease| lease.beneficiary) } /// What a deposit's daily spend cap allows at `now`: its bucket refilled by the cap /// every `GAMMA_REFILL_BLOCKS`, up to the cap. `None` when the deposit has no cap. pub(crate) fn gamma_allowance( deposit: &GammaDeposit, now: u64, ) -> Option { let GammaDailySpendCap { cap, bucket } = deposit.daily_spend_cap?; let (available, block) = bucket; let cap = cap.to_u64(); let refill = Self::mul_div_u64(cap, now.saturating_sub(block), crate::GAMMA_REFILL_BLOCKS); Some(available.to_u64().saturating_add(refill).min(cap).into()) } /// A deposit's daily spend cap. pub(crate) fn gamma_cap(deposit: &GammaDeposit) -> Option { deposit.daily_spend_cap.map(|cap| cap.cap) } /// `cap` with a full bucket, as a deposit opens with. fn full_gamma_cap(cap: Option) -> Option { let now = Self::get_current_block_as_u64(); cap.map(|cap| GammaDailySpendCap { cap, bucket: (cap, now), }) } /// The price a mint values alpha at, read before its trade: the lower of the gamma price and /// the spot price. pub(crate) fn gamma_mark(netuid: NetUid) -> U64F64 { let spot: U64F64 = T::SwapInterface::current_alpha_price(netuid.into()); SubnetGammaPrice::::get(netuid).min(spot) } /// The TAO value and gamma of `alpha` burned at `mark`, at the mint TAO/USD price. pub(crate) fn gamma_for_alpha( alpha: AlphaBalance, mark: U64F64, ) -> Result<(TaoBalance, GammaBalance), DispatchError> { let value = mark .saturating_mul(U64F64::saturating_from_num(alpha.to_u64())) .saturating_to_num::(); let price = Self::mint_tao_usd_price(Self::get_current_block_as_u64())?; let gamma = Self::mul_div_u64(value, price, 1_000_000_000); ensure!(gamma >= crate::MIN_GAMMA_AMOUNT, Error::::AmountTooLow); Ok((value.into(), gamma.into())) } /// Buys alpha with `amount` of the coldkey's TAO, as staking does without a stake /// position, and burns it. Returns the TAO taken and the alpha burned. fn buy_and_burn_alpha( netuid: NetUid, coldkey: &T::AccountId, amount: TaoBalance, ) -> Result<(TaoBalance, AlphaBalance), DispatchError> { let tao_in = Self::transfer_tao_to_subnet(netuid, coldkey, amount)?; let swap = Self::swap_tao_for_alpha(netuid, tao_in, T::SwapInterface::max_price(), false)?; let alpha: AlphaBalance = swap.amount_paid_out; Self::settle_tao_fee_to_author(netuid, swap.fee_to_block_author)?; let refund = tao_in.saturating_sub(swap.amount_paid_in.saturating_add(swap.fee_paid)); if !refund.is_zero() { Self::transfer_tao_from_subnet(netuid, coldkey, refund)?; } Self::record_tao_inflow(netuid, swap.amount_paid_in); Self::burn_subnet_alpha(netuid, alpha); Ok((tao_in.saturating_sub(refund), alpha)) } /// Credits `gamma` to the coldkey's deposit at `hotkey`, which must name `spender` and /// have `daily_spend_cap` when they are given, or opens one with them, the default spender /// and no cap otherwise, and a full bucket. Returns the deposit's spender and cap. fn credit_gamma( netuid: NetUid, coldkey: &T::AccountId, hotkey: &T::AccountId, gamma: GammaBalance, spender: Option, daily_spend_cap: Option, ) -> Result<(T::AccountId, Option), DispatchError> { match GammaDeposits::::get(netuid, hotkey) { Some(mut deposit) => { ensure!( deposit.coldkey == *coldkey, Error::::NonAssociatedColdKey ); ensure!( spender.is_none_or(|spender| spender == deposit.spender), Error::::GammaDepositSpenderMismatch ); ensure!( daily_spend_cap.is_none_or(|cap| Self::gamma_cap(&deposit) == Some(cap)), Error::::GammaDepositDailySpendCapMismatch ); deposit.balance = deposit .balance .to_u64() .checked_add(gamma.to_u64()) .ok_or(ArithmeticError::Overflow)? .into(); let terms = (deposit.spender.clone(), Self::gamma_cap(&deposit)); GammaDeposits::::insert(netuid, hotkey, deposit); Ok(terms) } None => { let spender = spender.unwrap_or_else(|| Self::gamma_default_spender(netuid)); Self::insert_gamma_deposit( netuid, hotkey, GammaDeposit { coldkey: coldkey.clone(), spender: spender.clone(), balance: gamma, daily_spend_cap: Self::full_gamma_cap(daily_spend_cap), }, )?; Ok((spender, daily_spend_cap)) } } } /// Buys alpha with `amount` TAO, burns it and credits its gamma to the signer's deposit /// at `hotkey`, which must name `spender` and have `daily_spend_cap` when they are given, or /// opens with them. pub fn do_mint_gamma( origin: OriginFor, hotkey: T::AccountId, netuid: NetUid, amount: TaoBalance, min_amount_out: GammaBalance, spender: Option, daily_spend_cap: Option, ) -> DispatchResult { let coldkey = ensure_signed(origin)?; Self::ensure_gamma_mintable(netuid)?; ensure!( Self::can_remove_balance_from_coldkey_account(&coldkey, amount), Error::::NotEnoughBalanceToStake ); Self::ensure_add_stake_input_within_swap_limit(netuid, amount)?; let mark = Self::gamma_mark(netuid); let (tao, alpha) = Self::buy_and_burn_alpha(netuid, &coldkey, amount)?; let (value, gamma) = Self::gamma_for_alpha(alpha, mark)?; ensure!(gamma >= min_amount_out, Error::::SlippageTooHigh); let (spender, daily_spend_cap) = Self::credit_gamma(netuid, &coldkey, &hotkey, gamma, spender, daily_spend_cap)?; Self::deposit_event(Event::GammaMinted { netuid, coldkey, hotkey, spender, daily_spend_cap, tao, alpha, value, gamma, }); Ok(()) } /// Burns up to `alpha_amount` of the signer's stake on `origin_hotkey`, as `burn_alpha` /// does, and credits its gamma to the signer's deposit at `destination_hotkey`, which /// must name `spender` and have `daily_spend_cap` when they are given, or opens with them. pub fn do_mint_gamma_with_stake( origin: OriginFor, origin_hotkey: T::AccountId, destination_hotkey: T::AccountId, netuid: NetUid, alpha_amount: AlphaBalance, min_amount_out: GammaBalance, spender: Option, daily_spend_cap: Option, ) -> DispatchResult { let coldkey = ensure_signed(origin.clone())?; Self::ensure_gamma_mintable(netuid)?; let mark = Self::gamma_mark(netuid); let alpha = Self::do_burn_alpha(origin, origin_hotkey, alpha_amount, netuid)?; let (value, gamma) = Self::gamma_for_alpha(alpha, mark)?; ensure!(gamma >= min_amount_out, Error::::SlippageTooHigh); let (spender, daily_spend_cap) = Self::credit_gamma( netuid, &coldkey, &destination_hotkey, gamma, spender, daily_spend_cap, )?; Self::deposit_event(Event::GammaMinted { netuid, coldkey, hotkey: destination_hotkey, spender, daily_spend_cap, tao: TaoBalance::ZERO, alpha, value, gamma, }); Ok(()) } /// Moves up to `amount` between two of the signer's deposits. Emptying the origin closes /// it; a destination it opens takes the origin's spender and daily spend cap, with a /// full bucket. pub fn do_move_gamma( origin: OriginFor, origin_hotkey: T::AccountId, destination_hotkey: T::AccountId, netuid: NetUid, amount: GammaBalance, ) -> DispatchResult { let coldkey = ensure_signed(origin)?; ensure!( origin_hotkey != destination_hotkey, Error::::InvalidValue ); let mut deposit = Self::own_gamma_deposit(netuid, &origin_hotkey, &coldkey)?; let amount = amount.min(deposit.balance); ensure!(!amount.is_zero(), Error::::AmountTooLow); let destination = match GammaDeposits::::get(netuid, &destination_hotkey) { Some(mut destination) => { ensure!( destination.coldkey == coldkey, Error::::NonAssociatedColdKey ); destination.balance = destination .balance .to_u64() .checked_add(amount.to_u64()) .ok_or(ArithmeticError::Overflow)? .into(); Some(destination) } None => None, }; deposit.balance = deposit.balance.saturating_sub(amount); let closes = deposit.balance.is_zero(); if closes { Self::remove_gamma_deposit(netuid, &origin_hotkey, &coldkey); } else { GammaDeposits::::insert(netuid, &origin_hotkey, &deposit); } match destination { Some(destination) => { GammaDeposits::::insert(netuid, &destination_hotkey, destination) } None => Self::insert_gamma_deposit( netuid, &destination_hotkey, GammaDeposit { coldkey: coldkey.clone(), spender: deposit.spender, balance: amount, daily_spend_cap: Self::full_gamma_cap( deposit.daily_spend_cap.map(|cap| cap.cap), ), }, )?, } Self::deposit_event(Event::GammaMoved { netuid, coldkey: coldkey.clone(), origin_hotkey: origin_hotkey.clone(), destination_hotkey, amount, }); if closes { Self::deposit_event(Event::GammaDepositClosed { netuid, coldkey, hotkey: origin_hotkey, }); } Ok(()) } /// Closes the signer's empty deposit, releasing the hotkey. pub fn do_close_gamma_deposit( origin: OriginFor, hotkey: T::AccountId, netuid: NetUid, ) -> DispatchResult { let coldkey = ensure_signed(origin)?; let deposit = Self::own_gamma_deposit(netuid, &hotkey, &coldkey)?; ensure!(deposit.balance.is_zero(), Error::::GammaDepositNotEmpty); Self::remove_gamma_deposit(netuid, &hotkey, &coldkey); Self::deposit_event(Event::GammaDepositClosed { netuid, coldkey, hotkey, }); Ok(()) } /// Sets the deposit's spender, or the default spender when `spender` is `None`. pub fn do_set_gamma_deposit_spender( origin: OriginFor, hotkey: T::AccountId, netuid: NetUid, spender: Option, ) -> DispatchResult { let coldkey = ensure_signed(origin)?; let mut deposit = Self::own_gamma_deposit(netuid, &hotkey, &coldkey)?; let spender = spender.unwrap_or_else(|| Self::gamma_default_spender(netuid)); deposit.spender = spender.clone(); GammaDeposits::::insert(netuid, &hotkey, deposit); Self::deposit_event(Event::GammaDepositSpenderSet { netuid, coldkey, hotkey, spender, }); Ok(()) } /// Sets the deposit's daily spend cap, or removes it when `daily_spend_cap` is `None`. A cap /// set where there was none starts with a full bucket, and a changed cap keeps what the /// bucket allows now, up to the new cap. pub fn do_set_gamma_deposit_daily_spend_cap( origin: OriginFor, hotkey: T::AccountId, netuid: NetUid, daily_spend_cap: Option, ) -> DispatchResult { let coldkey = ensure_signed(origin)?; let mut deposit = Self::own_gamma_deposit(netuid, &hotkey, &coldkey)?; let now = Self::get_current_block_as_u64(); let allowance = Self::gamma_allowance(&deposit, now); deposit.daily_spend_cap = daily_spend_cap.map(|cap| GammaDailySpendCap { cap, bucket: (allowance.map_or(cap, |allowance| allowance.min(cap)), now), }); GammaDeposits::::insert(netuid, &hotkey, deposit); Self::deposit_event(Event::GammaDepositDailySpendCapSet { netuid, coldkey, hotkey, daily_spend_cap, }); Ok(()) } /// Burns, in order, each entry's amount from the deposits the signer spends. Entries the /// signer cannot spend, or whose amount is zero or above the deposit's balance or what its /// daily spend cap allows, are skipped with the reason. Returns how many entries spent. pub fn do_spend_gamma( origin: OriginFor, netuid: NetUid, spends: &[(T::AccountId, GammaBalance, [u8; 32])], ) -> Result { let signer = ensure_signed(origin)?; ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); ensure!(!spends.is_empty(), Error::::InvalidValue); let now = Self::get_current_block_as_u64(); let mut spent: u32 = 0; for (hotkey, amount, reference) in spends { match Self::spend_gamma_entry(netuid, &signer, hotkey, *amount, now) { Ok(coldkey) => { Self::deposit_event(Event::GammaSpent { netuid, coldkey, hotkey: hotkey.clone(), spender: signer.clone(), amount: *amount, reference: *reference, }); spent = spent.saturating_add(1); } Err(reason) => Self::deposit_event(Event::GammaSpendSkipped { netuid, hotkey: hotkey.clone(), spender: signer.clone(), amount: *amount, reference: *reference, reason: reason.into(), }), } } Ok(spent) } /// Burns `amount` from the deposit at `hotkey` if `spender` can spend all of it now, and /// returns the deposit's coldkey; otherwise the error says why the entry is skipped. fn spend_gamma_entry( netuid: NetUid, spender: &T::AccountId, hotkey: &T::AccountId, amount: GammaBalance, now: u64, ) -> Result> { let mut deposit = GammaDeposits::::get(netuid, hotkey).ok_or(Error::::GammaDepositDoesNotExist)?; ensure!( deposit.spender == *spender, Error::::NotGammaDepositSpender ); ensure!(!amount.is_zero(), Error::::AmountTooLow); ensure!( amount <= deposit.balance, Error::::InsufficientGammaBalance ); let allowance = Self::gamma_allowance(&deposit, now); ensure!( allowance.is_none_or(|allowance| amount <= allowance), Error::::GammaDepositDailySpendCapExceeded ); deposit.balance = deposit.balance.saturating_sub(amount); if let (Some(allowance), Some(cap)) = (allowance, deposit.daily_spend_cap.as_mut()) { cap.bucket = (allowance.saturating_sub(amount), now); } let coldkey = deposit.coldkey.clone(); GammaDeposits::::insert(netuid, hotkey, deposit); Ok(coldkey) } /// Moves the old coldkey's deposits on `netuid` to the new coldkey, terms unchanged; fails /// if the new coldkey would hold more than [`crate::MAX_GAMMA_DEPOSITS`] there. pub(crate) fn transfer_coldkey_gamma_deposits( netuid: NetUid, old_coldkey: &T::AccountId, new_coldkey: &T::AccountId, ) -> DispatchResult { let hotkeys = ColdkeyGammaHotkeys::::get(netuid, old_coldkey); if hotkeys.is_empty() { return Ok(()); } ColdkeyGammaHotkeys::::remove(netuid, old_coldkey); let mut merged = ColdkeyGammaHotkeys::::get(netuid, new_coldkey); for hotkey in hotkeys { GammaDeposits::::mutate(netuid, &hotkey, |deposit| { if let Some(deposit) = deposit { deposit.coldkey = new_coldkey.clone(); } }); merged .try_push(hotkey) .map_err(|_| Error::::TooManyGammaDeposits)?; } ColdkeyGammaHotkeys::::insert(netuid, new_coldkey, merged); Ok(()) } /// The number of gamma deposits `coldkey` holds across subnets. pub(crate) fn coldkey_gamma_deposit_count(coldkey: &T::AccountId) -> u32 { Self::get_all_subnet_netuids() .into_iter() .map(|netuid| ColdkeyGammaHotkeys::::decode_len(netuid, coldkey).unwrap_or(0) as u32) .fold(0, u32::saturating_add) } }