diff --git a/.github/workflows/require-clean-merges.yml b/.github/workflows/require-clean-merges.yml index c00a4b47e2..ac0a5f31fe 100644 --- a/.github/workflows/require-clean-merges.yml +++ b/.github/workflows/require-clean-merges.yml @@ -34,6 +34,14 @@ jobs: else echo "MERGE_BRANCHES=devnet-ready devnet testnet main" >> $GITHUB_ENV fi + + - name: Add Fork Remote and Fetch PR Branch + if: github.event.pull_request.head.repo.fork == true + run: | + PR_BRANCH="${{ github.event.pull_request.head.ref }}" + PR_FORK="${{ github.event.pull_request.head.repo.clone_url }}" + git remote add fork $PR_FORK + git fetch --no-tags --prune fork $PR_BRANCH - name: Check Merge Cleanliness run: | @@ -42,25 +50,33 @@ jobs: echo "Fetching all branches..." git fetch --all --prune + if [[ "${{github.event.pull_request.head.repo.fork}}" == "true" ]]; then + PR_BRANCH_REF="fork/$PR_BRANCH" + echo "Using fork reference: $PR_BRANCH_REF" + else + PR_BRANCH_REF="origin/$PR_BRANCH" + echo "Using origin reference: $PR_BRANCH_REF" + fi + echo "Checking out PR branch: $PR_BRANCH" - git checkout $PR_BRANCH - git reset --hard origin/$PR_BRANCH + git checkout $PR_BRANCH_REF + git reset --hard $PR_BRANCH_REF # Configure a temporary Git identity to allow merging git config --local user.email "github-actions@github.com" git config --local user.name "GitHub Actions" for branch in $MERGE_BRANCHES; do - echo "Checking merge from $branch into $PR_BRANCH..." + echo "Checking merge from $branch into $PR_BRANCH_REF..." # Ensure PR branch is up to date - git reset --hard origin/$PR_BRANCH + git reset --hard $PR_BRANCH_REF # Merge without committing to check for conflicts if git merge --no-commit --no-ff origin/$branch; then - echo "✅ Merge from $branch into $PR_BRANCH is clean." + echo "✅ Merge from $branch into $PR_BRANCH_REF is clean." else - echo "❌ Merge conflict detected when merging $branch into $PR_BRANCH" + echo "❌ Merge conflict detected when merging $branch into $PR_BRANCH_REF" exit 1 fi diff --git a/.github/workflows/run-benchmarks.yml b/.github/workflows/run-benchmarks.yml index 6040485eca..7d4cb9c2a7 100644 --- a/.github/workflows/run-benchmarks.yml +++ b/.github/workflows/run-benchmarks.yml @@ -24,18 +24,20 @@ jobs: if: ${{ env.SKIP_BENCHMARKS != '1' }} uses: actions/checkout@v4 with: - ref: ${{ github.head_ref }} + repository: ${{ github.event.pull_request.head.repo.full_name }} + ref: ${{ github.event.pull_request.head.ref }} fetch-depth: 0 - name: Install GitHub CLI - if: ${{ env.SKIP_BENCHMARKS != '1' }} + # We disallow skipping benchmarks for PRs from forks to avoid exposing secrets + if: ${{ github.event.pull_request.head.repo.full_name == github.repository && env.SKIP_BENCHMARKS != '1' }} run: | sudo apt-get update sudo apt-get install -y gh echo "${{ secrets.GITHUB_TOKEN }}" | gh auth login --with-token - name: Check skip label - if: ${{ env.SKIP_BENCHMARKS != '1' }} + if: ${{ github.event.pull_request.head.repo.full_name == github.repository && env.SKIP_BENCHMARKS != '1' }} run: | labels=$(gh pr view ${{ github.event.pull_request.number }} --json labels --jq '.labels[].name') if echo "$labels" | grep -q "skip-validate-benchmarks"; then @@ -50,7 +52,7 @@ jobs: sudo apt-get install -y clang curl libssl-dev llvm libudev-dev protobuf-compiler - name: Check skip label - if: ${{ env.SKIP_BENCHMARKS != '1' }} + if: ${{ github.event.pull_request.head.repo.full_name == github.repository && env.SKIP_BENCHMARKS != '1' }} run: | labels=$(gh pr view ${{ github.event.pull_request.number }} --json labels --jq '.labels[].name') if echo "$labels" | grep -q "skip-validate-benchmarks"; then @@ -66,7 +68,7 @@ jobs: toolchain: stable - name: Check skip label - if: ${{ env.SKIP_BENCHMARKS != '1' }} + if: ${{ github.event.pull_request.head.repo.full_name == github.repository && env.SKIP_BENCHMARKS != '1' }} run: | labels=$(gh pr view ${{ github.event.pull_request.number }} --json labels --jq '.labels[].name') if echo "$labels" | grep -q "skip-validate-benchmarks"; then @@ -81,7 +83,7 @@ jobs: key: bench-${{ hashFiles('**/Cargo.lock') }} - name: Check skip label - if: ${{ env.SKIP_BENCHMARKS != '1' }} + if: ${{ github.event.pull_request.head.repo.full_name == github.repository && env.SKIP_BENCHMARKS != '1' }} run: | labels=$(gh pr view ${{ github.event.pull_request.number }} --json labels --jq '.labels[].name') if echo "$labels" | grep -q "skip-validate-benchmarks"; then @@ -95,7 +97,7 @@ jobs: cargo build --profile production -p node-subtensor --features runtime-benchmarks - name: Check skip label - if: ${{ env.SKIP_BENCHMARKS != '1' }} + if: ${{ github.event.pull_request.head.repo.full_name == github.repository && env.SKIP_BENCHMARKS != '1' }} run: | labels=$(gh pr view ${{ github.event.pull_request.number }} --json labels --jq '.labels[].name') if echo "$labels" | grep -q "skip-validate-benchmarks"; then @@ -110,7 +112,7 @@ jobs: ./scripts/benchmark_action.sh - name: Check skip label after run - if: ${{ env.SKIP_BENCHMARKS != '1' }} + if: ${{ github.event.pull_request.head.repo.full_name == github.repository && env.SKIP_BENCHMARKS != '1' }} run: | labels=$(gh pr view ${{ github.event.pull_request.number }} --json labels --jq '.labels[].name') if echo "$labels" | grep -q "skip-validate-benchmarks"; then diff --git a/Cargo.lock b/Cargo.lock index b0c56ffb2e..d71d5dca69 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6006,9 +6006,11 @@ dependencies = [ "frame-system", "log", "pallet-balances", + "pallet-crowdloan", "pallet-drand", "pallet-evm-chain-id", "pallet-grandpa", + "pallet-preimage", "pallet-scheduler", "pallet-subtensor", "parity-scale-codec", @@ -6496,9 +6498,11 @@ dependencies = [ "num-traits", "pallet-balances", "pallet-collective", + "pallet-crowdloan", "pallet-drand", "pallet-membership", "pallet-preimage", + "pallet-proxy 38.0.0", "pallet-scheduler", "pallet-transaction-payment", "pallet-utility 38.0.0", @@ -6523,6 +6527,7 @@ dependencies = [ "sp-version", "substrate-fixed", "subtensor-macros", + "subtensor-runtime-common", "tle", "w3f-bls", ] diff --git a/common/src/lib.rs b/common/src/lib.rs index 75b18e3b14..335606b2ad 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -50,6 +50,7 @@ pub enum ProxyType { RootWeights, ChildKeys, SudoUncheckedSetCode, + SubnetLeaseBeneficiary, // Used to operate the leased subnet } impl Default for ProxyType { diff --git a/pallets/admin-utils/Cargo.toml b/pallets/admin-utils/Cargo.toml index b3c1410cca..01a8fb1e31 100644 --- a/pallets/admin-utils/Cargo.toml +++ b/pallets/admin-utils/Cargo.toml @@ -42,6 +42,8 @@ pallet-balances = { workspace = true, features = ["std"] } pallet-scheduler = { workspace = true } pallet-grandpa = { workspace = true } sp-std = { workspace = true } +pallet-crowdloan = { workspace = true, default-features = false } +pallet-preimage = { workspace = true, default-features = false } [features] default = ["std"] @@ -57,6 +59,8 @@ std = [ "pallet-grandpa/std", "pallet-scheduler/std", "pallet-subtensor/std", + "pallet-crowdloan/std", + "pallet-preimage/std", "scale-info/std", "sp-consensus-aura/std", "sp-consensus-grandpa/std", @@ -77,6 +81,8 @@ runtime-benchmarks = [ "pallet-grandpa/runtime-benchmarks", "pallet-scheduler/runtime-benchmarks", "pallet-subtensor/runtime-benchmarks", + "pallet-crowdloan/runtime-benchmarks", + "pallet-preimage/runtime-benchmarks", "sp-runtime/runtime-benchmarks", ] try-runtime = [ @@ -88,5 +94,7 @@ try-runtime = [ "pallet-grandpa/try-runtime", "pallet-scheduler/try-runtime", "pallet-subtensor/try-runtime", + "pallet-crowdloan/try-runtime", + "pallet-preimage/try-runtime", "sp-runtime/try-runtime", ] diff --git a/pallets/admin-utils/src/tests/mock.rs b/pallets/admin-utils/src/tests/mock.rs index f8b3e6a9b6..81a1a0bd89 100644 --- a/pallets/admin-utils/src/tests/mock.rs +++ b/pallets/admin-utils/src/tests/mock.rs @@ -1,7 +1,7 @@ #![allow(clippy::arithmetic_side_effects, clippy::unwrap_used)] use frame_support::{ - assert_ok, derive_impl, parameter_types, + PalletId, assert_ok, derive_impl, parameter_types, traits::{Everything, Hooks, PrivilegeCmp}, weights, }; @@ -32,6 +32,8 @@ frame_support::construct_runtime!( Drand: pallet_drand::{Pallet, Call, Storage, Event} = 6, Grandpa: pallet_grandpa = 7, EVMChainId: pallet_evm_chain_id = 8, + Preimage: pallet_preimage::{Pallet, Call, Storage, Event} = 9, + Crowdloan: pallet_crowdloan::{Pallet, Call, Storage, Event} = 10, } ); @@ -140,6 +142,7 @@ parameter_types! { pub const InitialTaoWeight: u64 = u64::MAX/10; // 10% global weight. pub const InitialEmaPriceHalvingPeriod: u64 = 201_600_u64; // 4 weeks pub const DurationOfStartCall: u64 = 7 * 24 * 60 * 60 / 12; // 7 days + pub const LeaseDividendsDistributionInterval: u32 = 100; // 100 blocks } impl pallet_subtensor::Config for Test { @@ -209,6 +212,47 @@ impl pallet_subtensor::Config for Test { type InitialTaoWeight = InitialTaoWeight; type InitialEmaPriceHalvingPeriod = InitialEmaPriceHalvingPeriod; type DurationOfStartCall = DurationOfStartCall; + type ProxyInterface = (); + type LeaseDividendsDistributionInterval = LeaseDividendsDistributionInterval; +} + +parameter_types! { + pub const PreimageMaxSize: u32 = 4096 * 1024; + pub const PreimageBaseDeposit: Balance = 1; + pub const PreimageByteDeposit: Balance = 1; +} + +impl pallet_preimage::Config for Test { + type WeightInfo = pallet_preimage::weights::SubstrateWeight; + type RuntimeEvent = RuntimeEvent; + type Currency = Balances; + type ManagerOrigin = EnsureRoot; + type Consideration = (); +} + +parameter_types! { + pub const CrowdloanPalletId: PalletId = PalletId(*b"bt/cloan"); + pub const MinimumDeposit: u64 = 50; + pub const AbsoluteMinimumContribution: u64 = 10; + pub const MinimumBlockDuration: u64 = 20; + pub const MaximumBlockDuration: u64 = 100; + pub const RefundContributorsLimit: u32 = 5; + pub const MaxContributors: u32 = 10; +} + +impl pallet_crowdloan::Config for Test { + type PalletId = CrowdloanPalletId; + type Currency = Balances; + type RuntimeCall = RuntimeCall; + type RuntimeEvent = RuntimeEvent; + type WeightInfo = pallet_crowdloan::weights::SubstrateWeight; + type Preimages = Preimage; + type MinimumDeposit = MinimumDeposit; + type AbsoluteMinimumContribution = AbsoluteMinimumContribution; + type MinimumBlockDuration = MinimumBlockDuration; + type MaximumBlockDuration = MaximumBlockDuration; + type RefundContributorsLimit = RefundContributorsLimit; + type MaxContributors = MaxContributors; } #[derive_impl(frame_system::config_preludes::TestDefaultConfig)] diff --git a/pallets/subtensor/Cargo.toml b/pallets/subtensor/Cargo.toml index ab17cf5bdc..f41bb5ff83 100644 --- a/pallets/subtensor/Cargo.toml +++ b/pallets/subtensor/Cargo.toml @@ -49,13 +49,18 @@ pallet-collective = { version = "4.0.0-dev", default-features = false, path = ". pallet-drand = { path = "../drand", default-features = false } pallet-membership = { workspace = true } hex-literal = { workspace = true } -num-traits = { version = "0.2.19", default-features = false, features = ["libm"] } +num-traits = { version = "0.2.19", default-features = false, features = [ + "libm", +] } tle = { workspace = true, default-features = false } ark-bls12-381 = { workspace = true, default-features = false } ark-serialize = { workspace = true, default-features = false } w3f-bls = { workspace = true, default-features = false } sha2 = { workspace = true } rand_chacha = { workspace = true } +pallet-crowdloan = { workspace = true, default-features = false } +pallet-proxy = { workspace = true, default-features = false } +subtensor-runtime-common = { workspace = true, default-features = false } [dev-dependencies] pallet-balances = { workspace = true, features = ["std"] } @@ -109,7 +114,10 @@ std = [ "rand_chacha/std", "safe-math/std", "sha2/std", - "share-pool/std" + "share-pool/std", + "pallet-proxy/std", + "pallet-crowdloan/std", + "subtensor-runtime-common/std" ] runtime-benchmarks = [ "frame-benchmarking/runtime-benchmarks", @@ -122,7 +130,9 @@ runtime-benchmarks = [ "pallet-collective/runtime-benchmarks", "pallet-preimage/runtime-benchmarks", "pallet-scheduler/runtime-benchmarks", - "pallet-drand/runtime-benchmarks" + "pallet-drand/runtime-benchmarks", + "pallet-proxy/runtime-benchmarks", + "pallet-crowdloan/runtime-benchmarks", ] try-runtime = [ "frame-support/try-runtime", @@ -135,7 +145,9 @@ try-runtime = [ "pallet-utility/try-runtime", "sp-runtime/try-runtime", "pallet-collective/try-runtime", - "pallet-drand/try-runtime" + "pallet-drand/try-runtime", + "pallet-proxy/try-runtime", + "pallet-crowdloan/try-runtime", ] pow-faucet = [] fast-blocks = [] diff --git a/pallets/subtensor/src/benchmarks.rs b/pallets/subtensor/src/benchmarks.rs index a7cd03e652..76d56125fe 100644 --- a/pallets/subtensor/src/benchmarks.rs +++ b/pallets/subtensor/src/benchmarks.rs @@ -6,12 +6,12 @@ use crate::Pallet as Subtensor; use crate::*; use codec::Compact; use frame_benchmarking::v2::*; -use frame_support::assert_ok; +use frame_support::{StorageDoubleMap, assert_ok}; use frame_system::{RawOrigin, pallet_prelude::BlockNumberFor}; pub use pallet::*; use sp_core::H256; use sp_runtime::{ - BoundedVec, + BoundedVec, Percent, traits::{BlakeTwo256, Hash}, }; use sp_std::vec; @@ -1485,4 +1485,150 @@ mod pallet_benchmarks { #[extrinsic_call] _(RawOrigin::Signed(coldkey.clone()), hotkey.clone()); } + + #[benchmark(extra)] + fn register_leased_network(k: Linear<2, { T::MaxContributors::get() }>) { + // Setup a crowdloan + let crowdloan_id = 0; + let beneficiary: T::AccountId = whitelisted_caller(); + let deposit = 20_000_000_000; // 20 TAO + let now = frame_system::Pallet::::block_number(); // not really important here + let end = now + T::MaximumBlockDuration::get(); + let cap = 2_000_000_000_000; // 2000 TAO + + let funds_account: T::AccountId = account("funds", 0, 0); + Subtensor::::add_balance_to_coldkey_account(&funds_account, cap); + + pallet_crowdloan::Crowdloans::::insert( + crowdloan_id, + pallet_crowdloan::CrowdloanInfo { + creator: beneficiary.clone(), + deposit, + min_contribution: 0, + end, + cap, + raised: cap, + finalized: false, + funds_account: funds_account.clone(), + call: None, + target_address: None, + contributors_count: T::MaxContributors::get(), + }, + ); + + // Set the block to the end of the crowdloan + frame_system::Pallet::::set_block_number(end); + + // Simulate deposit + pallet_crowdloan::Contributions::::insert(crowdloan_id, &beneficiary, deposit); + + // Simulate k - 1 contributions, the deposit is already taken into account + let contributors = k - 1; + let amount = (cap - deposit) / contributors as u64; + for i in 0..contributors { + let contributor = account::("contributor", i.try_into().unwrap(), 0); + pallet_crowdloan::Contributions::::insert(crowdloan_id, contributor, amount); + } + + // Mark the crowdloan as finalizing + pallet_crowdloan::CurrentCrowdloanId::::set(Some(0)); + + let emissions_share = Percent::from_percent(30); + #[extrinsic_call] + _( + RawOrigin::Signed(beneficiary.clone()), + emissions_share, + None, + ); + + // Ensure the lease was created + let lease_id = 0; + let lease = SubnetLeases::::get(lease_id).unwrap(); + assert_eq!(lease.beneficiary, beneficiary); + assert_eq!(lease.emissions_share, emissions_share); + assert_eq!(lease.end_block, None); + + // Ensure the subnet exists + assert!(SubnetMechanism::::contains_key(lease.netuid)); + } + + #[benchmark(extra)] + fn terminate_lease(k: Linear<2, { T::MaxContributors::get() }>) { + // Setup a crowdloan + let crowdloan_id = 0; + let beneficiary: T::AccountId = whitelisted_caller(); + let deposit = 20_000_000_000; // 20 TAO + let now = frame_system::Pallet::::block_number(); // not really important here + let crowdloan_end = now + T::MaximumBlockDuration::get(); + let cap = 2_000_000_000_000; // 2000 TAO + + let funds_account: T::AccountId = account("funds", 0, 0); + Subtensor::::add_balance_to_coldkey_account(&funds_account, cap); + + pallet_crowdloan::Crowdloans::::insert( + crowdloan_id, + pallet_crowdloan::CrowdloanInfo { + creator: beneficiary.clone(), + deposit, + min_contribution: 0, + end: crowdloan_end, + cap, + raised: cap, + finalized: false, + funds_account: funds_account.clone(), + call: None, + target_address: None, + contributors_count: T::MaxContributors::get(), + }, + ); + + // Set the block to the end of the crowdloan + frame_system::Pallet::::set_block_number(crowdloan_end); + + // Simulate deposit + pallet_crowdloan::Contributions::::insert(crowdloan_id, &beneficiary, deposit); + + // Simulate k - 1 contributions, the deposit is already taken into account + let contributors = k - 1; + let amount = (cap - deposit) / contributors as u64; + for i in 0..contributors { + let contributor = account::("contributor", i.try_into().unwrap(), 0); + pallet_crowdloan::Contributions::::insert(crowdloan_id, contributor, amount); + } + + // Mark the crowdloan as finalizing + pallet_crowdloan::CurrentCrowdloanId::::set(Some(0)); + + // Register the leased network + let emissions_share = Percent::from_percent(30); + let lease_end = crowdloan_end + 1000u32.into(); + assert_ok!(Subtensor::::register_leased_network( + RawOrigin::Signed(beneficiary.clone()).into(), + emissions_share, + Some(lease_end), + )); + + // Set the block to the end of the lease + frame_system::Pallet::::set_block_number(lease_end); + + let lease_id = 0; + let lease = SubnetLeases::::get(0).unwrap(); + let hotkey = account::("beneficiary_hotkey", 0, 0); + Subtensor::::create_account_if_non_existent(&beneficiary, &hotkey); + #[extrinsic_call] + _( + RawOrigin::Signed(beneficiary.clone()), + lease_id, + hotkey.clone(), + ); + + // Ensure the beneficiary is now the owner of the subnet + assert_eq!(SubnetOwner::::get(lease.netuid), beneficiary); + assert_eq!(SubnetOwnerHotkey::::get(lease.netuid), hotkey); + + // Ensure everything has been cleaned up + assert_eq!(SubnetLeases::::get(lease_id), None); + assert!(!SubnetLeaseShares::::contains_prefix(lease_id)); + assert!(!AccumulatedLeaseDividends::::contains_key(lease_id)); + } } diff --git a/pallets/subtensor/src/coinbase/run_coinbase.rs b/pallets/subtensor/src/coinbase/run_coinbase.rs index 00b0c2fa55..49ccc813df 100644 --- a/pallets/subtensor/src/coinbase/run_coinbase.rs +++ b/pallets/subtensor/src/coinbase/run_coinbase.rs @@ -410,12 +410,16 @@ impl Pallet { owner_coldkey, owner_cut ); - Self::increase_stake_for_hotkey_and_coldkey_on_subnet( + let real_owner_cut = Self::increase_stake_for_hotkey_and_coldkey_on_subnet( &owner_hotkey, &owner_coldkey, netuid, owner_cut, ); + // If the subnet is leased, notify the lease logic that owner cut has been distributed. + if let Some(lease_id) = SubnetUidToLeaseId::::get(netuid) { + Self::distribute_leased_network_dividends(lease_id, real_owner_cut); + } } } diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index 197cd5f8f7..03c86badd8 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -68,6 +68,8 @@ pub const MAX_CRV3_COMMIT_SIZE_BYTES: u32 = 5000; pub mod pallet { use crate::RateLimitKey; use crate::migrations; + use crate::subnets::leasing::{LeaseId, SubnetLeaseOf}; + use frame_support::Twox64Concat; use frame_support::{ BoundedVec, dispatch::GetDispatchInfo, @@ -78,7 +80,7 @@ pub mod pallet { }; use frame_system::pallet_prelude::*; use pallet_drand::types::RoundNumber; - use sp_core::{ConstU32, H160, H256}; + use sp_core::{ConstU32, ConstU64, H160, H256}; use sp_runtime::traits::{Dispatchable, TrailingZeroInput}; use sp_std::collections::vec_deque::VecDeque; use sp_std::vec; @@ -1696,6 +1698,32 @@ pub mod pallet { pub type AssociatedEvmAddress = StorageDoubleMap<_, Twox64Concat, u16, Twox64Concat, u16, (H160, u64), OptionQuery>; + /// ======================== + /// ==== Subnet Leasing ==== + /// ======================== + #[pallet::storage] + /// --- MAP ( lease_id ) --> subnet lease | The subnet lease for a given lease id. + pub type SubnetLeases = + StorageMap<_, Twox64Concat, LeaseId, SubnetLeaseOf, OptionQuery>; + + #[pallet::storage] + /// --- DMAP ( lease_id, contributor ) --> shares | The shares of a contributor for a given lease. + pub type SubnetLeaseShares = + StorageDoubleMap<_, Twox64Concat, LeaseId, Identity, T::AccountId, U64F64, ValueQuery>; + + #[pallet::storage] + // --- MAP ( netuid ) --> lease_id | The lease id for a given netuid. + pub type SubnetUidToLeaseId = StorageMap<_, Twox64Concat, u16, LeaseId, OptionQuery>; + + #[pallet::storage] + /// --- ITEM ( next_lease_id ) | The next lease id. + pub type NextSubnetLeaseId = StorageValue<_, LeaseId, ValueQuery, ConstU32<0>>; + + #[pallet::storage] + /// --- MAP ( lease_id ) --> accumulated_dividends | The accumulated dividends for a given lease that needs to be distributed. + pub type AccumulatedLeaseDividends = + StorageMap<_, Twox64Concat, LeaseId, u64, ValueQuery, ConstU64<0>>; + /// ================== /// ==== Genesis ===== /// ================== @@ -2700,3 +2728,19 @@ pub enum RateLimitKey { // The setting sn owner hotkey operation is rate limited per netuid SetSNOwnerHotkey(u16), } + +pub trait ProxyInterface { + fn add_lease_beneficiary_proxy(beneficiary: &AccountId, lease: &AccountId) -> DispatchResult; + fn remove_lease_beneficiary_proxy(beneficiary: &AccountId, lease: &AccountId) + -> DispatchResult; +} + +impl ProxyInterface for () { + fn add_lease_beneficiary_proxy(_: &T, _: &T) -> DispatchResult { + Ok(()) + } + + fn remove_lease_beneficiary_proxy(_: &T, _: &T) -> DispatchResult { + Ok(()) + } +} diff --git a/pallets/subtensor/src/macros/config.rs b/pallets/subtensor/src/macros/config.rs index 4377d9f016..f8d359de8f 100644 --- a/pallets/subtensor/src/macros/config.rs +++ b/pallets/subtensor/src/macros/config.rs @@ -7,7 +7,9 @@ use frame_support::pallet_macros::pallet_section; mod config { /// Configure the pallet by specifying the parameters and types on which it depends. #[pallet::config] - pub trait Config: frame_system::Config + pallet_drand::Config { + pub trait Config: + frame_system::Config + pallet_drand::Config + pallet_crowdloan::Config + { /// call type type RuntimeCall: Parameter + Dispatchable @@ -47,6 +49,9 @@ mod config { /// the preimage to store the call data. type Preimages: QueryPreimage + StorePreimage; + /// Interface to allow interacting with the proxy pallet. + type ProxyInterface: crate::ProxyInterface; + /// ================================= /// ==== Initial Value Constants ==== /// ================================= @@ -224,5 +229,8 @@ mod config { /// Block number after a new subnet accept the start call extrinsic. #[pallet::constant] type DurationOfStartCall: Get; + /// Number of blocks between dividends distribution. + #[pallet::constant] + type LeaseDividendsDistributionInterval: Get>; } } diff --git a/pallets/subtensor/src/macros/dispatches.rs b/pallets/subtensor/src/macros/dispatches.rs index 261c55345e..fbbb4cec49 100644 --- a/pallets/subtensor/src/macros/dispatches.rs +++ b/pallets/subtensor/src/macros/dispatches.rs @@ -6,11 +6,12 @@ use frame_support::pallet_macros::pallet_section; /// This can later be imported into the pallet using [`import_section`]. #[pallet_section] mod dispatches { + use crate::subnets::leasing::SubnetLeasingWeightInfo; use frame_support::traits::schedule::DispatchTime; use frame_support::traits::schedule::v3::Anon as ScheduleAnon; use frame_system::pallet_prelude::BlockNumberFor; use sp_core::ecdsa::Signature; - use sp_runtime::traits::Saturating; + use sp_runtime::{Percent, traits::Saturating}; use crate::MAX_CRV3_COMMIT_SIZE_BYTES; /// Dispatchable functions allow users to interact with the pallet and invoke state changes. @@ -1361,7 +1362,7 @@ mod dispatches { swap_cost, }; - let bound_call = T::Preimages::bound(LocalCallOf::::from(call.clone())) + let bound_call = ::Preimages::bound(LocalCallOf::::from(call.clone())) .map_err(|_| Error::::FailedToSchedule)?; T::Scheduler::schedule( @@ -2339,5 +2340,57 @@ mod dispatches { // ) -> DispatchResult { // Self::do_unstake_all_alpha_aggregate(origin, hotkey) // } + + /// Register a new leased network. + /// + /// 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. + /// + /// # Args: + /// * `origin` - (::Origin): + /// - The signature of the caller's coldkey. + /// + /// * `emissions_share` (Percent): + /// - The share of the emissions that the contributors will receive as dividends. + /// + /// * `end_block` (Option>): + /// - The block at which the lease will end. If not defined, the lease is perpetual. + #[pallet::call_index(109)] + #[pallet::weight(SubnetLeasingWeightInfo::::do_register_leased_network(T::MaxContributors::get()))] + pub fn register_leased_network( + origin: T::RuntimeOrigin, + emissions_share: Percent, + end_block: Option>, + ) -> DispatchResultWithPostInfo { + Self::do_register_leased_network(origin, emissions_share, end_block) + } + + /// 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. + /// + /// **The hotkey must be owned by the beneficiary coldkey.** + /// + /// # Args: + /// * `origin` - (::Origin): + /// - The signature of the caller's coldkey. + /// + /// * `lease_id` (LeaseId): + /// - The ID of the lease to terminate. + /// + /// * `hotkey` (T::AccountId): + /// - The hotkey of the beneficiary to mark as subnet owner hotkey. + #[pallet::call_index(110)] + #[pallet::weight(SubnetLeasingWeightInfo::::do_terminate_lease(T::MaxContributors::get()))] + pub fn terminate_lease( + origin: T::RuntimeOrigin, + lease_id: LeaseId, + hotkey: T::AccountId, + ) -> DispatchResultWithPostInfo { + Self::do_terminate_lease(origin, lease_id, hotkey) + } } } diff --git a/pallets/subtensor/src/macros/errors.rs b/pallets/subtensor/src/macros/errors.rs index 2a8e5bc346..c27c2311d2 100644 --- a/pallets/subtensor/src/macros/errors.rs +++ b/pallets/subtensor/src/macros/errors.rs @@ -214,5 +214,23 @@ mod errors { ZeroMaxStakeAmount, /// Invalid netuid duplication SameNetuid, + /// Invalid lease beneficiary to register the leased network. + InvalidLeaseBeneficiary, + /// Lease cannot end in the past. + LeaseCannotEndInThePast, + /// Couldn't find the lease netuid. + LeaseNetuidNotFound, + /// Lease does not exist. + LeaseDoesNotExist, + /// Lease has no end block. + LeaseHasNoEndBlock, + /// Lease has not ended. + LeaseHasNotEnded, + /// An overflow occurred. + Overflow, + /// Beneficiary does not own hotkey. + BeneficiaryDoesNotOwnHotkey, + /// Expected beneficiary origin. + ExpectedBeneficiaryOrigin, } } diff --git a/pallets/subtensor/src/macros/events.rs b/pallets/subtensor/src/macros/events.rs index 9849a517ee..6d6bfcccf1 100644 --- a/pallets/subtensor/src/macros/events.rs +++ b/pallets/subtensor/src/macros/events.rs @@ -351,5 +351,25 @@ mod events { /// - **netuid**: The network identifier. /// - **Enabled**: Is Commit-Reveal enabled. CommitRevealEnabled(u16, bool), + + /// A subnet lease has been created. + SubnetLeaseCreated { + /// The beneficiary of the lease. + beneficiary: T::AccountId, + /// The lease ID + lease_id: LeaseId, + /// The subnet ID + netuid: u16, + /// The end block of the lease + end_block: Option>, + }, + + /// A subnet lease has been terminated. + SubnetLeaseTerminated { + /// The beneficiary of the lease. + beneficiary: T::AccountId, + /// The subnet ID + netuid: u16, + }, } } diff --git a/pallets/subtensor/src/migrations/migrate_total_issuance.rs b/pallets/subtensor/src/migrations/migrate_total_issuance.rs index c00bb916b8..a7adfe8ffb 100644 --- a/pallets/subtensor/src/migrations/migrate_total_issuance.rs +++ b/pallets/subtensor/src/migrations/migrate_total_issuance.rs @@ -59,7 +59,7 @@ pub fn migrate_total_issuance(test: bool) -> Weight { .saturating_add(T::DbWeight::get().reads(SubnetLocked::::iter().count() as u64)); // Retrieve the total balance sum - let total_balance = T::Currency::total_issuance(); + let total_balance = ::Currency::total_issuance(); // Add weight for reading total issuance weight = weight.saturating_add(T::DbWeight::get().reads(1)); diff --git a/pallets/subtensor/src/staking/helpers.rs b/pallets/subtensor/src/staking/helpers.rs index 9ee04f36a8..95f586bbdc 100644 --- a/pallets/subtensor/src/staking/helpers.rs +++ b/pallets/subtensor/src/staking/helpers.rs @@ -201,7 +201,7 @@ impl Pallet { amount: <::Currency as fungible::Inspect<::AccountId>>::Balance, ) { // infallible - let _ = T::Currency::deposit(coldkey, amount, Precision::BestEffort); + let _ = ::Currency::deposit(coldkey, amount, Precision::BestEffort); } pub fn can_remove_balance_from_coldkey_account( @@ -215,7 +215,7 @@ impl Pallet { // This bit is currently untested. @todo - T::Currency::can_withdraw(coldkey, amount) + ::Currency::can_withdraw(coldkey, amount) .into_result(false) .is_ok() } @@ -224,7 +224,11 @@ impl Pallet { coldkey: &T::AccountId, ) -> <::Currency as fungible::Inspect<::AccountId>>::Balance { - T::Currency::reducible_balance(coldkey, Preservation::Expendable, Fortitude::Polite) + ::Currency::reducible_balance( + coldkey, + Preservation::Expendable, + Fortitude::Polite, + ) } #[must_use = "Balance must be used to preserve total issuance of token"] @@ -236,7 +240,7 @@ impl Pallet { return Ok(0); } - let credit = T::Currency::withdraw( + let credit = ::Currency::withdraw( coldkey, amount, Precision::BestEffort, @@ -261,7 +265,7 @@ impl Pallet { return Ok(0); } - let credit = T::Currency::withdraw( + let credit = ::Currency::withdraw( coldkey, amount, Precision::Exact, diff --git a/pallets/subtensor/src/subnets/leasing.rs b/pallets/subtensor/src/subnets/leasing.rs new file mode 100644 index 0000000000..293c92528e --- /dev/null +++ b/pallets/subtensor/src/subnets/leasing.rs @@ -0,0 +1,391 @@ +use super::*; +use frame_support::{ + dispatch::RawOrigin, + pallet_prelude::*, + traits::{Defensive, fungible::*, tokens::Preservation}, +}; +use frame_system::pallet_prelude::*; +use sp_core::blake2_256; +use sp_runtime::{Percent, traits::TrailingZeroInput}; +use substrate_fixed::types::{U64F64, U96F32}; + +pub type LeaseId = u32; + +pub type CurrencyOf = ::Currency; + +pub type BalanceOf = + as fungible::Inspect<::AccountId>>::Balance; + +#[freeze_struct("75abd76dca254c88")] +#[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: u16, + /// 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: T::RuntimeOrigin, + 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); + + ::Currency::transfer( + &crowdloan.funds_account, + &lease_coldkey, + crowdloan.raised, + Preservation::Expendable, + )?; + + 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); + + // 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(amount).saturating_div(U64F64::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(leftover_cap)) + .floor() + .to_num::(); + ::Currency::transfer( + &lease_coldkey, + &contributor, + contributor_refund, + Preservation::Expendable, + )?; + 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); + ::Currency::transfer( + &lease_coldkey, + &who, + beneficiary_refund, + Preservation::Expendable, + )?; + + 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(SubnetLeasingWeightInfo::::do_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: T::RuntimeOrigin, + 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); + + // 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, + }); + + if clear_result.unique < T::MaxContributors::get() { + // We have cleared less than the max number of shareholders, so we need to refund the difference + Ok(Some(SubnetLeasingWeightInfo::::do_terminate_lease( + clear_result.unique, + )) + .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 will ensure the subnet has enough alpha in its liquidity pool before swapping it to tao to be distributed, + /// and if not enough liquidity is available, it will accumulate the dividends for later distribution. + pub fn distribute_leased_network_dividends(lease_id: LeaseId, owner_cut_alpha: u64) { + // 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); + + // 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); + + // 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; + } + + // Ensure there is enough liquidity to unstake the contributors cut + if let Err(err) = Self::validate_remove_stake( + &lease.coldkey, + &lease.hotkey, + lease.netuid, + total_contributors_cut_alpha, + total_contributors_cut_alpha, + false, + ) { + log::debug!("Couldn't distributing dividends for lease {lease_id}: {err:?}"); + AccumulatedLeaseDividends::::set(lease_id, total_contributors_cut_alpha); + return; + } + + // Unstake the contributors cut from the subnet as tao to the lease coldkey + let fee = Self::calculate_staking_fee( + Some((&lease.hotkey, lease.netuid)), + &lease.coldkey, + None, + &lease.coldkey, + U96F32::saturating_from_num(total_contributors_cut_alpha), + ); + let tao_unstaked = Self::unstake_from_subnet( + &lease.hotkey, + &lease.coldkey, + lease.netuid, + total_contributors_cut_alpha, + fee, + ); + + // Distribute the contributors cut to the contributors and accumulate the tao + // distributed so far to obtain how much tao is left to distribute to the beneficiary + let mut tao_distributed = 0u64; + for (contributor, share) in SubnetLeaseShares::::iter_prefix(lease_id) { + let tao_for_contributor = share + .saturating_mul(U64F64::from(tao_unstaked)) + .floor() + .to_num::(); + Self::add_balance_to_coldkey_account(&contributor, tao_for_contributor); + tao_distributed = tao_distributed.saturating_add(tao_for_contributor); + } + + // Distribute the leftover tao to the beneficiary + let beneficiary_cut_tao = tao_unstaked.saturating_sub(tao_distributed); + Self::add_balance_to_coldkey_account(&lease.beneficiary, beneficiary_cut_tao); + + // Reset the accumulated dividends + AccumulatedLeaseDividends::::insert(lease_id, 0); + } + + fn lease_coldkey(lease_id: LeaseId) -> T::AccountId { + let entropy = ("leasing/coldkey", lease_id).using_encoded(blake2_256); + Decode::decode(&mut TrailingZeroInput::new(entropy.as_ref())) + .expect("infinite length input; no invalid inputs for type; qed") + } + + fn lease_hotkey(lease_id: LeaseId) -> T::AccountId { + let entropy = ("leasing/hotkey", lease_id).using_encoded(blake2_256); + Decode::decode(&mut TrailingZeroInput::new(entropy.as_ref())) + .expect("infinite length input; no invalid inputs for type; qed") + } + + 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)) + } +} + +/// Weight functions needed for subnet leasing. +pub struct SubnetLeasingWeightInfo(PhantomData); +impl SubnetLeasingWeightInfo { + pub fn do_register_leased_network(k: u32) -> Weight { + Weight::from_parts(301_560_714, 10079) + .saturating_add(Weight::from_parts(26_884_006, 0).saturating_mul(k.into())) + .saturating_add(T::DbWeight::get().reads(41_u64)) + .saturating_add(T::DbWeight::get().reads(2_u64.saturating_mul(k.into()))) + .saturating_add(T::DbWeight::get().writes(55_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64.saturating_mul(k.into()))) + .saturating_add(Weight::from_parts(0, 2579).saturating_mul(k.into())) + } + + pub fn do_terminate_lease(k: u32) -> Weight { + Weight::from_parts(56_635_122, 6148) + .saturating_add(Weight::from_parts(912_993, 0).saturating_mul(k.into())) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(k.into()))) + .saturating_add(T::DbWeight::get().writes(6_u64)) + .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(k.into()))) + .saturating_add(Weight::from_parts(0, 2529).saturating_mul(k.into())) + } +} diff --git a/pallets/subtensor/src/subnets/mod.rs b/pallets/subtensor/src/subnets/mod.rs index 4bbe0c276e..a823773395 100644 --- a/pallets/subtensor/src/subnets/mod.rs +++ b/pallets/subtensor/src/subnets/mod.rs @@ -1,4 +1,5 @@ use super::*; +pub mod leasing; pub mod registration; pub mod serving; pub mod subnet; diff --git a/pallets/subtensor/src/tests/leasing.rs b/pallets/subtensor/src/tests/leasing.rs new file mode 100644 index 0000000000..89e4ca296d --- /dev/null +++ b/pallets/subtensor/src/tests/leasing.rs @@ -0,0 +1,916 @@ +#![allow( + clippy::arithmetic_side_effects, + clippy::unwrap_used, + clippy::indexing_slicing +)] +use super::mock::*; +use crate::{subnets::leasing::SubnetLeaseOf, *}; +use frame_support::{StorageDoubleMap, assert_err, assert_ok}; +use sp_core::U256; +use sp_runtime::Percent; +use substrate_fixed::types::U64F64; + +#[test] +fn test_register_leased_network_works() { + new_test_ext(1).execute_with(|| { + // Setup a crowdloan + let crowdloan_id = 0; + let beneficiary = U256::from(1); + let deposit = 10_000_000_000; // 10 TAO + let cap = 1_000_000_000_000; // 1000 TAO + let contributions = vec![ + (U256::from(2), 600_000_000_000), // 600 TAO + (U256::from(3), 390_000_000_000), // 390 TAO + ]; + setup_crowdloan(crowdloan_id, deposit, cap, beneficiary, &contributions); + + // Register the leased network + let end_block = 500; + let emissions_share = Percent::from_percent(30); + assert_ok!(SubtensorModule::register_leased_network( + RuntimeOrigin::signed(beneficiary), + emissions_share, + Some(end_block), + )); + + // Ensure the lease was created + let lease_id = 0; + let lease = SubnetLeases::::get(lease_id).unwrap(); + assert_eq!(lease.beneficiary, beneficiary); + assert_eq!(lease.emissions_share, emissions_share); + assert_eq!(lease.end_block, Some(end_block)); + + // Ensure the subnet exists + assert!(SubnetMechanism::::contains_key(lease.netuid)); + + // Ensure the subnet uid to lease id mapping exists + assert_eq!( + SubnetUidToLeaseId::::get(lease.netuid), + Some(lease_id) + ); + + // Ensure the beneficiary has been added as a proxy + assert!(PROXIES.with_borrow(|proxies| proxies.0 == vec![(lease.coldkey, beneficiary)])); + + // Ensure the lease shares have been created for each contributor + let contributor1_share = U64F64::from(contributions[0].1).saturating_div(U64F64::from(cap)); + assert_eq!( + SubnetLeaseShares::::get(lease_id, contributions[0].0), + contributor1_share + ); + let contributor2_share = U64F64::from(contributions[1].1).saturating_div(U64F64::from(cap)); + assert_eq!( + SubnetLeaseShares::::get(lease_id, contributions[1].0), + contributor2_share + ); + + // Ensure each contributor and beneficiary has been refunded their share of the leftover cap + let leftover_cap = cap.saturating_sub(lease.cost); + + let expected_contributor1_refund = U64F64::from(leftover_cap) + .saturating_mul(contributor1_share) + .floor() + .to_num::(); + assert_eq!( + SubtensorModule::get_coldkey_balance(&contributions[0].0), + expected_contributor1_refund + ); + + let expected_contributor2_refund = U64F64::from(leftover_cap) + .saturating_mul(contributor2_share) + .floor() + .to_num::(); + assert_eq!( + SubtensorModule::get_coldkey_balance(&contributions[1].0), + expected_contributor2_refund + ); + assert_eq!( + SubtensorModule::get_coldkey_balance(&beneficiary), + leftover_cap - (expected_contributor1_refund + expected_contributor2_refund) + ); + + // Ensure the event is emitted + assert_eq!( + last_event(), + crate::Event::::SubnetLeaseCreated { + beneficiary, + lease_id, + netuid: lease.netuid, + end_block: Some(end_block), + } + .into() + ); + }); +} + +#[test] +fn test_register_leased_network_fails_if_bad_origin() { + new_test_ext(1).execute_with(|| { + let end_block = 500; + let emissions_share = Percent::from_percent(30); + + assert_err!( + SubtensorModule::register_leased_network( + RuntimeOrigin::none(), + emissions_share, + Some(end_block), + ), + DispatchError::BadOrigin, + ); + + assert_err!( + SubtensorModule::register_leased_network( + RuntimeOrigin::root(), + emissions_share, + Some(end_block), + ), + DispatchError::BadOrigin, + ); + }); +} + +#[test] +fn test_register_leased_network_fails_if_crowdloan_does_not_exists() { + new_test_ext(1).execute_with(|| { + let beneficiary = U256::from(1); + let end_block = 500; + let emissions_share = Percent::from_percent(30); + + assert_err!( + SubtensorModule::register_leased_network( + RuntimeOrigin::signed(beneficiary), + emissions_share, + Some(end_block), + ), + pallet_crowdloan::Error::::InvalidCrowdloanId, + ); + }); +} + +#[test] +fn test_register_lease_network_fails_if_current_crowdloan_id_is_not_set() { + new_test_ext(1).execute_with(|| { + // Setup a crowdloan + let crowdloan_id = 0; + let beneficiary = U256::from(1); + let deposit = 10_000_000_000; // 10 TAO + let cap = 1_000_000_000_000; // 1000 TAO + let contributions = vec![ + (U256::from(2), 600_000_000_000), // 600 TAO + (U256::from(3), 390_000_000_000), // 390 TAO + ]; + setup_crowdloan(crowdloan_id, deposit, cap, beneficiary, &contributions); + + // Mark as if the current crowdloan id is not set + pallet_crowdloan::CurrentCrowdloanId::::set(None); + + let end_block = 500; + let emissions_share = Percent::from_percent(30); + + assert_err!( + SubtensorModule::register_leased_network( + RuntimeOrigin::signed(beneficiary), + emissions_share, + Some(end_block), + ), + pallet_crowdloan::Error::::InvalidCrowdloanId, + ); + }); +} + +#[test] +fn test_register_leased_network_fails_if_origin_is_not_crowdloan_creator() { + new_test_ext(1).execute_with(|| { + // Setup a crowdloan + let crowdloan_id = 0; + let beneficiary = U256::from(1); + let deposit = 10_000_000_000; // 10 TAO + let cap = 1_000_000_000_000; // 1000 TAO + let contributions = vec![ + (U256::from(2), 600_000_000_000), // 600 TAO + (U256::from(3), 390_000_000_000), // 390 TAO + ]; + setup_crowdloan(crowdloan_id, deposit, cap, beneficiary, &contributions); + + let end_block = 500; + let emissions_share = Percent::from_percent(30); + + assert_err!( + SubtensorModule::register_leased_network( + RuntimeOrigin::signed(U256::from(2)), + emissions_share, + Some(end_block), + ), + Error::::InvalidLeaseBeneficiary, + ); + }); +} + +#[test] +fn test_register_lease_network_fails_if_end_block_is_in_the_past() { + new_test_ext(501).execute_with(|| { + let crowdloan_id = 0; + let beneficiary = U256::from(1); + let deposit = 10_000_000_000; // 10 TAO + let cap = 1_000_000_000_000; // 1000 TAO + let contributions = vec![ + (U256::from(2), 600_000_000_000), // 600 TAO + (U256::from(3), 390_000_000_000), // 390 TAO + ]; + setup_crowdloan(crowdloan_id, deposit, cap, beneficiary, &contributions); + + let end_block = 500; + let emissions_share = Percent::from_percent(30); + + assert_err!( + SubtensorModule::register_leased_network( + RuntimeOrigin::signed(beneficiary), + emissions_share, + Some(end_block), + ), + Error::::LeaseCannotEndInThePast, + ); + }); +} + +#[test] +fn test_terminate_lease_works() { + new_test_ext(1).execute_with(|| { + // Setup a crowdloan + let crowdloan_id = 0; + let beneficiary = U256::from(1); + let deposit = 10_000_000_000; // 10 TAO + let cap = 1_000_000_000_000; // 1000 TAO + let contributions = vec![(U256::from(2), 990_000_000_000)]; // 990 TAO + setup_crowdloan(crowdloan_id, deposit, cap, beneficiary, &contributions); + + // Setup a leased network + let end_block = 500; + let tao_to_stake = 100_000_000_000; // 100 TAO + let emissions_share = Percent::from_percent(30); + let (lease_id, lease) = setup_leased_network( + beneficiary, + emissions_share, + Some(end_block), + Some(tao_to_stake), + ); + + // Run to the end of the lease + run_to_block(end_block); + + // Create a hotkey for the beneficiary + let hotkey = U256::from(3); + SubtensorModule::create_account_if_non_existent(&beneficiary, &hotkey); + + // Terminate the lease + assert_ok!(SubtensorModule::terminate_lease( + RuntimeOrigin::signed(beneficiary), + lease_id, + hotkey, + )); + + // Ensure the beneficiary is now the owner of the subnet + assert_eq!(SubnetOwner::::get(lease.netuid), beneficiary); + assert_eq!(SubnetOwnerHotkey::::get(lease.netuid), hotkey); + + // Ensure everything has been cleaned up + assert_eq!(SubnetLeases::::get(lease_id), None); + assert!(!SubnetLeaseShares::::contains_prefix(lease_id)); + assert!(!AccumulatedLeaseDividends::::contains_key(lease_id)); + + // Ensure the beneficiary has been removed as a proxy + assert!(PROXIES.with_borrow(|proxies| proxies.0.is_empty())); + + // Ensure the event is emitted + assert_eq!( + last_event(), + crate::Event::::SubnetLeaseTerminated { + beneficiary: lease.beneficiary, + netuid: lease.netuid, + } + .into() + ); + }); +} + +#[test] +fn test_terminate_lease_fails_if_bad_origin() { + new_test_ext(1).execute_with(|| { + let lease_id = 0; + let hotkey = U256::from(1); + + assert_err!( + SubtensorModule::terminate_lease(RuntimeOrigin::none(), lease_id, hotkey), + DispatchError::BadOrigin, + ); + + assert_err!( + SubtensorModule::terminate_lease(RuntimeOrigin::root(), lease_id, hotkey), + DispatchError::BadOrigin, + ); + }); +} + +#[test] +fn test_terminate_lease_fails_if_lease_does_not_exist() { + new_test_ext(1).execute_with(|| { + let lease_id = 0; + let beneficiary = U256::from(1); + let hotkey = U256::from(2); + + assert_err!( + SubtensorModule::terminate_lease(RuntimeOrigin::signed(beneficiary), lease_id, hotkey), + Error::::LeaseDoesNotExist, + ); + }); +} + +#[test] +fn test_terminate_lease_fails_if_origin_is_not_beneficiary() { + new_test_ext(1).execute_with(|| { + // Setup a crowdloan + let crowdloan_id = 0; + let beneficiary = U256::from(1); + let deposit = 10_000_000_000; // 10 TAO + let cap = 1_000_000_000_000; // 1000 TAO + let contributions = vec![(U256::from(2), 990_000_000_000)]; // 990 TAO + setup_crowdloan(crowdloan_id, deposit, cap, beneficiary, &contributions); + + // Setup a leased network + let end_block = 500; + let tao_to_stake = 100_000_000_000; // 100 TAO + let emissions_share = Percent::from_percent(30); + let (lease_id, _lease) = setup_leased_network( + beneficiary, + emissions_share, + Some(end_block), + Some(tao_to_stake), + ); + + // Run to the end of the lease + run_to_block(end_block); + + // Create a hotkey for the beneficiary + let hotkey = U256::from(3); + SubtensorModule::create_account_if_non_existent(&beneficiary, &hotkey); + + // Terminate the lease + assert_err!( + SubtensorModule::terminate_lease( + RuntimeOrigin::signed(U256::from(42)), + lease_id, + hotkey, + ), + Error::::ExpectedBeneficiaryOrigin, + ); + }); +} + +#[test] +fn test_terminate_lease_fails_if_lease_has_no_end_block() { + new_test_ext(1).execute_with(|| { + // Setup a crowdloan + let crowdloan_id = 0; + let beneficiary = U256::from(1); + let deposit = 10_000_000_000; // 10 TAO + let cap = 1_000_000_000_000; // 1000 TAO + let contributions = vec![(U256::from(2), 990_000_000_000)]; // 990 TAO + setup_crowdloan(crowdloan_id, deposit, cap, beneficiary, &contributions); + + // Setup a leased network + let tao_to_stake = 100_000_000_000; // 100 TAO + let emissions_share = Percent::from_percent(30); + let (lease_id, lease) = + setup_leased_network(beneficiary, emissions_share, None, Some(tao_to_stake)); + + // Create a hotkey for the beneficiary + let hotkey = U256::from(3); + SubtensorModule::create_account_if_non_existent(&beneficiary, &hotkey); + + // Terminate the lease + assert_err!( + SubtensorModule::terminate_lease( + RuntimeOrigin::signed(lease.beneficiary), + lease_id, + hotkey, + ), + Error::::LeaseHasNoEndBlock, + ); + }); +} + +#[test] +fn test_terminate_lease_fails_if_lease_has_not_ended() { + new_test_ext(1).execute_with(|| { + // Setup a crowdloan + let crowdloan_id = 0; + let beneficiary = U256::from(1); + let deposit = 10_000_000_000; // 10 TAO + let cap = 1_000_000_000_000; // 1000 TAO + let contributions = vec![(U256::from(2), 990_000_000_000)]; // 990 TAO + setup_crowdloan(crowdloan_id, deposit, cap, beneficiary, &contributions); + + // Setup a leased network + let end_block = 500; + let tao_to_stake = 100_000_000_000; // 100 TAO + let emissions_share = Percent::from_percent(30); + let (lease_id, lease) = setup_leased_network( + beneficiary, + emissions_share, + Some(end_block), + Some(tao_to_stake), + ); + + // Create a hotkey for the beneficiary + let hotkey = U256::from(3); + SubtensorModule::create_account_if_non_existent(&beneficiary, &hotkey); + + // Terminate the lease + assert_err!( + SubtensorModule::terminate_lease( + RuntimeOrigin::signed(lease.beneficiary), + lease_id, + hotkey, + ), + Error::::LeaseHasNotEnded, + ); + }); +} + +#[test] +fn test_terminate_lease_fails_if_beneficiary_does_not_own_hotkey() { + new_test_ext(1).execute_with(|| { + // Setup a crowdloan + let crowdloan_id = 0; + let beneficiary = U256::from(1); + let deposit = 10_000_000_000; // 10 TAO + let cap = 1_000_000_000_000; // 1000 TAO + let contributions = vec![(U256::from(2), 990_000_000_000)]; // 990 TAO + setup_crowdloan(crowdloan_id, deposit, cap, beneficiary, &contributions); + + // Setup a leased network + let end_block = 500; + let tao_to_stake = 100_000_000_000; // 100 TAO + let emissions_share = Percent::from_percent(30); + let (lease_id, lease) = setup_leased_network( + beneficiary, + emissions_share, + Some(end_block), + Some(tao_to_stake), + ); + + // Run to the end of the lease + run_to_block(end_block); + + // Terminate the lease + assert_err!( + SubtensorModule::terminate_lease( + RuntimeOrigin::signed(lease.beneficiary), + lease_id, + U256::from(42), + ), + Error::::BeneficiaryDoesNotOwnHotkey, + ); + }); +} +#[test] +fn test_distribute_lease_network_dividends_multiple_contributors_works() { + new_test_ext(1).execute_with(|| { + // Setup a crowdloan + let crowdloan_id = 0; + let beneficiary = U256::from(1); + let deposit = 10_000_000_000; // 10 TAO + let cap = 1_000_000_000_000; // 1000 TAO + let contributions = vec![ + (U256::from(2), 600_000_000_000), // 600 TAO + (U256::from(3), 390_000_000_000), // 390 TAO + ]; + setup_crowdloan(crowdloan_id, deposit, cap, beneficiary, &contributions); + + // Setup a leased network + let end_block = 500; + let emissions_share = Percent::from_percent(30); + let tao_to_stake = 100_000_000_000; // 100 TAO + let (lease_id, lease) = setup_leased_network( + beneficiary, + emissions_share, + Some(end_block), + Some(tao_to_stake), + ); + + // Setup the correct block to distribute dividends + run_to_block(::LeaseDividendsDistributionInterval::get() as u64); + + // Get the initial subnet tao after stake and ensure all contributor + // balances are in initial state + let subnet_tao_before = SubnetTAO::::get(lease.netuid); + let contributor1_balance_before = SubtensorModule::get_coldkey_balance(&contributions[0].0); + let contributor2_balance_before = SubtensorModule::get_coldkey_balance(&contributions[1].0); + let beneficiary_balance_before = SubtensorModule::get_coldkey_balance(&beneficiary); + + // Setup some previously accumulated dividends + let accumulated_dividends = 5_000_000; + AccumulatedLeaseDividends::::insert(lease_id, accumulated_dividends); + + // Distribute the dividends + let owner_cut_alpha = 5_000_000; + SubtensorModule::distribute_leased_network_dividends(lease_id, owner_cut_alpha); + + // Ensure the dividends were distributed correctly relative to their shares + let distributed_tao = subnet_tao_before - SubnetTAO::::get(lease.netuid); + let contributor1_balance_delta = SubtensorModule::get_coldkey_balance(&contributions[0].0) + .saturating_sub(contributor1_balance_before); + let contributor2_balance_delta = SubtensorModule::get_coldkey_balance(&contributions[1].0) + .saturating_sub(contributor2_balance_before); + let beneficiary_balance_delta = SubtensorModule::get_coldkey_balance(&beneficiary) + .saturating_sub(beneficiary_balance_before); + + assert_eq!( + distributed_tao, + beneficiary_balance_delta + contributor1_balance_delta + contributor2_balance_delta + ); + + let expected_contributor1_balance = + SubnetLeaseShares::::get(lease_id, contributions[0].0) + .saturating_mul(U64F64::from(distributed_tao)) + .floor() + .to_num::(); + assert_eq!(contributor1_balance_delta, expected_contributor1_balance); + + let expected_contributor2_balance = + SubnetLeaseShares::::get(lease_id, contributions[1].0) + .saturating_mul(U64F64::from(distributed_tao)) + .floor() + .to_num::(); + assert_eq!(contributor2_balance_delta, expected_contributor2_balance); + + // The beneficiary should have received the remaining dividends + let expected_beneficiary_balance = + distributed_tao - (expected_contributor1_balance + expected_contributor2_balance); + assert_eq!(beneficiary_balance_delta, expected_beneficiary_balance); + + // Ensure nothing was accumulated for later distribution + assert_eq!(AccumulatedLeaseDividends::::get(lease_id), 0); + }); +} + +#[test] +fn test_distribute_lease_network_dividends_only_beneficiary_works() { + new_test_ext(1).execute_with(|| { + // Setup a crowdloan + let crowdloan_id = 0; + let beneficiary = U256::from(1); + let deposit = 10_000_000_000; // 10 TAO + let cap = 1_000_000_000_000; // 1000 TAO + let contributions = vec![(U256::from(1), 990_000_000_000)]; // 990 TAO + setup_crowdloan(crowdloan_id, deposit, cap, beneficiary, &contributions); + + // Setup a leased network + let end_block = 500; + let emissions_share = Percent::from_percent(30); + let tao_to_stake = 100_000_000_000; // 100 TAO + let (lease_id, lease) = setup_leased_network( + beneficiary, + emissions_share, + Some(end_block), + Some(tao_to_stake), + ); + + // Setup the correct block to distribute dividends + run_to_block(::LeaseDividendsDistributionInterval::get() as u64); + + // Get the initial subnet tao after stake and beneficiary balance + let subnet_tao_before = SubnetTAO::::get(lease.netuid); + let beneficiary_balance_before = SubtensorModule::get_coldkey_balance(&beneficiary); + + // Setup some previously accumulated dividends + let accumulated_dividends = 5_000_000; + AccumulatedLeaseDividends::::insert(lease_id, accumulated_dividends); + + // Distribute the dividends + let owner_cut_alpha = 5_000_000; + SubtensorModule::distribute_leased_network_dividends(lease_id, owner_cut_alpha); + + // Ensure the dividends were distributed correctly relative to their shares + let distributed_tao = subnet_tao_before - SubnetTAO::::get(lease.netuid); + let beneficiary_balance_delta = SubtensorModule::get_coldkey_balance(&beneficiary) + .saturating_sub(beneficiary_balance_before); + assert_eq!(distributed_tao, beneficiary_balance_delta); + + // Ensure nothing was accumulated for later distribution + assert_eq!(AccumulatedLeaseDividends::::get(lease_id), 0); + }); +} + +#[test] +fn test_distribute_lease_network_dividends_accumulates_if_not_the_correct_block() { + new_test_ext(1).execute_with(|| { + // Setup a crowdloan + let crowdloan_id = 0; + let beneficiary = U256::from(1); + let deposit = 10_000_000_000; // 10 TAO + let cap = 1_000_000_000_000; // 1000 TAO + let contributions = vec![ + (U256::from(2), 600_000_000_000), // 600 TAO + (U256::from(3), 390_000_000_000), // 390 TAO + ]; + setup_crowdloan(crowdloan_id, deposit, cap, beneficiary, &contributions); + + // Setup a leased network + let end_block = 500; + let emissions_share = Percent::from_percent(30); + let tao_to_stake = 100_000_000_000; // 100 TAO + let (lease_id, _) = setup_leased_network( + beneficiary, + emissions_share, + Some(end_block), + Some(tao_to_stake), + ); + + // Setup incorrect block to distribute dividends + run_to_block(::LeaseDividendsDistributionInterval::get() as u64 + 1); + + // Get the initial subnet tao after stake and ensure all contributor + let contributor1_balance_before = SubtensorModule::get_coldkey_balance(&contributions[0].0); + let contributor2_balance_before = SubtensorModule::get_coldkey_balance(&contributions[1].0); + let beneficiary_balance_before = SubtensorModule::get_coldkey_balance(&beneficiary); + + // Setup some previously accumulated dividends + let accumulated_dividends = 5_000_000; + AccumulatedLeaseDividends::::insert(lease_id, accumulated_dividends); + + // Distribute the dividends + let owner_cut_alpha = 5_000_000; + SubtensorModule::distribute_leased_network_dividends(lease_id, owner_cut_alpha); + + // Ensure the dividends were not distributed + assert_eq!( + SubtensorModule::get_coldkey_balance(&contributions[0].0), + contributor1_balance_before + ); + assert_eq!( + SubtensorModule::get_coldkey_balance(&contributions[1].0), + contributor2_balance_before + ); + assert_eq!( + SubtensorModule::get_coldkey_balance(&beneficiary), + beneficiary_balance_before + ); + + // Ensure we correctly accumulated the dividends + assert_eq!( + AccumulatedLeaseDividends::::get(lease_id), + accumulated_dividends + emissions_share.mul_ceil(owner_cut_alpha) + ); + }); +} + +#[test] +fn test_distribute_lease_network_dividends_does_nothing_if_lease_does_not_exist() { + new_test_ext(1).execute_with(|| { + let lease_id = 0; + let owner_cut_alpha = 5_000_000; + SubtensorModule::distribute_leased_network_dividends(lease_id, owner_cut_alpha); + }); +} + +#[test] +fn test_distribute_lease_network_dividends_does_nothing_if_lease_has_ended() { + new_test_ext(1).execute_with(|| { + // Setup a crowdloan + let crowdloan_id = 0; + let beneficiary = U256::from(1); + let deposit = 10_000_000_000; // 10 TAO + let cap = 1_000_000_000_000; // 1000 TAO + let contributions = vec![ + (U256::from(2), 600_000_000_000), // 600 TAO + (U256::from(3), 390_000_000_000), // 390 TAO + ]; + setup_crowdloan(crowdloan_id, deposit, cap, beneficiary, &contributions); + + // Setup a leased network + let end_block = 500; + let tao_to_stake = 100_000_000_000; // 100 TAO + let emissions_share = Percent::from_percent(30); + let (lease_id, lease) = setup_leased_network( + beneficiary, + emissions_share, + Some(end_block), + Some(tao_to_stake), + ); + + // Run to the end of the lease + run_to_block(end_block); + + let subnet_tao_before = SubnetTAO::::get(lease.netuid); + let contributor1_balance_before = SubtensorModule::get_coldkey_balance(&contributions[0].0); + let contributor2_balance_before = SubtensorModule::get_coldkey_balance(&contributions[1].0); + let beneficiary_balance_before = SubtensorModule::get_coldkey_balance(&beneficiary); + let accumulated_dividends_before = AccumulatedLeaseDividends::::get(lease_id); + + // Try to distribute the dividends + let owner_cut_alpha = 5_000_000; + SubtensorModule::distribute_leased_network_dividends(lease_id, owner_cut_alpha); + + // Ensure the dividends were not distributed + assert_eq!(SubnetTAO::::get(lease.netuid), subnet_tao_before); + assert_eq!( + SubtensorModule::get_coldkey_balance(&contributions[0].0), + contributor1_balance_before + ); + assert_eq!( + SubtensorModule::get_coldkey_balance(&contributions[1].0), + contributor2_balance_before + ); + assert_eq!( + SubtensorModule::get_coldkey_balance(&beneficiary), + beneficiary_balance_before + ); + // Ensure nothing was accumulated for later distribution + assert_eq!( + AccumulatedLeaseDividends::::get(lease_id), + accumulated_dividends_before + ); + }); +} + +#[test] +fn test_distribute_lease_network_dividends_accumulates_if_amount_is_too_low() { + new_test_ext(1).execute_with(|| { + // Setup a crowdloan + let crowdloan_id = 0; + let beneficiary = U256::from(1); + let deposit = 10_000_000_000; // 10 TAO + let cap = 1_000_000_000_000; // 1000 TAO + let contributions = vec![ + (U256::from(2), 600_000_000_000), // 600 TAO + (U256::from(3), 390_000_000_000), // 390 TAO + ]; + + setup_crowdloan(crowdloan_id, deposit, cap, beneficiary, &contributions); + + // Setup a leased network + let end_block = 500; + let emissions_share = Percent::from_percent(30); + let (lease_id, lease) = setup_leased_network( + beneficiary, + emissions_share, + Some(end_block), + None, // We don't add any liquidity + ); + + let subnet_tao_before = SubnetTAO::::get(lease.netuid); + let contributor1_balance_before = SubtensorModule::get_coldkey_balance(&contributions[0].0); + let contributor2_balance_before = SubtensorModule::get_coldkey_balance(&contributions[1].0); + let beneficiary_balance_before = SubtensorModule::get_coldkey_balance(&beneficiary); + + // Try to distribute the dividends + let owner_cut_alpha = 5_000; + SubtensorModule::distribute_leased_network_dividends(lease_id, owner_cut_alpha); + + // Ensure the dividends were not distributed + assert_eq!(SubnetTAO::::get(lease.netuid), subnet_tao_before); + assert_eq!( + SubtensorModule::get_coldkey_balance(&contributions[0].0), + contributor1_balance_before + ); + assert_eq!( + SubtensorModule::get_coldkey_balance(&contributions[1].0), + contributor2_balance_before + ); + assert_eq!( + SubtensorModule::get_coldkey_balance(&beneficiary), + beneficiary_balance_before + ); + // Ensure the correct amount of alpha was accumulated for later dividends distribution + assert_eq!( + AccumulatedLeaseDividends::::get(lease_id), + emissions_share.mul_ceil(owner_cut_alpha) + ); + }); +} + +#[test] +fn test_distribute_lease_network_dividends_accumulates_if_insufficient_liquidity() { + new_test_ext(1).execute_with(|| { + // Setup a crowdloan + let crowdloan_id = 0; + let beneficiary = U256::from(1); + let deposit = 10_000_000_000; // 10 TAO + let cap = 1_000_000_000_000; // 1000 TAO + let contributions = vec![ + (U256::from(2), 600_000_000_000), // 600 TAO + (U256::from(3), 390_000_000_000), // 390 TAO + ]; + + setup_crowdloan(crowdloan_id, deposit, cap, beneficiary, &contributions); + + // Setup a leased network + let end_block = 500; + let emissions_share = Percent::from_percent(30); + let (lease_id, lease) = setup_leased_network( + beneficiary, + emissions_share, + Some(end_block), + None, // We don't add any liquidity + ); + + let subnet_tao_before = SubnetTAO::::get(lease.netuid); + let contributor1_balance_before = SubtensorModule::get_coldkey_balance(&contributions[0].0); + let contributor2_balance_before = SubtensorModule::get_coldkey_balance(&contributions[1].0); + let beneficiary_balance_before = SubtensorModule::get_coldkey_balance(&beneficiary); + + // Try to distribute the dividends + let owner_cut_alpha = 5_000_000; + SubtensorModule::distribute_leased_network_dividends(lease_id, owner_cut_alpha); + + // Ensure the dividends were not distributed + assert_eq!(SubnetTAO::::get(lease.netuid), subnet_tao_before); + assert_eq!( + SubtensorModule::get_coldkey_balance(&contributions[0].0), + contributor1_balance_before + ); + assert_eq!( + SubtensorModule::get_coldkey_balance(&contributions[1].0), + contributor2_balance_before + ); + assert_eq!( + SubtensorModule::get_coldkey_balance(&beneficiary), + beneficiary_balance_before + ); + // Ensure the correct amount of alpha was accumulated for later dividends distribution + assert_eq!( + AccumulatedLeaseDividends::::get(lease_id), + emissions_share.mul_ceil(owner_cut_alpha) + ); + }); +} + +fn setup_crowdloan( + id: u32, + deposit: u64, + cap: u64, + beneficiary: U256, + contributions: &[(U256, u64)], +) { + let funds_account = U256::from(42424242 + id); + + pallet_crowdloan::Crowdloans::::insert( + id, + pallet_crowdloan::CrowdloanInfo { + creator: beneficiary, + deposit, + min_contribution: 0, + end: 0, + cap, + raised: cap, + finalized: false, + funds_account, + call: None, + target_address: None, + contributors_count: 1 + contributions.len() as u32, + }, + ); + + // Simulate contributions + pallet_crowdloan::Contributions::::insert(id, beneficiary, deposit); + for (contributor, amount) in contributions { + pallet_crowdloan::Contributions::::insert(id, contributor, amount); + } + + SubtensorModule::add_balance_to_coldkey_account(&funds_account, cap); + + // Mark the crowdloan as finalizing + pallet_crowdloan::CurrentCrowdloanId::::set(Some(0)); +} + +fn setup_leased_network( + beneficiary: U256, + emissions_share: Percent, + end_block: Option, + tao_to_stake: Option, +) -> (u32, SubnetLeaseOf) { + let lease_id = 0; + assert_ok!(SubtensorModule::do_register_leased_network( + RuntimeOrigin::signed(beneficiary), + emissions_share, + end_block, + )); + + // Configure subnet and add some stake + let lease = SubnetLeases::::get(lease_id).unwrap(); + let netuid = lease.netuid; + SubtokenEnabled::::insert(netuid, true); + + if let Some(tao_to_stake) = tao_to_stake { + SubtensorModule::add_balance_to_coldkey_account(&lease.coldkey, tao_to_stake); + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(lease.coldkey), + lease.hotkey, + netuid, + tao_to_stake + )); + } + + (lease_id, lease) +} diff --git a/pallets/subtensor/src/tests/mock.rs b/pallets/subtensor/src/tests/mock.rs index 221d802ccd..1387a8f4b0 100644 --- a/pallets/subtensor/src/tests/mock.rs +++ b/pallets/subtensor/src/tests/mock.rs @@ -1,9 +1,9 @@ #![allow(clippy::arithmetic_side_effects, clippy::unwrap_used)] use crate::utils::rate_limiting::TransactionType; -use frame_support::derive_impl; use frame_support::dispatch::DispatchResultWithPostInfo; use frame_support::weights::Weight; use frame_support::weights::constants::RocksDbWeight; +use frame_support::{PalletId, derive_impl}; use frame_support::{ assert_ok, parameter_types, traits::{Everything, Hooks, PrivilegeCmp}, @@ -17,7 +17,7 @@ use sp_runtime::{ BuildStorage, traits::{BlakeTwo256, IdentityLookup}, }; -use sp_std::cmp::Ordering; +use sp_std::{cell::RefCell, cmp::Ordering}; use crate::*; @@ -38,6 +38,7 @@ frame_support::construct_runtime!( Scheduler: pallet_scheduler::{Pallet, Call, Storage, Event} = 9, Preimage: pallet_preimage::{Pallet, Call, Storage, Event} = 10, Drand: pallet_drand::{Pallet, Call, Storage, Event} = 11, + Crowdloan: pallet_crowdloan::{Pallet, Call, Storage, Event} = 12, } ); @@ -190,6 +191,8 @@ parameter_types! { pub const InitialTaoWeight: u64 = 0; // 100% global weight. pub const InitialEmaPriceHalvingPeriod: u64 = 201_600_u64; // 4 weeks pub const DurationOfStartCall: u64 = 7 * 24 * 60 * 60 / 12; // Default as 7 days + pub const MaxContributorsPerLeaseToRemove: u32 = 3; + pub const LeaseDividendsDistributionInterval: u32 = 100; } // Configure collective pallet for council @@ -418,6 +421,8 @@ impl crate::Config for Test { type InitialTaoWeight = InitialTaoWeight; type InitialEmaPriceHalvingPeriod = InitialEmaPriceHalvingPeriod; type DurationOfStartCall = DurationOfStartCall; + type ProxyInterface = FakeProxier; + type LeaseDividendsDistributionInterval = LeaseDividendsDistributionInterval; } pub struct OriginPrivilegeCmp; @@ -469,6 +474,56 @@ impl pallet_preimage::Config for Test { type Consideration = (); } +thread_local! { + pub static PROXIES: RefCell = const { RefCell::new(FakeProxier(vec![])) }; +} + +pub struct FakeProxier(pub Vec<(U256, U256)>); + +impl ProxyInterface for FakeProxier { + fn add_lease_beneficiary_proxy(beneficiary: &AccountId, lease: &AccountId) -> DispatchResult { + PROXIES.with_borrow_mut(|proxies| { + proxies.0.push((*beneficiary, *lease)); + }); + Ok(()) + } + + fn remove_lease_beneficiary_proxy( + beneficiary: &AccountId, + lease: &AccountId, + ) -> DispatchResult { + PROXIES.with_borrow_mut(|proxies| { + proxies.0.retain(|(b, l)| b != beneficiary && l != lease); + }); + Ok(()) + } +} + +parameter_types! { + pub const CrowdloanPalletId: PalletId = PalletId(*b"bt/cloan"); + pub const MinimumDeposit: u64 = 50; + pub const AbsoluteMinimumContribution: u64 = 10; + pub const MinimumBlockDuration: u64 = 20; + pub const MaximumBlockDuration: u64 = 100; + pub const RefundContributorsLimit: u32 = 5; + pub const MaxContributors: u32 = 10; +} + +impl pallet_crowdloan::Config for Test { + type PalletId = CrowdloanPalletId; + type Currency = Balances; + type RuntimeCall = RuntimeCall; + type RuntimeEvent = RuntimeEvent; + type WeightInfo = pallet_crowdloan::weights::SubstrateWeight; + type Preimages = Preimage; + type MinimumDeposit = MinimumDeposit; + type AbsoluteMinimumContribution = AbsoluteMinimumContribution; + type MinimumBlockDuration = MinimumBlockDuration; + type MaximumBlockDuration = MaximumBlockDuration; + type RefundContributorsLimit = RefundContributorsLimit; + type MaxContributors = MaxContributors; +} + mod test_crypto { use super::KEY_TYPE; use sp_core::{ @@ -846,3 +901,8 @@ pub fn increase_stake_on_hotkey_account(hotkey: &U256, increment: u64, netuid: u netuid, ); } + +#[allow(dead_code)] +pub(crate) fn last_event() -> RuntimeEvent { + System::events().pop().expect("RuntimeEvent expected").event +} diff --git a/pallets/subtensor/src/tests/mod.rs b/pallets/subtensor/src/tests/mod.rs index 161749a923..23e5258038 100644 --- a/pallets/subtensor/src/tests/mod.rs +++ b/pallets/subtensor/src/tests/mod.rs @@ -7,6 +7,7 @@ mod difficulty; mod emission; mod epoch; mod evm; +mod leasing; mod math; mod migration; mod mock; diff --git a/pallets/subtensor/src/utils/try_state.rs b/pallets/subtensor/src/utils/try_state.rs index 1fb75fd4bb..1b11695c83 100644 --- a/pallets/subtensor/src/utils/try_state.rs +++ b/pallets/subtensor/src/utils/try_state.rs @@ -7,7 +7,7 @@ impl Pallet { /// locked. pub(crate) fn check_total_issuance() -> Result<(), sp_runtime::TryRuntimeError> { // Get the total currency issuance - let currency_issuance = T::Currency::total_issuance(); + let currency_issuance = ::Currency::total_issuance(); // Calculate the expected total issuance let expected_total_issuance = currency_issuance.saturating_add(TotalStake::::get()); diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 39a8b5dbe9..73f10ed4a5 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -12,6 +12,7 @@ pub mod check_nonce; mod migrations; use codec::{Compact, Decode, Encode}; +use frame_support::dispatch::DispatchResult; use frame_support::traits::Imbalance; use frame_support::{ PalletId, @@ -497,7 +498,7 @@ impl CanVote for CanVoteToTriumvirate { } } -use pallet_subtensor::{CollectiveInterface, MemberManagement}; +use pallet_subtensor::{CollectiveInterface, MemberManagement, ProxyInterface}; pub struct ManageSenateMembers; impl MemberManagement for ManageSenateMembers { fn add_member(account: &AccountId) -> DispatchResultWithPostInfo { @@ -807,6 +808,67 @@ impl InstanceFilter for ProxyType { } _ => false, }, + ProxyType::SubnetLeaseBeneficiary => matches!( + c, + RuntimeCall::SubtensorModule(pallet_subtensor::Call::start_call { .. }) + | RuntimeCall::AdminUtils( + pallet_admin_utils::Call::sudo_set_serving_rate_limit { .. } + ) + | RuntimeCall::AdminUtils( + pallet_admin_utils::Call::sudo_set_min_difficulty { .. } + ) + | RuntimeCall::AdminUtils( + pallet_admin_utils::Call::sudo_set_max_difficulty { .. } + ) + | RuntimeCall::AdminUtils( + pallet_admin_utils::Call::sudo_set_weights_version_key { .. } + ) + | RuntimeCall::AdminUtils( + pallet_admin_utils::Call::sudo_set_adjustment_alpha { .. } + ) + | RuntimeCall::AdminUtils( + pallet_admin_utils::Call::sudo_set_max_weight_limit { .. } + ) + | RuntimeCall::AdminUtils( + pallet_admin_utils::Call::sudo_set_immunity_period { .. } + ) + | RuntimeCall::AdminUtils( + pallet_admin_utils::Call::sudo_set_min_allowed_weights { .. } + ) + | RuntimeCall::AdminUtils(pallet_admin_utils::Call::sudo_set_kappa { .. }) + | RuntimeCall::AdminUtils(pallet_admin_utils::Call::sudo_set_rho { .. }) + | RuntimeCall::AdminUtils( + pallet_admin_utils::Call::sudo_set_activity_cutoff { .. } + ) + | RuntimeCall::AdminUtils( + pallet_admin_utils::Call::sudo_set_network_registration_allowed { .. } + ) + | RuntimeCall::AdminUtils( + pallet_admin_utils::Call::sudo_set_network_pow_registration_allowed { .. } + ) + | RuntimeCall::AdminUtils(pallet_admin_utils::Call::sudo_set_max_burn { .. }) + | RuntimeCall::AdminUtils( + pallet_admin_utils::Call::sudo_set_bonds_moving_average { .. } + ) + | RuntimeCall::AdminUtils( + pallet_admin_utils::Call::sudo_set_bonds_penalty { .. } + ) + | RuntimeCall::AdminUtils( + pallet_admin_utils::Call::sudo_set_commit_reveal_weights_enabled { .. } + ) + | RuntimeCall::AdminUtils( + pallet_admin_utils::Call::sudo_set_liquid_alpha_enabled { .. } + ) + | RuntimeCall::AdminUtils( + pallet_admin_utils::Call::sudo_set_alpha_values { .. } + ) + | RuntimeCall::AdminUtils( + pallet_admin_utils::Call::sudo_set_commit_reveal_weights_interval { .. } + ) + | RuntimeCall::AdminUtils( + pallet_admin_utils::Call::sudo_set_toggle_transfer { .. } + ) + ), } } fn is_superset(&self, o: &Self) -> bool { @@ -840,6 +902,30 @@ impl pallet_proxy::Config for Runtime { type AnnouncementDepositFactor = AnnouncementDepositFactor; } +pub struct Proxier; +impl ProxyInterface for Proxier { + fn add_lease_beneficiary_proxy(lease: &AccountId, beneficiary: &AccountId) -> DispatchResult { + pallet_proxy::Pallet::::add_proxy_delegate( + lease, + beneficiary.clone(), + ProxyType::SubnetLeaseBeneficiary, + 0, + ) + } + + fn remove_lease_beneficiary_proxy( + lease: &AccountId, + beneficiary: &AccountId, + ) -> DispatchResult { + pallet_proxy::Pallet::::remove_proxy_delegate( + lease, + beneficiary.clone(), + ProxyType::SubnetLeaseBeneficiary, + 0, + ) + } +} + parameter_types! { pub MaximumSchedulerWeight: Weight = Perbill::from_percent(80) * BlockWeights::get().max_block; @@ -1096,6 +1182,7 @@ parameter_types! { } else { 7 * 24 * 60 * 60 / 12 // 7 days }; + pub const LeaseDividendsDistributionInterval: BlockNumber = 100; // 100 blocks } impl pallet_subtensor::Config for Runtime { @@ -1165,6 +1252,8 @@ impl pallet_subtensor::Config for Runtime { type InitialDissolveNetworkScheduleDuration = InitialDissolveNetworkScheduleDuration; type InitialEmaPriceHalvingPeriod = InitialEmaPriceHalvingPeriod; type DurationOfStartCall = DurationOfStartCall; + type ProxyInterface = Proxier; + type LeaseDividendsDistributionInterval = LeaseDividendsDistributionInterval; } use sp_runtime::BoundedVec;