Skip to content

feat: net TAO flow for emission allocation#4

Open
igoraxz wants to merge 3 commits into
devnet-readyfrom
feat/net-tao-flow
Open

feat: net TAO flow for emission allocation#4
igoraxz wants to merge 3 commits into
devnet-readyfrom
feat/net-tao-flow

Conversation

@igoraxz
Copy link
Copy Markdown
Owner

@igoraxz igoraxz commented May 3, 2026

Summary

Replace gross user flow ema(buys - sells) with net flow ema(buys - sells) - ema(emission + chain_buys - root_sells) for emission share computation.

Subnets only receive emissions when they generate positive net TAO inflow for the network.

Simulation results (30-day backtest):

  • Current gross flow: 55 subnets EMA > 0 → aggregate profit = −362 τ/day (network losing TAO)
  • Net flow: 33 subnets EMA > 0 → aggregate profit = +571 τ/day (network gaining TAO)

Rationale

Currently the network subsidizes subnets for gross TAO flow (buys − sells). This heavily misallocates incentives to subnets with negative net TAO flow for the network: e.g. a subnet gets 1 TAO in user buys → receives 4 TAO in protocol subsidy → net TAO flow ("subnet profit") is −3 TAO.

The fix is to change the emission allocator to maximize subnet TAO inflow net of protocol subsidy.

The net protocol cost to a subnet per block is:

net_subsidy = (emission_injected + chain_buys) - root_sells
  • Emission injects TAO into pool → network gives
  • Chain buys (excess TAO swapped for alpha) inject TAO → network gives
  • Root sells (root validators claiming dividends as TAO) remove TAO → network takes back

A subnet that receives 10 TAO emission but returns 3 TAO via root sells has an actual protocol cost of 7 TAO. It should only need to attract 7 TAO in user buys to be net-positive, not 10.

This update also addresses the runaway effect of chain buys — runaway emission cannot be sustained unless a subnet continuously attracts MORE external TAO than the protocol gives it in emission + chain buys.

Flow separation (no cross-contamination)

User flows and protocol flows are tracked in completely independent accumulators:

Flow Code path SubnetTaoFlow (user) SubnetProtocolFlow (protocol)
User buy (stake) stake_into_subnetrecord_tao_inflow ✅ (+)
User sell (unstake) unstake_from_subnetrecord_tao_outflow ✅ (−)
Emission injection adjust_protocol_liquidity (direct) ✅ (+)
Chain buys (excess) swap_tao_for_alpha (direct, not via stake) ✅ (+)
Root sells swap_alpha_for_tao (direct, not via unstake) ✅ (−)

Root sells are only recorded on the swap path (root validator claims TAO). If a root validator keeps alpha instead of swapping (Keep mode), no TAO leaves the pool and nothing is recorded. The recorded amount is amount_paid_out — the actual TAO that left the pool via AMM, not a theoretical value.

Block-level sequencing

Block N:
  User extrinsics → SubnetTaoFlow accumulates
  Step 4: run_coinbase
    ├─ SubnetRootSellTao::clear()           ← reset audit counter from N-1
    ├─ get_shares_flow()
    │   ├─ get_ema_flow()                   ← fold SubnetTaoFlow (from N-1), reset
    │   └─ get_ema_protocol_flow()          ← fold SubnetProtocolFlow (from N-1), reset
    │   └─ net_ema = user_ema - protocol_ema (if toggle on)
    └─ inject_and_maybe_swap()
        ├─ SubnetProtocolFlow += emission
        └─ SubnetProtocolFlow += excess
  Step 8: run_auto_claim_root_divs
    └─ SubnetProtocolFlow -= root_sell_tao
    └─ SubnetRootSellTao += root_sell_tao

→ Block N's accumulated protocol cost folded into EMA at block N+1
  (same one-block delay as user flows — consistent timing)

Changes (7 files, +127 lines)

New per-block storage (auditable base values):

  • SubnetExcessTao — excess TAO swapped (chain buys) per block
  • SubnetRootSellTao — TAO from root dividend sells per block

Protocol cost EMA:

  • SubnetProtocolFlow — per-block accumulator (emit + chain_buys − root_sells)
  • SubnetEmaProtocolFlow — EMA with same smoothing factor as user EMA

Share computation:

  • net_ema = ema(user_flow) - ema(protocol_cost) when toggle is on
  • Protocol EMA always computed regardless of toggle (stays warm for instant switching)

Sudo toggle:

  • NetTaoFlowEnabled (default: on) — sudo_set_net_tao_flow_enabled (call_index 91)
  • Set to false to instantly revert to gross flow behavior

Cleanup: all new storage items removed on subnet deregistration.

Test plan

  • Unit tests for get_ema_protocol_flow with known inputs
  • Verify get_shares_flow produces correct net shares when protocol EMA > 0
  • Verify toggle off reverts to gross flow behavior
  • Verify new storage items cleaned up on deregistration
  • Integration test: simulate emission + chain buys + root sells over multiple blocks, verify protocol EMA converges
  • Verify no regression on existing SubnetEmaTaoFlow behavior

🤖 Generated with Claude Code

