Skip to content

fix(subtensor): price basket deposits in alpha space; re-enable stake_into_basket - #3043

Open
unarbos wants to merge 1 commit into
mainfrom
fix/basket-deposit-nav-physical-alpha
Open

fix(subtensor): price basket deposits in alpha space; re-enable stake_into_basket#3043
unarbos wants to merge 1 commit into
mainfrom
fix/basket-deposit-nav-physical-alpha

Conversation

@unarbos

@unarbos unarbos commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Completes the state-growth review of stake_into_basket (gated in #3042) and re-enables the call with a corrected deposit share-pricing model.

Redemption redeems a fraction of every fund holding (physical alpha), while the mint previously priced deposits against the fund's TAO/NAV mark. On a concave AMM curve those units disagree — a deposit's ΔNAV re-marks existing alpha up by exactly the buy's own slippage — so a deposit-then-claim round trip could settle for more than it deposited. This reworks the mint to price in the same physical-alpha space a claim redeems in.

Changes

  • Deposits mirror the fund once shares exist (do_stake_into_basket): TAO is deployed pro-rata across current value-bearing holdings, so a deposit buys exactly the pro-rata basket a later claim sells. The first deposit (no holders yet) still follows the curated weight vector, and an empty/uncurated fund still holds cash in the root slot.
  • Physical-alpha share mint (try_stake_into_basket): shares = P * min_i(bought_i / pre_alpha_i) over the mirrored holdings, via an exact flooring mul_div_u64. This bounds the redeemable fraction so a claim returns at most the alpha the deposit itself added; cross-slot slack is donated to existing holders. Par (first deposit) and drained-fund revival keep the NAV/par rule.
  • Re-enables the stake_into_basket extrinsic (reverts the gate body from chore(subtensor): gate stake_into_basket pending state-growth review #3042) and restores its direct-path tests.
  • spec_version 443 → 444.

Test plan

  • cargo test -p pallet-subtensor --lib stake_into_basket — 11 pass (9 existing + 2 new guards)
  • cargo test -p pallet-subtensor --lib claim_root — 50 pass (dividend/redemption path unchanged)
  • cargo clippy -p pallet-subtensor --lib --all-features -- -D warnings — clean
  • cargo fmt --all -- --check — clean
  • New round-trip guards verified to fail against the previous NAV mint (confirming they catch the regression) and pass with this change

Notes for reviewers

The two added tests (test_stake_into_basket_cannot_extract_from_existing_holder, test_stake_into_basket_large_deposit_cannot_extract) set up a fund holding a position on a thin pool and assert a deposit→claim round trip neither profits nor shrinks an existing holder's position — the core invariant this pricing change enforces.

Made with Cursor

…_into_basket

Completes the state-growth review of `stake_into_basket` (gated in #3042) and
re-enables the call with a corrected deposit share-pricing model.

Redemption redeems a fraction of every fund holding (physical alpha), while the
mint previously priced deposits against the fund's TAO/NAV mark. On a concave
AMM curve those units disagree: a deposit's ΔNAV re-marks existing alpha up by
exactly the buy's own slippage, so a deposit-then-claim round trip could settle
for more than it deposited. This reworks the mint to price in the same
physical-alpha space a claim redeems in:

- User deposits mirror the fund once shares exist: TAO is deployed pro-rata
  across current value-bearing holdings, so a deposit buys exactly the pro-rata
  basket a later claim sells. The first deposit (no holders yet) still follows
  the curated weight vector.
- Shares mint as `P * min_i(bought_i / pre_alpha_i)` over the mirrored holdings,
  computed with an exact flooring `mul_div_u64`. This bounds the redeemable
  fraction so a claim returns at most the alpha the deposit itself added; any
  cross-slot slack is donated to existing holders. Par (first deposit) and
  drained-fund revival keep the NAV/par rule.

Existing basket tests are unchanged in behavior; adds two round-trip guards
(small fractional deposit into a large discounted position, and a large deposit)
asserting a deposit->claim cannot profit and cannot shrink an existing holder's
position. Bumps spec_version 443 -> 444.

Co-authored-by: Cursor <cursoragent@cursor.com>
@vercel

vercel Bot commented Aug 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
subtensor Ready Ready Preview Aug 4, 2026 6:58pm

Request Review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

AI review — see the sticky summary comment for the verdict and the inline comments below for specific findings.

Comment on lines 399 to 402
.filter_map(|(netuid, alpha)| {
let value = Self::realizable_tao_for_alpha(netuid, alpha.to_u64());
(value > 0).then_some((netuid, value))
let value = Self::realizable_tao_for_alpha(*netuid, alpha.to_u64());
(value > 0).then_some((*netuid, value))
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[HIGH] Zero-valued holdings bypass the physical-alpha mint bound

Claims redeem a fraction of every nonzero holding, but this filter omits holdings whose current realizable quote is zero. The omitted slot is consequently absent from pre_alpha and cannot constrain the minted shares. A depositor can therefore receive an unfunded fraction of existing alpha when a pool is temporarily drained or its quote rounds to zero; if liquidity returns, that value is taken from prior holders. Include every nonzero holding in the mirrored vector (or reject deposits while any holding cannot be priced), so every redeemable slot participates in the physical-alpha bound.

Suggested change
.filter_map(|(netuid, alpha)| {
let value = Self::realizable_tao_for_alpha(netuid, alpha.to_u64());
(value > 0).then_some((netuid, value))
let value = Self::realizable_tao_for_alpha(*netuid, alpha.to_u64());
(value > 0).then_some((*netuid, value))
})
.map(|(netuid, alpha)| {
let value = Self::realizable_tao_for_alpha(*netuid, alpha.to_u64());
(*netuid, value.max(1))
})

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

🛡️ AI Review — Skeptic (security review)

VERDICT: VULNERABLE

MEDIUM scrutiny: account is under one year old, but has write access and extensive substantive merged history; no Gittensor association detected. Branch targets main.

The physical-alpha bound only covers holdings selected for deposit deployment. A nonzero holding whose current liquidation quote rounds to zero is excluded even though claims continue to redeem it.

Findings

Sev File Finding
HIGH pallets/subtensor/src/staking/claim_root.rs:402 Zero-valued holdings bypass the physical-alpha mint bound inline

Conclusion

The revised mint can grant depositors unfunded exposure to temporarily worthless holdings, diluting existing holders if those holdings regain value. This economic extraction path must be closed before re-enabling the extrinsic.


# 🔍 AI Review — Auditor (domain review) has not yet run on this PR.

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

🔄 AI review updated — Skeptic: VULNERABLE

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.

1 participant