fix(subtensor): price basket deposits in alpha space; re-enable stake_into_basket - #3043
fix(subtensor): price basket deposits in alpha space; re-enable stake_into_basket#3043unarbos wants to merge 1 commit into
Conversation
…_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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
| .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)) | ||
| }) |
There was a problem hiding this comment.
[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.
| .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)) | |
| }) |
🛡️ 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
ConclusionThe 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. |
|
🔄 AI review updated — Skeptic: VULNERABLE |
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
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.try_stake_into_basket):shares = P * min_i(bought_i / pre_alpha_i)over the mirrored holdings, via an exact flooringmul_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.stake_into_basketextrinsic (reverts the gate body from chore(subtensor): gate stake_into_basket pending state-growth review #3042) and restores its direct-path tests.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— cleancargo fmt --all -- --check— cleanNotes 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