Skip to content
Closed
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
fb47d0c
Merge mock and mock_high_ed
lorismoulin Jun 10, 2026
485f4bf
Added SubnetSaleOffers and frozen coldkey/hotkey storage
lorismoulin Jun 10, 2026
25d83ba
Added extrinsic to create/cancel sale offer
lorismoulin Jun 10, 2026
5e9801f
Cleanup sale offer on subnet removal
lorismoulin Jun 10, 2026
a93d4a9
Added tests
lorismoulin Jun 10, 2026
35bf96b
Added CheckSubnetSale dispatch extension
lorismoulin Jun 10, 2026
54cf502
Added benchmarks and weights placeholder
lorismoulin Jun 10, 2026
4e25ad2
Merge branch 'devnet-ready' into subnet-sale-offer
lorismoulin Jun 10, 2026
79fbaad
Added creation date for later use
lorismoulin Jun 10, 2026
175bd10
Merge branch 'devnet-ready' into subnet-sale-offer
lorismoulin Jul 1, 2026
d664ff9
Fix some issues
lorismoulin Jul 5, 2026
1733a3b
Added SaleOfferId and harden protection against swaps
lorismoulin Jul 5, 2026
36752d1
Add missing benchmark for extension
lorismoulin Jul 5, 2026
54a4601
cargo fmt
lorismoulin Jul 5, 2026
6fe155c
auto-update benchmark weights
github-actions[bot] Jul 6, 2026
f327d37
Add sale offer calls to SubtensorCommonCalls
lorismoulin Jul 6, 2026
9cd2a95
Merge remote-tracking branch 'refs/remotes/origin/subnet-sale-offer' …
lorismoulin Jul 6, 2026
e97ff3a
Sale offer creation excluded from every group and cancel as common
lorismoulin Jul 6, 2026
bf1b3c8
cargo fmt
lorismoulin Jul 6, 2026
3858895
Merge branch 'devnet-ready' into subnet-sale-offer
lorismoulin Jul 8, 2026
d5e046f
Merge branch 'devnet-ready' into subnet-sale-offer
lorismoulin Jul 9, 2026
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
64 changes: 64 additions & 0 deletions pallets/subtensor/src/benchmarks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,24 @@ mod pallet_benchmarks {
ecdsa::Signature::from_raw(signature)
}

fn setup_subnet_sale_offer<T: Config>()
-> (NetUid, T::AccountId, T::AccountId, T::AccountId, TaoBalance) {
let netuid = NetUid::from(1);
let seller: T::AccountId = whitelisted_caller();
let owner_hotkey: T::AccountId = account("owner_hotkey", 0, 0);
let authorized_buyer: T::AccountId = account("authorized_buyer", 0, 0);
let price = TaoBalance::from(1_000_000_000_u64);

Subtensor::<T>::init_new_network(netuid, 1);
SubnetOwner::<T>::insert(netuid, seller.clone());
assert_ok!(Subtensor::<T>::set_subnet_owner_hotkey(
netuid,
&owner_hotkey
));

(netuid, seller, owner_hotkey, authorized_buyer, price)
}