// Record protocol costs for net-tao-flow EMA.
SubnetExcessTao::<T>::insert(*netuid_i, tao_to_swap_with);
Self::record_protocol_inflow(*netuid_i, injected_tao);
Self::record_protocol_inflow(*netuid_i, tao_to_swap_with);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this be more accurate as
Self::record_protocol_inflow(*netuid_i, buy_swap_result_ok.amount_paid_in); instead?

@igoraxz igoraxz force-pushed the feat/net-tao-flow branch 2 times, most recently from 0e16e0e to f8f89f6 Compare May 4, 2026 20:03
Replace gross user flow (buys - sells) with net flow (user flow - protocol cost)
for emission share computation. This ensures subnets only receive emissions when
they generate positive net TAO inflow for the network, preventing subsidized
extraction where a subnet receives more in protocol emissions than it attracts
in external capital.

New per-block storage (auditable base values):
- SubnetExcessTao: excess TAO swapped (chain buys) per block
- SubnetRootSellTao: TAO from root dividend sells per block

New protocol cost tracking:
- SubnetProtocolFlow: per-block accumulator (emit + chain_buys - root_sells)
- SubnetEmaProtocolFlow: EMA of protocol cost, same smoothing as user flow EMA

Emission share computation:
- net_ema = ema(user_flow) - ema(protocol_cost)
- Protocol EMA starts from zero and scales in over ~30 days
- Sudo toggle NetTaoFlowEnabled (default: on) to switch between net and gross flow

Sudo call: sudo_set_net_tao_flow_enabled (call_index 91)

Cleanup: all new storage items removed on subnet deregistration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@igoraxz igoraxz force-pushed the feat/net-tao-flow branch from f8f89f6 to ea465d0 Compare May 4, 2026 20:07
@igoraxz igoraxz changed the base branch from main to devnet-ready May 4, 2026 20:08
@igoraxz igoraxz force-pushed the feat/net-tao-flow branch from c531b0a to d67015b Compare May 13, 2026 16:27
#[allow(dead_code)]
fn get_shares_flow(subnets_to_emit_to: &[NetUid]) -> BTreeMap<NetUid, U64F64> {
let net_flow_enabled = NetTaoFlowEnabled::<T>::get();
let protocol_cost_discount = I64F64::saturating_from_num(ProtocolCostDiscount::<T>::get());
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should probably use U64F64 here

@igoraxz igoraxz force-pushed the feat/net-tao-flow branch 11 times, most recently from 7434139 to 260f015 Compare May 15, 2026 18:13
Problem: net flow (user - protocol) creates a feedback loop when used
for emission allocation. Fewer qualifying subnets → each gets more
emissions → higher protocol cost → even fewer qualify. In simulation
this collapses from 70 to 10 subsidized subnets over 41 days.

Solution: scale protocol EMA by α before subtracting, where α normalizes
the total positive protocol EMA to match total positive user EMA:

  α = min(1, Σ max(user_ema, 0) / Σ max(proto_ema, 0))
  net_flow = user_ema − α × proto_ema

α is computed each block from current EMA state. No tunable parameters.

Guarantee: the aggregate net flow across all gross-positive subnets
is always ≥ 0. Proof: Σ net_i over {user > 0} = Σ user_i − α × Σ proto_i.
Since Σ proto_i (over user > 0) ≤ sum_pos_proto, and α × sum_pos_proto
≤ sum_pos_user (by construction), the sum is non-negative. QED.

This converts the feedback loop into a self-correcting mechanism:
more concentration → α drops → softer deduction → rebalances.

Simulation result: 70 → 46 subnets (vs 10 without normalization).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@igoraxz igoraxz force-pushed the feat/net-tao-flow branch from 260f015 to 175a463 Compare May 15, 2026 18:27
Adds a second EMA layer on top of the existing user-flow EMA so
emission credit from buy spikes accrues slowly, while sell pressure
still debits the signal immediately:

  raw      = EMA_30d(buys - sells)          [existing]
  slow     = EMA_30d(raw)                   [new, stored separately]
  matured  = min(raw, slow)                 [read-time clamp, not stored]
  signal   = max(matured - α × proto, 0)    [used for emission shares]

Buy spike: raw rises before slow → matured = slow (delayed credit).
Sell spike: raw falls below slow → matured = raw (immediate debit).
Steady flow: raw ≈ slow → matured ≈ raw (no distortion).

Key implementation detail: SubnetEmaSlowTaoFlow stores EMA(raw),
not the clamped min(raw, slow). The clamp is applied at read time
only. This ensures the slow EMA tracks the true long-run raw signal
without being polluted by the clamp.

On first access for existing subnets, slow EMA initializes to the
current raw EMA to avoid an emission cliff at deployment.

Simulation: recycling attack (10k τ, 30d hold / 30d cool, 365d):
  Gross flow:       +73,023 τ  extra emissions captured
  Net normalized:   +27,562 τ  (2.6× lower than gross)
  Matured + net:     +8,238 τ  (8.9× lower than gross)

Uses the existing FlowEmaSmoothingFactor for both EMAs — no new
tunable parameters.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants