From 3bac30ac5aec4fe1fd03b3f097d492fcae785e2e Mon Sep 17 00:00:00 2001 From: michael trestman Date: Tue, 12 Aug 2025 11:51:39 -0700 Subject: [PATCH 01/16] wip --- docs/subnets/crowdloans.md | 383 +++++++++++++++++++++++++++++++++++++ sidebars.js | 1 + 2 files changed, 384 insertions(+) create mode 100644 docs/subnets/crowdloans.md diff --git a/docs/subnets/crowdloans.md b/docs/subnets/crowdloans.md new file mode 100644 index 0000000000..99dd2ff985 --- /dev/null +++ b/docs/subnets/crowdloans.md @@ -0,0 +1,383 @@ +--- +title: "Crowdloans" +--- + +- ## What is a subnet crowdloan? + - Problem it solves + - How it differs from traditional “token sale” funding + - High‑level flow + +- ## Who is this for? + - Sponsors/creators + - Contributors + - Beneficiaries/operators + - Validators and the broader network + +- ## Key concepts and roles + - Crowdloan ID and crowdloan account + - Creator (sponsor) + - Contributors + - Beneficiary and the `SubnetLeaseBeneficiary` proxy + - Cap, end block, minimum contribution, deposit + - The immutable “target” and “call” fields + +- ## Lifecycle at a glance + - Create → Contribute → (optional) Update parameters → Finalize + - Emissions distribution during the lease + - Refunds and dissolve if unsuccessful + +- ## Creating a subnet via crowdloan + - Selecting parameters: cap, end block, min contribution + - Setting the target account (optional) and the call + - Using `subtensor::register_leased_network` as the call + - Deposits, fees, and execution gas considerations + +- ## Contributing and managing your position + - Contribute and partial‑cap handling + - Withdraw before finalization + - Tracking your share and expected emissions + +- ## Finalization and lease activation + - Success criteria + - Funds transfer behavior + - Executing the `register_leased_network` call + - What gets created on success: the subnet and the proxy + +- ## Emissions distribution + - How emissions are shared pro‑rata among contributors + - When distribution starts and when it ends + - Perpetual vs fixed‑term leases and their implications + +- ## Operating the leased subnet + - The `SubnetLeaseBeneficiary` proxy: scope and permissions + - Managing subnet parameters and configuration (“hyperparameters”) + - Operational responsibilities and best practices + +- ## Updating and failure modes + - Updating min contribution, end block, and cap + - Refunds: partial and full + - Dissolving the crowdloan + +- ## Security and trust model + - Immutability of the call/target + - Role separation and permissioning via proxy + - Risks, safeguards, and recommended governance patterns + +- ## How‑to guides + - Step‑by‑step: launch a subnet via crowdloan + - Step‑by‑step: contribute and track your emissions share + +- ## Reference + - Extrinsics: `create`, `contribute`, `withdraw`, `refund`, `finalize`, `dissolve`, `update_min_contribution`, `update_end`, `update_cap` + - Integration: `subtensor::register_leased_network` + - Runtime parameters (e.g., refund limits) + - Links to CLI/API examples and code + +- ## FAQ + - Common operational and economic questions + - Troubleshooting + +### Intro draft + +The crowdloan feature lets a group of people collectively fund the registration of a new Bittensor subnet and share the resulting emissions according to each person’s contribution. Instead of a single sponsor paying the full lease cost up front, a creator opens a crowdloan with a funding cap and end block, contributors deposit funds until the cap is met, and—on success—the pallet finalizes the crowdloan by funding subnet registration and activating emissions for the group. + +At finalization, the system executes an on‑chain call—typically `subtensor::register_leased_network`—using the crowdloan’s funds. This registers the subnet and creates a dedicated proxy, `SubnetLeaseBeneficiary`, for the designated beneficiary. That proxy is authorized to operate the subnet (for example, configuring subnet parameters and other allowed controls) without having custody of contributor funds or emissions splits. + +While the lease is active, emissions flow to contributors pro‑rata based on their contributed share. If the crowdloan does not reach its cap by the end block, the creator can trigger refunds and dissolve the crowdloan. The call and target address specified at creation are immutable, ensuring that the purpose of the crowdloan cannot be changed mid‑campaign. This model makes subnet bootstrapping collaborative, transparent, and permissioned through a narrowly scoped proxy for safe, ongoing operations. + +- Strong defaults: immutable target and call, capped funding, clear end block +- Shared upside: emissions distributed proportionally to contributions +- Safe operations: a dedicated proxy to manage the subnet within defined permissions + + + +## Key concepts and roles + +- **Crowdloan info and storage** + - Each crowdloan is tracked by an incrementing `CrowdloanId` and stored in `Crowdloans` alongside contributor balances in `Contributions`. +```151:175:subtensor/pallets/crowdloan/src/lib.rs + #[pallet::storage] + pub type Crowdloans = + StorageMap<_, Twox64Concat, CrowdloanId, CrowdloanInfoOf, OptionQuery>; + #[pallet::storage] + pub type NextCrowdloanId = StorageValue<_, CrowdloanId, ValueQuery, ConstU32<0>>; + #[pallet::storage] + pub type Contributions = StorageDoubleMap< + _, Twox64Concat, CrowdloanId, Identity, T::AccountId, BalanceOf, OptionQuery, + >; + #[pallet::storage] + pub type CurrentCrowdloanId = StorageValue<_, CrowdloanId, OptionQuery>; +``` + +- **Immutable purpose** + - The `call` and `target_address` are set at creation and used during `finalize`. The pallet exposes the `CurrentCrowdloanId` only during dispatch so the called extrinsic can read which crowdloan is being finalized. +```553:617:subtensor/pallets/crowdloan/src/lib.rs +/// Finalize a successful crowdloan. +// ... existing code ... +// Set the current crowdloan id so the dispatched call can access it temporarily +CurrentCrowdloanId::::put(crowdloan_id); +// Retrieve and dispatch the stored call with creator origin +stored_call.dispatch(frame_system::RawOrigin::Signed(who).into())?; +// Clear the current crowdloan id +CurrentCrowdloanId::::kill(); +``` + + +## Lifecycle and extrinsics + +- **Create** a campaign with deposit, cap, end, min contribution, optional `call` and `target_address`. +```318:326:subtensor/pallets/crowdloan/src/lib.rs +pub fn create( + origin: OriginFor, + #[pallet::compact] deposit: BalanceOf, + #[pallet::compact] min_contribution: BalanceOf, + #[pallet::compact] cap: BalanceOf, + #[pallet::compact] end: BlockNumberFor, + call: Option::RuntimeCall>>, + target_address: Option, +) -> DispatchResult +``` + +- **Contribute** funds; amounts are clipped to remaining cap; contributors are counted. +```413:420:subtensor/pallets/crowdloan/src/lib.rs +pub fn contribute( + origin: OriginFor, + #[pallet::compact] crowdloan_id: CrowdloanId, + #[pallet::compact] amount: BalanceOf, +) -> DispatchResult { + let contributor = ensure_signed(origin)?; + let now = frame_system::Pallet::::block_number(); +``` + +- **Withdraw** before finalization; creator cannot withdraw below their deposit. +```505:525:subtensor/pallets/crowdloan/src/lib.rs +pub fn withdraw( + origin: OriginFor, + #[pallet::compact] crowdloan_id: CrowdloanId, +) -> DispatchResult { + let who = ensure_signed(origin)?; + // creator keeps the deposit + if who == crowdloan.creator { /* ... */ } +``` + +- **Finalize** after end when cap is fully raised. Optionally transfers to `target_address` and dispatches the stored `call`. +```566:581:subtensor/pallets/crowdloan/src/lib.rs +pub fn finalize( + origin: OriginFor, + #[pallet::compact] crowdloan_id: CrowdloanId, +) -> DispatchResult { + let who = ensure_signed(origin)?; + ensure!(now >= crowdloan.end, Error::::ContributionPeriodNotEnded); + ensure!(crowdloan.raised == crowdloan.cap, Error::::CapNotRaised); +``` + +- **Refund** loop refunds up to `RefundContributorsLimit` per call; may need multiple calls. +```637:646:subtensor/pallets/crowdloan/src/lib.rs +pub fn refund( + origin: OriginFor, + #[pallet::compact] crowdloan_id: CrowdloanId, +) -> DispatchResultWithPostInfo { + let now = frame_system::Pallet::::block_number(); +``` + +- **Dissolve** after refunds; creator’s deposit is returned and storage cleaned up. +```711:721:subtensor/pallets/crowdloan/src/lib.rs +pub fn dissolve( + origin: OriginFor, + #[pallet::compact] crowdloan_id: CrowdloanId, +) -> DispatchResult { + let who = ensure_signed(origin)?; + ensure!(!crowdloan.finalized, Error::::AlreadyFinalized); +``` + + +## Creating a subnet via crowdloan + +- Use `subtensor::register_leased_network` as the `call` when you `create` the crowdloan. On success, the call is executed with the creator’s origin during `finalize`. +```2107:2114:subtensor/pallets/subtensor/src/macros/dispatches.rs +#[pallet::call_index(110)] +pub fn register_leased_network( + origin: T::RuntimeOrigin, + emissions_share: Percent, + end_block: Option>, +) -> DispatchResultWithPostInfo +``` + +- The leasing logic consumes the crowdloan, registers the subnet, creates a proxy for the beneficiary, records contributor shares, and refunds unspent cap pro‑rata. +```69:101:subtensor/pallets/subtensor/src/subnets/leasing.rs +pub fn do_register_leased_network( + origin: T::RuntimeOrigin, + emissions_share: Percent, + end_block: Option>, +) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + let (crowdloan_id, crowdloan) = Self::get_crowdloan_being_finalized()?; + // Transfer funds to lease coldkey and register subnet + ::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)?; +``` +```112:129:subtensor/pallets/subtensor/src/subnets/leasing.rs +// Enable the beneficiary to operate the subnet through a proxy +T::ProxyInterface::add_lease_beneficiary_proxy(&lease_coldkey, &who)?; +// Compute cost and store lease metadata +SubnetLeases::::insert(lease_id, SubnetLease { beneficiary: who.clone(), /* ... */ emissions_share, end_block, netuid, cost }); +``` +```139:157:subtensor/pallets/subtensor/src/subnets/leasing.rs +// Record contributor shares and refund leftover cap proportionally +for (contributor, amount) in contributions { + let share: U64F64 = U64F64::from(amount).saturating_div(U64F64::from(crowdloan.raised)); + SubnetLeaseShares::::insert(lease_id, &contributor, share); + let contributor_refund = share.saturating_mul(U64F64::from(leftover_cap)).floor().saturating_to_num::(); + ::Currency::transfer(&lease_coldkey, &contributor, contributor_refund, Preservation::Expendable)?; +} +``` + + +## Emissions distribution during a lease + +- When owner rewards are paid to a leased subnet, they are split into contributor dividends and a beneficiary cut. +```450:452:subtensor/pallets/subtensor/src/coinbase/run_coinbase.rs +if let Some(lease_id) = SubnetUidToLeaseId::::get(netuid) { + Self::distribute_leased_network_dividends(lease_id, real_owner_cut); +} +``` + +- Distribution is pro‑rata by recorded share; any remainder goes to the beneficiary. +```324:339:subtensor/pallets/subtensor/src/subnets/leasing.rs +for (contributor, share) in SubnetLeaseShares::::iter_prefix(lease_id) { + let tao_for_contributor = share.saturating_mul(U64F64::from(tao_unstaked.to_u64())).floor().saturating_to_num::(); + Self::add_balance_to_coldkey_account(&contributor, tao_for_contributor); + tao_distributed = tao_distributed.saturating_add(tao_for_contributor.into()); +} +let beneficiary_cut_tao = tao_unstaked.saturating_sub(tao_distributed); +Self::add_balance_to_coldkey_account(&lease.beneficiary, beneficiary_cut_tao.into()); +``` + + +## Operating the leased subnet via proxy + +- On successful registration, a `SubnetLeaseBeneficiary` proxy is added from the lease coldkey to the beneficiary. This proxy can call a narrowly scoped set of operations to operate the subnet. +```886:907:subtensor/runtime/src/lib.rs +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, + ) + } +} +``` + +- Allowed calls for `ProxyType::SubnetLeaseBeneficiary` include starting subnet calls and selected admin‑utils setters (hyperparameters), not unrestricted sudo. +```792:852:subtensor/runtime/src/lib.rs +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 { .. }) +) +``` + + +## Runtime parameters (defaults) + +These constants define crowdloan requirements and operational limits in the runtime: +```1556:1571:subtensor/runtime/src/lib.rs +parameter_types! { + pub const CrowdloanPalletId: PalletId = PalletId(*b"bt/cloan"); + pub const MinimumDeposit: Balance = 10_000_000_000; // 10 TAO + pub const AbsoluteMinimumContribution: Balance = 100_000_000; // 0.1 TAO + pub const MinimumBlockDuration: BlockNumber = /* 7 days or 50 on fast-blocks */; + pub const MaximumBlockDuration: BlockNumber = /* 60 days or 20000 on fast-blocks */; + pub const RefundContributorsLimit: u32 = 50; + pub const MaxContributors: u32 = 500; +} +``` + +Implications: +- **Refund batching**: Up to 50 contributors are processed per `refund` call. +- **Duration bounds**: Ensures campaigns are neither too short nor too long. +- **Contribution floor**: Enforces a minimum "ticket size" for contributors. + +---Below-is-CRUFT-to-delete-only-when-article-is-finished +# source to use and link to +from https://academy.binance.com/en/glossary/polkadot-crowdloan: + +Polkadot Crowdloan refers to the process of staking Polkadot (DOT) tokens to support specific projects in the Polkadot Slot Auction. In return, participants can receive rewards from the projects. +Polkadot (DOT) is an open-source protocol that allows different blockchains to exchange data and applications. In the Polkadot ecosystem, there are two types of blockchains. The main chain is called the Relay Chain, while parallel blockchains are each called a parachain. You can think of Relay Chain as the heart of Polkadot that can connect different parachains. Similar to Ethereum Plasma chains, Parachains can process transactions independently of the Relay Chain. This allows Parachains to greatly improve blockchain scalability. +In order to connect parachains to the Relay Chain, parachain projects need to lease a parachain slot via the Parachain Slot Auction. Projects can bid for a slot in the auction by staking DOT, the native token of Polkadot. Projects that are willing to stake the most DOT tokens can become a Polkdot parachain and lease the slot for 12 to 96 weeks. + +To acquire more DOT tokens for the bidding, parachain teams can use Polkadot Crowdloan to obtain DOT from the community. Crowdloan is a crowdsourcing system that allows participants to support specific parachain projects by staking DOT. In a crowdloan campaign, participants that stake DOT can receive rewards from the project. These rewards can take many forms, such as tokens from the parachain they support. Once they participate in the crowdloan, the staked DOT will be locked up in the project’s slot auction bid. If the project wins the bidding, it can lease a slot to connect its parachain to the Relay Chain. The DOT tokens sourced from the crowdloan will be locked into the parachain slot for the entire lease period (between 12 to 96 weeks). + + +# Crowdloan Pallet +source: https://github.com/opentensor/subtensor/blob/main/pallets/crowdloan/README.md + +## Overview + +A pallet that enables the creation and management of generic crowdloans for transferring funds and executing an arbitrary call. + +Users of this pallet can create a crowdloan by providing a deposit, a cap, an end block, an optional target address and an optional call. + +Users can contribute to a crowdloan by providing funds to the crowdloan they choose to support. The contribution can be withdrawn while the crowdloan is not finalized. + +Once the crowdloan is finalized, the funds will be transferred to the target address if provided; otherwise, the end user is expected to transfer them manually on-chain if the call is a pallet extrinsic. The call will be dispatched with the current crowdloan ID stored as a temporary item. + +If the crowdloan fails to reach the cap, the creator can decide to refund all contributors and dissolve the crowdloan. The initial deposit will be refunded. + +*The call or target address provided when creating the crowdloan is guaranteed to never change. Only the minimum contribution, end block and cap can be updated from the crowdloan creator.* + +## Interface + +- `create`: Create a crowdloan that will raise funds up to a maximum cap and if successful, will transfer funds to the target address if provided and/or dispatch the call (using creator origin). The initial deposit will be transfered to the crowdloan account and will be refunded in case the crowdloan fails to raise the cap. Additionally, the creator will pay for the execution of the call. + +- `contribute`: Contribute to an active crowdloan. The contribution will be transfered to the crowdloan account and will be refunded if the crowdloan fails to raise the cap. If the contribution would raise the amount above the cap, the contribution will be set to the amount that is left to be raised. + +- `withdraw`: Withdraw a contribution from an active (not yet finalized or dissolved) crowdloan. Only contributions over the deposit can be withdrawn by the creator. + +- `refund`: Try to refund all contributors (excluding the creator) up to the limit defined by a runtime parameter *RefundContributorsLimit* (currently set to 5). If the limit is reached, the call will stop and the crowdloan will be marked as partially refunded. It may be needed to dispatch this call multiple times to refund all contributors. + +The following functions are only callable by the creator of the crowdloan: + +- `finalize`: Finalize a successful crowdloan. The call will transfer the raised amount to the target address if it was provided when the crowdloan was created and dispatch the call that was provided using the creator origin. + +- `dissolve`: Dissolve a crowdloan. The crowdloan will be removed from the storage. All contributions must have been refunded before the crowdloan can be dissolved (except the creator's one). + +- `update_min_contribution`: Update the minimum contribution of a non-finalized crowdloan. + +- `update_end`: Update the end block of a non-finalized crowdloan. + +- `update_cap`: Update the cap of a non-finalized crowdloan. + +## Integration with subnet leasing (from the subtensor pallet) + +The `crowdloan` pallet can be used to create a crowdloan that will be used to register a new leased network through a crowdloan using the `register_leased_network` extrinsic from the `subtensor` pallet 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. + +When active, the lease will distribute dividends to the contributors according to their contribution to the crowdloan and the lease can be operated by the beneficiary using the proxy created `SubnetLeaseBeneficiary`. + +If the lease is perpetual, the lease will never be terminated and emissions will continue to be distributed to the contributors. + +If the lease has an end block, the lease can be terminated when end block has passed and the subnet ownership will be transferred to the beneficiary. + + diff --git a/sidebars.js b/sidebars.js index f2119fcca1..4b32ca8a09 100644 --- a/sidebars.js +++ b/sidebars.js @@ -102,6 +102,7 @@ const sidebars = { items: [ "subnets/metagraph", "subnets/create-a-subnet", + "subnets/crowdloans", "subnets/subnet-creators-btcli-guide", "subnets/subnet-hyperparameters", "subnets/working-with-subnets", From 1672a0a4e52948289ceed05cb613aeb7702ade40 Mon Sep 17 00:00:00 2001 From: michael trestman Date: Tue, 12 Aug 2025 18:00:32 -0700 Subject: [PATCH 02/16] wip --- docs/subnets/crowdloans-tutorial.md | 256 ++++++++++++++++++++++++++++ 1 file changed, 256 insertions(+) create mode 100644 docs/subnets/crowdloans-tutorial.md diff --git a/docs/subnets/crowdloans-tutorial.md b/docs/subnets/crowdloans-tutorial.md new file mode 100644 index 0000000000..d8cc6a60ad --- /dev/null +++ b/docs/subnets/crowdloans-tutorial.md @@ -0,0 +1,256 @@ +--- +title: "Launch a Subnet with a Crowdloan (Local chain + Polkadot‑JS)" +--- + +# Launch a Subnet with a Crowdloan + +This hands‑on tutorial walks through creating a subnet via the crowdloan pallet on a locally deployed Bittensor chain, using the Polkadot‑JS web app to submit extrinsics. It follows the same instructional style as the multisig tutorial: step‑by‑step, with concrete UI actions. + +## Summary + +- Create: `crowdloan.create(deposit, min_contribution, cap, end, call, target_address)` + - Use base units (e.g., 10 TAO = 10_000_000_000). Ensure `deposit ≥ MinimumDeposit`, `min_contribution ≥ AbsoluteMinimumContribution`, `cap > deposit`, and `end > current` within duration bounds. + - Set `call = Some(subtensor.registerLeasedNetwork(emissions_share, end_block))` to auto‑register the subnet on finalize. +- Contribute: `crowdloan.contribute(crowdloan_id, amount)` from separate wallets until `raised == cap` (before `end`). +- Finalize: after `end` and with full cap, the creator calls `crowdloan.finalize(crowdloan_id)` to dispatch the nested call and create the lease. +- Verify: check `subtensor.SubnetLeases`, `SubnetLeaseShares`, `SubnetUidToLeaseId`, and beneficiary proxy. +- Dividends: owner emissions periodically split pro‑rata to contributors and the beneficiary; accumulate if not the right block or insufficient liquidity. +- Fallback: if cap not reached, call `crowdloan.refund` repeatedly (batched) then `crowdloan.dissolve` (creator only). +- Get crowdloan_id: read from the `crowdloan.Created` event, `crowdloan.nextCrowdloanId` (last = next-1), or list keys via JS console. + +## Prerequisites + +- A locally running subtensor development chain. + - Start a local node (any standard Substrate dev node setup is fine). In Polkadot‑JS, we will connect to `ws://127.0.0.1:9944`. +- Polkadot‑JS browser app and extension installed. +- Test accounts funded with dev TAO: + - Creator (opens the crowdloan and later finalizes it) + - Beneficiary (will operate the subnet via proxy) + - One or more Contributors (fund the crowdloan) + +Tips: +- Keep three separate accounts handy: `creator`, `beneficiary`, `contrib1` (and optionally `contrib2`). Give them balances via faucet or sudo as appropriate on your dev chain. + +## Connect Polkadot‑JS to your local chain + +1. Open the Polkadot‑JS app. +2. In the network selector, choose Development → custom endpoint `ws://127.0.0.1:9944`. +3. Confirm your local chain metadata loads and your test accounts appear in the Accounts tab. + +## Create a crowdloan + +We will create a campaign whose purpose is to register a leased subnet on finalize. + +1. Go to Developer → Extrinsics. +2. Under “using the selected account”, pick the `creator` account. +3. Under “submit the following extrinsic”, choose module `crowdloan`, call `create`. +4. Fill the parameters: + - deposit: choose an amount (e.g., `10_000_000_000` = 10 TAO on default dev config) + - min_contribution: e.g., `100_000_000` + - cap: e.g., `2_000_000_000_000` (1000 TAO) + - end: pick a block height in the near future (e.g., current + 100) + - call (optional): expand this field and select the nested call module `subtensor`, call `register_leased_network` + - emissions_share (Percent): e.g., `30` + - end_block (Option): pick Some and set the lease end (e.g., current + 500). For a perpetual lease, choose None. + - target_address (Option): leave as None (the lease logic will internally move funds as needed). + +Important: +- Set `cap` higher than the projected subnet lock cost plus proxy deposit (and a small fee buffer). On most dev setups the baseline lock cost is 1,000 TAO (1_000_000_000_000 RAO). If `cap` equals the lock cost exactly, the lease coldkey may lack enough to pay proxy deposits and finalize can fail with insufficient balance. +- Quick checks in Polkadot‑JS: +```javascript +// Lower bound for lock cost (min lock) +(await api.query.subtensor.networkMinLockCost()).toHuman() +// If available in your build, current computed lock cost +(await api.call.subtensorApi.getNetworkLockCost?.())?.toHuman?.() +// Proxy deposit components +(await api.consts.proxy.proxyDepositBase).toHuman() +(await api.consts.proxy.proxyDepositFactor).toHuman() +``` + +5. Click Submit Transaction and sign with `creator`. + +Expected result: +- An event like `crowdloan.Created` with a new `crowdloan_id`. + +## Get the crowdloan_id + +There is no "list crowdloans" extrinsic. Use one of these: + +- From Events (easiest): + - Open the Events panel after submitting `crowdloan.create`. The `crowdloan.Created` event payload includes `crowdloan_id`. +- From storage (quick): + - Developer → Chain state → Storage → `crowdloan.nextCrowdloanId`. The last created id is `next - 1` (assuming no concurrent creates). +- From the JS console (lists all ids): Developer → JavaScript, run: +```javascript +// List all existing crowdloan ids +const keys = await api.query.crowdloan.crowdloans.keys(); +keys.map((k) => k.args[0].toNumber()); +``` + +## Contribute to the crowdloan + +Contributions must occur before the `end` block and will be clipped to the `cap`. + +Repeat for each contributor account: +1. Developer → Extrinsics → using account `contrib1` (then `contrib2`, etc.). +2. Select `crowdloan.contribute(crowdloan_id, amount)`. +3. Provide the `crowdloan_id` (typically 0 on a fresh chain) and an amount. +4. Submit and sign. + +Notes: +- Cap is the maximum total raise, not a minimum. Contributions that would exceed the remaining amount are clipped; after cap is reached, further contributions are rejected with `CapRaised`. +- There is no per‑contributor max beyond the remaining amount and runtime checks, but there is a global `MaxContributors` limit. + +Verify: +- Check Events for `crowdloan.Contributed`. +- In Developer → Chain state → Storage, query `crowdloan.Crowdloans(crowdloan_id)` and `crowdloan.Contributions(crowdloan_id, contributor)`. + +## Finalize the crowdloan + +Once the end block has passed and the cap has been fully raised (`raised == cap`), the creator finalizes. + +1. Wait for the chain to reach the `end` block (watch the status bar or use the Blocks tab). +2. Developer → Extrinsics → using account `creator`. +3. Select `crowdloan.finalize(crowdloan_id)`. +4. Submit and sign. + +On success: +- If `target_address` was provided, the raised amount is transferred there. +- The stored `subtensor.register_leased_network` call executes with creator origin, and the lease is created. + +actual success!!! +``` +system.ExtrinsicSuccess +balances.Withdraw (x2) +system.NewAccount (x2) +balances.Endowed +balances.Transfer (x2) +subtensorModule.RegistrationAllowed +subtensorModule.MaxAllowedUidsSet +subtensorModule.MaxAllowedValidatorsSet +subtensorModule.MinAllowedWeightSet +subtensorModule.MaxWeightLimitSet +subtensorModule.AdjustmentIntervalSet +subtensorModule.RegistrationPerIntervalSet +subtensorModule.AdjustmentAlphaSet +subtensorModule.ImmunityPeriodSet +subtensorModule.MinDifficultySet +subtensorModule.MaxDifficultySet +subtensorModule.NetworkAdded +balances.Reserved +proxy.ProxyAdded +subtensorModule.SubnetLeaseCreated +crowdloan.Finalized +balances.Deposit +transactionPayment.TransactionFeePaid +extrinsic event + +``` + + +Notes: +- Finalizing before the contribution period ends fails with `ContributionPeriodNotEnded`. This keeps the window open so others can contribute until `end`. + +## Verify the leased subnet + +Open Developer → Chain state → Storage, module `subtensor` and check: +- `SubnetLeases(lease_id)` → shows beneficiary, emissions_share, end_block, netuid, cost +- `SubnetUidToLeaseId(netuid)` → maps subnet to lease id +- `SubnetLeaseShares(lease_id, contributor)` → pro‑rata shares per contributor + +Also verify a proxy was added for the beneficiary: +- Module `proxy` → query `Proxies(lease_coldkey)` (if available in your UI) to see the `SubnetLeaseBeneficiary` delegate. + +## Operate the leased subnet (via proxy) + +The beneficiary (i.e., the finalizing creator) operates the subnet by acting as a proxy for the `lease.coldkey` using proxy type `SubnetLeaseBeneficiary`. + +Steps in Polkadot‑JS (Developer → Extrinsics): +- Using account: your beneficiary (the crowdloan creator that finalized) +- Toggle “Use a proxy for this call” + - Proxied account: paste the `lease.coldkey` from `subtensorModule.subnetLeases(lease_id)` + - Proxy type: `SubnetLeaseBeneficiary` +- Pick an allowed call, for example: + - `subtensorModule.start_call { ... }` + - or a network parameter via `adminUtils` such as `sudo_set_min_difficulty`, `sudo_set_network_registration_allowed`, etc. +- Fill arguments → Submit and sign. + +Notes: +- If you submit without the proxy toggle, you’ll see errors like “wallet doesn’t own the subnet.” Always proxy through the `lease.coldkey` with type `SubnetLeaseBeneficiary`. +- If `subtensorModule.subnetUidToLeaseId(netuid)` returns None, the subnet was not created via leasing; operate it using `subtensorModule.subnetOwner(netuid)` instead. + +### Alternative: submit via explicit proxy.proxy extrinsic + +If the “Use a proxy for this call” toggle does not appear, submit the proxy call directly: + +1) Build the inner call (to get its encoding) [optional] +- Developer → Extrinsics → pick the inner call you want (e.g., `subtensorModule.start_call`) and fill its arguments. +- Copy the “encoded call data” (hex starting with `0x…`). + +2) Submit the proxy +- Using account: your beneficiary +- Module/method: `proxy → proxy` +- Fields: + - real: `lease.coldkey` from `subtensorModule.subnetLeases(lease_id)` + - forceProxyType: `SubnetLeaseBeneficiary` + - call: either expand and select the inner call again with arguments, or paste the encoded call hex from step 1 +- Leave delay at 0 → Submit and sign. + +Tips: +- Add the `lease.coldkey` to your Address book so it’s selectable in the UI. +- Verify the proxy exists on‑chain before submitting: `proxy.Proxies(lease_coldkey)` should show your beneficiary with type `SubnetLeaseBeneficiary`. + +### Interpreting SubnetLeaseShares (fixed‑point) + +`SubnetLeaseShares` values are stored as `U64F64` fixed‑point numbers. Polkadot‑JS often renders them as `{ bits: }`. Convert to a human percent by dividing by 2^64. + +Example (Developer → JavaScript): +```javascript +// bits is a BigInt from the on-chain value, e.g. 18354510353341003857n +function u64f64ToPercent(bitsBig) { + const scale = 1n << 64n; // 2^64 + const bps = (bitsBig * 10_000n) / scale; // basis points (1/100 of a percent) + return Number(bps) / 100; // percent with 2 decimals +} + +const val = await api.query.subtensorModule.subnetLeaseShares(leaseId, contributor); +const bits = BigInt(val.toJSON().bits); +console.log(u64f64ToPercent(bits), '%'); +``` + +Notes: +- Shares are stored for contributors excluding the beneficiary. If only the beneficiary funded, `SubnetLeaseShares` may be empty; all dividends go to the beneficiary. +- The beneficiary’s effective share is the leftover after summing all stored contributor shares. + +## (Optional) Observe dividends distribution + +Owner emissions are periodically split among contributors and the beneficiary: +- Distribution happens automatically via the coinbase logic. On a dev chain, you may need to advance blocks and ensure the lease dividends distribution interval has elapsed. +- Balances credited go to each contributor’s coldkey and the beneficiary’s coldkey. You can observe changes by querying balances over time. + +Note: If there is insufficient liquidity or it’s not the correct distribution block, dividends are accumulated and distributed later. + +## Alternative path: Refund and dissolve + +If the cap is not reached by `end`: +1. Anyone can call `crowdloan.refund(crowdloan_id)` repeatedly until all contributors (except the creator) are refunded (batched per call). +2. After refunds complete (only the creator’s deposit remains), the `creator` can call `crowdloan.dissolve(crowdloan_id)` to clean up and recover the deposit. + +## Troubleshooting + +- Finalize fails with CapNotRaised + - Ensure total `raised` equals `cap`. Add contributions or adjust `cap` via `update_cap` (creator‑only) before `finalize`. +- Finalize fails with ContributionPeriodNotEnded + - Wait until the `end` block is reached. +- Finalize fails with CallUnavailable + - Ensure the nested call was supplied during `create`. The pallet stores it as a preimage; if unavailable, it errors and drops the reference. +- Refund does nothing + - Refunds only after `end` and only for non‑finalized campaigns. It processes up to `RefundContributorsLimit` contributors per call. + +## What you learned + +- How to create and fund a crowdloan whose purpose is to register a leased subnet. +- How to finalize and verify the subnet lease, contributor shares, and beneficiary proxy. +- How to recover funds via refund/dissolve if the campaign does not reach its cap. + + From 5f6934327e3eea7c34bc349df7ad71a4deed64e4 Mon Sep 17 00:00:00 2001 From: michael trestman Date: Wed, 13 Aug 2025 06:38:12 -0700 Subject: [PATCH 03/16] wip --- docs/subnets/crowdloans-tutorial.md | 26 ++++- docs/subnets/crowdloans.md | 174 ++++++++++++++++++++++++++-- 2 files changed, 188 insertions(+), 12 deletions(-) diff --git a/docs/subnets/crowdloans-tutorial.md b/docs/subnets/crowdloans-tutorial.md index d8cc6a60ad..c7762eff0d 100644 --- a/docs/subnets/crowdloans-tutorial.md +++ b/docs/subnets/crowdloans-tutorial.md @@ -224,11 +224,27 @@ Notes: ## (Optional) Observe dividends distribution -Owner emissions are periodically split among contributors and the beneficiary: -- Distribution happens automatically via the coinbase logic. On a dev chain, you may need to advance blocks and ensure the lease dividends distribution interval has elapsed. -- Balances credited go to each contributor’s coldkey and the beneficiary’s coldkey. You can observe changes by querying balances over time. - -Note: If there is insufficient liquidity or it’s not the correct distribution block, dividends are accumulated and distributed later. +Owner emissions are periodically split among contributors and the beneficiary, but only when all of these are true: +- The subnet is leased and active (lease has not ended). +- A coinbase cycle paid an owner cut to the subnet owner for the given `netuid`. +- Current block is an exact multiple of `LeaseDividendsDistributionInterval` (check in Constants). +- There is sufficient liquidity to unstake the contributors’ cut from the subnet at or above the minimum swap price. + +What the code does (high level): +- Compute contributors’ share in alpha: `contributors_alpha = ceil(emissions_share% × owner_cut_alpha)` and add any `AccumulatedLeaseDividends`. +- If not at the distribution block or insufficient liquidity, set/accumulate `AccumulatedLeaseDividends(lease_id)` and exit. +- Otherwise, unstake `contributors_alpha` to TAO, then distribute TAO pro‑rata using `SubnetLeaseShares(lease_id, contributor)`; the leftover TAO goes to the beneficiary. + +Alpha vs TAO: +- Emissions accrue in Alpha (subnet share units). On distribution, the contributors’ alpha is unstaked/swapped to TAO using the subnet pool; if swap/unstake cannot proceed (liquidity/price), the alpha is accumulated for later. + +How to debug when nothing moves: +- Ensure `subtensorModule.subnetUidToLeaseId(netuid)` is Some and `subtensorModule.subnetLeases(lease_id)` has `end_block` unset or in the future. +- Check `subtensorModule.accumulatedLeaseDividends(lease_id)`; if growing, you’re not at the interval or liquidity is insufficient. +- Verify interval constant: Developer → Chain state → Constants → look for `LeaseDividendsDistributionInterval`. +- Ensure the subnet pool has liquidity (initial TAO was locked at creation; more stake/liquidity improves distribution). + +Balances credited go to each contributor’s coldkey and the beneficiary’s coldkey. You can observe changes by querying balances over time. ## Alternative path: Refund and dissolve diff --git a/docs/subnets/crowdloans.md b/docs/subnets/crowdloans.md index 99dd2ff985..5b3673e13b 100644 --- a/docs/subnets/crowdloans.md +++ b/docs/subnets/crowdloans.md @@ -3,8 +3,7 @@ title: "Crowdloans" --- - ## What is a subnet crowdloan? - - Problem it solves - - How it differs from traditional “token sale” funding + - Problem it solves (shared ownership of subnets) - High‑level flow - ## Who is this for? @@ -32,6 +31,23 @@ title: "Crowdloans" - Using `subtensor::register_leased_network` as the call - Deposits, fees, and execution gas considerations +### Practical tips (Polkadot‑JS, local dev) + +- Balances are in base units (not TAO). On the default runtime, the pallet enforces: + - `MinimumDeposit = 10_000_000_000` (10 TAO) and + - `AbsoluteMinimumContribution = 100_000_000` (0.1 TAO). + - Example values that satisfy checks: `deposit: 10_000_000_000`, `min_contribution: 100_000_000`, `cap: 1_000_000_000_000`. + - Verify your node’s exact constants in Polkadot‑JS: Developer → Chain state → Constants → `crowdloan`. + +- `end` must be strictly greater than the current block and within duration bounds: + - Check the current block at the top-left of the Polkadot‑JS UI (or the Blocks tab). On local chains with fast‑blocks enabled, this advances quickly. + - Choose an `end` sufficiently ahead (e.g., current + 100) and within `MinimumBlockDuration` and `MaximumBlockDuration` (see Constants). + +- Other creation checks to keep in mind: + - `cap > deposit` is required. + - The creator must have enough balance to pay `deposit`. + - Finalization later requires `now >= end` and `raised == cap`. + - ## Contributing and managing your position - Contribute and partial‑cap handling - Withdraw before finalization @@ -73,9 +89,7 @@ title: "Crowdloans" - Runtime parameters (e.g., refund limits) - Links to CLI/API examples and code -- ## FAQ - - Common operational and economic questions - - Troubleshooting + ### Intro draft @@ -83,7 +97,7 @@ The crowdloan feature lets a group of people collectively fund the registration At finalization, the system executes an on‑chain call—typically `subtensor::register_leased_network`—using the crowdloan’s funds. This registers the subnet and creates a dedicated proxy, `SubnetLeaseBeneficiary`, for the designated beneficiary. That proxy is authorized to operate the subnet (for example, configuring subnet parameters and other allowed controls) without having custody of contributor funds or emissions splits. -While the lease is active, emissions flow to contributors pro‑rata based on their contributed share. If the crowdloan does not reach its cap by the end block, the creator can trigger refunds and dissolve the crowdloan. The call and target address specified at creation are immutable, ensuring that the purpose of the crowdloan cannot be changed mid‑campaign. This model makes subnet bootstrapping collaborative, transparent, and permissioned through a narrowly scoped proxy for safe, ongoing operations. +While the lease is active, emissions flow to contributors pro‑rata based on their contributed share. If the crowdloan is not finalized after the end block, anyone can call refunds; once all contributors are refunded, the creator can dissolve the crowdloan. The call and target address specified at creation are immutable, ensuring that the purpose of the crowdloan cannot be changed mid‑campaign. This model makes subnet bootstrapping collaborative, transparent, and permissioned through a narrowly scoped proxy for safe, ongoing operations. - Strong defaults: immutable target and call, capped funding, clear end block - Shared upside: emissions distributed proportionally to contributions @@ -320,6 +334,152 @@ Implications: - **Duration bounds**: Ensures campaigns are neither too short nor too long. - **Contribution floor**: Enforces a minimum "ticket size" for contributors. + +## FAQ + +### What problem do crowdloans solve? +They enable shared funding and ownership of subnets so that no single sponsor must front the entire lock cost. Emissions are shared pro‑rata among contributors while a designated beneficiary operates the subnet via a scoped proxy. + +### How does the end‑to‑end flow work? +Creator calls `create` with deposit, cap, end, and a `call` of `subtensor::register_leased_network`. Contributors fund until the cap is hit. After the end block, creator calls `finalize`; funds transfer and the stored call executes with creator origin. A subnet and a `SubnetLeaseBeneficiary` proxy are set up; contributor shares are recorded, leftover cap is refunded. + +### Where can I see the exact extrinsics and storage? +See the cited code blocks in this article for `create`, `contribute`, `withdraw`, `finalize`, `refund`, `dissolve` and the storage maps `Crowdloans`, `Contributions`, `CurrentCrowdloanId`. + +### Can the purpose of a crowdloan be changed after it starts? +No. The `call` and optional `target_address` are bound at creation and used at `finalize`. The pallet exposes `CurrentCrowdloanId` only during dispatch to the called extrinsic, preventing mid‑campaign repurposing. + +### Who can finalize a crowdloan and when? +Only the creator, after the end block, and only if `raised == cap` and it hasn’t already been finalized. + +### What happens if the cap is not reached? +Anyone can call `refund` to batch‑refund contributors (excluding the creator) up to `RefundContributorsLimit` per call. After all refunds, only the creator can `dissolve` to recover the deposit and clean storage. + +### How are emissions split during a lease? +Owner rewards are split pro‑rata to contributors by their recorded `SubnetLeaseShares`; any remainder goes to the beneficiary. This runs automatically during coinbase distribution. + +### What permissions does the beneficiary proxy have? +`ProxyType::SubnetLeaseBeneficiary` can invoke a curated set of calls (e.g., start subnet calls and selected admin‑utils setters like difficulty, weights, limits). It cannot perform unrestricted sudo. + +### Can the campaign parameters be updated mid‑flight? +The creator can update `min_contribution`, `end`, and `cap` on a non‑finalized crowdloan, subject to checks (duration bounds, cap >= raised, etc.). The `call` and `target_address` are immutable. + +### What are the defaults for deposits, contribution minimums, and timing? +Runtime defaults currently set minimum deposit, absolute minimum contribution, min/max block durations, refund batch size, and max contributors. See the Runtime parameters section for exact constants. + +### Is there a maximum number of contributors? +Yes. `MaxContributors` limits unique contributors per crowdloan; contributions beyond that will be rejected. + +### How are leftover funds handled at lease creation? +Any leftover cap (after paying registration + proxy cost) is refunded pro‑rata to contributors; the residual remainder goes to the beneficiary. + +### How do I track my expected emissions? +Your share equals your contribution divided by total raised at `finalize`. Emissions are distributed to your coldkey during the lease according to that share. + +### Can a lease be terminated early? +No. The beneficiary may terminate only after the optional `end_block` has passed; for perpetual leases there is no end block. + +### What if the preimage call is missing at finalize? +`finalize` errors with `CallUnavailable` and drops the preimage reference for that call, per the pallet’s error handling. Ensure the `call` was stored as a preimage at `create` time. + + +## Tutorial: Launch a subnet via crowdloan (local dev with Polkadot‑JS) + +This hands‑on guide mirrors production flow and uses Polkadot‑JS to submit each extrinsic from distinct wallets. + +### 0) Setup + +- Prepare accounts: `creator` (opens and finalizes), `beneficiary` (operates subnet), `contrib1`, `contrib2`. +- Fund them with dev TAO. +- Connect Polkadot‑JS to your local node (e.g., `ws://127.0.0.1:9944`). + +Notes for local dev: +- All amounts are base units. Typical defaults: `MinimumDeposit = 10_000_000_000` (10 TAO), `AbsoluteMinimumContribution = 100_000_000` (0.1 TAO). +- Inspect your runtime constants in Polkadot‑JS: Developer → Chain state → Constants → `crowdloan`. +- `end` must be strictly greater than the current block (see top‑left block counter in Polkadot‑JS) and within `MinimumBlockDuration`/`MaximumBlockDuration`. With fast‑blocks, pick a sufficiently ahead block (e.g., current + 100). + +### 1) Create the crowdloan (creator) + +Polkadot‑JS: Developer → Extrinsics +- Using account: `creator` +- Call: `crowdloan.create(deposit, min_contribution, cap, end, call, target_address)` + - deposit: e.g., `10_000_000_000` + - min_contribution: e.g., `100_000_000` + - cap: e.g., `1_000_000_000_000` (must be > deposit) + - end: current block + 100 (adjust for duration bounds) + - call (Option): set to `subtensor.register_leased_network(emissions_share, end_block)` + - emissions_share: e.g., `30` (Percent) + - end_block: Some(current + 500) for a fixed term, or None for perpetual + - target_address: None +- Submit and sign. + +Expected resposne: +``` +system.ExtrinsicSuccess +balances.Withdraw +system.NewAccount +balances.Endowed +balances.Transfer +crowdloan.Created +balances.Deposit +transactionPayment.TransactionFeePaid +extrinsic event +``` +### 2) Contribute (each contributor) + +Repeat for `creator` (optional), `contrib1`, `contrib2` before `end`: +- Using account: contributor wallet +- Call: `crowdloan.contribute(crowdloan_id, amount)` +- Submit and sign. + +Notes: +- Amounts below `min_contribution` are rejected. +- If a contribution would exceed `cap`, it’s clipped to the remaining amount. +- You can verify state via Storage: `crowdloan.Crowdloans(id)` and `crowdloan.Contributions(id, account)`. + +### 3) Finalize (creator) + +After `end` passes and `raised == cap`: +- Using account: `creator` +- Call: `crowdloan.finalize(crowdloan_id)` +- Submit and sign. + +On success: +- If `target_address` was set, funds are transferred to it. +- The nested `subtensor.register_leased_network` is dispatched with creator origin; the lease is created and proxy added. + +### 4) Verify the leased subnet + +Storage checks (Developer → Chain state → Storage): +- `subtensor.SubnetLeases(lease_id)` → shows `beneficiary`, `emissions_share`, `end_block`, `netuid`, `cost`. +- `subtensor.SubnetUidToLeaseId(netuid)` → maps subnet to lease id. +- `subtensor.SubnetLeaseShares(lease_id, contributor)` → contributor pro‑rata shares. +- Proxy (runtime dependent): proxy mappings reflect `SubnetLeaseBeneficiary` authorization for the `beneficiary`. + +### 5) Optional: Observe dividends + +Owner emissions are periodically split among contributors (by share) and the beneficiary. On dev, advance blocks; if it’s not the distribution block or liquidity is insufficient, dividends accumulate for later distribution. + +### 6) Alternative path: Refund and dissolve (if cap not reached) + +- Anyone signed: `crowdloan.refund(crowdloan_id)` repeatedly until all contributors (excluding creator) are refunded (batched up to `RefundContributorsLimit`). +- Creator: `crowdloan.dissolve(crowdloan_id)` when only the creator’s deposit remains. This cleans storage and returns the deposit. + +### 7) Useful adjustments (creator) + +Before finalization: +- `crowdloan.update_min_contribution(crowdloan_id, new_min)` → must be ≥ `AbsoluteMinimumContribution`. +- `crowdloan.update_end(crowdloan_id, new_end)` → must satisfy duration bounds and be > current block. +- `crowdloan.update_cap(crowdloan_id, new_cap)` → must be ≥ `raised`. + +### 8) Optional: Withdraw + +Before finalization: +- Any contributor can `crowdloan.withdraw(crowdloan_id)` to recover their contribution. +- The creator can only withdraw amounts above the kept deposit; the deposit itself remains until refund/dissolve. + + + ---Below-is-CRUFT-to-delete-only-when-article-is-finished # source to use and link to from https://academy.binance.com/en/glossary/polkadot-crowdloan: @@ -356,7 +516,7 @@ If the crowdloan fails to reach the cap, the creator can decide to refund all co - `withdraw`: Withdraw a contribution from an active (not yet finalized or dissolved) crowdloan. Only contributions over the deposit can be withdrawn by the creator. -- `refund`: Try to refund all contributors (excluding the creator) up to the limit defined by a runtime parameter *RefundContributorsLimit* (currently set to 5). If the limit is reached, the call will stop and the crowdloan will be marked as partially refunded. It may be needed to dispatch this call multiple times to refund all contributors. +- `refund`: Try to refund all contributors (excluding the creator) up to the limit defined by runtime parameter `RefundContributorsLimit` (currently set to 50 in the default runtime). If the limit is reached, the call will stop and the crowdloan will be marked as partially refunded. It may be needed to dispatch this call multiple times to refund all contributors. The following functions are only callable by the creator of the crowdloan: From 85499f2a395b1c43a2ce2362a8e85dd19165cda4 Mon Sep 17 00:00:00 2001 From: michael trestman Date: Thu, 14 Aug 2025 12:14:32 -0700 Subject: [PATCH 04/16] wip --- docs/subnets/crowdloans.md | 242 +++++-------------------------------- 1 file changed, 32 insertions(+), 210 deletions(-) diff --git a/docs/subnets/crowdloans.md b/docs/subnets/crowdloans.md index 5b3673e13b..83dfddf9c5 100644 --- a/docs/subnets/crowdloans.md +++ b/docs/subnets/crowdloans.md @@ -108,226 +108,51 @@ While the lease is active, emissions flow to contributors pro‑rata based on th ## Key concepts and roles - **Crowdloan info and storage** - - Each crowdloan is tracked by an incrementing `CrowdloanId` and stored in `Crowdloans` alongside contributor balances in `Contributions`. -```151:175:subtensor/pallets/crowdloan/src/lib.rs - #[pallet::storage] - pub type Crowdloans = - StorageMap<_, Twox64Concat, CrowdloanId, CrowdloanInfoOf, OptionQuery>; - #[pallet::storage] - pub type NextCrowdloanId = StorageValue<_, CrowdloanId, ValueQuery, ConstU32<0>>; - #[pallet::storage] - pub type Contributions = StorageDoubleMap< - _, Twox64Concat, CrowdloanId, Identity, T::AccountId, BalanceOf, OptionQuery, - >; - #[pallet::storage] - pub type CurrentCrowdloanId = StorageValue<_, CrowdloanId, OptionQuery>; -``` + - Each crowdloan is tracked by an incrementing `CrowdloanId` and stored in `Crowdloans` alongside contributor balances in `Contributions`. [Source code](https://github.com/opentensor/subtensor/blob/main/pallets/crowdloan/src/lib.rs#L151-L175) - **Immutable purpose** - - The `call` and `target_address` are set at creation and used during `finalize`. The pallet exposes the `CurrentCrowdloanId` only during dispatch so the called extrinsic can read which crowdloan is being finalized. -```553:617:subtensor/pallets/crowdloan/src/lib.rs -/// Finalize a successful crowdloan. -// ... existing code ... -// Set the current crowdloan id so the dispatched call can access it temporarily -CurrentCrowdloanId::::put(crowdloan_id); -// Retrieve and dispatch the stored call with creator origin -stored_call.dispatch(frame_system::RawOrigin::Signed(who).into())?; -// Clear the current crowdloan id -CurrentCrowdloanId::::kill(); -``` + - The `call` and `target_address` are set at creation and used during `finalize`. The pallet exposes the `CurrentCrowdloanId` only during dispatch so the called extrinsic can read which crowdloan is being finalized. [Source code](https://github.com/opentensor/subtensor/blob/main/pallets/crowdloan/src/lib.rs#L553-L617) ## Lifecycle and extrinsics -- **Create** a campaign with deposit, cap, end, min contribution, optional `call` and `target_address`. -```318:326:subtensor/pallets/crowdloan/src/lib.rs -pub fn create( - origin: OriginFor, - #[pallet::compact] deposit: BalanceOf, - #[pallet::compact] min_contribution: BalanceOf, - #[pallet::compact] cap: BalanceOf, - #[pallet::compact] end: BlockNumberFor, - call: Option::RuntimeCall>>, - target_address: Option, -) -> DispatchResult -``` - -- **Contribute** funds; amounts are clipped to remaining cap; contributors are counted. -```413:420:subtensor/pallets/crowdloan/src/lib.rs -pub fn contribute( - origin: OriginFor, - #[pallet::compact] crowdloan_id: CrowdloanId, - #[pallet::compact] amount: BalanceOf, -) -> DispatchResult { - let contributor = ensure_signed(origin)?; - let now = frame_system::Pallet::::block_number(); -``` - -- **Withdraw** before finalization; creator cannot withdraw below their deposit. -```505:525:subtensor/pallets/crowdloan/src/lib.rs -pub fn withdraw( - origin: OriginFor, - #[pallet::compact] crowdloan_id: CrowdloanId, -) -> DispatchResult { - let who = ensure_signed(origin)?; - // creator keeps the deposit - if who == crowdloan.creator { /* ... */ } -``` - -- **Finalize** after end when cap is fully raised. Optionally transfers to `target_address` and dispatches the stored `call`. -```566:581:subtensor/pallets/crowdloan/src/lib.rs -pub fn finalize( - origin: OriginFor, - #[pallet::compact] crowdloan_id: CrowdloanId, -) -> DispatchResult { - let who = ensure_signed(origin)?; - ensure!(now >= crowdloan.end, Error::::ContributionPeriodNotEnded); - ensure!(crowdloan.raised == crowdloan.cap, Error::::CapNotRaised); -``` - -- **Refund** loop refunds up to `RefundContributorsLimit` per call; may need multiple calls. -```637:646:subtensor/pallets/crowdloan/src/lib.rs -pub fn refund( - origin: OriginFor, - #[pallet::compact] crowdloan_id: CrowdloanId, -) -> DispatchResultWithPostInfo { - let now = frame_system::Pallet::::block_number(); -``` - -- **Dissolve** after refunds; creator’s deposit is returned and storage cleaned up. -```711:721:subtensor/pallets/crowdloan/src/lib.rs -pub fn dissolve( - origin: OriginFor, - #[pallet::compact] crowdloan_id: CrowdloanId, -) -> DispatchResult { - let who = ensure_signed(origin)?; - ensure!(!crowdloan.finalized, Error::::AlreadyFinalized); -``` +- **Create** a campaign with deposit, cap, end, min contribution, optional `call` and `target_address`. [Source code](https://github.com/opentensor/subtensor/blob/main/pallets/crowdloan/src/lib.rs#L318-L326) + +- **Contribute** funds; amounts are clipped to remaining cap; contributors are counted. [Source code](https://github.com/opentensor/subtensor/blob/main/pallets/crowdloan/src/lib.rs#L413-L420) + +- **Withdraw** before finalization; creator cannot withdraw below their deposit. [Source code](https://github.com/opentensor/subtensor/blob/main/pallets/crowdloan/src/lib.rs#L505-L525) + +- **Finalize** after end when cap is fully raised. Optionally transfers to `target_address` and dispatches the stored `call`. [Source code](https://github.com/opentensor/subtensor/blob/main/pallets/crowdloan/src/lib.rs#L566-L581) + +- **Refund** loop refunds up to `RefundContributorsLimit` per call; may need multiple calls. [Source code](https://github.com/opentensor/subtensor/blob/main/pallets/crowdloan/src/lib.rs#L637-L646) + +- **Dissolve** after refunds; creator's deposit is returned and storage cleaned up. [Source code](https://github.com/opentensor/subtensor/blob/main/pallets/crowdloan/src/lib.rs#L711-L721) ## Creating a subnet via crowdloan -- Use `subtensor::register_leased_network` as the `call` when you `create` the crowdloan. On success, the call is executed with the creator’s origin during `finalize`. -```2107:2114:subtensor/pallets/subtensor/src/macros/dispatches.rs -#[pallet::call_index(110)] -pub fn register_leased_network( - origin: T::RuntimeOrigin, - emissions_share: Percent, - end_block: Option>, -) -> DispatchResultWithPostInfo -``` - -- The leasing logic consumes the crowdloan, registers the subnet, creates a proxy for the beneficiary, records contributor shares, and refunds unspent cap pro‑rata. -```69:101:subtensor/pallets/subtensor/src/subnets/leasing.rs -pub fn do_register_leased_network( - origin: T::RuntimeOrigin, - emissions_share: Percent, - end_block: Option>, -) -> DispatchResultWithPostInfo { - let who = ensure_signed(origin)?; - let (crowdloan_id, crowdloan) = Self::get_crowdloan_being_finalized()?; - // Transfer funds to lease coldkey and register subnet - ::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)?; -``` -```112:129:subtensor/pallets/subtensor/src/subnets/leasing.rs -// Enable the beneficiary to operate the subnet through a proxy -T::ProxyInterface::add_lease_beneficiary_proxy(&lease_coldkey, &who)?; -// Compute cost and store lease metadata -SubnetLeases::::insert(lease_id, SubnetLease { beneficiary: who.clone(), /* ... */ emissions_share, end_block, netuid, cost }); -``` -```139:157:subtensor/pallets/subtensor/src/subnets/leasing.rs -// Record contributor shares and refund leftover cap proportionally -for (contributor, amount) in contributions { - let share: U64F64 = U64F64::from(amount).saturating_div(U64F64::from(crowdloan.raised)); - SubnetLeaseShares::::insert(lease_id, &contributor, share); - let contributor_refund = share.saturating_mul(U64F64::from(leftover_cap)).floor().saturating_to_num::(); - ::Currency::transfer(&lease_coldkey, &contributor, contributor_refund, Preservation::Expendable)?; -} -``` +- Use `subtensor::register_leased_network` as the `call` when you `create` the crowdloan. On success, the call is executed with the creator's origin during `finalize`. [Source code](https://github.com/opentensor/subtensor/blob/main/pallets/subtensor/src/macros/dispatches.rs#L2107-L2114) + +- The leasing logic consumes the crowdloan, registers the subnet, creates a proxy for the beneficiary, records contributor shares, and refunds unspent cap pro‑rata. [Source code](https://github.com/opentensor/subtensor/blob/main/pallets/subtensor/src/subnets/leasing.rs#L69-L157) ## Emissions distribution during a lease -- When owner rewards are paid to a leased subnet, they are split into contributor dividends and a beneficiary cut. -```450:452:subtensor/pallets/subtensor/src/coinbase/run_coinbase.rs -if let Some(lease_id) = SubnetUidToLeaseId::::get(netuid) { - Self::distribute_leased_network_dividends(lease_id, real_owner_cut); -} -``` - -- Distribution is pro‑rata by recorded share; any remainder goes to the beneficiary. -```324:339:subtensor/pallets/subtensor/src/subnets/leasing.rs -for (contributor, share) in SubnetLeaseShares::::iter_prefix(lease_id) { - let tao_for_contributor = share.saturating_mul(U64F64::from(tao_unstaked.to_u64())).floor().saturating_to_num::(); - Self::add_balance_to_coldkey_account(&contributor, tao_for_contributor); - tao_distributed = tao_distributed.saturating_add(tao_for_contributor.into()); -} -let beneficiary_cut_tao = tao_unstaked.saturating_sub(tao_distributed); -Self::add_balance_to_coldkey_account(&lease.beneficiary, beneficiary_cut_tao.into()); -``` +- When owner rewards are paid to a leased subnet, they are split into contributor dividends and a beneficiary cut. [Source code](https://github.com/opentensor/subtensor/blob/main/pallets/subtensor/src/coinbase/run_coinbase.rs#L450-L452) + +- Distribution is pro‑rata by recorded share; any remainder goes to the beneficiary. [Source code](https://github.com/opentensor/subtensor/blob/main/pallets/subtensor/src/subnets/leasing.rs#L324-L339) ## Operating the leased subnet via proxy -- On successful registration, a `SubnetLeaseBeneficiary` proxy is added from the lease coldkey to the beneficiary. This proxy can call a narrowly scoped set of operations to operate the subnet. -```886:907:subtensor/runtime/src/lib.rs -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, - ) - } -} -``` - -- Allowed calls for `ProxyType::SubnetLeaseBeneficiary` include starting subnet calls and selected admin‑utils setters (hyperparameters), not unrestricted sudo. -```792:852:subtensor/runtime/src/lib.rs -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 { .. }) -) -``` +- On successful registration, a `SubnetLeaseBeneficiary` proxy is added from the lease coldkey to the beneficiary. This proxy can call a narrowly scoped set of operations to operate the subnet. [Source code](https://github.com/opentensor/subtensor/blob/main/runtime/src/lib.rs#L886-L907) + +- Allowed calls for `ProxyType::SubnetLeaseBeneficiary` include starting subnet calls and selected admin‑utils setters (hyperparameters), not unrestricted sudo. [Source code](https://github.com/opentensor/subtensor/blob/main/runtime/src/lib.rs#L792-L852) ## Runtime parameters (defaults) -These constants define crowdloan requirements and operational limits in the runtime: -```1556:1571:subtensor/runtime/src/lib.rs -parameter_types! { - pub const CrowdloanPalletId: PalletId = PalletId(*b"bt/cloan"); - pub const MinimumDeposit: Balance = 10_000_000_000; // 10 TAO - pub const AbsoluteMinimumContribution: Balance = 100_000_000; // 0.1 TAO - pub const MinimumBlockDuration: BlockNumber = /* 7 days or 50 on fast-blocks */; - pub const MaximumBlockDuration: BlockNumber = /* 60 days or 20000 on fast-blocks */; - pub const RefundContributorsLimit: u32 = 50; - pub const MaxContributors: u32 = 500; -} -``` +These constants define crowdloan requirements and operational limits in the runtime: [Source code](https://github.com/opentensor/subtensor/blob/main/runtime/src/lib.rs#L1556-L1571) Implications: - **Refund batching**: Up to 50 contributors are processed per `refund` call. @@ -413,18 +238,15 @@ Polkadot‑JS: Developer → Extrinsics - target_address: None - Submit and sign. -Expected resposne: -``` -system.ExtrinsicSuccess -balances.Withdraw -system.NewAccount -balances.Endowed -balances.Transfer -crowdloan.Created -balances.Deposit -transactionPayment.TransactionFeePaid -extrinsic event -``` +Expected response: +- `system.ExtrinsicSuccess` +- `balances.Withdraw` +- `system.NewAccount` +- `balances.Endowed` +- `balances.Transfer` +- `crowdloan.Created` +- `balances.Deposit` +- `transactionPayment.TransactionFeePaid` ### 2) Contribute (each contributor) Repeat for `creator` (optional), `contrib1`, `contrib2` before `end`: From 68eb8e70978d10a743e580e957f865567bf90efe Mon Sep 17 00:00:00 2001 From: michael trestman Date: Thu, 14 Aug 2025 12:45:37 -0700 Subject: [PATCH 05/16] wip --- docs/subnets/crowdloans-tutorial.md | 4 ++-- docs/subnets/crowdloans.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/subnets/crowdloans-tutorial.md b/docs/subnets/crowdloans-tutorial.md index c7762eff0d..a075ee2240 100644 --- a/docs/subnets/crowdloans-tutorial.md +++ b/docs/subnets/crowdloans-tutorial.md @@ -51,8 +51,8 @@ We will create a campaign whose purpose is to register a leased subnet on finali - end: pick a block height in the near future (e.g., current + 100) - call (optional): expand this field and select the nested call module `subtensor`, call `register_leased_network` - emissions_share (Percent): e.g., `30` - - end_block (Option): pick Some and set the lease end (e.g., current + 500). For a perpetual lease, choose None. - - target_address (Option): leave as None (the lease logic will internally move funds as needed). + - end_block: pick Some and set the lease end (e.g., current + 500). For a perpetual lease, choose None. + - target_address: leave as None (the lease logic will internally move funds as needed). Important: - Set `cap` higher than the projected subnet lock cost plus proxy deposit (and a small fee buffer). On most dev setups the baseline lock cost is 1,000 TAO (1_000_000_000_000 RAO). If `cap` equals the lock cost exactly, the lease coldkey may lack enough to pay proxy deposits and finalize can fail with insufficient balance. diff --git a/docs/subnets/crowdloans.md b/docs/subnets/crowdloans.md index 83dfddf9c5..296e40c853 100644 --- a/docs/subnets/crowdloans.md +++ b/docs/subnets/crowdloans.md @@ -232,7 +232,7 @@ Polkadot‑JS: Developer → Extrinsics - min_contribution: e.g., `100_000_000` - cap: e.g., `1_000_000_000_000` (must be > deposit) - end: current block + 100 (adjust for duration bounds) - - call (Option): set to `subtensor.register_leased_network(emissions_share, end_block)` + - call: set to `subtensor.register_leased_network(emissions_share, end_block)` - emissions_share: e.g., `30` (Percent) - end_block: Some(current + 500) for a fixed term, or None for perpetual - target_address: None From 43c300bd9c232c194059bdf587b04014bec1d05c Mon Sep 17 00:00:00 2001 From: michael trestman Date: Thu, 14 Aug 2025 13:18:28 -0700 Subject: [PATCH 06/16] wip --- docs/subnets/crowdloans-tutorial.md | 83 ++++------------------------- 1 file changed, 11 insertions(+), 72 deletions(-) diff --git a/docs/subnets/crowdloans-tutorial.md b/docs/subnets/crowdloans-tutorial.md index a075ee2240..fec9ca15c9 100644 --- a/docs/subnets/crowdloans-tutorial.md +++ b/docs/subnets/crowdloans-tutorial.md @@ -49,23 +49,13 @@ We will create a campaign whose purpose is to register a leased subnet on finali - min_contribution: e.g., `100_000_000` - cap: e.g., `2_000_000_000_000` (1000 TAO) - end: pick a block height in the near future (e.g., current + 100) - - call (optional): expand this field and select the nested call module `subtensor`, call `register_leased_network` + - call: expand this field and select the nested call module `subtensor`, call `register_leased_network` - emissions_share (Percent): e.g., `30` - end_block: pick Some and set the lease end (e.g., current + 500). For a perpetual lease, choose None. - target_address: leave as None (the lease logic will internally move funds as needed). Important: - Set `cap` higher than the projected subnet lock cost plus proxy deposit (and a small fee buffer). On most dev setups the baseline lock cost is 1,000 TAO (1_000_000_000_000 RAO). If `cap` equals the lock cost exactly, the lease coldkey may lack enough to pay proxy deposits and finalize can fail with insufficient balance. -- Quick checks in Polkadot‑JS: -```javascript -// Lower bound for lock cost (min lock) -(await api.query.subtensor.networkMinLockCost()).toHuman() -// If available in your build, current computed lock cost -(await api.call.subtensorApi.getNetworkLockCost?.())?.toHuman?.() -// Proxy deposit components -(await api.consts.proxy.proxyDepositBase).toHuman() -(await api.consts.proxy.proxyDepositFactor).toHuman() -``` 5. Click Submit Transaction and sign with `creator`. @@ -147,7 +137,6 @@ extrinsic event ``` - Notes: - Finalizing before the contribution period ends fails with `ContributionPeriodNotEnded`. This keeps the window open so others can contribute until `end`. @@ -161,27 +150,8 @@ Open Developer → Chain state → Storage, module `subtensor` and check: Also verify a proxy was added for the beneficiary: - Module `proxy` → query `Proxies(lease_coldkey)` (if available in your UI) to see the `SubnetLeaseBeneficiary` delegate. -## Operate the leased subnet (via proxy) - -The beneficiary (i.e., the finalizing creator) operates the subnet by acting as a proxy for the `lease.coldkey` using proxy type `SubnetLeaseBeneficiary`. - -Steps in Polkadot‑JS (Developer → Extrinsics): -- Using account: your beneficiary (the crowdloan creator that finalized) -- Toggle “Use a proxy for this call” - - Proxied account: paste the `lease.coldkey` from `subtensorModule.subnetLeases(lease_id)` - - Proxy type: `SubnetLeaseBeneficiary` -- Pick an allowed call, for example: - - `subtensorModule.start_call { ... }` - - or a network parameter via `adminUtils` such as `sudo_set_min_difficulty`, `sudo_set_network_registration_allowed`, etc. -- Fill arguments → Submit and sign. +## Start the leased subnet (via proxy) -Notes: -- If you submit without the proxy toggle, you’ll see errors like “wallet doesn’t own the subnet.” Always proxy through the `lease.coldkey` with type `SubnetLeaseBeneficiary`. -- If `subtensorModule.subnetUidToLeaseId(netuid)` returns None, the subnet was not created via leasing; operate it using `subtensorModule.subnetOwner(netuid)` instead. - -### Alternative: submit via explicit proxy.proxy extrinsic - -If the “Use a proxy for this call” toggle does not appear, submit the proxy call directly: 1) Build the inner call (to get its encoding) [optional] - Developer → Extrinsics → pick the inner call you want (e.g., `subtensorModule.start_call`) and fill its arguments. @@ -200,29 +170,10 @@ Tips: - Add the `lease.coldkey` to your Address book so it’s selectable in the UI. - Verify the proxy exists on‑chain before submitting: `proxy.Proxies(lease_coldkey)` should show your beneficiary with type `SubnetLeaseBeneficiary`. -### Interpreting SubnetLeaseShares (fixed‑point) - -`SubnetLeaseShares` values are stored as `U64F64` fixed‑point numbers. Polkadot‑JS often renders them as `{ bits: }`. Convert to a human percent by dividing by 2^64. - -Example (Developer → JavaScript): -```javascript -// bits is a BigInt from the on-chain value, e.g. 18354510353341003857n -function u64f64ToPercent(bitsBig) { - const scale = 1n << 64n; // 2^64 - const bps = (bitsBig * 10_000n) / scale; // basis points (1/100 of a percent) - return Number(bps) / 100; // percent with 2 decimals -} - -const val = await api.query.subtensorModule.subnetLeaseShares(leaseId, contributor); -const bits = BigInt(val.toJSON().bits); -console.log(u64f64ToPercent(bits), '%'); -``` +## Observe dividends distribution -Notes: -- Shares are stored for contributors excluding the beneficiary. If only the beneficiary funded, `SubnetLeaseShares` may be empty; all dividends go to the beneficiary. -- The beneficiary’s effective share is the leftover after summing all stored contributor shares. +Emissions accrue in Alpha (subnet share units). On distribution, the contributors’ alpha is unstaked/swapped to TAO using the subnet pool; if swap/unstake cannot proceed (liquidity/price), the alpha is accumulated for later. -## (Optional) Observe dividends distribution Owner emissions are periodically split among contributors and the beneficiary, but only when all of these are true: - The subnet is leased and active (lease has not ended). @@ -230,19 +181,6 @@ Owner emissions are periodically split among contributors and the beneficiary, b - Current block is an exact multiple of `LeaseDividendsDistributionInterval` (check in Constants). - There is sufficient liquidity to unstake the contributors’ cut from the subnet at or above the minimum swap price. -What the code does (high level): -- Compute contributors’ share in alpha: `contributors_alpha = ceil(emissions_share% × owner_cut_alpha)` and add any `AccumulatedLeaseDividends`. -- If not at the distribution block or insufficient liquidity, set/accumulate `AccumulatedLeaseDividends(lease_id)` and exit. -- Otherwise, unstake `contributors_alpha` to TAO, then distribute TAO pro‑rata using `SubnetLeaseShares(lease_id, contributor)`; the leftover TAO goes to the beneficiary. - -Alpha vs TAO: -- Emissions accrue in Alpha (subnet share units). On distribution, the contributors’ alpha is unstaked/swapped to TAO using the subnet pool; if swap/unstake cannot proceed (liquidity/price), the alpha is accumulated for later. - -How to debug when nothing moves: -- Ensure `subtensorModule.subnetUidToLeaseId(netuid)` is Some and `subtensorModule.subnetLeases(lease_id)` has `end_block` unset or in the future. -- Check `subtensorModule.accumulatedLeaseDividends(lease_id)`; if growing, you’re not at the interval or liquidity is insufficient. -- Verify interval constant: Developer → Chain state → Constants → look for `LeaseDividendsDistributionInterval`. -- Ensure the subnet pool has liquidity (initial TAO was locked at creation; more stake/liquidity improves distribution). Balances credited go to each contributor’s coldkey and the beneficiary’s coldkey. You can observe changes by querying balances over time. @@ -252,6 +190,13 @@ If the cap is not reached by `end`: 1. Anyone can call `crowdloan.refund(crowdloan_id)` repeatedly until all contributors (except the creator) are refunded (batched per call). 2. After refunds complete (only the creator’s deposit remains), the `creator` can call `crowdloan.dissolve(crowdloan_id)` to clean up and recover the deposit. + +### Optional: Withdraw + +Before finalization: +- Any contributor can `crowdloan.withdraw(crowdloan_id)` to recover their contribution. +- The creator can only withdraw amounts above the kept deposit; the deposit itself remains until refund/dissolve. + ## Troubleshooting - Finalize fails with CapNotRaised @@ -263,10 +208,4 @@ If the cap is not reached by `end`: - Refund does nothing - Refunds only after `end` and only for non‑finalized campaigns. It processes up to `RefundContributorsLimit` contributors per call. -## What you learned - -- How to create and fund a crowdloan whose purpose is to register a leased subnet. -- How to finalize and verify the subnet lease, contributor shares, and beneficiary proxy. -- How to recover funds via refund/dissolve if the campaign does not reach its cap. - From 8a5849af60830a9b25c63708bdc9dae24c342f41 Mon Sep 17 00:00:00 2001 From: michael trestman Date: Thu, 14 Aug 2025 13:19:48 -0700 Subject: [PATCH 07/16] wip --- docs/subnets/crowdloans.md | 258 +------------------------------------ 1 file changed, 2 insertions(+), 256 deletions(-) diff --git a/docs/subnets/crowdloans.md b/docs/subnets/crowdloans.md index 296e40c853..bf78f43764 100644 --- a/docs/subnets/crowdloans.md +++ b/docs/subnets/crowdloans.md @@ -2,96 +2,8 @@ title: "Crowdloans" --- -- ## What is a subnet crowdloan? - - Problem it solves (shared ownership of subnets) - - High‑level flow - -- ## Who is this for? - - Sponsors/creators - - Contributors - - Beneficiaries/operators - - Validators and the broader network - -- ## Key concepts and roles - - Crowdloan ID and crowdloan account - - Creator (sponsor) - - Contributors - - Beneficiary and the `SubnetLeaseBeneficiary` proxy - - Cap, end block, minimum contribution, deposit - - The immutable “target” and “call” fields - -- ## Lifecycle at a glance - - Create → Contribute → (optional) Update parameters → Finalize - - Emissions distribution during the lease - - Refunds and dissolve if unsuccessful - -- ## Creating a subnet via crowdloan - - Selecting parameters: cap, end block, min contribution - - Setting the target account (optional) and the call - - Using `subtensor::register_leased_network` as the call - - Deposits, fees, and execution gas considerations - -### Practical tips (Polkadot‑JS, local dev) - -- Balances are in base units (not TAO). On the default runtime, the pallet enforces: - - `MinimumDeposit = 10_000_000_000` (10 TAO) and - - `AbsoluteMinimumContribution = 100_000_000` (0.1 TAO). - - Example values that satisfy checks: `deposit: 10_000_000_000`, `min_contribution: 100_000_000`, `cap: 1_000_000_000_000`. - - Verify your node’s exact constants in Polkadot‑JS: Developer → Chain state → Constants → `crowdloan`. - -- `end` must be strictly greater than the current block and within duration bounds: - - Check the current block at the top-left of the Polkadot‑JS UI (or the Blocks tab). On local chains with fast‑blocks enabled, this advances quickly. - - Choose an `end` sufficiently ahead (e.g., current + 100) and within `MinimumBlockDuration` and `MaximumBlockDuration` (see Constants). - -- Other creation checks to keep in mind: - - `cap > deposit` is required. - - The creator must have enough balance to pay `deposit`. - - Finalization later requires `now >= end` and `raised == cap`. - -- ## Contributing and managing your position - - Contribute and partial‑cap handling - - Withdraw before finalization - - Tracking your share and expected emissions - -- ## Finalization and lease activation - - Success criteria - - Funds transfer behavior - - Executing the `register_leased_network` call - - What gets created on success: the subnet and the proxy - -- ## Emissions distribution - - How emissions are shared pro‑rata among contributors - - When distribution starts and when it ends - - Perpetual vs fixed‑term leases and their implications - -- ## Operating the leased subnet - - The `SubnetLeaseBeneficiary` proxy: scope and permissions - - Managing subnet parameters and configuration (“hyperparameters”) - - Operational responsibilities and best practices - -- ## Updating and failure modes - - Updating min contribution, end block, and cap - - Refunds: partial and full - - Dissolving the crowdloan - -- ## Security and trust model - - Immutability of the call/target - - Role separation and permissioning via proxy - - Risks, safeguards, and recommended governance patterns - -- ## How‑to guides - - Step‑by‑step: launch a subnet via crowdloan - - Step‑by‑step: contribute and track your emissions share - -- ## Reference - - Extrinsics: `create`, `contribute`, `withdraw`, `refund`, `finalize`, `dissolve`, `update_min_contribution`, `update_end`, `update_cap` - - Integration: `subtensor::register_leased_network` - - Runtime parameters (e.g., refund limits) - - Links to CLI/API examples and code - - - -### Intro draft +### Overview + The crowdloan feature lets a group of people collectively fund the registration of a new Bittensor subnet and share the resulting emissions according to each person’s contribution. Instead of a single sponsor paying the full lease cost up front, a creator opens a crowdloan with a funding cap and end block, contributors deposit funds until the cap is met, and—on success—the pallet finalizes the crowdloan by funding subnet registration and activating emissions for the group. @@ -105,14 +17,6 @@ While the lease is active, emissions flow to contributors pro‑rata based on th -## Key concepts and roles - -- **Crowdloan info and storage** - - Each crowdloan is tracked by an incrementing `CrowdloanId` and stored in `Crowdloans` alongside contributor balances in `Contributions`. [Source code](https://github.com/opentensor/subtensor/blob/main/pallets/crowdloan/src/lib.rs#L151-L175) - -- **Immutable purpose** - - The `call` and `target_address` are set at creation and used during `finalize`. The pallet exposes the `CurrentCrowdloanId` only during dispatch so the called extrinsic can read which crowdloan is being finalized. [Source code](https://github.com/opentensor/subtensor/blob/main/pallets/crowdloan/src/lib.rs#L553-L617) - ## Lifecycle and extrinsics @@ -204,162 +108,4 @@ Your share equals your contribution divided by total raised at `finalize`. Emiss ### Can a lease be terminated early? No. The beneficiary may terminate only after the optional `end_block` has passed; for perpetual leases there is no end block. -### What if the preimage call is missing at finalize? -`finalize` errors with `CallUnavailable` and drops the preimage reference for that call, per the pallet’s error handling. Ensure the `call` was stored as a preimage at `create` time. - - -## Tutorial: Launch a subnet via crowdloan (local dev with Polkadot‑JS) - -This hands‑on guide mirrors production flow and uses Polkadot‑JS to submit each extrinsic from distinct wallets. - -### 0) Setup - -- Prepare accounts: `creator` (opens and finalizes), `beneficiary` (operates subnet), `contrib1`, `contrib2`. -- Fund them with dev TAO. -- Connect Polkadot‑JS to your local node (e.g., `ws://127.0.0.1:9944`). - -Notes for local dev: -- All amounts are base units. Typical defaults: `MinimumDeposit = 10_000_000_000` (10 TAO), `AbsoluteMinimumContribution = 100_000_000` (0.1 TAO). -- Inspect your runtime constants in Polkadot‑JS: Developer → Chain state → Constants → `crowdloan`. -- `end` must be strictly greater than the current block (see top‑left block counter in Polkadot‑JS) and within `MinimumBlockDuration`/`MaximumBlockDuration`. With fast‑blocks, pick a sufficiently ahead block (e.g., current + 100). - -### 1) Create the crowdloan (creator) - -Polkadot‑JS: Developer → Extrinsics -- Using account: `creator` -- Call: `crowdloan.create(deposit, min_contribution, cap, end, call, target_address)` - - deposit: e.g., `10_000_000_000` - - min_contribution: e.g., `100_000_000` - - cap: e.g., `1_000_000_000_000` (must be > deposit) - - end: current block + 100 (adjust for duration bounds) - - call: set to `subtensor.register_leased_network(emissions_share, end_block)` - - emissions_share: e.g., `30` (Percent) - - end_block: Some(current + 500) for a fixed term, or None for perpetual - - target_address: None -- Submit and sign. - -Expected response: -- `system.ExtrinsicSuccess` -- `balances.Withdraw` -- `system.NewAccount` -- `balances.Endowed` -- `balances.Transfer` -- `crowdloan.Created` -- `balances.Deposit` -- `transactionPayment.TransactionFeePaid` -### 2) Contribute (each contributor) - -Repeat for `creator` (optional), `contrib1`, `contrib2` before `end`: -- Using account: contributor wallet -- Call: `crowdloan.contribute(crowdloan_id, amount)` -- Submit and sign. - -Notes: -- Amounts below `min_contribution` are rejected. -- If a contribution would exceed `cap`, it’s clipped to the remaining amount. -- You can verify state via Storage: `crowdloan.Crowdloans(id)` and `crowdloan.Contributions(id, account)`. - -### 3) Finalize (creator) - -After `end` passes and `raised == cap`: -- Using account: `creator` -- Call: `crowdloan.finalize(crowdloan_id)` -- Submit and sign. - -On success: -- If `target_address` was set, funds are transferred to it. -- The nested `subtensor.register_leased_network` is dispatched with creator origin; the lease is created and proxy added. - -### 4) Verify the leased subnet - -Storage checks (Developer → Chain state → Storage): -- `subtensor.SubnetLeases(lease_id)` → shows `beneficiary`, `emissions_share`, `end_block`, `netuid`, `cost`. -- `subtensor.SubnetUidToLeaseId(netuid)` → maps subnet to lease id. -- `subtensor.SubnetLeaseShares(lease_id, contributor)` → contributor pro‑rata shares. -- Proxy (runtime dependent): proxy mappings reflect `SubnetLeaseBeneficiary` authorization for the `beneficiary`. - -### 5) Optional: Observe dividends - -Owner emissions are periodically split among contributors (by share) and the beneficiary. On dev, advance blocks; if it’s not the distribution block or liquidity is insufficient, dividends accumulate for later distribution. - -### 6) Alternative path: Refund and dissolve (if cap not reached) - -- Anyone signed: `crowdloan.refund(crowdloan_id)` repeatedly until all contributors (excluding creator) are refunded (batched up to `RefundContributorsLimit`). -- Creator: `crowdloan.dissolve(crowdloan_id)` when only the creator’s deposit remains. This cleans storage and returns the deposit. - -### 7) Useful adjustments (creator) - -Before finalization: -- `crowdloan.update_min_contribution(crowdloan_id, new_min)` → must be ≥ `AbsoluteMinimumContribution`. -- `crowdloan.update_end(crowdloan_id, new_end)` → must satisfy duration bounds and be > current block. -- `crowdloan.update_cap(crowdloan_id, new_cap)` → must be ≥ `raised`. - -### 8) Optional: Withdraw - -Before finalization: -- Any contributor can `crowdloan.withdraw(crowdloan_id)` to recover their contribution. -- The creator can only withdraw amounts above the kept deposit; the deposit itself remains until refund/dissolve. - - - ----Below-is-CRUFT-to-delete-only-when-article-is-finished -# source to use and link to -from https://academy.binance.com/en/glossary/polkadot-crowdloan: - -Polkadot Crowdloan refers to the process of staking Polkadot (DOT) tokens to support specific projects in the Polkadot Slot Auction. In return, participants can receive rewards from the projects. -Polkadot (DOT) is an open-source protocol that allows different blockchains to exchange data and applications. In the Polkadot ecosystem, there are two types of blockchains. The main chain is called the Relay Chain, while parallel blockchains are each called a parachain. You can think of Relay Chain as the heart of Polkadot that can connect different parachains. Similar to Ethereum Plasma chains, Parachains can process transactions independently of the Relay Chain. This allows Parachains to greatly improve blockchain scalability. -In order to connect parachains to the Relay Chain, parachain projects need to lease a parachain slot via the Parachain Slot Auction. Projects can bid for a slot in the auction by staking DOT, the native token of Polkadot. Projects that are willing to stake the most DOT tokens can become a Polkdot parachain and lease the slot for 12 to 96 weeks. - -To acquire more DOT tokens for the bidding, parachain teams can use Polkadot Crowdloan to obtain DOT from the community. Crowdloan is a crowdsourcing system that allows participants to support specific parachain projects by staking DOT. In a crowdloan campaign, participants that stake DOT can receive rewards from the project. These rewards can take many forms, such as tokens from the parachain they support. Once they participate in the crowdloan, the staked DOT will be locked up in the project’s slot auction bid. If the project wins the bidding, it can lease a slot to connect its parachain to the Relay Chain. The DOT tokens sourced from the crowdloan will be locked into the parachain slot for the entire lease period (between 12 to 96 weeks). - - -# Crowdloan Pallet -source: https://github.com/opentensor/subtensor/blob/main/pallets/crowdloan/README.md - -## Overview - -A pallet that enables the creation and management of generic crowdloans for transferring funds and executing an arbitrary call. - -Users of this pallet can create a crowdloan by providing a deposit, a cap, an end block, an optional target address and an optional call. - -Users can contribute to a crowdloan by providing funds to the crowdloan they choose to support. The contribution can be withdrawn while the crowdloan is not finalized. - -Once the crowdloan is finalized, the funds will be transferred to the target address if provided; otherwise, the end user is expected to transfer them manually on-chain if the call is a pallet extrinsic. The call will be dispatched with the current crowdloan ID stored as a temporary item. - -If the crowdloan fails to reach the cap, the creator can decide to refund all contributors and dissolve the crowdloan. The initial deposit will be refunded. - -*The call or target address provided when creating the crowdloan is guaranteed to never change. Only the minimum contribution, end block and cap can be updated from the crowdloan creator.* - -## Interface - -- `create`: Create a crowdloan that will raise funds up to a maximum cap and if successful, will transfer funds to the target address if provided and/or dispatch the call (using creator origin). The initial deposit will be transfered to the crowdloan account and will be refunded in case the crowdloan fails to raise the cap. Additionally, the creator will pay for the execution of the call. - -- `contribute`: Contribute to an active crowdloan. The contribution will be transfered to the crowdloan account and will be refunded if the crowdloan fails to raise the cap. If the contribution would raise the amount above the cap, the contribution will be set to the amount that is left to be raised. - -- `withdraw`: Withdraw a contribution from an active (not yet finalized or dissolved) crowdloan. Only contributions over the deposit can be withdrawn by the creator. - -- `refund`: Try to refund all contributors (excluding the creator) up to the limit defined by runtime parameter `RefundContributorsLimit` (currently set to 50 in the default runtime). If the limit is reached, the call will stop and the crowdloan will be marked as partially refunded. It may be needed to dispatch this call multiple times to refund all contributors. - -The following functions are only callable by the creator of the crowdloan: - -- `finalize`: Finalize a successful crowdloan. The call will transfer the raised amount to the target address if it was provided when the crowdloan was created and dispatch the call that was provided using the creator origin. - -- `dissolve`: Dissolve a crowdloan. The crowdloan will be removed from the storage. All contributions must have been refunded before the crowdloan can be dissolved (except the creator's one). - -- `update_min_contribution`: Update the minimum contribution of a non-finalized crowdloan. - -- `update_end`: Update the end block of a non-finalized crowdloan. - -- `update_cap`: Update the cap of a non-finalized crowdloan. - -## Integration with subnet leasing (from the subtensor pallet) - -The `crowdloan` pallet can be used to create a crowdloan that will be used to register a new leased network through a crowdloan using the `register_leased_network` extrinsic from the `subtensor` pallet 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. - -When active, the lease will distribute dividends to the contributors according to their contribution to the crowdloan and the lease can be operated by the beneficiary using the proxy created `SubnetLeaseBeneficiary`. - -If the lease is perpetual, the lease will never be terminated and emissions will continue to be distributed to the contributors. - -If the lease has an end block, the lease can be terminated when end block has passed and the subnet ownership will be transferred to the beneficiary. - From 63da54c671650e534b98543e46c0e6a36aed8684 Mon Sep 17 00:00:00 2001 From: michael trestman Date: Thu, 14 Aug 2025 13:24:59 -0700 Subject: [PATCH 08/16] wip --- docs/subnets/crowdloans-tutorial.md | 4 ++-- sidebars.js | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/subnets/crowdloans-tutorial.md b/docs/subnets/crowdloans-tutorial.md index fec9ca15c9..858d5d9dc9 100644 --- a/docs/subnets/crowdloans-tutorial.md +++ b/docs/subnets/crowdloans-tutorial.md @@ -1,8 +1,8 @@ --- -title: "Launch a Subnet with a Crowdloan (Local chain + Polkadot‑JS)" +title: "Create a Subnet with a Crowdloan" --- -# Launch a Subnet with a Crowdloan +# Create a Subnet with a Crowdloan This hands‑on tutorial walks through creating a subnet via the crowdloan pallet on a locally deployed Bittensor chain, using the Polkadot‑JS web app to submit extrinsics. It follows the same instructional style as the multisig tutorial: step‑by‑step, with concrete UI actions. diff --git a/sidebars.js b/sidebars.js index 4a3603d3f3..bafb6ec7ea 100644 --- a/sidebars.js +++ b/sidebars.js @@ -103,6 +103,7 @@ const sidebars = { "subnets/metagraph", "subnets/create-a-subnet", "subnets/crowdloans", + "subnets/crowdloans-tutorial", "subnets/subnet-creators-btcli-guide", "subnets/subnet-hyperparameters", "subnets/working-with-subnets", From 783bbdfa3f79e6302d4294745707aa4c0244f5c4 Mon Sep 17 00:00:00 2001 From: michael trestman Date: Thu, 14 Aug 2025 13:26:30 -0700 Subject: [PATCH 09/16] wip --- docs/subnets/crowdloans-tutorial.md | 2 ++ docs/subnets/crowdloans.md | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/subnets/crowdloans-tutorial.md b/docs/subnets/crowdloans-tutorial.md index 858d5d9dc9..4e4905616b 100644 --- a/docs/subnets/crowdloans-tutorial.md +++ b/docs/subnets/crowdloans-tutorial.md @@ -6,6 +6,8 @@ title: "Create a Subnet with a Crowdloan" This hands‑on tutorial walks through creating a subnet via the crowdloan pallet on a locally deployed Bittensor chain, using the Polkadot‑JS web app to submit extrinsics. It follows the same instructional style as the multisig tutorial: step‑by‑step, with concrete UI actions. +See also [Crowdloans Overview](./crowdloans) + ## Summary - Create: `crowdloan.create(deposit, min_contribution, cap, end, call, target_address)` diff --git a/docs/subnets/crowdloans.md b/docs/subnets/crowdloans.md index bf78f43764..e65e81f3b7 100644 --- a/docs/subnets/crowdloans.md +++ b/docs/subnets/crowdloans.md @@ -16,7 +16,7 @@ While the lease is active, emissions flow to contributors pro‑rata based on th - Safe operations: a dedicated proxy to manage the subnet within defined permissions - +See also [Create a Subnet with a Crowdloan](./crowdloans-tutorial) ## Lifecycle and extrinsics From c1f9460e0dd798bac0d5eb35c86a93a626556f34 Mon Sep 17 00:00:00 2001 From: michael trestman Date: Mon, 18 Aug 2025 18:50:36 -0700 Subject: [PATCH 10/16] wip --- docs/subnets/crowdloans-tutorial.md | 25 +++++++-------------- docs/subnets/crowdloans.md | 34 ++++++++++++++--------------- 2 files changed, 25 insertions(+), 34 deletions(-) diff --git a/docs/subnets/crowdloans-tutorial.md b/docs/subnets/crowdloans-tutorial.md index 4e4905616b..059672b216 100644 --- a/docs/subnets/crowdloans-tutorial.md +++ b/docs/subnets/crowdloans-tutorial.md @@ -4,34 +4,25 @@ title: "Create a Subnet with a Crowdloan" # Create a Subnet with a Crowdloan -This hands‑on tutorial walks through creating a subnet via the crowdloan pallet on a locally deployed Bittensor chain, using the Polkadot‑JS web app to submit extrinsics. It follows the same instructional style as the multisig tutorial: step‑by‑step, with concrete UI actions. +This page describes creating a subnet via **crowdloan** on a locally deployed Bittensor chain. We will use the Polkadot‑JS web app to submit extrinsics. See also [Crowdloans Overview](./crowdloans) -## Summary +The following steps will take us through the lifecycle of a subnet creation crowdloan: + +- First, we will **create** a crowdloan for a subnet. This is a special contract that will conditionally create the subnet if enough funds are raised (this threshold is called a crowdloan's **cap**). +- Next, we will **contribute** enough funds for the crowdloan to reach its cap. +- Next we must **finalize** the crowdloan, which executes the action wrapped inside the crowdloan—the creation of the subnet. +- Finally, we will verify the successful creation of the subnet by starting its emissions and observing the flow of liquidity to validator and creator hotkeys. -- Create: `crowdloan.create(deposit, min_contribution, cap, end, call, target_address)` - - Use base units (e.g., 10 TAO = 10_000_000_000). Ensure `deposit ≥ MinimumDeposit`, `min_contribution ≥ AbsoluteMinimumContribution`, `cap > deposit`, and `end > current` within duration bounds. - - Set `call = Some(subtensor.registerLeasedNetwork(emissions_share, end_block))` to auto‑register the subnet on finalize. -- Contribute: `crowdloan.contribute(crowdloan_id, amount)` from separate wallets until `raised == cap` (before `end`). -- Finalize: after `end` and with full cap, the creator calls `crowdloan.finalize(crowdloan_id)` to dispatch the nested call and create the lease. -- Verify: check `subtensor.SubnetLeases`, `SubnetLeaseShares`, `SubnetUidToLeaseId`, and beneficiary proxy. -- Dividends: owner emissions periodically split pro‑rata to contributors and the beneficiary; accumulate if not the right block or insufficient liquidity. -- Fallback: if cap not reached, call `crowdloan.refund` repeatedly (batched) then `crowdloan.dissolve` (creator only). -- Get crowdloan_id: read from the `crowdloan.Created` event, `crowdloan.nextCrowdloanId` (last = next-1), or list keys via JS console. ## Prerequisites - A locally running subtensor development chain. - Start a local node (any standard Substrate dev node setup is fine). In Polkadot‑JS, we will connect to `ws://127.0.0.1:9944`. - Polkadot‑JS browser app and extension installed. -- Test accounts funded with dev TAO: - - Creator (opens the crowdloan and later finalizes it) - - Beneficiary (will operate the subnet via proxy) - - One or more Contributors (fund the crowdloan) +- An accessible 'Alice' wallet (see: [Provision Wallets for Local Deploy](../local-build/provision-wallets)) -Tips: -- Keep three separate accounts handy: `creator`, `beneficiary`, `contrib1` (and optionally `contrib2`). Give them balances via faucet or sudo as appropriate on your dev chain. ## Connect Polkadot‑JS to your local chain diff --git a/docs/subnets/crowdloans.md b/docs/subnets/crowdloans.md index e65e81f3b7..e73b6a0436 100644 --- a/docs/subnets/crowdloans.md +++ b/docs/subnets/crowdloans.md @@ -18,7 +18,7 @@ While the lease is active, emissions flow to contributors pro‑rata based on th See also [Create a Subnet with a Crowdloan](./crowdloans-tutorial) -## Lifecycle and extrinsics +## Crowdloan Lifecycle - **Create** a campaign with deposit, cap, end, min contribution, optional `call` and `target_address`. [Source code](https://github.com/opentensor/subtensor/blob/main/pallets/crowdloan/src/lib.rs#L318-L326) @@ -33,13 +33,6 @@ See also [Create a Subnet with a Crowdloan](./crowdloans-tutorial) - **Dissolve** after refunds; creator's deposit is returned and storage cleaned up. [Source code](https://github.com/opentensor/subtensor/blob/main/pallets/crowdloan/src/lib.rs#L711-L721) -## Creating a subnet via crowdloan - -- Use `subtensor::register_leased_network` as the `call` when you `create` the crowdloan. On success, the call is executed with the creator's origin during `finalize`. [Source code](https://github.com/opentensor/subtensor/blob/main/pallets/subtensor/src/macros/dispatches.rs#L2107-L2114) - -- The leasing logic consumes the crowdloan, registers the subnet, creates a proxy for the beneficiary, records contributor shares, and refunds unspent cap pro‑rata. [Source code](https://github.com/opentensor/subtensor/blob/main/pallets/subtensor/src/subnets/leasing.rs#L69-L157) - - ## Emissions distribution during a lease - When owner rewards are paid to a leased subnet, they are split into contributor dividends and a beneficiary cut. [Source code](https://github.com/opentensor/subtensor/blob/main/pallets/subtensor/src/coinbase/run_coinbase.rs#L450-L452) @@ -67,45 +60,52 @@ Implications: ## FAQ ### What problem do crowdloans solve? -They enable shared funding and ownership of subnets so that no single sponsor must front the entire lock cost. Emissions are shared pro‑rata among contributors while a designated beneficiary operates the subnet via a scoped proxy. +Crowdloans enable shared funding and ownership of subnets so that no single sponsor must front the entire lock cost. Emissions are shared among contributors while a designated beneficiary operates the subnet via a scoped proxy. ### How does the end‑to‑end flow work? + Creator calls `create` with deposit, cap, end, and a `call` of `subtensor::register_leased_network`. Contributors fund until the cap is hit. After the end block, creator calls `finalize`; funds transfer and the stored call executes with creator origin. A subnet and a `SubnetLeaseBeneficiary` proxy are set up; contributor shares are recorded, leftover cap is refunded. -### Where can I see the exact extrinsics and storage? -See the cited code blocks in this article for `create`, `contribute`, `withdraw`, `finalize`, `refund`, `dissolve` and the storage maps `Crowdloans`, `Contributions`, `CurrentCrowdloanId`. ### Can the purpose of a crowdloan be changed after it starts? + No. The `call` and optional `target_address` are bound at creation and used at `finalize`. The pallet exposes `CurrentCrowdloanId` only during dispatch to the called extrinsic, preventing mid‑campaign repurposing. ### Who can finalize a crowdloan and when? + Only the creator, after the end block, and only if `raised == cap` and it hasn’t already been finalized. ### What happens if the cap is not reached? + Anyone can call `refund` to batch‑refund contributors (excluding the creator) up to `RefundContributorsLimit` per call. After all refunds, only the creator can `dissolve` to recover the deposit and clean storage. ### How are emissions split during a lease? -Owner rewards are split pro‑rata to contributors by their recorded `SubnetLeaseShares`; any remainder goes to the beneficiary. This runs automatically during coinbase distribution. + +Owner rewards are split to contributors by their recorded `SubnetLeaseShares`; any remainder goes to the beneficiary. This runs automatically during coinbase distribution. ### What permissions does the beneficiary proxy have? -`ProxyType::SubnetLeaseBeneficiary` can invoke a curated set of calls (e.g., start subnet calls and selected admin‑utils setters like difficulty, weights, limits). It cannot perform unrestricted sudo. + +They can invoke a curated set of calls (e.g., start subnet calls and selected admin‑utils setters like difficulty, weights, limits). + ### Can the campaign parameters be updated mid‑flight? -The creator can update `min_contribution`, `end`, and `cap` on a non‑finalized crowdloan, subject to checks (duration bounds, cap >= raised, etc.). The `call` and `target_address` are immutable. -### What are the defaults for deposits, contribution minimums, and timing? -Runtime defaults currently set minimum deposit, absolute minimum contribution, min/max block durations, refund batch size, and max contributors. See the Runtime parameters section for exact constants. +The creator can update `min_contribution`, `end`, and `cap` on a non‑finalized crowdloan, subject to checks (duration bounds, cap >= raised, etc.). The `call` and `target_address` are immutable. ### Is there a maximum number of contributors? + Yes. `MaxContributors` limits unique contributors per crowdloan; contributions beyond that will be rejected. ### How are leftover funds handled at lease creation? -Any leftover cap (after paying registration + proxy cost) is refunded pro‑rata to contributors; the residual remainder goes to the beneficiary. + +Any leftover cap (after paying registration + proxy cost) is refunded to contributors; the residual remainder goes to the beneficiary. ### How do I track my expected emissions? + Your share equals your contribution divided by total raised at `finalize`. Emissions are distributed to your coldkey during the lease according to that share. ### Can a lease be terminated early? + No. The beneficiary may terminate only after the optional `end_block` has passed; for perpetual leases there is no end block. From 43825b3e5453562960fa101c28aa69c0e2a806ca Mon Sep 17 00:00:00 2001 From: Dera Okeke Date: Wed, 3 Sep 2025 22:49:42 +0100 Subject: [PATCH 11/16] crowdloans wip --- .../{ => crowdloans}/crowdloans-tutorial.md | 112 +++++++++++------- .../{crowdloans.md => crowdloans/index.md} | 0 sidebars.js | 14 ++- 3 files changed, 77 insertions(+), 49 deletions(-) rename docs/subnets/{ => crowdloans}/crowdloans-tutorial.md (64%) rename docs/subnets/{crowdloans.md => crowdloans/index.md} (100%) diff --git a/docs/subnets/crowdloans-tutorial.md b/docs/subnets/crowdloans/crowdloans-tutorial.md similarity index 64% rename from docs/subnets/crowdloans-tutorial.md rename to docs/subnets/crowdloans/crowdloans-tutorial.md index 059672b216..a34c24d758 100644 --- a/docs/subnets/crowdloans-tutorial.md +++ b/docs/subnets/crowdloans/crowdloans-tutorial.md @@ -15,76 +15,93 @@ The following steps will take us through the lifecycle of a subnet creation crow - Next we must **finalize** the crowdloan, which executes the action wrapped inside the crowdloan—the creation of the subnet. - Finally, we will verify the successful creation of the subnet by starting its emissions and observing the flow of liquidity to validator and creator hotkeys. - ## Prerequisites -- A locally running subtensor development chain. - - Start a local node (any standard Substrate dev node setup is fine). In Polkadot‑JS, we will connect to `ws://127.0.0.1:9944`. -- Polkadot‑JS browser app and extension installed. -- An accessible 'Alice' wallet (see: [Provision Wallets for Local Deploy](../local-build/provision-wallets)) - +- A locally running subtensor development chain. For more information, see [run a local Bittensor blockchain instance](../../local-build/deploy.md). +- [Polkadot‑JS browser app](https://polkadot.js.org/apps/?#/explorer) and [Polkadot‑JS browser extension](https://chrome.google.com/webstore/detail/polkadot%7Bjs%7D-extension/mopnmbcafieddcagagdcbnhejhlodfdd) installed. +- An accessible 'Alice' wallet (see: [Provision Wallets for Local Deploy](../../local-build/provision-wallets)) ## Connect Polkadot‑JS to your local chain 1. Open the Polkadot‑JS app. 2. In the network selector, choose Development → custom endpoint `ws://127.0.0.1:9944`. -3. Confirm your local chain metadata loads and your test accounts appear in the Accounts tab. +3. Confirm your local chain metadata loads and your test accounts appear in the Accounts tab. To do this, see [create and import accounts to the Polkadot-JS extension](../../keys/multisig.md#create-and-import-3-coldkey-pairs-accounts-in-the-polkadot-js-browser-extension). + +:::tip +If the web app does not connect to your local chain, your browser’s privacy or security settings may be blocking it. Try adjusting those settings and reconnecting. +::: ## Create a crowdloan We will create a campaign whose purpose is to register a leased subnet on finalize. -1. Go to Developer → Extrinsics. -2. Under “using the selected account”, pick the `creator` account. -3. Under “submit the following extrinsic”, choose module `crowdloan`, call `create`. +1. Go to **Developer** → **Extrinsics**. +2. Under “**using the selected account**”, pick the crowdloan "`creator`" account. +3. Under “**submit the following extrinsic**”, choose module `crowdloan`, call `create`. 4. Fill the parameters: - - deposit: choose an amount (e.g., `10_000_000_000` = 10 TAO on default dev config) - - min_contribution: e.g., `100_000_000` - - cap: e.g., `2_000_000_000_000` (1000 TAO) - - end: pick a block height in the near future (e.g., current + 100) - - call: expand this field and select the nested call module `subtensor`, call `register_leased_network` - - emissions_share (Percent): e.g., `30` - - end_block: pick Some and set the lease end (e.g., current + 500). For a perpetual lease, choose None. - - target_address: leave as None (the lease logic will internally move funds as needed). -Important: -- Set `cap` higher than the projected subnet lock cost plus proxy deposit (and a small fee buffer). On most dev setups the baseline lock cost is 1,000 TAO (1_000_000_000_000 RAO). If `cap` equals the lock cost exactly, the lease coldkey may lack enough to pay proxy deposits and finalize can fail with insufficient balance. + - deposit: choose an amount (e.g., `10,000,000,000` = 10 TAO on default dev config) + - min_contribution: e.g., `100,000,000` (0.1 TAO) + - cap: e.g., `2,000,000,000,000` (2000 TAO) + - end: pick a block height in the near future (e.g., current + 50,400) + - call: leave as **None**. + - target_address: leave as **None**. + + :::info + + - Set the `cap` value higher than the projected subnet lock cost plus proxy deposit (and a small fee buffer). On most dev setups the baseline lock cost is 1,000 TAO (1,000,000,000,000 RAO). If `cap` equals the lock cost exactly, the lease coldkey may lack enough to pay proxy deposits and finalize can fail with insufficient balance. + - The minimum duration for a crowdloan is one week (≈ 50,400 blocks). Therefore, the `end` value must be set at least 50,400 blocks after the current block. + ::: + +5. Click **Submit Transaction** and sign with the `creator` account. + +## Get the crowdloan ID -5. Click Submit Transaction and sign with `creator`. +Crowdloan IDs are allocated sequentially, starting from `0`, with each new crowdloan assigned the next incremental ID. There is no extrinsic to list created crowdloans. Therefore, to check the identity of crowdloans created, you must use one of these methods. -Expected result: -- An event like `crowdloan.Created` with a new `crowdloan_id`. +- **From Events**: + 1. Navigate to the **block explorer** after submitting the crowdload transaction. + 2. In the **Explorer** tab, the block in which the transaction occured. + 3. In the **Events** panel, find the `crowdloan.create` extrinsic. The `crowdloan.Created` event payload includes `crowdloanId` that represents the ID of the crowdloan. +- **From storage**: -## Get the crowdloan_id + 1. From the **Developer** dropdown, navigate to **Chain state** → **Storage**. + 2. Click the **selected state query** menu and select `crowdloan.nextCrowdloanId`. + 3. Click the **+** icon to run the query. -There is no "list crowdloans" extrinsic. Use one of these: + :::tip + This query returns the ID assigned to the next crowdloan that will be created. Subtract 1 from the returned value to determine the total number of crowdloans that currently exist. + ::: + +- **From the JS console**: + 1. From the **Developer** dropdown, navigate to **Javascript**. + 2. Next, paste the following code block in the editor and run: -- From Events (easiest): - - Open the Events panel after submitting `crowdloan.create`. The `crowdloan.Created` event payload includes `crowdloan_id`. -- From storage (quick): - - Developer → Chain state → Storage → `crowdloan.nextCrowdloanId`. The last created id is `next - 1` (assuming no concurrent creates). -- From the JS console (lists all ids): Developer → JavaScript, run: ```javascript // List all existing crowdloan ids const keys = await api.query.crowdloan.crowdloans.keys(); -keys.map((k) => k.args[0].toNumber()); +console.log(keys.map((k) => k.args[0].toNumber())); ``` ## Contribute to the crowdloan -Contributions must occur before the `end` block and will be clipped to the `cap`. +All contributions must occur before the defined `end` block and will be clipped to the `cap` value provided. -Repeat for each contributor account: -1. Developer → Extrinsics → using account `contrib1` (then `contrib2`, etc.). -2. Select `crowdloan.contribute(crowdloan_id, amount)`. -3. Provide the `crowdloan_id` (typically 0 on a fresh chain) and an amount. -4. Submit and sign. +To contribute to the crowdloan, repeat the following steps for each contributor account: + +1. From the **Developer** dropdown, navigate to **Extrinsics** +2. Under “**using the selected account**”, select the crowdloan "contributor(s)" account. +3. Under “**submit the following extrinsic**”, choose module `crowdloan`, call `contribute (crowdloan_id, amount)`. +4. Provide the `crowdloan_id` (typically 0 on a fresh chain) and an amount. +5. Submit and sign. Notes: + - Cap is the maximum total raise, not a minimum. Contributions that would exceed the remaining amount are clipped; after cap is reached, further contributions are rejected with `CapRaised`. - There is no per‑contributor max beyond the remaining amount and runtime checks, but there is a global `MaxContributors` limit. Verify: + - Check Events for `crowdloan.Contributed`. - In Developer → Chain state → Storage, query `crowdloan.Crowdloans(crowdloan_id)` and `crowdloan.Contributions(crowdloan_id, contributor)`. @@ -98,10 +115,12 @@ Once the end block has passed and the cap has been fully raised (`raised == cap` 4. Submit and sign. On success: + - If `target_address` was provided, the raised amount is transferred there. - The stored `subtensor.register_leased_network` call executes with creator origin, and the lease is created. actual success!!! + ``` system.ExtrinsicSuccess balances.Withdraw (x2) @@ -131,26 +150,30 @@ extrinsic event ``` Notes: + - Finalizing before the contribution period ends fails with `ContributionPeriodNotEnded`. This keeps the window open so others can contribute until `end`. ## Verify the leased subnet Open Developer → Chain state → Storage, module `subtensor` and check: + - `SubnetLeases(lease_id)` → shows beneficiary, emissions_share, end_block, netuid, cost - `SubnetUidToLeaseId(netuid)` → maps subnet to lease id - `SubnetLeaseShares(lease_id, contributor)` → pro‑rata shares per contributor Also verify a proxy was added for the beneficiary: + - Module `proxy` → query `Proxies(lease_coldkey)` (if available in your UI) to see the `SubnetLeaseBeneficiary` delegate. ## Start the leased subnet (via proxy) +1. Build the inner call (to get its encoding) [optional] -1) Build the inner call (to get its encoding) [optional] - Developer → Extrinsics → pick the inner call you want (e.g., `subtensorModule.start_call`) and fill its arguments. - Copy the “encoded call data” (hex starting with `0x…`). -2) Submit the proxy +2. Submit the proxy + - Using account: your beneficiary - Module/method: `proxy → proxy` - Fields: @@ -160,33 +183,34 @@ Also verify a proxy was added for the beneficiary: - Leave delay at 0 → Submit and sign. Tips: + - Add the `lease.coldkey` to your Address book so it’s selectable in the UI. - Verify the proxy exists on‑chain before submitting: `proxy.Proxies(lease_coldkey)` should show your beneficiary with type `SubnetLeaseBeneficiary`. -## Observe dividends distribution +## Observe dividends distribution Emissions accrue in Alpha (subnet share units). On distribution, the contributors’ alpha is unstaked/swapped to TAO using the subnet pool; if swap/unstake cannot proceed (liquidity/price), the alpha is accumulated for later. - Owner emissions are periodically split among contributors and the beneficiary, but only when all of these are true: + - The subnet is leased and active (lease has not ended). - A coinbase cycle paid an owner cut to the subnet owner for the given `netuid`. - Current block is an exact multiple of `LeaseDividendsDistributionInterval` (check in Constants). - There is sufficient liquidity to unstake the contributors’ cut from the subnet at or above the minimum swap price. - Balances credited go to each contributor’s coldkey and the beneficiary’s coldkey. You can observe changes by querying balances over time. ## Alternative path: Refund and dissolve If the cap is not reached by `end`: + 1. Anyone can call `crowdloan.refund(crowdloan_id)` repeatedly until all contributors (except the creator) are refunded (batched per call). 2. After refunds complete (only the creator’s deposit remains), the `creator` can call `crowdloan.dissolve(crowdloan_id)` to clean up and recover the deposit. - ### Optional: Withdraw Before finalization: + - Any contributor can `crowdloan.withdraw(crowdloan_id)` to recover their contribution. - The creator can only withdraw amounts above the kept deposit; the deposit itself remains until refund/dissolve. @@ -200,5 +224,3 @@ Before finalization: - Ensure the nested call was supplied during `create`. The pallet stores it as a preimage; if unavailable, it errors and drops the reference. - Refund does nothing - Refunds only after `end` and only for non‑finalized campaigns. It processes up to `RefundContributorsLimit` contributors per call. - - diff --git a/docs/subnets/crowdloans.md b/docs/subnets/crowdloans/index.md similarity index 100% rename from docs/subnets/crowdloans.md rename to docs/subnets/crowdloans/index.md diff --git a/sidebars.js b/sidebars.js index 41f9675d6c..2fd69c9a37 100644 --- a/sidebars.js +++ b/sidebars.js @@ -55,7 +55,7 @@ const sidebars = { link: { type: "doc", id: "staking-and-delegation/delegation" }, items: [ "staking-and-delegation/delegation", - "staking-and-delegation/stakers-btcli-guide", + "staking-and-delegation/stakers-btcli-guide", "staking-and-delegation/managing-stake-btcli", "staking-and-delegation/managing-stake-sdk", "learn/price-protection", @@ -102,9 +102,15 @@ const sidebars = { collapsed: true, items: [ "subnets/metagraph", - "subnets/create-a-subnet", - "subnets/crowdloans", - "subnets/crowdloans-tutorial", + "subnets/create-a-subnet", + { + type: "category", + label: "Crowdloans", + collapsible: true, + collapsed: true, + link: { type: "doc", id: "subnets/crowdloans/index" }, + items: ["subnets/crowdloans/crowdloans-tutorial"], + }, "subnets/subnet-creators-btcli-guide", "subnets/subnet-hyperparameters", "subnets/working-with-subnets", From 186c9a400d3260e91639eb5a05305ce63a848bdf Mon Sep 17 00:00:00 2001 From: Dera Okeke Date: Wed, 3 Sep 2025 23:58:54 +0100 Subject: [PATCH 12/16] fixed link --- docs/subnets/crowdloans/crowdloans-tutorial.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/subnets/crowdloans/crowdloans-tutorial.md b/docs/subnets/crowdloans/crowdloans-tutorial.md index a34c24d758..28555e093a 100644 --- a/docs/subnets/crowdloans/crowdloans-tutorial.md +++ b/docs/subnets/crowdloans/crowdloans-tutorial.md @@ -6,7 +6,7 @@ title: "Create a Subnet with a Crowdloan" This page describes creating a subnet via **crowdloan** on a locally deployed Bittensor chain. We will use the Polkadot‑JS web app to submit extrinsics. -See also [Crowdloans Overview](./crowdloans) +See also [Crowdloans Overview](./index.md) The following steps will take us through the lifecycle of a subnet creation crowdloan: From d0870a97b046f4044716c7b683971bca3ba3f054 Mon Sep 17 00:00:00 2001 From: Dera Okeke Date: Thu, 4 Sep 2025 20:29:44 +0100 Subject: [PATCH 13/16] crowdloans updates wip --- .../subnets/crowdloans/crowdloans-tutorial.md | 156 +++++++++++++----- docs/subnets/crowdloans/index.md | 14 +- 2 files changed, 121 insertions(+), 49 deletions(-) diff --git a/docs/subnets/crowdloans/crowdloans-tutorial.md b/docs/subnets/crowdloans/crowdloans-tutorial.md index 28555e093a..ab0a63fef2 100644 --- a/docs/subnets/crowdloans/crowdloans-tutorial.md +++ b/docs/subnets/crowdloans/crowdloans-tutorial.md @@ -21,7 +21,7 @@ The following steps will take us through the lifecycle of a subnet creation crow - [Polkadot‑JS browser app](https://polkadot.js.org/apps/?#/explorer) and [Polkadot‑JS browser extension](https://chrome.google.com/webstore/detail/polkadot%7Bjs%7D-extension/mopnmbcafieddcagagdcbnhejhlodfdd) installed. - An accessible 'Alice' wallet (see: [Provision Wallets for Local Deploy](../../local-build/provision-wallets)) -## Connect Polkadot‑JS to your local chain +## Step 1: Connect Polkadot‑JS to your local chain 1. Open the Polkadot‑JS app. 2. In the network selector, choose Development → custom endpoint `ws://127.0.0.1:9944`. @@ -31,7 +31,26 @@ The following steps will take us through the lifecycle of a subnet creation crow If the web app does not connect to your local chain, your browser’s privacy or security settings may be blocking it. Try adjusting those settings and reconnecting. ::: -## Create a crowdloan +## Step 2: Generate call hash + +Before creating the crowdloan, you must first generate the hash that registers the subnet and creates a dedicated proxy for the designated beneficiary. +To begin: + +1. Go to **Developer** → **Extrinsics**. +2. Under “**using the selected account**”, pick the crowdloan "`creator`" account. +3. Under “**submit the following extrinsic**”, choose module `subtensorModule`, call `registerLeasedNetwork(emissionsShare, endBlock)`. +4. Fill the parameters: + + - `emissionsShare`: choose a percentage, e.g, 30. + - `endBlock`: leave as none. + +5. Copy the hex code shown in the **encoded call data** field. You will use this to create the crowdloan in the next step. + +:::info +Do not submit the transaction after entering the parameters. Only copy the encoded call data once all parameters are provided. +::: + +## Step 3: Create a crowdloan We will create a campaign whose purpose is to register a leased subnet on finalize. @@ -40,29 +59,31 @@ We will create a campaign whose purpose is to register a leased subnet on finali 3. Under “**submit the following extrinsic**”, choose module `crowdloan`, call `create`. 4. Fill the parameters: - - deposit: choose an amount (e.g., `10,000,000,000` = 10 TAO on default dev config) - - min_contribution: e.g., `100,000,000` (0.1 TAO) - - cap: e.g., `2,000,000,000,000` (2000 TAO) - - end: pick a block height in the near future (e.g., current + 50,400) - - call: leave as **None**. - - target_address: leave as **None**. + - `deposit`: choose an amount (e.g., `10,000,000,000` = 10 TAO on default dev config) + - `min_contribution`: e.g., `100,000,000` (0.1 TAO) + - `cap`: e.g., `2,000,000,000,000` (2000 TAO) + - `end`: pick a block height in the near future (e.g., current + 5000) + - `call`: leave as **None**. + - `target_address`: leave as **None**. :::info - Set the `cap` value higher than the projected subnet lock cost plus proxy deposit (and a small fee buffer). On most dev setups the baseline lock cost is 1,000 TAO (1,000,000,000,000 RAO). If `cap` equals the lock cost exactly, the lease coldkey may lack enough to pay proxy deposits and finalize can fail with insufficient balance. - - The minimum duration for a crowdloan is one week (≈ 50,400 blocks). Therefore, the `end` value must be set at least 50,400 blocks after the current block. + - If your local subtensor node uses non-fast blocks, the minimum duration for a crowdloan is one week (≈ 50,400 blocks). Therefore, the `end` value must be set at least 50,400 blocks after the current block. ::: 5. Click **Submit Transaction** and sign with the `creator` account. -## Get the crowdloan ID +### Get the crowdloan ID Crowdloan IDs are allocated sequentially, starting from `0`, with each new crowdloan assigned the next incremental ID. There is no extrinsic to list created crowdloans. Therefore, to check the identity of crowdloans created, you must use one of these methods. - **From Events**: + 1. Navigate to the **block explorer** after submitting the crowdload transaction. - 2. In the **Explorer** tab, the block in which the transaction occured. - 3. In the **Events** panel, find the `crowdloan.create` extrinsic. The `crowdloan.Created` event payload includes `crowdloanId` that represents the ID of the crowdloan. + 2. In the **Explorer** tab, find the block in which the transaction occured. + 3. In the **Events** panel, locate the `crowdloan.create` extrinsic. The `crowdloan.Created` event payload includes `crowdloanId` that represents the ID of the crowdloan. + - **From storage**: 1. From the **Developer** dropdown, navigate to **Chain state** → **Storage**. @@ -83,7 +104,7 @@ const keys = await api.query.crowdloan.crowdloans.keys(); console.log(keys.map((k) => k.args[0].toNumber())); ``` -## Contribute to the crowdloan +## Step 4: Contribute to the crowdloan All contributions must occur before the defined `end` block and will be clipped to the `cap` value provided. @@ -95,31 +116,46 @@ To contribute to the crowdloan, repeat the following steps for each contributor 4. Provide the `crowdloan_id` (typically 0 on a fresh chain) and an amount. 5. Submit and sign. -Notes: +:::info -- Cap is the maximum total raise, not a minimum. Contributions that would exceed the remaining amount are clipped; after cap is reached, further contributions are rejected with `CapRaised`. -- There is no per‑contributor max beyond the remaining amount and runtime checks, but there is a global `MaxContributors` limit. +The crowdloan cap is the maximum total raise. If a contribution would push the total above this cap, the contribution is clipped to fit the remaining available amount. Once the cap is reached, any further contributions are rejected and a `crowdloan.CapRaised` event is triggered. +::: -Verify: +### Verify crowdloan contributions -- Check Events for `crowdloan.Contributed`. -- In Developer → Chain state → Storage, query `crowdloan.Crowdloans(crowdloan_id)` and `crowdloan.Contributions(crowdloan_id, contributor)`. +To verify crowdload contributions: -## Finalize the crowdloan +- **From Events**: + + 1. Navigate to the **block explorer** after contributing to the crowdload. + 2. In the **Explorer** tab, find the block in which the transaction occured. + 3. In the **Events** panel, locate the `crowdloan.contribute` extrinsic. The `crowdloan.Contributed` event payload contains the `crowdloanId`, the contributing account, and amount contributed. + +- **From storage**: + + 1. From the **Developer** dropdown, navigate to **Chain state** → **Storage**. + 2. Click the **selected state query** menu and select one of the following: -Once the end block has passed and the cap has been fully raised (`raised == cap`), the creator finalizes. + - `crowdloan.Crowdloans(crowdloan_id)` to check details of the crowdloan + - `crowdloan.Contributions(crowdloan_id, contributor)` to check contributions by an account. -1. Wait for the chain to reach the `end` block (watch the status bar or use the Blocks tab). -2. Developer → Extrinsics → using account `creator`. -3. Select `crowdloan.finalize(crowdloan_id)`. -4. Submit and sign. + 3. Click the **+** icon to run the query. + +## Step 5: Finalize the crowdloan + +The crowdload can be finalized by the creator when the end block has passed and the cap has been fully raised (`raised == cap`). -On success: +1. Wait for the chain to reach the `end` block. +2. From the **Developer** dropdown, go to **Extrinsics**. +3. Under **using the selected account**, select the crowdloan creator account. +4. Select `crowdloan.finalize(crowdloan_id)` and put the ID of the crowdload. +5. Submit and sign. + +:::info - If `target_address` was provided, the raised amount is transferred there. - The stored `subtensor.register_leased_network` call executes with creator origin, and the lease is created. - -actual success!!! + ::: ``` system.ExtrinsicSuccess @@ -149,23 +185,63 @@ extrinsic event ``` -Notes: +:::info + +Even if the `cap` has been raised, the crowdloan cannot be finalized before the `end` block. Finalizing before the contribution period ends fails with `ContributionPeriodNotEnded`. + +::: + +### Verify the leased subnet + +Finalizing the crowdloan registers a new subnet and creates a dedicated proxy for the designated beneficiary. Use one of the following methods to verify the creation of the leased subnet: + +- **Using BTCLI**: + + You can verify the creation of the new subnet by running the following command in your terminal: + + ```sh + btcli subnets list --network local + ``` + + This command lists all created subnets on the chain. Notice the addition of a new subnet among the listed subnets. Notice netuid `2` in the following output. + +
+Show Sample Output + +```console +Using the specified network local from config +[15:49:40] Warning: Verify your local subtensor is running on port 9944. subtensor_interface.py:89 -- Finalizing before the contribution period ends fails with `ContributionPeriodNotEnded`. This keeps the window open so others can contribute until `end`. + Subnets + Network: local -## Verify the leased subnet -Open Developer → Chain state → Storage, module `subtensor` and check: + ┃ ┃ Price ┃ Market Cap ┃ ┃ ┃ ┃ ┃ +Netuid ┃ Name ┃ (Τ_in/α_in) ┃ (α * Price) ┃ Emission (Τ) ┃ P (Τ_in, α_in) ┃ Stake (α_out) ┃ Supply (α) ┃ Tempo (k/n) +━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━ + 0 │ τ root │ 1.0000 τ/Τ │ τ 0.00 │ τ 0.0000 │ -, - │ Τ 0.00 │ 0.00 Τ /21M │ -/- + 2 │ β omron │ 0.0000 τ/β │ τ 0.00 │ τ 0.0000 │ τ 1.00k, 1.00k β │ 0.00 β │ 1.00k β /21M │ 3/10 + 1 │ α apex │ 0.0000 τ/α │ τ 0.00 │ τ 0.0000 │ τ 10.00, 10.00 α │ 1.00 α │ 11.00 α /21M │ 28/100 +────────┼───────────┼─────────────┼─────────────┼──────────────┼────────────────────────┼───────────────┼──────────────┼───────────── + 4 │ │ τ 0.0 │ │ τ 0.0 │ τ 2.01k/20.93k (9.60%) │ │ │ -- `SubnetLeases(lease_id)` → shows beneficiary, emissions_share, end_block, netuid, cost -- `SubnetUidToLeaseId(netuid)` → maps subnet to lease id -- `SubnetLeaseShares(lease_id, contributor)` → pro‑rata shares per contributor +``` + +
+ +- **From storage**: + + 1. From the **Developer** dropdown, navigate to **Chain state** → **Storage**. + 2. Click the **selected state query** menu and select `subtensorModule.SubnetLeases(lease_id)` to display details of the subnet lease, including the beneficiary, emission share, end block, lease coldkey and hotkey, netuid, and creation cost. + 3. Click the **+** icon to run the query. -Also verify a proxy was added for the beneficiary: +Also, verify a proxy was added for the beneficiary: -- Module `proxy` → query `Proxies(lease_coldkey)` (if available in your UI) to see the `SubnetLeaseBeneficiary` delegate. +- **From storage**: + 1. From the **Developer** dropdown, navigate to **Chain state** → **Storage**. + 2. Click the **selected state query** menu and select `proxy.Proxies(lease_coldkey)` to display the `SubnetLeaseBeneficiary` delegate -## Start the leased subnet (via proxy) +## Step 6: Start the leased subnet (via proxy) 1. Build the inner call (to get its encoding) [optional] @@ -182,11 +258,13 @@ Also verify a proxy was added for the beneficiary: - call: either expand and select the inner call again with arguments, or paste the encoded call hex from step 1 - Leave delay at 0 → Submit and sign. -Tips: +:::tip - Add the `lease.coldkey` to your Address book so it’s selectable in the UI. - Verify the proxy exists on‑chain before submitting: `proxy.Proxies(lease_coldkey)` should show your beneficiary with type `SubnetLeaseBeneficiary`. +::: + ## Observe dividends distribution Emissions accrue in Alpha (subnet share units). On distribution, the contributors’ alpha is unstaked/swapped to TAO using the subnet pool; if swap/unstake cannot proceed (liquidity/price), the alpha is accumulated for later. diff --git a/docs/subnets/crowdloans/index.md b/docs/subnets/crowdloans/index.md index e73b6a0436..2392537f7f 100644 --- a/docs/subnets/crowdloans/index.md +++ b/docs/subnets/crowdloans/index.md @@ -4,7 +4,6 @@ title: "Crowdloans" ### Overview - The crowdloan feature lets a group of people collectively fund the registration of a new Bittensor subnet and share the resulting emissions according to each person’s contribution. Instead of a single sponsor paying the full lease cost up front, a creator opens a crowdloan with a funding cap and end block, contributors deposit funds until the cap is met, and—on success—the pallet finalizes the crowdloan by funding subnet registration and activating emissions for the group. At finalization, the system executes an on‑chain call—typically `subtensor::register_leased_network`—using the crowdloan’s funds. This registers the subnet and creates a dedicated proxy, `SubnetLeaseBeneficiary`, for the designated beneficiary. That proxy is authorized to operate the subnet (for example, configuring subnet parameters and other allowed controls) without having custody of contributor funds or emissions splits. @@ -15,8 +14,7 @@ While the lease is active, emissions flow to contributors pro‑rata based on th - Shared upside: emissions distributed proportionally to contributions - Safe operations: a dedicated proxy to manage the subnet within defined permissions - -See also [Create a Subnet with a Crowdloan](./crowdloans-tutorial) +See also [Create a Subnet with a Crowdloan](./crowdloans-tutorial.md) ## Crowdloan Lifecycle @@ -32,41 +30,38 @@ See also [Create a Subnet with a Crowdloan](./crowdloans-tutorial) - **Dissolve** after refunds; creator's deposit is returned and storage cleaned up. [Source code](https://github.com/opentensor/subtensor/blob/main/pallets/crowdloan/src/lib.rs#L711-L721) - ## Emissions distribution during a lease - When owner rewards are paid to a leased subnet, they are split into contributor dividends and a beneficiary cut. [Source code](https://github.com/opentensor/subtensor/blob/main/pallets/subtensor/src/coinbase/run_coinbase.rs#L450-L452) - Distribution is pro‑rata by recorded share; any remainder goes to the beneficiary. [Source code](https://github.com/opentensor/subtensor/blob/main/pallets/subtensor/src/subnets/leasing.rs#L324-L339) - ## Operating the leased subnet via proxy - On successful registration, a `SubnetLeaseBeneficiary` proxy is added from the lease coldkey to the beneficiary. This proxy can call a narrowly scoped set of operations to operate the subnet. [Source code](https://github.com/opentensor/subtensor/blob/main/runtime/src/lib.rs#L886-L907) - Allowed calls for `ProxyType::SubnetLeaseBeneficiary` include starting subnet calls and selected admin‑utils setters (hyperparameters), not unrestricted sudo. [Source code](https://github.com/opentensor/subtensor/blob/main/runtime/src/lib.rs#L792-L852) - ## Runtime parameters (defaults) These constants define crowdloan requirements and operational limits in the runtime: [Source code](https://github.com/opentensor/subtensor/blob/main/runtime/src/lib.rs#L1556-L1571) Implications: + - **Refund batching**: Up to 50 contributors are processed per `refund` call. - **Duration bounds**: Ensures campaigns are neither too short nor too long. - **Contribution floor**: Enforces a minimum "ticket size" for contributors. - ## FAQ ### What problem do crowdloans solve? + Crowdloans enable shared funding and ownership of subnets so that no single sponsor must front the entire lock cost. Emissions are shared among contributors while a designated beneficiary operates the subnet via a scoped proxy. ### How does the end‑to‑end flow work? Creator calls `create` with deposit, cap, end, and a `call` of `subtensor::register_leased_network`. Contributors fund until the cap is hit. After the end block, creator calls `finalize`; funds transfer and the stored call executes with creator origin. A subnet and a `SubnetLeaseBeneficiary` proxy are set up; contributor shares are recorded, leftover cap is refunded. - ### Can the purpose of a crowdloan be changed after it starts? No. The `call` and optional `target_address` are bound at creation and used at `finalize`. The pallet exposes `CurrentCrowdloanId` only during dispatch to the called extrinsic, preventing mid‑campaign repurposing. @@ -86,6 +81,7 @@ Owner rewards are split to contributors by their recorded `SubnetLeaseShares`; a ### What permissions does the beneficiary proxy have? They can invoke a curated set of calls (e.g., start subnet calls and selected admin‑utils setters like difficulty, weights, limits). + ### Can the campaign parameters be updated mid‑flight? @@ -107,5 +103,3 @@ Your share equals your contribution divided by total raised at `finalize`. Emiss ### Can a lease be terminated early? No. The beneficiary may terminate only after the optional `end_block` has passed; for perpetual leases there is no end block. - - From 1bbadae74e6e37678816068d27d4e4192179a99b Mon Sep 17 00:00:00 2001 From: Dera Okeke Date: Thu, 4 Sep 2025 21:05:42 +0100 Subject: [PATCH 14/16] minor updates --- docs/subnets/crowdloans/crowdloans-tutorial.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/subnets/crowdloans/crowdloans-tutorial.md b/docs/subnets/crowdloans/crowdloans-tutorial.md index ab0a63fef2..6866ad17d7 100644 --- a/docs/subnets/crowdloans/crowdloans-tutorial.md +++ b/docs/subnets/crowdloans/crowdloans-tutorial.md @@ -63,7 +63,7 @@ We will create a campaign whose purpose is to register a leased subnet on finali - `min_contribution`: e.g., `100,000,000` (0.1 TAO) - `cap`: e.g., `2,000,000,000,000` (2000 TAO) - `end`: pick a block height in the near future (e.g., current + 5000) - - `call`: leave as **None**. + - `call`: put the hex code of the encoded call data saved from the previous step. - `target_address`: leave as **None**. :::info From 2b3936579e4811e5b2bf63c63d27a131e8478c86 Mon Sep 17 00:00:00 2001 From: Dera Okeke Date: Mon, 8 Sep 2025 17:31:39 +0100 Subject: [PATCH 15/16] crowdloans cleanup done --- .../subnets/crowdloans/crowdloans-tutorial.md | 83 ++++++++++--------- 1 file changed, 42 insertions(+), 41 deletions(-) diff --git a/docs/subnets/crowdloans/crowdloans-tutorial.md b/docs/subnets/crowdloans/crowdloans-tutorial.md index 6866ad17d7..1e860b0a37 100644 --- a/docs/subnets/crowdloans/crowdloans-tutorial.md +++ b/docs/subnets/crowdloans/crowdloans-tutorial.md @@ -69,7 +69,7 @@ We will create a campaign whose purpose is to register a leased subnet on finali :::info - Set the `cap` value higher than the projected subnet lock cost plus proxy deposit (and a small fee buffer). On most dev setups the baseline lock cost is 1,000 TAO (1,000,000,000,000 RAO). If `cap` equals the lock cost exactly, the lease coldkey may lack enough to pay proxy deposits and finalize can fail with insufficient balance. - - If your local subtensor node uses non-fast blocks, the minimum duration for a crowdloan is one week (≈ 50,400 blocks). Therefore, the `end` value must be set at least 50,400 blocks after the current block. + - If your local subtensor node uses non-fast blocks, the minimum duration for a crowdloan is one week (≈ 50,400 blocks). Therefore, the `end` value must be set at least 50,400 blocks after the current block. This limitation also applies on testnet and mainnet. ::: 5. Click **Submit Transaction** and sign with the `creator` account. @@ -151,12 +151,8 @@ The crowdload can be finalized by the creator when the end block has passed and 4. Select `crowdloan.finalize(crowdloan_id)` and put the ID of the crowdload. 5. Submit and sign. -:::info - -- If `target_address` was provided, the raised amount is transferred there. -- The stored `subtensor.register_leased_network` call executes with creator origin, and the lease is created. - ::: - +
+Show Event Output ``` system.ExtrinsicSuccess balances.Withdraw (x2) @@ -182,14 +178,16 @@ crowdloan.Finalized balances.Deposit transactionPayment.TransactionFeePaid extrinsic event - ``` +
:::info -Even if the `cap` has been raised, the crowdloan cannot be finalized before the `end` block. Finalizing before the contribution period ends fails with `ContributionPeriodNotEnded`. - -::: +- Even if the `cap` has been raised, the crowdloan cannot be finalized before the `end` block. Finalizing before the contribution period ends fails with a `ContributionPeriodNotEnded` event. +- If `target_address` was provided, the raised amount is transferred there. +- The stored `subtensor.register_leased_network` call executes with creator origin, and the subnet lease is created. +- The created subnet lease includes the coldkey and hotkey of the proxy wallet that manages the subnet. See [Get the lease coldkey](#get-the-lease-coldkey). + ::: ### Verify the leased subnet @@ -203,7 +201,7 @@ Finalizing the crowdloan registers a new subnet and creates a dedicated proxy fo btcli subnets list --network local ``` - This command lists all created subnets on the chain. Notice the addition of a new subnet among the listed subnets. Notice netuid `2` in the following output. + This command lists all created subnets on the chain. Notice the addition of a new subnet among the listed subnets—netuid `2` in the following output.
Show Sample Output @@ -229,41 +227,34 @@ Netuid ┃ Name ┃ (Τ_in/α_in) ┃ (α * Price) ┃ Emission (Τ) ┃ P
-- **From storage**: - - 1. From the **Developer** dropdown, navigate to **Chain state** → **Storage**. - 2. Click the **selected state query** menu and select `subtensorModule.SubnetLeases(lease_id)` to display details of the subnet lease, including the beneficiary, emission share, end block, lease coldkey and hotkey, netuid, and creation cost. - 3. Click the **+** icon to run the query. +## Step 6: Start the leased subnet (via proxy) -Also, verify a proxy was added for the beneficiary: +Before starting the subnet, you must first get the address of the proxy wallet specified in the subnet lease, as this wallet controls the subnet. -- **From storage**: - 1. From the **Developer** dropdown, navigate to **Chain state** → **Storage**. - 2. Click the **selected state query** menu and select `proxy.Proxies(lease_coldkey)` to display the `SubnetLeaseBeneficiary` delegate - -## Step 6: Start the leased subnet (via proxy) +#### **Get the lease coldkey** -1. Build the inner call (to get its encoding) [optional] +1. From the **Developer** dropdown, navigate to **Chain state** → **Storage**. +2. Click the **selected state query** menu and select `subtensorModule.SubnetLeases(lease_id)` to display details of the subnet lease, including the beneficiary, emission share, end block, lease coldkey and hotkey, netuid, and creation cost. +3. Click the **+** icon to run the query. +4. Copy the value of the `lease.coldkey` in the response. You can add the lease coldkey to the address book on the Polkadot.js web app so that it's selectable in the UI. -- Developer → Extrinsics → pick the inner call you want (e.g., `subtensorModule.start_call`) and fill its arguments. -- Copy the “encoded call data” (hex starting with `0x…`). +:::tip -2. Submit the proxy +- In your local environment, the `lease_id` would be the same as the ID of the crowdloan created. You can confirm the `lease_id` by examining the block where the subnet lease was created for a `subtensorModule.SubnetLeaseCreated` event. -- Using account: your beneficiary -- Module/method: `proxy → proxy` -- Fields: - - real: `lease.coldkey` from `subtensorModule.subnetLeases(lease_id)` - - forceProxyType: `SubnetLeaseBeneficiary` - - call: either expand and select the inner call again with arguments, or paste the encoded call hex from step 1 -- Leave delay at 0 → Submit and sign. + ::: -:::tip +Next, follow the following steps to start the subnet: -- Add the `lease.coldkey` to your Address book so it’s selectable in the UI. -- Verify the proxy exists on‑chain before submitting: `proxy.Proxies(lease_coldkey)` should show your beneficiary with type `SubnetLeaseBeneficiary`. +1. Go to **Developer** → **Extrinsics**. +2. Under “**using the selected account**”, pick the crowdloan "`creator`" account. +3. Under “**submit the following extrinsic**”, choose module `proxy`, call `proxy(real, forceProxyType, call)`. +4. Fill the parameters: -::: + - `real`: enter the `lease.coldkey` gotten from the previous query. + - `forceProxyType`: click the toggle and then choose the `SubnetLeaseBeneficiary`option in the dropdown. + - `call`: choose `subtensorModule.start_call` and then enter the netuid of the subnet you want to start. + - Submit and sign. ## Observe dividends distribution @@ -294,11 +285,21 @@ Before finalization: ## Troubleshooting -- Finalize fails with CapNotRaised +- Call fails with `InvalidCrowdloadId` + - Ensure that the crowdloan ID exists. +- Call fails with `InvalidOrigin` + - Ensure that the selected account that is responsible for signing the transaction. +- Call fails with `BlockDurationTooShort` + - Ensure that the crowdloan `end` is set at least one week away—~50,400 blocks. +- Call fails with `BlockDurationTooLong` + - Ensure that the crowdloan `end` is set between a week to 2 months away. +- Contribution call fails with `ContributionPeriodEnded` + - Extend the `end` value on the crowdloan using the `crowdloan.updateEnd` extrinsic. +- Finalize fails with `CapNotRaised` - Ensure total `raised` equals `cap`. Add contributions or adjust `cap` via `update_cap` (creator‑only) before `finalize`. -- Finalize fails with ContributionPeriodNotEnded +- Finalize fails with `ContributionPeriodNotEnded` - Wait until the `end` block is reached. -- Finalize fails with CallUnavailable +- Finalize fails with `CallUnavailable` - Ensure the nested call was supplied during `create`. The pallet stores it as a preimage; if unavailable, it errors and drops the reference. - Refund does nothing - Refunds only after `end` and only for non‑finalized campaigns. It processes up to `RefundContributorsLimit` contributors per call. From 80008ed24cd68a44ab1b4f6111e3a888785a43c9 Mon Sep 17 00:00:00 2001 From: michael trestman Date: Mon, 15 Sep 2025 11:47:52 -0700 Subject: [PATCH 16/16] wip --- .../subnets/crowdloans/crowdloans-tutorial.md | 2 +- docs/subnets/crowdloans/index.md | 31 ++++++++++++++----- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/docs/subnets/crowdloans/crowdloans-tutorial.md b/docs/subnets/crowdloans/crowdloans-tutorial.md index 1e860b0a37..89fe6e1e22 100644 --- a/docs/subnets/crowdloans/crowdloans-tutorial.md +++ b/docs/subnets/crowdloans/crowdloans-tutorial.md @@ -258,7 +258,7 @@ Next, follow the following steps to start the subnet: ## Observe dividends distribution -Emissions accrue in Alpha (subnet share units). On distribution, the contributors’ alpha is unstaked/swapped to TAO using the subnet pool; if swap/unstake cannot proceed (liquidity/price), the alpha is accumulated for later. +Emissions accrue in Alpha (subnet share units), but are distributed in TAO. On distribution, the contributors' alpha is unstaked/swapped to TAO using the subnet pool; if swap/unstake cannot proceed (liquidity/price), the alpha is accumulated for later. Owner emissions are periodically split among contributors and the beneficiary, but only when all of these are true: diff --git a/docs/subnets/crowdloans/index.md b/docs/subnets/crowdloans/index.md index 2392537f7f..82e071d56a 100644 --- a/docs/subnets/crowdloans/index.md +++ b/docs/subnets/crowdloans/index.md @@ -4,16 +4,25 @@ title: "Crowdloans" ### Overview -The crowdloan feature lets a group of people collectively fund the registration of a new Bittensor subnet and share the resulting emissions according to each person’s contribution. Instead of a single sponsor paying the full lease cost up front, a creator opens a crowdloan with a funding cap and end block, contributors deposit funds until the cap is met, and—on success—the pallet finalizes the crowdloan by funding subnet registration and activating emissions for the group. +The crowdloan feature lets a group of people collectively fund an extrinsic execution or a balance transfer to a specific address. For example, it can be used to fund the registration of a new Bittensor subnet and share the resulting emissions according to each person's contribution. Instead of a single sponsor paying the full lease cost up front, a creator opens a crowdloan with a funding cap and end block, contributors deposit funds until the cap is met, and—on success—the pallet finalizes the crowdloan by funding subnet registration and activating emissions for the group. -At finalization, the system executes an on‑chain call—typically `subtensor::register_leased_network`—using the crowdloan’s funds. This registers the subnet and creates a dedicated proxy, `SubnetLeaseBeneficiary`, for the designated beneficiary. That proxy is authorized to operate the subnet (for example, configuring subnet parameters and other allowed controls) without having custody of contributor funds or emissions splits. +At finalization, the system executes an on‑chain call—typically `subtensor::register_leased_network`—using the crowdloan's funds. This registers the subnet and creates a dedicated proxy, `SubnetLeaseBeneficiary`, for the designated beneficiary (the crowdloan creator). That proxy is authorized to operate the subnet (for example, configuring subnet parameters and other allowed controls) without having custody of contributor funds or emissions splits. -While the lease is active, emissions flow to contributors pro‑rata based on their contributed share. If the crowdloan is not finalized after the end block, anyone can call refunds; once all contributors are refunded, the creator can dissolve the crowdloan. The call and target address specified at creation are immutable, ensuring that the purpose of the crowdloan cannot be changed mid‑campaign. This model makes subnet bootstrapping collaborative, transparent, and permissioned through a narrowly scoped proxy for safe, ongoing operations. +If the crowdloan is finalized and a lease is created, emissions flow to contributors pro‑rata based on their contributed share. If the crowdloan is not finalized after the end block, anyone can call refunds; once all contributors are refunded, the creator can dissolve the crowdloan. The call and target address specified at creation are immutable, ensuring that the purpose of the crowdloan cannot be changed mid‑campaign. This model makes subnet bootstrapping collaborative, transparent, and permissioned through a narrowly scoped proxy for safe, ongoing operations. +Design features: - Strong defaults: immutable target and call, capped funding, clear end block - Shared upside: emissions distributed proportionally to contributions - Safe operations: a dedicated proxy to manage the subnet within defined permissions +:::info +**Crowdloans** and **Leasing** are two different but related concepts: + +**Crowdloan** enables someone to fund some extrinsic execution or a balance transfer at some end date, with contributors able to participate to a cap. When finalized, this will execute the extrinsic (substrate defined logic where we can move the funds and do something else) or transfer the balance to an address (EVM smart contract address for example). + +**Leasing** is split profit ownership of a subnet. This is tightly coupled to Crowdloan because a lease can only be created through a Crowdloan where the crowdloan extrinsic will be the `register_leased_network`. The logic to create a lease uses the contributors from the crowdloan as the lease shareholders, and the crowdloan creator as the lease beneficiary. Parameters like the lease end block and lease emissions shares are defined when you create the crowdloan. +::: + See also [Create a Subnet with a Crowdloan](./crowdloans-tutorial.md) ## Crowdloan Lifecycle @@ -30,11 +39,16 @@ See also [Create a Subnet with a Crowdloan](./crowdloans-tutorial.md) - **Dissolve** after refunds; creator's deposit is returned and storage cleaned up. [Source code](https://github.com/opentensor/subtensor/blob/main/pallets/crowdloan/src/lib.rs#L711-L721) +- **Update** parameters while crowdloan is running (creator only): + - `update_min_contribution` - adjust minimum contribution amount + - `update_end` - extend the end block + - `update_cap` - adjust the funding cap + ## Emissions distribution during a lease -- When owner rewards are paid to a leased subnet, they are split into contributor dividends and a beneficiary cut. [Source code](https://github.com/opentensor/subtensor/blob/main/pallets/subtensor/src/coinbase/run_coinbase.rs#L450-L452) +- When owner rewards are paid to a leased subnet, they are split into contributor dividends and a beneficiary cut. [Source code](https://github.com/opentensor/subtensor/blob/81ee047fd124f8837555fd79e8a3957688c5b0c6/pallets/subtensor/src/subnets/leasing.rs#L250) -- Distribution is pro‑rata by recorded share; any remainder goes to the beneficiary. [Source code](https://github.com/opentensor/subtensor/blob/main/pallets/subtensor/src/subnets/leasing.rs#L324-L339) +- Distribution is pro‑rata by recorded share; any remainder goes to the beneficiary. A lease can be created with an emissions share from 0 to 100%, which determines the share distributed to contributors. For example, if the emissions share is 50%, it means that 50% of the owner cut (18% currently) so 9% will be split proportionally to their share to contributors. [Source code](https://github.com/opentensor/subtensor/blob/main/pallets/subtensor/src/subnets/leasing.rs#L324-L339) ## Operating the leased subnet via proxy @@ -56,7 +70,9 @@ Implications: ### What problem do crowdloans solve? -Crowdloans enable shared funding and ownership of subnets so that no single sponsor must front the entire lock cost. Emissions are shared among contributors while a designated beneficiary operates the subnet via a scoped proxy. +Crowdloans enable someone to fund some extrinsic execution or a balance transfer at some end date, with contributors able to participate to a cap. When finalized, this will execute the extrinsic (substrate defined logic where we can move the funds and do something else) or transfer the balance to an address (EVM smart contract address for example). + +Leasing is defined as split profit ownership of a subnet. This is tightly coupled to Crowdloan because a lease can only be created through a Crowdloan where the crowdloan extrinsic will be the "register_leased_network". The logic to create a lease will use the contributors from the crowdloan as the lease shareholders, the lease beneficiary will be crowdloan creator. Parameters like lease end block (some block in the future, probably much farther than crowdloan end) or lease emissions share are defined when you create the crowdloan and you pass the register_lease_network call with the parameters filled. ### How does the end‑to‑end flow work? @@ -76,13 +92,12 @@ Anyone can call `refund` to batch‑refund contributors (excluding the creator) ### How are emissions split during a lease? -Owner rewards are split to contributors by their recorded `SubnetLeaseShares`; any remainder goes to the beneficiary. This runs automatically during coinbase distribution. +Owner rewards are split to contributors by their recorded `SubnetLeaseShares`; any remainder goes to the beneficiary. The emissions are swapped for TAO and TAO is distributed to the contributors, not alpha. This runs automatically during coinbase distribution. ### What permissions does the beneficiary proxy have? They can invoke a curated set of calls (e.g., start subnet calls and selected admin‑utils setters like difficulty, weights, limits). - ### Can the campaign parameters be updated mid‑flight?