#[benchmark]
fn register() {
let netuid = NetUid::from(1);
Expand Down Expand Up @@ -1851,6 +1869,52 @@ mod pallet_benchmarks {
assert!(!AccumulatedLeaseDividends::<T>::contains_key(lease_id));
}

#[benchmark]
fn create_sale_offer() {
let (netuid, seller, owner_hotkey, authorized_buyer, price) =
setup_subnet_sale_offer::<T>();

#[extrinsic_call]
_(
RawOrigin::Signed(seller.clone()),
netuid,
price,
Some(authorized_buyer.clone()),
);

let offer = SubnetSaleOffers::<T>::get(netuid).unwrap();
assert_eq!(offer.netuid, netuid);
assert_eq!(offer.seller, seller);
assert_eq!(offer.owner_hotkey, owner_hotkey);
assert_eq!(offer.authorized_buyer, Some(authorized_buyer));
assert_eq!(offer.price, price.into());
assert_eq!(offer.created_at, frame_system::Pallet::<T>::block_number());
assert!(SubnetSaleFrozenColdkeys::<T>::contains_key(&offer.seller));
assert!(SubnetSaleFrozenHotkeys::<T>::contains_key(
&offer.owner_hotkey
));
}

#[benchmark]
fn cancel_sale_offer() {
let (netuid, seller, owner_hotkey, authorized_buyer, price) =
setup_subnet_sale_offer::<T>();

assert_ok!(Subtensor::<T>::create_sale_offer(
RawOrigin::Signed(seller.clone()).into(),
netuid,
price,
Some(authorized_buyer),
));

#[extrinsic_call]
_(RawOrigin::Signed(seller.clone()), netuid);

assert!(!SubnetSaleOffers::<T>::contains_key(netuid));
assert!(!SubnetSaleFrozenColdkeys::<T>::contains_key(seller));
assert!(!SubnetSaleFrozenHotkeys::<T>::contains_key(owner_hotkey));
}

#[benchmark]
fn update_symbol() {
let coldkey: T::AccountId = whitelisted_caller();
Expand Down
6 changes: 6 additions & 0 deletions pallets/subtensor/src/coinbase/root.rs
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,12 @@ impl<T: Config> Pallet<T> {
AccumulatedLeaseDividends::<T>::remove(lease_id);
}

// --- 23. Subnet sale offers: release sale locks.
if let Some(offer) = SubnetSaleOffers::<T>::take(netuid) {
SubnetSaleFrozenColdkeys::<T>::remove(&offer.seller);
SubnetSaleFrozenHotkeys::<T>::remove(&offer.owner_hotkey);
}

// --- 23: Locks cleanup
Self::destroy_lock_maps(netuid);

Expand Down
22 changes: 19 additions & 3 deletions pallets/subtensor/src/extensions/subtensor.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::{
Call, CheckColdkeySwap, CheckDelegateTake, CheckEvmKeyAssociation, CheckRateLimits,
CheckServingEndpoints, CheckWeights, Config, Error, guards::applicable_call,
CheckServingEndpoints, CheckSubnetSale, CheckWeights, Config, Error, guards::applicable_call,
};
use codec::{Decode, DecodeWithMemTracking, Encode};
use frame_support::{
Expand Down Expand Up @@ -88,6 +88,7 @@ impl<T: Config + Send + Sync + TypeInfo> SubtensorTransactionExtension<T> {
};

CheckColdkeySwap::<T>::check(who, call)?;
CheckSubnetSale::<T>::check(who, call)?;

if let Some(call) = applicable_call(call, CheckWeights::<T>::applies_to) {
CheckWeights::<T>::check(who, call)?;
Expand Down Expand Up @@ -126,6 +127,7 @@ where
fn weight(&self, call: &CallOf<T>) -> Weight {
use DispatchExtension as DE;
<CheckColdkeySwap<T> as DE<CallOf<T>>>::weight(call)
.saturating_add(<CheckSubnetSale<T> as DE<CallOf<T>>>::weight(call))
.saturating_add(<CheckWeights<T> as DE<CallOf<T>>>::weight(call))
.saturating_add(<CheckRateLimits<T> as DE<CallOf<T>>>::weight(call))
.saturating_add(<CheckDelegateTake<T> as DE<CallOf<T>>>::weight(call))
Expand Down Expand Up @@ -157,8 +159,8 @@ mod tests {
use super::SubtensorTransactionExtension;
use crate::{
CheckColdkeySwap, CheckDelegateTake, CheckEvmKeyAssociation, CheckRateLimits,
CheckServingEndpoints, CheckWeights, ColdkeySwapAnnouncements, ColdkeySwapDisputes,
tests::mock::*,
CheckServingEndpoints, CheckSubnetSale, CheckWeights, ColdkeySwapAnnouncements,
ColdkeySwapDisputes, SubnetSaleFrozenColdkeys, tests::mock::*,
};
use frame_support::{
assert_ok,
Expand Down Expand Up @@ -197,6 +199,7 @@ mod tests {
fn expected_transaction_extension_weight(call: &RuntimeCall) -> frame_support::weights::Weight {
use DispatchExtension as DE;
<CheckColdkeySwap<Test> as DE<RuntimeCall>>::weight(call)
.saturating_add(<CheckSubnetSale<Test> as DE<RuntimeCall>>::weight(call))
.saturating_add(<CheckWeights<Test> as DE<RuntimeCall>>::weight(call))
.saturating_add(<CheckRateLimits<Test> as DE<RuntimeCall>>::weight(call))
.saturating_add(<CheckDelegateTake<Test> as DE<RuntimeCall>>::weight(call))
Expand Down Expand Up @@ -278,6 +281,19 @@ mod tests {
});
}

#[test]
fn validate_rejects_sale_frozen_coldkey_call() {
new_test_ext(1).execute_with(|| {
let seller = U256::from(1);
let call = RuntimeCall::System(frame_system::Call::remark { remark: vec![] });

SubnetSaleFrozenColdkeys::<Test>::insert(seller, ());

let err = validate_signed(seller, &call).unwrap_err();
assert_eq!(err, CustomTransactionError::BadRequest.into());
});
}

#[test]
fn weight_matches_top_level_dispatch_extension_checks() {
new_test_ext(1).execute_with(|| {
Expand Down
5 changes: 5 additions & 0 deletions pallets/subtensor/src/guards/check_coldkey_swap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,11 @@ mod tests {
RuntimeCall::SubtensorModule(crate::Call::register_network {
hotkey: U256::from(1),
}),
RuntimeCall::SubtensorModule(crate::Call::create_sale_offer {
netuid: 1u16.into(),
price: TaoBalance::from(1_000_u64),
authorized_buyer: None,
}),
]
}

Expand Down
Loading
Loading