|
| 1 | +--- |
| 2 | +title: "Coinbase Implementation" |
| 3 | +--- |
| 4 | + |
| 5 | +# Coinbase Implementation |
| 6 | + |
| 7 | +This document provides a technical deep dive into the `run_coinbase()` function that orchestrates [TAO](../resources/glossary.md#tao-τ) and alpha [emission](../resources/glossary.md#emission) distribution across [subnets](../resources/glossary.md#subnet). The coinbase mechanism serves as Bittensor's economic heartbeat, connecting [subnet validators](../resources/glossary.md#validator), [subnet miners](../resources/glossary.md#subnet-miner), and [stakers](../resources/glossary.md#staking) through emission distribution. |
| 8 | + |
| 9 | +For conceptual understanding of emission mechanisms, see [Emissions](../learn/emissions.md). |
| 10 | + |
| 11 | +The coinbase mechanism orchestrates Bittensor's tokenomic engine, running every 12-second [block](../resources/glossary.md#block) to ensure continuous flow of liquidity into the network. |
| 12 | + |
| 13 | +Every block, the coinbase mechanism performs three critical functions: |
| 14 | + |
| 15 | +1. **Liquidity Injection**: Adds TAO and subnet-specific alpha tokens to each subnet's liquidity pools. |
| 16 | +2. **Accumulation**: Builds up pending [emissions](../resources/glossary.md#emission) (also known as "alpha outstanding") bound for distribution to [subnet miners](../resources/glossary.md#subnet-miner) and [validators](../resources/glossary.md#validator) during the next [epoch](../resources/glossary.md#tempo). |
| 17 | +3. **Consensus Triggering**: Initiates each subnet's [Yuma Consensus](../resources/glossary.md#yuma-consensus) epochs, the process that distributes emissions to participants within each subnet. Epochs are staggered to avoid overloading the blockchain with the computation involved. |
| 18 | + |
| 19 | +For broader conceptual understanding of emission mechanisms, see [Emissions](../learn/emissions.md). |
| 20 | + |
| 21 | +## Core Function: `run_coinbase()` |
| 22 | + |
| 23 | +**Location**: [`run_coinbase.rs`](https://github.com/opentensor/subtensor/blob/main/pallets/subtensor/src/coinbase/run_coinbase.rs) |
| 24 | + |
| 25 | +```rust |
| 26 | +pub fn run_coinbase(block_emission: U96F32) |
| 27 | +``` |
| 28 | + |
| 29 | +**Parameters**: |
| 30 | +- `block_emission`: Total TAO to distribute across all subnets this block. Currently 1 $\tau$, this amount will follow a halving schedule. |
| 31 | + |
| 32 | +The function implements a multistep process that handles liquidity injection, reward accumulation, epoch triggering, and EMA updates. |
| 33 | + |
| 34 | +## Implementation Flow |
| 35 | + |
| 36 | +### 1. Subnet Discovery and Filtering |
| 37 | + |
| 38 | +The process begins with identifying subnets eligible for emissions, applying filters to ensure only active, established subnets participate in the reward distribution. |
| 39 | + |
| 40 | +```rust |
| 41 | +// Get all netuids (filter out root) |
| 42 | +let subnets: Vec<NetUid> = Self::get_all_subnet_netuids() |
| 43 | + .into_iter() |
| 44 | + .filter(|netuid| *netuid != NetUid::ROOT) |
| 45 | + .collect(); |
| 46 | + |
| 47 | +// Filter out subnets with no first emission block number |
| 48 | +let subnets_to_emit_to: Vec<NetUid> = subnets |
| 49 | + .clone() |
| 50 | + .into_iter() |
| 51 | + .filter(|netuid| FirstEmissionBlockNumber::<T>::get(*netuid).is_some()) |
| 52 | + .collect(); |
| 53 | +``` |
| 54 | + |
| 55 | +**Subnet Eligibility Rules:** |
| 56 | +- **[Root Subnet](../resources/glossary.md#root-subnetsubnet-zero) Exclusion**: [Subnet Zero](../resources/glossary.md#root-subnetsubnet-zero) operates differently—it has no [subnet miners](../resources/glossary.md#subnet-miner) and serves as a TAO staking pool for [delegates](../resources/glossary.md#delegate), so it's excluded from direct alpha emissions |
| 57 | +- **Emission Readiness**: Only subnets that have been started (and hence been assigned a `FirstEmissionBlockNumber`) receive emissions. |
| 58 | +### 2. Emission Allocation to Subnets |
| 59 | + |
| 60 | +Each subnet's share of the block's TAO emission depends on its alpha token's price, smoothed with an [exponential moving average (EMA)](../learn/ema) function to prevent price manipulation while maintaining market responsiveness. |
| 61 | + |
| 62 | +```rust |
| 63 | +let mut total_moving_prices = U96F32::saturating_from_num(0.0); |
| 64 | +for netuid_i in subnets_to_emit_to.iter() { |
| 65 | + total_moving_prices = total_moving_prices |
| 66 | + .saturating_add(Self::get_moving_alpha_price(*netuid_i)); |
| 67 | +} |
| 68 | +``` |
| 69 | + |
| 70 | +**EMA Price Smoothing Implementation:** |
| 71 | +The moving price for each subnet is calculated using a custom [EMA](../learn/ema) that adapts its responsiveness based on subnet maturity. This creates a **double-smoothing effect**: new subnets have extremely slow price adaptation (preventing launch manipulation), while mature subnets respond more quickly to legitimate market signals. |
| 72 | + |
| 73 | +**Price-Driven Distribution:** |
| 74 | +Each subnet receives TAO emissions proportional to its EMA-smoothed alpha token price: |
| 75 | + |
| 76 | +$$ |
| 77 | +\text{tao\_allocation}_i = \text{block\_emission} \times \frac{\text{moving\_price}_i}{\sum_{j} \text{moving\_price}_j} |
| 78 | +$$ |
| 79 | + |
| 80 | +**EMA Update Timing:** The EMA is updated **after** being used for emission calculations in each `run_coinbase()` call ([Line 246](https://github.com/opentensor/subtensor/blob/main/pallets/subtensor/src/coinbase/run_coinbase.rs#L246)), ensuring that current block emissions are based on the previous block's smoothed prices while continuously updating the moving average for future calculations. |
| 81 | + |
| 82 | +### 3. Token Pool Injections and Emissions |
| 83 | + |
| 84 | +For each subnet, the coinbase calculates critical values that govern the subnet's token economics and determine how fresh liquidity flows into the system. |
| 85 | + |
| 86 | +#### TAO In (`tao_in`): Fresh Liquidity Injection |
| 87 | +- Represents new TAO flowing into the subnet's liquidity pool |
| 88 | +- Calculated from the subnet's proportional share of block emissions |
| 89 | +- May be reduced through the subsidy mechanism to maintain price stability |
| 90 | + |
| 91 | +#### Alpha In (`alpha_in`): Liquidity Pool Balance |
| 92 | +- Alpha tokens injected to maintain healthy AMM pool ratios |
| 93 | +- Ensures the TAO injection doesn't create excessive [slippage](../resources/glossary.md#slippage) for [stakers](../resources/glossary.md#staking) |
| 94 | +- Calculated as: `tao_in / current_price` during normal operations |
| 95 | + |
| 96 | +#### Alpha Out (`alpha_out`): Participant Emissions |
| 97 | +- Alpha tokens emitted for distribution to [miners](../resources/glossary.md#subnet-miner) and [subnet validators](../resources/glossary.md#validator) |
| 98 | +- Represents the subnet's emission budget for [incentives](../resources/glossary.md#incentives) and validator dividends |
| 99 | +- Forms the reward pool that will be processed during [epochs](../resources/glossary.md#tempo) |
| 100 | + |
| 101 | +#### Subsidy Mechanism |
| 102 | + |
| 103 | +When a subnet's alpha price falls below its expected emission proportion, the mechanism automatically intervenes to maintain market stability: |
| 104 | +1. **Price Neutral Injection**: Downscales both TAO and ALPHA injected to provide a price neutral injection |
| 105 | +2. **Market Making**: Uses the excess TAO for buying pressure on alpha tokens |
| 106 | + |
| 107 | +This encourages alpha prices to move towards their emission ratio, or to encourage the sum of prices to be at/above 1. |
| 108 | + |
| 109 | +```rust |
| 110 | +for netuid_i in subnets_to_emit_to.iter() { |
| 111 | + let price_i = T::SwapInterface::current_alpha_price((*netuid_i).into()); |
| 112 | + let moving_price_i: U96F32 = Self::get_moving_alpha_price(*netuid_i); |
| 113 | + |
| 114 | + let default_tao_in_i: U96F32 = block_emission |
| 115 | + .saturating_mul(moving_price_i) |
| 116 | + .checked_div(total_moving_prices) |
| 117 | + .unwrap_or(asfloat!(0.0)); |
| 118 | + |
| 119 | + let alpha_emission_i: U96F32 = asfloat!( |
| 120 | + Self::get_block_emission_for_issuance( |
| 121 | + Self::get_alpha_issuance(*netuid_i).into() |
| 122 | + ).unwrap_or(0) |
| 123 | + ); |
| 124 | + |
| 125 | + |
| 126 | + let tao_in_ratio: U96F32 = default_tao_in_i.safe_div_or( |
| 127 | + U96F32::saturating_from_num(block_emission), |
| 128 | + U96F32::saturating_from_num(0.0), |
| 129 | + ); |
| 130 | + |
| 131 | + if price_i < tao_in_ratio { |
| 132 | + tao_in_i = price_i.saturating_mul(block_emission); |
| 133 | + alpha_in_i = block_emission; |
| 134 | + let difference_tao: U96F32 = default_tao_in_i.saturating_sub(tao_in_i); |
| 135 | + |
| 136 | + let buy_swap_result = Self::swap_tao_for_alpha( |
| 137 | + *netuid_i, |
| 138 | + tou64!(difference_tao).into(), |
| 139 | + T::SwapInterface::max_price().into(), |
| 140 | + true, // skip fees |
| 141 | + ); |
| 142 | + } else { |
| 143 | + // Normal operation |
| 144 | + tao_in_i = default_tao_in_i; |
| 145 | + alpha_in_i = tao_in_i.safe_div_or(price_i, alpha_emission_i); |
| 146 | + } |
| 147 | +} |
| 148 | +``` |
| 149 | + |
| 150 | +### 4. Liquidity Pool Updates |
| 151 | + |
| 152 | +The coinbase updates each subnet's liquidity pools. |
| 153 | + |
| 154 | + |
| 155 | +**Critical State Updates:** |
| 156 | +- **`SubnetAlphaIn`**: Alpha reserves backing the AMM, enabling liquid [staking](../resources/glossary.md#staking) and unstaking operations. |
| 157 | +- **`SubnetAlphaOut`**: Accumulated emissions and/or ALPHA outside the pool (ALPHA emissions + ALPHA taken out from pool). |
| 158 | +- **`SubnetTAO`**: TAO reserves backing the AMM, providing price stability and liquidity for unstaking. |
| 159 | +- **`TotalIssuance`**: Global TAO supply (see [Issuance](../resources/glossary.md#issuance)). |
| 160 | + |
| 161 | +```rust |
| 162 | +for netuid_i in subnets_to_emit_to.iter() { |
| 163 | + // Inject Alpha in (AMM liquidity) |
| 164 | + let alpha_in_i = AlphaCurrency::from( |
| 165 | + tou64!(*alpha_in.get(netuid_i).unwrap_or(&asfloat!(0))) |
| 166 | + ); |
| 167 | + SubnetAlphaIn::<T>::mutate(*netuid_i, |total| { |
| 168 | + *total = total.saturating_add(alpha_in_i); |
| 169 | + }); |
| 170 | + |
| 171 | + // Inject Alpha outstanding |
| 172 | + let alpha_out_i = AlphaCurrency::from( |
| 173 | + tou64!(*alpha_out.get(netuid_i).unwrap_or(&asfloat!(0))) |
| 174 | + ); |
| 175 | + SubnetAlphaOut::<T>::mutate(*netuid_i, |total| { |
| 176 | + *total = total.saturating_add(alpha_out_i); |
| 177 | + }); |
| 178 | + |
| 179 | + // Inject TAO in (AMM liquidity) |
| 180 | + let tao_in_i: TaoCurrency = |
| 181 | + tou64!(*tao_in.get(netuid_i).unwrap_or(&asfloat!(0))).into(); |
| 182 | + SubnetTAO::<T>::mutate(*netuid_i, |total| { |
| 183 | + *total = total.saturating_add(tao_in_i.into()); |
| 184 | + }); |
| 185 | + |
| 186 | + // Update global TAO supply tracking |
| 187 | + TotalIssuance::<T>::mutate(|total| { |
| 188 | + *total = total.saturating_add(tao_in_i.into()); |
| 189 | + }); |
| 190 | + |
| 191 | + // Notify AMM of new liquidity |
| 192 | + T::SwapInterface::adjust_protocol_liquidity(*netuid_i, tao_in_i, alpha_in_i); |
| 193 | +} |
| 194 | +``` |
| 195 | + |
| 196 | +### 5. Subnet Owner Emissions |
| 197 | + |
| 198 | +Before distributing rewards to [miners](../resources/glossary.md#subnet-miner) and [subnet validators](../resources/glossary.md#validator), the system allocates a percentage to [subnet owners](../resources/glossary.md#subnet-creator). |
| 199 | + |
| 200 | +[Subnet owners](../resources/glossary.md#subnet-creator) receive 18% of alpha emissions. The subnet owner cut is calculated before other distributions so that the owner cut can be passed to `drain_pending_emission` (there was a bug before where the owner cut was incorrectly calculated after). Subnet owner emissions accumulate in `PendingOwnerCut` until the next [epoch](../resources/glossary.md#tempo). |
| 201 | + |
| 202 | + |
| 203 | +```rust |
| 204 | +let cut_percent: U96F32 = Self::get_float_subnet_owner_cut(); // Default: ~18% |
| 205 | +let mut owner_cuts: BTreeMap<NetUid, U96F32> = BTreeMap::new(); |
| 206 | + |
| 207 | +for netuid_i in subnets_to_emit_to.iter() { |
| 208 | + let alpha_out_i: U96F32 = *alpha_out.get(netuid_i).unwrap_or(&asfloat!(0)); |
| 209 | + let owner_cut_i: U96F32 = alpha_out_i.saturating_mul(cut_percent); |
| 210 | + |
| 211 | + owner_cuts.insert(*netuid_i, owner_cut_i); |
| 212 | + alpha_out.insert(*netuid_i, alpha_out_i.saturating_sub(owner_cut_i)); |
| 213 | + |
| 214 | + PendingOwnerCut::<T>::mutate(*netuid_i, |total| { |
| 215 | + *total = total.saturating_add(tou64!(owner_cut_i).into()); |
| 216 | + }); |
| 217 | +} |
| 218 | +``` |
| 219 | + |
| 220 | + |
| 221 | +### 6. Calculating Root Proportion |
| 222 | + |
| 223 | +The root proportion on each subnet determines how much of the dividends (41% of ALPHA emissions) are being sold for each block and being distributed to stakers on root. |
| 224 | + |
| 225 | +$$ |
| 226 | +\text{root\_proportion} = \frac{\text{root\_tao} \times \text{tao\_weight}}{\text{root\_tao} \times \text{tao\_weight} + \text{alpha\_issuance}} |
| 227 | +$$ |
| 228 | + |
| 229 | +Where: |
| 230 | +- `root_tao`: Total TAO [staked](../resources/glossary.md#staking) in [Root Subnet](../resources/glossary.md#root-subnetsubnet-zero) |
| 231 | +- `tao_weight`: Global parameter ([TAO Weight](../resources/glossary.md#tao-weight)) determining TAO vs alpha influence |
| 232 | +- `alpha_issuance`: Total alpha tokens for this specific subnet |
| 233 | + |
| 234 | + |
| 235 | +```rust |
| 236 | +for netuid_i in subnets_to_emit_to.iter() { |
| 237 | + let alpha_out_i: U96F32 = *alpha_out.get(netuid_i).unwrap_or(&asfloat!(0.0)); |
| 238 | + let root_tao: U96F32 = asfloat!(SubnetTAO::<T>::get(NetUid::ROOT)); |
| 239 | + let alpha_issuance: U96F32 = asfloat!(Self::get_alpha_issuance(*netuid_i)); |
| 240 | + let tao_weight: U96F32 = root_tao.saturating_mul(Self::get_tao_weight()); |
| 241 | + |
| 242 | + // Calculate root subnet's proportional share |
| 243 | + let root_proportion: U96F32 = tao_weight |
| 244 | + .checked_div(tao_weight.saturating_add(alpha_issuance)) |
| 245 | + .unwrap_or(asfloat!(0.0)); |
| 246 | + |
| 247 | + // 50% of proportional alpha goes to root validators |
| 248 | + let root_alpha: U96F32 = root_proportion |
| 249 | + .saturating_mul(alpha_out_i) |
| 250 | + .saturating_mul(asfloat!(0.5)); |
| 251 | + |
| 252 | + let pending_alpha: U96F32 = alpha_out_i.saturating_sub(root_alpha); |
| 253 | + |
| 254 | + // Convert root alpha to TAO through AMM (if not subsidized) |
| 255 | + // If the subnet is subsidized by the Subsidy Mechanism then no ALPHA will be sold - so the dividends for root are stopped. |
| 256 | + if !subsidized { |
| 257 | + let swap_result = Self::swap_alpha_for_tao( |
| 258 | + *netuid_i, |
| 259 | + tou64!(root_alpha).into(), |
| 260 | + T::SwapInterface::min_price().into(), |
| 261 | + true, // skip fees |
| 262 | + ); |
| 263 | + |
| 264 | + if let Ok(ok_result) = swap_result { |
| 265 | + PendingRootDivs::<T>::mutate(*netuid_i, |total| { |
| 266 | + *total = total.saturating_add(ok_result.amount_paid_out.into()); |
| 267 | + }); |
| 268 | + } |
| 269 | + } |
| 270 | + |
| 271 | + PendingEmission::<T>::mutate(*netuid_i, |total| { |
| 272 | + *total = total.saturating_add(tou64!(pending_alpha).into()); |
| 273 | + }); |
| 274 | +} |
| 275 | +``` |
| 276 | + |
| 277 | + |
| 278 | +### 7. Epoch Execution |
| 279 | + |
| 280 | +When each subnet's [tempo](../resources/glossary.md#tempo) interval completes, the coinbase triggers execution of its Yuma Consensus *epoch*. Epochs execute when `(block_number + netuid + 1) % (tempo + 1) == 0`, creating a predictable, staggered schedule of epoch execution. |
| 281 | + |
| 282 | +The coinbase passes accumulated emissions to `drain_pending_emission()`, which executes the [full Yuma Consensus algorithm](./epoch.md) including validator weight processing, consensus calculation, bond updates, and final emission distribution to participants. |
| 283 | + |
| 284 | +For detailed implementation of the consensus mechanism, validator weight processing, and emission distribution, see [Epoch Implementation](./epoch.md). |
| 285 | + |
| 286 | +```rust |
| 287 | +for &netuid in subnets.iter() { |
| 288 | + // Process matured commit-reveal weight submissions |
| 289 | + if let Err(e) = Self::reveal_crv3_commits(netuid) { |
| 290 | + log::warn!("Failed to reveal commits for subnet {netuid} due to error: {e:?}"); |
| 291 | + } |
| 292 | + |
| 293 | + if Self::should_run_epoch(netuid, current_block) { |
| 294 | + // Reset epoch timing and collect accumulated emissions |
| 295 | + BlocksSinceLastStep::<T>::insert(netuid, 0); |
| 296 | + LastMechansimStepBlock::<T>::insert(netuid, current_block); |
| 297 | + |
| 298 | + // Execute Yuma Consensus with accumulated rewards |
| 299 | + Self::drain_pending_emission(netuid, pending_alpha, pending_tao, pending_swapped, owner_cut); |
| 300 | + } else { |
| 301 | + BlocksSinceLastStep::<T>::mutate(netuid, |total| *total = total.saturating_add(1)); |
| 302 | + } |
| 303 | +} |
| 304 | +``` |
| 305 | + |
| 306 | + |
| 307 | + |
0 commit comments