//! This file defines abstraction for subnet leasing. //! //! It is used to register a new leased network through a crowdloan using the `register_leased_network` extrinsic //! as a call parameter to the crowdloan pallet `create` extrinsic. A new subnet will be registered //! paying the lock cost using the crowdloan funds and a proxy will be created for the beneficiary //! to operate the subnet. //! //! The crowdloan's contributions are used to compute the share of the emissions that the contributors //! will receive as dividends. The leftover cap is refunded to the contributors and the beneficiary. //! //! The lease can have a defined end block, after which the lease will be terminated and the subnet //! will be transferred to the beneficiary. In case the lease is perpetual, the lease will never be //! terminated and emissions will continue to be distributed to the contributors. //! //! The lease can be terminated by the beneficiary after the end block has passed (if any) and the subnet //! ownership will be transferred to the beneficiary. use super::*; use crate::weights::WeightInfo; use frame_support::{ dispatch::RawOrigin, traits::{Defensive, fungible::*}, }; use frame_system::pallet_prelude::OriginFor; use frame_system::pallet_prelude::*; use sp_core::blake2_256; use sp_runtime::{Percent, traits::TrailingZeroInput}; use substrate_fixed::types::U64F64; use subtensor_runtime_common::{AlphaBalance, NetUid}; pub type LeaseId = u32; pub type CurrencyOf = ::Currency; pub type BalanceOf = as fungible::Inspect<::AccountId>>::Balance; #[freeze_struct("8cc3d0594faed7dd")] #[derive(Encode, Decode, Eq, PartialEq, Ord, PartialOrd, RuntimeDebug, TypeInfo)] pub struct SubnetLease { /// The beneficiary of the lease, able to operate the subnet through /// a proxy and taking ownership of the subnet at the end of the lease (if defined). pub beneficiary: AccountId, /// The coldkey of the lease. pub coldkey: AccountId, /// The hotkey of the lease. pub hotkey: AccountId, /// The share of the emissions that the contributors will receive. pub emissions_share: Percent, /// The block at which the lease will end. If not defined, the lease is perpetual. pub end_block: Option, /// The netuid of the subnet that the lease is for. pub netuid: NetUid, /// The cost of the lease including the network registration and proxy. pub cost: Balance, } pub type SubnetLeaseOf = SubnetLease<::AccountId, BlockNumberFor, BalanceOf>; impl Pallet { /// Register a new leased network through a crowdloan. A new subnet will be registered /// paying the lock cost using the crowdloan funds and a proxy will be created for the beneficiary /// to operate the subnet. /// /// The crowdloan's contributions are used to compute the share of the emissions that the contributors /// will receive as dividends. /// /// The leftover cap is refunded to the contributors and the beneficiary. pub fn do_register_leased_network( origin: OriginFor, emissions_share: Percent, end_block: Option>, ) -> DispatchResultWithPostInfo { let who = ensure_signed(origin)?; let now = frame_system::Pallet::::block_number(); // Ensure the origin is the creator of the crowdloan let (crowdloan_id, crowdloan) = Self::get_crowdloan_being_finalized()?; ensure!( who == crowdloan.creator, Error::::InvalidLeaseBeneficiary ); if let Some(end_block) = end_block { ensure!(end_block > now, Error::::LeaseCannotEndInThePast); } // Initialize the lease id, coldkey and hotkey and keep track of them let lease_id = Self::get_next_lease_id()?; let lease_coldkey = Self::lease_coldkey(lease_id)?; let lease_hotkey = Self::lease_hotkey(lease_id)?; frame_system::Pallet::::inc_providers(&lease_coldkey); frame_system::Pallet::::inc_providers(&lease_hotkey); Self::transfer_tao(&crowdloan.funds_account, &lease_coldkey, crowdloan.raised)?; Self::do_register_network( RawOrigin::Signed(lease_coldkey.clone()).into(), &lease_hotkey, 1, None, )?; let netuid = Self::find_lease_netuid(&lease_coldkey).ok_or(Error::::LeaseNetuidNotFound)?; // Enable the beneficiary to operate the subnet through a proxy T::ProxyInterface::add_lease_beneficiary_proxy(&lease_coldkey, &who)?; // Get left leftover cap and compute the cost of the registration + proxy let leftover_cap = ::Currency::balance(&lease_coldkey); let cost = crowdloan.raised.saturating_sub(leftover_cap); SubnetLeases::::insert( lease_id, SubnetLease { beneficiary: who.clone(), coldkey: lease_coldkey.clone(), hotkey: lease_hotkey.clone(), emissions_share, end_block, netuid, cost, }, ); SubnetUidToLeaseId::::insert(netuid, lease_id); // The lease take should be 0% to allow all contributors to receive dividends entirely. Self::delegate_hotkey(&lease_hotkey, 0); // Get all the contributions to the crowdloan except for the beneficiary // because its share will be computed as the dividends are distributed let contributions = pallet_crowdloan::Contributions::::iter_prefix(crowdloan_id) .into_iter() .filter(|(contributor, _)| contributor != &who); let mut refunded_cap = 0u64; for (contributor, amount) in contributions { // Compute the share of the contributor to the lease let share: U64F64 = U64F64::from(u64::from(amount)) .saturating_div(U64F64::from(u64::from(crowdloan.raised))); SubnetLeaseShares::::insert(lease_id, &contributor, share); // Refund the unused part of the cap to the contributor relative to their share let contributor_refund = share .saturating_mul(U64F64::from(u64::from(leftover_cap))) .floor() .saturating_to_num::(); Self::transfer_tao(&lease_coldkey, &contributor, contributor_refund.into())?; refunded_cap = refunded_cap.saturating_add(contributor_refund); } // Refund what's left after refunding the contributors to the beneficiary let beneficiary_refund = leftover_cap.saturating_sub(refunded_cap.into()); Self::transfer_tao(&lease_coldkey, &who, beneficiary_refund)?; Self::deposit_event(Event::SubnetLeaseCreated { beneficiary: who, lease_id, netuid, end_block, }); if crowdloan.contributors_count < T::MaxContributors::get() { // We have less contributors than the max allowed, so we need to refund the difference Ok(Some(::WeightInfo::register_leased_network( crowdloan.contributors_count, )) .into()) } else { // We have the max number of contributors, so we don't need to refund anything Ok(().into()) } } /// Terminate a lease. /// /// The beneficiary can terminate the lease after the end block has passed and get the subnet ownership. /// The subnet is transferred to the beneficiary and the lease is removed from storage. pub fn do_terminate_lease( origin: OriginFor, lease_id: LeaseId, hotkey: T::AccountId, ) -> DispatchResultWithPostInfo { let who = ensure_signed(origin)?; let now = frame_system::Pallet::::block_number(); // Ensure the lease exists and the beneficiary is the caller let lease = SubnetLeases::::get(lease_id).ok_or(Error::::LeaseDoesNotExist)?; ensure!( lease.beneficiary == who, Error::::ExpectedBeneficiaryOrigin ); // Ensure the lease has an end block and we are past it let end_block = lease.end_block.ok_or(Error::::LeaseHasNoEndBlock)?; ensure!(now >= end_block, Error::::LeaseHasNotEnded); // Transfer ownership to the beneficiary ensure!( Self::coldkey_owns_hotkey(&lease.beneficiary, &hotkey), Error::::BeneficiaryDoesNotOwnHotkey ); SubnetOwner::::insert(lease.netuid, lease.beneficiary.clone()); Self::set_subnet_owner_hotkey(lease.netuid, &hotkey)?; // Stop tracking the lease coldkey and hotkey let _ = frame_system::Pallet::::dec_providers(&lease.coldkey).defensive(); let _ = frame_system::Pallet::::dec_providers(&lease.hotkey).defensive(); // Remove the lease, its contributors and accumulated dividends from storage let clear_result = SubnetLeaseShares::::clear_prefix(lease_id, T::MaxContributors::get(), None); AccumulatedLeaseDividends::::remove(lease_id); SubnetLeases::::remove(lease_id); SubnetUidToLeaseId::::remove(lease.netuid); // Remove the beneficiary proxy T::ProxyInterface::remove_lease_beneficiary_proxy(&lease.coldkey, &lease.beneficiary)?; Self::deposit_event(Event::SubnetLeaseTerminated { beneficiary: lease.beneficiary, netuid: lease.netuid, }); // Lease shares exclude the beneficiary, while the benchmark's `k` includes them. let contributors_count = clear_result.unique.saturating_add(1); if contributors_count < T::MaxContributors::get() { // We have cleared less than the max number of shareholders, so we need to refund the difference Ok(Some(::WeightInfo::terminate_lease( contributors_count, )) .into()) } else { // We have cleared the max number of shareholders, so we don't need to refund anything Ok(().into()) } } /// Hook used when the subnet owner's cut is distributed to split the amount into dividends /// for the contributors and the beneficiary in shares relative to their initial contributions. /// It accumulates dividends to be distributed later when the interval for distribution is reached. /// Distribution is made in alpha and stake to the contributor coldkey and lease hotkey. pub fn distribute_leased_network_dividends(lease_id: LeaseId, owner_cut_alpha: AlphaBalance) { // Ensure the lease exists let Some(lease) = SubnetLeases::::get(lease_id) else { log::debug!("Lease {lease_id} doesn't exists so we can't distribute dividends"); return; }; // Ensure the lease has not ended let now = frame_system::Pallet::::block_number(); if lease.end_block.is_some_and(|end_block| end_block <= now) { return; } // Get the actual amount of alpha to distribute from the owner's cut, // we voluntarily round up to favor the contributors let current_contributors_cut_alpha = lease.emissions_share.mul_ceil(owner_cut_alpha.to_u64()); // Get the total amount of alpha to distribute from the contributors // including the dividends accumulated so far let total_contributors_cut_alpha = AccumulatedLeaseDividends::::get(lease_id) .saturating_add(current_contributors_cut_alpha.into()); // Ensure the distribution interval is not zero let rem = now .into() .checked_rem(T::LeaseDividendsDistributionInterval::get().into()); if rem.is_none() { // This should never happen but we check it anyway log::error!("LeaseDividendsDistributionInterval must be greater than 0"); AccumulatedLeaseDividends::::set(lease_id, total_contributors_cut_alpha); return; } else if rem.is_some_and(|rem| rem > 0u32.into()) { // This is not the time to distribute dividends, so we accumulate the dividends AccumulatedLeaseDividends::::set(lease_id, total_contributors_cut_alpha); return; } // We use a storage layer to ensure the distribution is atomic. if let Err(err) = frame_support::storage::with_storage_layer(|| { let mut alpha_distributed = AlphaBalance::ZERO; // Distribute the contributors cut to the contributors and accumulate the alpha // distributed so far to obtain how much alpha is left to distribute to the beneficiary for (contributor, share) in SubnetLeaseShares::::iter_prefix(lease_id) { let alpha_for_contributor = share .saturating_mul(U64F64::from(total_contributors_cut_alpha.to_u64())) .ceil() .saturating_to_num::(); Self::transfer_stake_within_subnet( &lease.coldkey, &lease.hotkey, &contributor, &lease.hotkey, lease.netuid, alpha_for_contributor.into(), )?; alpha_distributed = alpha_distributed.saturating_add(alpha_for_contributor.into()); Self::deposit_event(Event::SubnetLeaseDividendsDistributed { lease_id, contributor, alpha: alpha_for_contributor.into(), }); } // Distribute the leftover alpha to the beneficiary let beneficiary_cut_alpha = total_contributors_cut_alpha.saturating_sub(alpha_distributed); Self::transfer_stake_within_subnet( &lease.coldkey, &lease.hotkey, &lease.beneficiary, &lease.hotkey, lease.netuid, beneficiary_cut_alpha.into(), )?; Self::deposit_event(Event::SubnetLeaseDividendsDistributed { lease_id, contributor: lease.beneficiary.clone(), alpha: beneficiary_cut_alpha.into(), }); // Reset the accumulated dividends AccumulatedLeaseDividends::::insert(lease_id, AlphaBalance::ZERO); Ok::<(), DispatchError>(()) }) { log::debug!("Couldn't distributing dividends for lease {lease_id}: {err:?}"); AccumulatedLeaseDividends::::set(lease_id, total_contributors_cut_alpha); }; } fn lease_coldkey(lease_id: LeaseId) -> Result { let entropy = ("leasing/coldkey", lease_id).using_encoded(blake2_256); T::AccountId::decode(&mut TrailingZeroInput::new(entropy.as_ref())) .map_err(|_| Error::::InvalidValue.into()) } fn lease_hotkey(lease_id: LeaseId) -> Result { let entropy = ("leasing/hotkey", lease_id).using_encoded(blake2_256); T::AccountId::decode(&mut TrailingZeroInput::new(entropy.as_ref())) .map_err(|_| Error::::InvalidValue.into()) } fn get_next_lease_id() -> Result> { let lease_id = NextSubnetLeaseId::::get(); // Increment the lease id let next_lease_id = lease_id.checked_add(1).ok_or(Error::::Overflow)?; NextSubnetLeaseId::::put(next_lease_id); Ok(lease_id) } fn find_lease_netuid(lease_coldkey: &T::AccountId) -> Option { SubnetOwner::::iter() .find(|(_, coldkey)| coldkey == lease_coldkey) .map(|(netuid, _)| netuid) } // Get the crowdloan being finalized from the crowdloan pallet when the call is executed, // and the current crowdloan ID is exposed to us. fn get_crowdloan_being_finalized() -> Result< ( pallet_crowdloan::CrowdloanId, pallet_crowdloan::CrowdloanInfoOf, ), pallet_crowdloan::Error, > { let crowdloan_id = pallet_crowdloan::CurrentCrowdloanId::::get() .ok_or(pallet_crowdloan::Error::::InvalidCrowdloanId)?; let crowdloan = pallet_crowdloan::Crowdloans::::get(crowdloan_id) .ok_or(pallet_crowdloan::Error::::InvalidCrowdloanId)?; Ok((crowdloan_id, crowdloan)) } }