diff --git a/docs/derivatives/DESIGN.md b/docs/derivatives/DESIGN.md new file mode 100644 index 0000000000..3641e2621d --- /dev/null +++ b/docs/derivatives/DESIGN.md @@ -0,0 +1,430 @@ +# Covered continuous-unwind derivatives — subtensor design + +Implementation design for the **Fixed-Liability Covered Continuous-Unwind Model v3.6.1** +(`shorting.pdf`) inside `pallet-subtensor`. This document maps the spec onto the existing +runtime, fixes the reserve-accounting model against the real AMM, and locks the storage, +extrinsic, hook, and runtime-API surface. The companion `IMPLEMENTATION_PLAN.md` has the +phased file-by-file plan and diff estimate. + +Launch scope is **shorts-first**. The long side is now **fully implemented and wired** +(`open_long`/`close_long`/`default_long`, decay, dereg settlement, read/RPC layer) but stays +**flag-gated off** (`LongsEnabled=false`) until the long-side trading games pass; shorts enable +first (`ShortsEnabled`). Everything below reuses primitives that already exist. + +**Price-reference caveat (load-bearing).** All risk references and the terminal `K_EMA` leg are +built on `SubnetMovingPrice` (`pEMA`), which upstream updates as `EMA(min(spot, 1.0))` with a +~30-day half-life and is `0` at cold start. Two consequences the safety arguments depend on: +(1) `pEMA` is **capped at ~1.0 TAO/alpha** — for any subnet whose true price exceeds 1.0 the EMA +leg saturates, so the conservative-reference and anti-suppression guarantees hold **only while +price ≤ ~1.0** (true for every mainnet subnet today; max observed ≈0.018). (2) The slow half-life +is what makes the terminal anti-attack margin work and is therefore a **governance-tuned +invariant** — see §3.4. If upstream ever redefines the moving-price clamp or half-life, the +derivative risk math must be re-validated. + +**Warm-EMA open guard.** `do_open_short`/`do_open_long` reject (`ColdEmaNotAllowed`) when +`pEMA == 0` (freshly registered subnet, no price history), since there the EMA risk reference +falls back to the live reserve and the terminal `K_EMA` anti-suppression leg is unavailable. +Positions can only be opened once the EMA warms; a position opened warm that later goes cold is +still bounded at settlement by the cold-EMA `K_D ≥ R` floor (short) / `u64::MAX` cover sentinel +(long). + +**Spec upgrades required (intended deviations from v3.6.1 text).** The implementation deliberately +diverges from three literal spec formulas. In every case the divergence is toward a *more +conservative* realization against the live AMM that never under-charges an attacker. These are +**intended** — the code is the source of truth and the spec text should be **upgraded** to match; +they are not bugs: + +1. **Terminal `K_EMA` is a slippage-aware CPMM buyback, not the scalar `Q·pEMA`** (spec §11.4, + §15.5, Appendix A.6). A scalar understates the TAO cost of repurchasing a large `Q` from a + finite pool. The implementation prices `K_EMA` as the CPMM buyback `⌈t·q/(a−q)⌉` (u128, + ceiling-rounded) against the EMA-implied reserve `T_EMA = pEMA·A_live`, with a cold-EMA floor + `K_D ≥ R`. Consequence: the §15.5 worked example (`K_EMA = 66` for `Q = 3900`) **no longer + reproduces** for large `Q` — the realized CPMM cost is strictly higher. This strengthens the + anti-extraction margin (§3.4) and must be folded into the spec's settlement formula and example. + **Semantics (explicit):** terminal `K_D` is *derivative-internal CPMM accounting*, **not** a + live fee/weight-aware spot-swap simulation. It uses the constant-product buyback + `⌈pay·amt/(recv−amt)⌉` against the frozen terminal reserves, ceiling-rounded so it **never + under-charges** relative to constant-product. On a fee/weighted live pool the actual spot + close-cost would be ≥ this (fees add cost), so the CPMM accounting is conservative for the + trader's recovery in the no-fee case and must be re-derived if terminal settlement is ever + routed through the real fee/weight-aware swap engine. It is also **split-neutral**: priced once + on the aggregate liability `Q_Σ` (resp. `D_Σ`) and allocated pro-rata, so `Σ K_i ≥ K(Q_Σ)`. +2. **Restoration zap is a one-sided reserve credit, not the min-swap-plus-balanced-add** (spec + §6.5/§6.6). Net CPMM effect is equivalent for a single full-range position; on a fee/weighted + pool the two forms differ and the reconciliation is gated on the trading-games suite (§14.5). +3. **Close/terminal settlement zap is a one-sided pair of increments, not the balanced settlement + zap** (spec §8.5). Same rationale and gate as (2). + +Action: upgrade the v3.6.1 spec text (settlement formula §11.4/A.6, the §15.5 example, and the zap +definitions §6.6/§8.5) so the authoritative document matches the conservative implementation. + +**Fee-pool divergence (consequence of the one-sided reserve ops).** Because open/close/restore/ +terminal zaps are one-sided reserve mutations rather than fee-charging swap-engine calls, on a +**fee-charging** pool the derivative's realized close-cost, break-even, and terminal economics do +**not** include the pool swap fee. This is acceptable for the launch design (the math is priced and +realized one-sidedly), but it means quoted break-even ≠ the cost of an equivalent fee-paying spot +swap. If a future variant routes derivative legs through the fee-adjusted swap engine, break-even / +terminal quotes must be re-derived. Tracked as a pre-mainnet decision alongside the κ ramp. + +**Close is in-kind only (UX note).** `close_short` requires the trader to already hold/stake the +Alpha liability `Q` on the position hotkey (`SubnetAlphaOut ≥ ρQ`), and `close_long` requires the +TAO liability `D`. There is **no auto-buy close path** in the launch design: a trader without the +liability asset must acquire/stake it first (the protocol liability `Q`/`D` is the same regardless, +but the incremental market close cost can be lower if the trader already holds the asset — spec §1.6). +This is intentional and safe; clients must surface "you need `Q` Alpha (`D` TAO) to close". + +--- + +## 1. Reality check: what the spec assumes vs. what subtensor has + +The spec is written against a **pure no-fee CPMM** (`x·y=k`). Subtensor's pool is different, +and that single fact drives most of the design decisions. + +| Spec assumption | Subtensor reality | Consequence | +|---|---|---| +| Pure `x·y=k` | **Balancer-weighted** pool (`pallet_subtensor_swap`), weights in `SwapBalancer`, default 0.5/0.5 (CPMM-like only at init) | **Authoritative model (as implemented):** derivative lifecycle legs (open / close / restoration / terminal settlement) are realized as **one-sided protocol reserve mutations** using the spec's constant-product closed-forms — *not* fee/weight-aware `SwapHandler::swap` calls. Read-only quoting (`quote_*`, est-close-cost) may use `sim_swap`. Fee/weighted-pool divergence from the one-sided CPMM accounting is an **accepted launch approximation, gated by the trading-games suite** before any κ ramp; routing realization through the engine is a deferred option (§3.4, §14.6). | +| User can remove/add liquidity | User LP is **deprecated** (`add_liquidity`/`remove_liquidity` → `Error::Deprecated`) | The "remove-and-sell-back" open and the restoration/settlement zaps are realized as **protocol reserve mutations**, not user LP ops. | +| Reserves `T`, `A` | `SubnetTAO` (TAO, the quote reserve), `SubnetAlphaIn` (alpha pool reserve), `SubnetAlphaOut` (staked alpha outside the pool) | Short open/restore are mostly `SubnetTAO` mutations; close settlement touches `SubnetAlphaIn`. | +| `pEMA` price reference | **Already exists**: `SubnetMovingPrice` (per-block halving EMA, TAO/alpha) | Reuse directly as the spec's `pEMA`. No new TWAP, no new price EMA. | +| `T_EMA`, `A_EMA` reserve EMAs | **Do not exist** | Derive `T_EMA` from `SubnetMovingPrice × SubnetAlphaIn` instead of storing a new per-block reserve EMA (see §4). | +| Recycle floor `P`, extinguish liability | **Already exists**: `recycle_tao(coldkey, amount)`, `recycle_subnet_alpha`/`burn_subnet_alpha` | Reuse for default and terminal settlement. | +| Per-block decay/unwind step | **Net-new** | One O(1)-per-subnet call added to `block_step()`. | +| Subnet deregistration hook | **Already exists**: `do_dissolve_network` (`coinbase/root.rs`) | Insert terminal derivative settlement before `destroy_alpha_in_out_stakes`. | +| Derivative flow-neutral for emissions | **Free by construction** | We mutate reserves directly and never call `record_tao_inflow/outflow`, so TaoFlow is untouched (spec §4.5). | + +**Key takeaway:** the spec's `pEMA`, recycle, and dereg primitives already exist. The genuinely +new state is (a) the position store, (b) per-side aggregate + decay accumulator, (c) a per-block +decay step, (d) ~4 extrinsics, (e) one runtime-API quote. Risk reserve EMAs are *derived*, not stored. + +--- + +## 2. Notation map (spec symbol → subtensor identifier) + +| Spec | Meaning | Subtensor binding | +|---|---|---| +| `T` | live TAO reserve | `SubnetTAO::::get(netuid)` | +| `A` | live alpha reserve | `SubnetAlphaIn::::get(netuid)` | +| `T_ref` | conservative TAO ref `min(T_live, T_EMA)` | `min(SubnetTAO, pEMA·A_live)` — derived (§4) | +| `pEMA` | EMA price (TAO/alpha) | `Pallet::get_moving_alpha_price(netuid)` (`SubnetMovingPrice`) | +| `P` | user position input / floor | `ShortPosition.p_floor: TaoBalance` | +| `C` | gross collateral (open-time only) | computed, **not stored** | +| `N` | retained proceeds = `R0` | computed at open → `r_stored` | +| `R(t)` | retained buffer (decays) | `ShortPosition.r_stored` × decay factor | +| `Q` | fixed alpha liability | `ShortPosition.q_liability: AlphaBalance` | +| `E(t)` | linked TAO escrow (decays) | `ShortPosition.e_stored: TaoBalance` | +| `B` | utilization footprint `λC` (TAO) | `ShortPosition.b_stored: TaoBalance` | +| `S` | aggregate active footprint | `ShortAgg.b_sigma` | +| `Ω_S` | short decay accumulator | `ShortAgg.omega: U64F64` | +| `Ω_entry` | per-position accumulator snapshot | `ShortPosition.omega_entry: U64F64` | +| `λ`, `λ_eff` | base / effective LTV | governance param `ShortBaseLtv`; `λ_eff` computed | +| `κ_S` | short footprint cap factor | governance param `ShortKappa` | +| `d_min`,`d_max` | decay bounds | `DecayMin`, `DecayMax` | +| `R_dust` | dust threshold | `ShortDust` | +| `K_D(Q)` | terminal liability value | computed at dereg: `max(K_spot,last, K_EMA)`, both slippage-aware CPMM buybacks (`K_spot` on live reserves, `K_EMA` on `T_EMA=pEMA·A_live`); floored at retained buffer `R` when `pEMA==0` (cold-EMA guard) | + +--- + +## 3. Reserve-accounting model (the load-bearing part) + +All pool impact is expressed as **one-sided reserve mutations** to `SubnetTAO` / `SubnetAlphaIn` +using the spec constant-product closed-forms — **not** fee/weight-aware `SwapHandler::swap` calls: + +- `increase_provided_tao_reserve` / `decrease_provided_tao_reserve` +- `increase_provided_alpha_reserve` / `decrease_provided_alpha_reserve` +- terminal cover is the constant-product buyback `⌈t·q/(a−q)⌉` (`buyback_cost_rao`, u128/ceiling). + +`T::SwapInterface::sim_swap` is used **only by the read-only quote layer** (`quote_*`, +`est_close_cost`); no `swap` is executed on any lifecycle leg. This is the single authoritative +model (see the reality-check table and §A.6); the fee/weighted-pool divergence from one-sided CPMM +accounting is an accepted launch approximation gated by the trading-games suite. + +### 3.1 Open short — net pool effect + +The spec's remove-and-sell-back (§4.3) on a pure CPMM nets to: **alpha reserve unchanged, TAO +reserve drops by `N + E`**, leaving the trader owing `Q = ϕA` alpha. We realize that directly: + +``` +TAO removed from pool = N + E = ϕ(2-ϕ)·T // = T - (1-ϕ)²T on pure CPMM +SubnetTAO -= (N + E) // the downward price impact +held by protocol = E (escrow) + N (becomes buffer R0) +position liability = Q = ϕ·A (alpha debt, virtual; alpha reserve untouched at open) +``` + +`ϕ`, `N`, `Q`, `E` are computed from the spec closed-forms (Appendix A.1); the realized TAO leg is a +**one-sided reserve decrement** `SubnetTAO -= (N + E)` (the CPMM closed-form), not a fee-adjusted +engine swap. The trader supplies `P = C − N` TAO, held against the floor and recycle-on-default. + +### 3.2 Continuous restoration (per block) — net pool effect + +For a short the decayed amount `dU = dR + dE` is TAO-side. The spec zap (swap min portion to +alpha, re-add balanced) nets, on a CPMM, to **alpha unchanged, TAO `+= dU`, price drifts up** — +exactly reversing the open impact over time: + +``` +restoration_zap(netuid, dU) ≡ increase_provided_tao_reserve(netuid, dU) +``` + +No weight change is needed (we *want* the upward drift), so this is a single reserve increment. +This conserves TAO: the `N + E` removed at open is returned over the position's life. (If +simulation later shows the weighted pool needs the explicit min-swap, swap `z = √(T(T+U)) − T` +via the engine then add the remainder — spec §6.6 — behind the same `restoration_zap` fn.) + +### 3.3 Close (partial fraction ρ, full = ρ=1) — net pool effect + +Trader repays `ρQ` alpha; protocol pairs it with the escrow slice `ρE` via the settlement zap +(§8.5), realized as **one-sided reserve increments**: `SubnetAlphaIn += ρQ`, `SubnetTAO += ρE` +(not an engine min-swap). Trader receives `ρ(P + R)` back. Position `P, Q, R, E, B` reduced +pro-rata; aggregates updated. + +### 3.4 Default (R ≤ R_dust) and terminal dereg + +- **Default:** restore residual `R + E` (restoration zap), `recycle_tao(coldkey, P)` for the floor, + extinguish `Q` (no alpha moves — it was virtual), drop position from aggregates. +- **Dereg terminal:** value liability at `K_D(Q) = max(K_spot,last(Q), K_EMA(Q))`, where both legs + are slippage-aware CPMM buybacks (`⌈t·q/(a−q)⌉` in u128 with ceiling rounding) — `K_spot` on live + reserves, `K_EMA` on the EMA-implied reserve `T_EMA = pEMA·A_live`. A scalar `Q·pEMA` is **not** + used (it understates the cost of a large `Q`). When `pEMA==0` (cold subnet, no slow reference) the + short floors `K_D ≥ R` so equity ≤ floor `P` (no pool-origin buffer is refunded); the long is + naturally safe (the cold leg hits the un-buyable sentinel → cover = collateral). equity = + `max(0, (P+R) − K_D)` paid to trader; `min(P+R, K_D)` recycled outside terminal distribution; `Q` + extinguished. Hooked into `do_dissolve_network` before `destroy_alpha_in_out_stakes`. +- **Settlement model — immediate sweep, no protocol cap on position count.** Derivative + terminal settlement follows the **same immediate enumerate-and-settle model as native + subnet alpha liquidation** (`destroy_alpha_in_out_stakes`): on deregistration every open + position on the subnet is enumerated and settled in one pass (largest-remainder-free, + split-neutral pro-rata cover, equity credited directly). The open-position count is + **not protocol-capped** — there is no `MAX_POSITIONS_CEILING` and no governance + `Short/LongMaxPositions` limit. Settlement work is bounded *operationally* by the + economic cost of opening a position (floor `P` + escrow, the `κ` capacity limit on + aggregate liability, and the min-input floor), exactly the way native liquidation relies + on the cost of acquiring stake. This is architecturally consistent with existing subnet + dereg (immediate holder sweep + direct coldkey crediting); it does not introduce a new + unbounded-work class. The settlement `WeightInfo` is benchmarked over a calibration range + of `p ∈ [0,1024]` and charged at the *actual* per-subnet position count — `[0,1024]` is a + benchmark calibration range, **not** a consensus-enforced cap. + **Governance invariant — the price EMA must be SLOW (short-dereg / whale-extortion defense).** + The terminal `K_EMA` leg is the *only* thing that keeps a short's dereg payout bounded when an + attacker — or a whale extorting a subnet — deliberately drives the subnet toward deregistration. + Because `K_D = max(K_spot, K_EMA)`, a *fast* EMA would track the attacker's crashed spot downward, + collapse `K_D`, and hand the attacker a cheap terminal buyback (a free short-to-dereg extraction). + The `SubnetMovingPrice` half-life must therefore be **slow relative to the realistic time to force + a dereg**, so that over the whole suppression window: + + Σ carry paid (decay on R+E, every block) ≥ bounded terminal equity max(0, (P+R) − K_D) + + holds with margin. Mechanism: while spot is suppressed, a slow `K_EMA` stays near the *pre-attack* + price, so `K_D` stays high and terminal equity stays ≈ 0 (verified on-chain — a pre-dereg spot + crash paid equity = 0), while the attacker keeps paying utilization carry on `R + E` every block + for the entire window. Tune the EMA half-life together with `κ` (which bounds how far one short can + move price — short impact *saturates* near `1 − √(1 − δ)`, so a single position cannot crash spot + to zero; see §A.3) so that a short-driven deregistration is **never net-profitable**. A short / + fast half-life **breaks this guarantee and must not be set**; if upstream shortens the moving-price + half-life, the short-dereg margin must be re-validated before `κ` is ramped. + +### 3.5 Conservation invariant (must be a test) + +Over any position lifecycle, total TAO returned to `SubnetTAO` via restoration + close-settlement + +default-restore, plus recycled floor/liability-cover, **equals** the `N + E` removed at open plus the +`P` the trader posted, minus equity paid out. This invariant is the acceptance gate for the +reserve math and is the first item in the spec's trading-games suite (§14.5). + +> **Primary implementation risk:** reconciling the spec's CPMM closed-forms with the Balancer +> weights. Mitigation: quote/size from closed-forms and **realize as one-sided reserve mutations** +> (CPMM-internal accounting — the authoritative model above, *not* fee/weight-aware engine swaps), +> gate launch on the conservation + capacity simulations the spec already mandates (§14.5). `κ_S` +> starts tiny. + +--- + +## 4. Risk reference reserves without new EMA storage + +The spec wants `T_ref = min(T_live, T_EMA)` to stop a same-block reserve pump from improving open +terms (§3.1–3.2). Subtensor has no reserve EMA, but it has an EMA *price*. Since +`price = (w_base/w_quote)·(T/A)`, we reconstruct: + +``` +T_EMA ≈ pEMA · A_live (pEMA already folds the weight ratio at EMA time) +T_ref = min(SubnetTAO, T_EMA) +``` + +This reuses `SubnetMovingPrice` and adds **zero** per-block EMA maintenance. `A_live` is still +manipulable, but with `κ_S` starting conservative and the footprint cap `S + B ≤ κ_S·T_ref`, the +launch exposure is bounded; a dedicated stored reserve-EMA can be added later if the trading games +show it is needed. Decay utilization uses the same `T_ref` (spec §3.3), so flash trades cannot grief +carry either. + +--- + +## 5. Storage layout + +New module `pallets/subtensor/src/derivatives/`. Storage declared inline in `lib.rs` (the repo's +convention — there is no storage macro file). `#[pallet::without_storage_info]` is already set, so +`MaxEncodedLen` is not required. + +### 5.1 Position struct + +One **merged** short position per `(coldkey, netuid)` — additional same-side opens merge after +materialization (spec §8.6), which keeps the store sparse and avoids a position-id index. + +```rust +#[freeze_struct("")] +#[derive(Encode, Decode, DecodeWithMemTracking, TypeInfo, Clone, PartialEq, Eq, Debug)] +pub struct ShortPosition { + pub p_floor: TaoBalance, // non-decaying floor (spec P) + pub q_liability: AlphaBalance,// fixed alpha debt (spec Q) + pub r_stored: TaoBalance, // buffer at last materialization (spec R) + pub e_stored: TaoBalance, // escrow at last materialization (spec E) + pub b_stored: TaoBalance, // footprint at last materialization (spec B) + pub omega_entry: U64F64, // Ω_S snapshot at last materialization + pub opened_at: u64, // block, for UX/telemetry only +} +``` + +```rust +// --- DMAP (netuid, coldkey) -> ShortPosition +#[pallet::storage] +pub type ShortPositions = StorageDoubleMap< + _, Identity, NetUid, Blake2_128Concat, T::AccountId, ShortPosition, OptionQuery>; +``` + +### 5.2 Per-subnet aggregate + decay accumulator + +```rust +#[freeze_struct("")] +#[derive(Encode, Decode, DecodeWithMemTracking, TypeInfo, Clone, PartialEq, Eq, Debug, Default)] +pub struct ShortAgg { + pub r_sigma: TaoBalance, // Σ current R + pub e_sigma: TaoBalance, // Σ current E + pub b_sigma: TaoBalance, // Σ current B == active footprint S + pub q_sigma: AlphaBalance, // Σ fixed liability (open interest) + pub omega: U64F64, // Ω_S cumulative decay accumulator +} + +#[pallet::storage] +pub type ShortAggregate = + StorageMap<_, Identity, NetUid, ShortAgg, ValueQuery, DefaultShortAgg>; +``` + +Materialization (spec §6.3): `f = exp(-(Ω - Ω_entry))`, multiply `r,e,b` by `f`, snapshot `Ω_entry = Ω`. +Aggregate tick is O(1) per subnet: `R,E,B *= g`, `Ω += -ln g` (spec §6.4). + +### 5.3 Governance parameters (global defaults; per-subnet override optional later) + +Stored as `StorageValue` with `#[pallet::type_value]` defaults; setters in `utils/misc.rs`; exposed +via `pallet-admin-utils` sudo/owner extrinsics (existing pattern). + +| Storage | Type | Default | Spec | +|---|---|---|---| +| `ShortsEnabled` | `bool` | `false` (flip on after games) | §14.1 | +| `LongsEnabled` | `bool` | `false` | §9.3 | +| `ShortBaseLtv` | `U64F64` | `0.50` | §14.1 | +| `ShortKappa` | `U64F64` | small, conservative | §5.1 | +| `DecayMin` | `U64F64` | `0.001`/day | §6.2 | +| `DecayMax` | `U64F64` | `0.015`/day | §6.2 | +| `ShortDust` | `TaoBalance` | `1 TAO` | §7.2 | + +No migration is required: new maps default cleanly; only `ShortsEnabled` flips via governance. + +--- + +## 6. Per-block decay step + +Add `Self::run_derivatives_decay()` to `block_step()` **after** `run_coinbase(...)` and **before** +`update_moving_prices()` (so decay sees post-emission reserves but feeds the same block's price EMA). +For each subnet with `ShortAggregate.b_sigma > 0`: + +``` +u = min(1, b_sigma / (ShortKappa · T_ref)) // EMA-smoothed via T_ref +d_day = DecayMin + (DecayMax - DecayMin)·u² +g = (1 - d_day)^(1 block / blocks_per_day) // const per-block factor +dR = r_sigma·(1-g); dE = e_sigma·(1-g); dB = b_sigma·(1-g) +r_sigma,e_sigma,b_sigma *= g; omega += -ln g +restoration_zap(netuid, dR + dE) // SubnetTAO += dR+dE +``` + +O(1) per active subnet, no per-position iteration. `(1-d_day)^(1/blocks_per_day)` is computed with +the existing `substrate_fixed` helpers; `blocks_per_day ≈ 7200`. + +**Defaults are lazy.** Because the tick never visits individual positions, a position that has decayed +below `R_dust` is settled (a) on its owner's next interaction (materialize → if dust, default), or +(b) by a permissionless `default_short(coldkey, netuid)` poke. This keeps the block hook O(1) and +matches the spec's MEV-insensitive, time-based default (§7.1, §7.4). + +--- + +## 7. Extrinsics (shorts launch) + +Thin dispatch wrappers in `macros/dispatches.rs` → `do_*` in `derivatives/`. Next free +`call_index` is **139**. + +| call_index | Extrinsic | Delegates to | Notes | +|---|---|---|---| +| 139 | `open_short(netuid, hotkey, position_input: TaoBalance, max_alpha_liability: AlphaBalance)` | `do_open_short` | gated by `ShortsEnabled`; solves `C,N,ϕ,Q,E`; rejects `SlippageTooHigh` if live-derived `Q > max_alpha_liability` (caller-signed bound, `MAX` opts out); capacity + domain checks; merges into existing position | +| 140 | `top_up_short(netuid, amount: TaoBalance)` | `do_top_up_short` | adds to `R` only (spec §8.2); fresh decaying capital | +| 141 | `close_short(netuid, fraction_ppb: u64)` | `do_close_short` | `ρ = fraction_ppb/1e9`; partial (`ρ<1`) and full (`ρ=1`); repays `ρQ`, returns `ρ(P+R)` (close is deterministic given the materialized position — no execution bound needed) | +| 142 | `default_short(coldkey, netuid)` | `do_default_short` | permissionless; only valid when materialized `R ≤ R_dust` | + +`hotkey` is carried so the position is associated with a `(hotkey, coldkey, netuid)` identity +consistent with the rest of staking, even though the merged position is keyed `(netuid, coldkey)`. +Long extrinsics are **not** added at launch (gated by spec §9; adding them later is symmetric). + +Weights: start with inline `DbWeight::get().reads_writes(r, w)` placeholders (an accepted in-repo +pattern), benchmark before mainnet. + +--- + +## 8. Events & errors + +**Events** (`macros/events.rs`): `ShortOpened { netuid, coldkey, p, n, q, e, phi }`, +`ShortToppedUp`, `ShortClosed { netuid, coldkey, fraction, repaid_q, returned }`, +`ShortDefaulted`, `ShortTerminalSettled { netuid, coldkey, equity, liability_cover }`. + +**Errors** (`macros/errors.rs`): `ShortsDisabled`, `ShortPositionNotFound`, +`EffectiveLtvNonPositive` (`λ_eff ≤ 0`), `RetainedProceedsNonPositive` (`N ≤ 0`), +`ShortCapacityExceeded` (`S + B > κ_S·T_ref`), `ReserveDomainExceeded` (`4N > T_live`), +`PositionNotDefaultEligible`, `SubnetNotDynamic` (mechanism ≠ 1 / root). + +--- + +## 9. Runtime API (read-only quote) + +Extend `runtime-api/src/lib.rs` + `rpc_info/` + `impl_runtime_apis!` (runtime/src/lib.rs). + +```rust +fn quote_open_short(netuid: NetUid, position_input: TaoBalance) -> ShortOpenQuote; +fn get_short_position(coldkey: AccountId32, netuid: NetUid) -> Option; +``` + +`ShortOpenQuote` carries the spec's pre-open trader view (§1.2): `c, n, q, e, phi, lambda_eff, +daily_decay, min/max_time_to_dust, est_close_cost (via sim_swap GetAlphaForTao for Q), +breakeven_close_price`. Pure reads + `sim_swap`; no state change. JSON-RPC wrapper is optional. + +--- + +## 10. Invariants enforced (spec §17) + +1. Shorts-first: `open_short` rejects unless `ShortsEnabled`; longs gated. +2. Covered: `P + N = C` at open. +3. No liquid proceeds: `N` is never paid out; it becomes `R0`. +4. Fixed liability: `Q` changes only on close / default / dereg. +5. Continuous unwind: `R,E,B` decay with one `g`; restored via `restoration_zap`. +6. No price-based liquidation: default iff `R ≤ R_dust`. +7. Limited recourse: residual `Q` extinguished at default/dereg. +8. Footprint cap: `S + B ≤ κ_S·T_ref` (also bounds same-block stacked opens via progressive `S`). +9. Flow neutrality: no `record_tao_*` calls on any derivative leg. +10. Dereg awareness: terminal alpha base read from subnet mode (legacy vs new, per `destroy_alpha_in_out_stakes` rules). +11. Terminal short settlement: `K_D(Q) = max(K_spot,last, K_EMA)` (both slippage-aware CPMM buybacks; cold-EMA floor `K_D ≥ R`). +12. Escrow bound: `E/R = 1/(1−ϕ)` stays bounded by `κ_S`-implied `ϕ_cap`, so dust default is MEV-trivial. + +--- + +## 11. Explicit deferrals (faithful to spec) + +- **Longs**: code-symmetric but flag-gated off (`LongsEnabled=false`). Long open mirrors with + alpha/TAO swapped, `D=ϕT`, ADR-adjusted LTV (§9.2). Not in the launch diff. +- **Derivative TaoFlow** (`χ_S`): off; flow-neutral (§4.5). Not wired. +- **Stored reserve EMA / TWAP**: replaced by derived `T_ref` from `pEMA` (§4). TWAP is an optional + later guard only (§3.4, §11.4). +- **Per-open `ϕ_max`**: not a control; only the `4N ≤ T_live` domain bound is enforced (§5.2). +- **Per-subnet param overrides**: launch uses globals; per-netuid maps can be added later without + touching call sites. diff --git a/docs/derivatives/IMPLEMENTATION_PLAN.md b/docs/derivatives/IMPLEMENTATION_PLAN.md new file mode 100644 index 0000000000..0de9b57308 --- /dev/null +++ b/docs/derivatives/IMPLEMENTATION_PLAN.md @@ -0,0 +1,180 @@ +# Implementation plan — covered continuous-unwind shorts + +Companion to `DESIGN.md`. Goal: land the spec's **shorts-first** launch with the smallest faithful +diff, reusing `SubnetMovingPrice` (pEMA), the swap engine (fees/weights), recycle, and the dereg +hook. Long paths are written symmetric but flag-gated off. + +Code is **not** written here — this is the build order, the exact files to touch, and the test/gate +plan. + +--- + +## Phase 0 — scaffolding (no behavior change) + +| File | Change | ~LOC | +|---|---|---| +| `pallets/subtensor/src/derivatives/mod.rs` | **new** module tree: `pub mod short; pub mod decay; pub mod settle; pub mod types;` | 10 | +| `pallets/subtensor/src/lib.rs` | `pub mod derivatives;`; add storage items (`ShortPositions`, `ShortAggregate`, governance `StorageValue`s + `type_value` defaults) | ~70 | +| `pallets/subtensor/src/derivatives/types.rs` | `ShortPosition`, `ShortAgg` structs (+ `freeze_struct`, derives) | ~40 | + +No migration (new empty maps default cleanly; confirmed against repo convention). No +`STORAGE_VERSION` bump. + +--- + +## Phase 1 — math core (pure, unit-testable, no extrinsics) + +All in `derivatives/short.rs` as `impl Pallet` helpers. These are the spec +closed-forms (Appendix A.1) used for quoting/sizing. + +| Function | Spec | Returns | +|---|---|---| +| `short_t_ref(netuid)` | §3.1, §4 | `min(SubnetTAO, pEMA·A_live)` | +| `solve_collateral(p, t_ref, lambda, s)` | §4.2 | `(C, N)` via quadratic; reject `N ≤ 0` | +| `lambda_eff(...)` | §4.1 | effective LTV; reject `≤ 0` | +| `solve_phi(n, t_live)` | §4.3 | `ϕ = (1 − √(1 − 4N/T))/2`; reject `4N > T` | +| `decay_factor_g(u)` | §6.2 | per-block `g` from `d_day(u)` | +| `materialize(pos, agg)` | §6.3 | `f = exp(-(Ω−Ω_entry))`, scale `r,e,b` | + +Each gets a focused unit test asserting the spec's worked examples (§1.7–1.8: `C=100`, `N=37.5`, +`ϕ≈0.039`, `Q≈3900`, `E=39`). + +--- + +## Phase 2 — reserve legs (the risky part, isolate + test) + +`derivatives/settle.rs`: the three pool-touching primitives, each a thin wrapper over existing +reserve helpers + the swap engine. + +| Function | Net reserve effect | Built from | +|---|---|---| +| `open_remove_sell_back(netuid, n, e, q)` | `SubnetTAO -= (N+E)`; book `Q` debt | `decrease_provided_tao_reserve`; engine quote to confirm realized `N` | +| `restoration_zap(netuid, dU)` | `SubnetTAO += dU` (price drifts up) | `increase_provided_tao_reserve` (escalate to min-swap form only if sim demands) | +| `settlement_zap(netuid, alpha_in, tao_in)` | balanced add of repaid `Q` + escrow | engine min-swap (§8.5) + `increase_provided_*` | + +**Gate for this phase:** the §3.5 conservation test — open → N blocks of decay → close (or default) +returns exactly the TAO removed plus posted floor, minus equity. Run on a Balancer pool with +non-default weights, not just 0.5/0.5. + +--- + +## Phase 3 — extrinsics + +| File | Change | ~LOC | +|---|---|---| +| `derivatives/short.rs` | `do_open_short`, `do_top_up_short`, `do_close_short`, `do_default_short` (ensure_signed, validate, materialize, mutate, emit) | ~220 | +| `macros/dispatches.rs` | 4 thin wrappers, `call_index` 139–142, placeholder `DbWeight` weights | ~50 | +| `macros/events.rs` | 5 event variants | ~25 | +| `macros/errors.rs` | ~8 error variants | ~12 | + +Validation order in `do_open_short` (spec §8.1): side flag → `SubnetMechanism==1` → solve `C,N` → +reject `N≤0` / `4N>T` / `S+B>κ_S·T_ref` → solve `ϕ,Q,E` → realize legs → store/merge → bump +aggregate. Same-block stacked opens read the progressively updated `b_sigma` (spec §5.2.1) for free, +because each open re-reads `ShortAggregate`. + +--- + +## Phase 4 — per-block decay hook + +| File | Change | ~LOC | +|---|---|---| +| `derivatives/decay.rs` | `run_derivatives_decay()` — iterate subnets with `b_sigma>0`, O(1) tick each (§6.4), call `restoration_zap` | ~70 | +| `coinbase/block_step.rs` | one call after `run_coinbase`, before `update_moving_prices` | ~2 | + +--- + +## Phase 5 — terminal dereg settlement + +| File | Change | ~LOC | +|---|---|---| +| `derivatives/settle.rs` | `settle_shorts_on_dereg(netuid)` — for each short: materialize, `K_D=max(K_spot,last, K_EMA)` (slippage-aware CPMM buyback, not scalar `Q·pEMA`), pay `equity`, `recycle_tao(liability_cover)`, extinguish `Q`, clear | ~90 | +| `coinbase/root.rs` (`do_dissolve_network`) | call `settle_shorts_on_dereg(netuid)` before `destroy_alpha_in_out_stakes` | ~2 | + +`K_spot,last(Q)` = `sim_swap(GetAlphaForTao, …)` cost to buy `Q` at the final executable state; +`pEMA` = `get_moving_alpha_price`. Buckets stay disjoint (liability-cover recycled outside terminal +distribution — same rule as default), so no terminal fixed-point (spec §11.3). + +--- + +## Phase 6 — runtime API + +| File | Change | ~LOC | +|---|---|---| +| `rpc_info/derivatives_info.rs` | **new** `ShortOpenQuote`, `ShortPositionInfo` DTOs + `quote_open_short`, `get_short_position` | ~110 | +| `rpc_info/mod.rs` | `pub mod derivatives_info;` | 1 | +| `runtime-api/src/lib.rs` | new trait `DerivativesRuntimeApi` (2 methods) + DTO imports | ~20 | +| `runtime/src/lib.rs` | `impl DerivativesRuntimeApi for Runtime` in `impl_runtime_apis!` | ~12 | + +JSON-RPC (`pallets/subtensor/rpc`, `node/src/rpc.rs`) only if external clients need it — deferred. + +--- + +## Phase 7 — governance wiring + +| File | Change | ~LOC | +|---|---|---| +| `utils/misc.rs` | `set_*` for each param (put + event), `get_*` readers | ~60 | +| `admin-utils/src/lib.rs` | sudo/owner extrinsics: `sudo_set_shorts_enabled`, `…_short_kappa`, `…_short_base_ltv`, `…_decay_bounds`, `…_short_dust` | ~90 | + +`ShortsEnabled` stays `false` until the trading-games gate passes. + +--- + +## Phase 8 — tests & trading-games gate (spec §14.5) + +`pallets/subtensor/src/tests/derivatives.rs` (+ eco-tests for adversarial sims). The spec makes these +the launch gate, not optional: + +1. **Conservation** (§3.5) on weighted pools. +2. **Same-block stacked opens** cannot bypass `S+B ≤ κ_S·T_ref` (§5.2.1). +3. **Worked examples** (§1.7–1.8, §15) reproduce exactly. +4. **Dust/escrow bound** `E/R ≤ 1/(1−ϕ_cap)` holds through top-ups/partials (§7.3). +5. **Short-driven dereg**: no free terminal extraction; payout bounded by `K_D(Q)` (§10.7). +6. **Flow neutrality**: assert `SubnetTaoFlow` unchanged across every derivative leg (§4.5). +7. **Decay schedule**: 365-day remaining-fraction table (§14.3) within tolerance. + +Only after 1–7 pass on a mainnet-like replica does governance flip `ShortsEnabled` and begin ramping +`κ_S` (spec §5.1, §14.6). + +--- + +## Diff estimate + +| Area | Files touched | New files | ~LOC | +|---|---|---|---| +| Storage + types | `lib.rs` | `derivatives/{mod,types}.rs` | ~120 | +| Math core | — | `derivatives/short.rs` (part) | ~120 | +| Reserve legs | — | `derivatives/settle.rs` (part) | ~140 | +| Extrinsics + FRAME surface | `dispatches.rs`, `events.rs`, `errors.rs` | — | ~90 | +| Decay hook | `coinbase/block_step.rs` | `derivatives/decay.rs` | ~72 | +| Dereg hook | `coinbase/root.rs` | — | ~92 | +| Runtime API | `runtime-api/src/lib.rs`, `runtime/src/lib.rs`, `rpc_info/mod.rs` | `rpc_info/derivatives_info.rs` | ~143 | +| Governance | `utils/misc.rs`, `admin-utils/src/lib.rs` | — | ~150 | +| **Total (excl. tests)** | **~10 edited** | **~6 new** | **~1,000** | + +No on-chain migration. No `STORAGE_VERSION` bump. Reuses pEMA, swap engine, recycle, and dereg +plumbing rather than re-implementing them — which is where the line-count is kept down. + +--- + +## Build / sanity commands + +```bash +# compile the pallet only (fast loop) +cargo check -p pallet-subtensor + +# pallet tests +cargo test -p pallet-subtensor derivatives + +# full runtime build (after runtime-api wiring) +cargo check -p node-subtensor-runtime +``` + +## Open decisions for the author + +1. **Position granularity**: merged-per-`(coldkey,netuid)` (chosen, minimal) vs. multi-position with + an id index. Merge is spec-sanctioned (§8.6); revisit only if UX needs distinct lots. +2. **Restoration realization**: net `SubnetTAO +=` (chosen) vs. explicit min-swap zap. Start with the + net form; escalate only if the conservation test on weighted pools fails. +3. **`hotkey` association**: carry it for identity/precompile parity, or drop it and key purely on + coldkey. Carrying it is cheap and keeps consistency with staking. diff --git a/docs/derivatives/QA_REPORT.md b/docs/derivatives/QA_REPORT.md new file mode 100644 index 0000000000..9f019bdde8 --- /dev/null +++ b/docs/derivatives/QA_REPORT.md @@ -0,0 +1,173 @@ +# Derivatives (covered shorts/longs) — QA & Test Report + +Scope: the pool-borrowing covered shorts/longs feature on the Alpha/TAO CPMM +(continuation of #2764). Feature is governance-gated **off** by default +(`ShortsEnabled`/`LongsEnabled = false`). This report records the QA performed on +the branch and an overall score. + +**Overall QA & test score: 9/10.** All CI-grade gates green, comprehensive unit +coverage on both sides, four independent adversarial review rounds passed, and a +full on-chain lifecycle exercised on a live local chain. Weight benchmarks are now +implemented and wired (extrinsics + the O(N) decay/dereg hooks); the remaining +pre-mainnet items are operational gates, not code-correctness gaps: regenerating +the weight constants on CI reference hardware, and the adversarial trading-games +matrix (incl. the EMA-slowness safety margin) before any `κ` ramp or +`ShortsEnabled` flip. + +--- + +## 1. Build (local laptop, aarch64 macOS) + +| Target | Result | +|---|---| +| `cargo check -p pallet-subtensor` | ✅ clean | +| `cargo check -p node-subtensor-runtime` (native, `SKIP_WASM_BUILD=1`) | ✅ clean | +| **wasm runtime** (`cargo build -p node-subtensor-runtime`) | ✅ built (1m04s) | +| **full node** (`cargo build -p node-subtensor`) | ✅ 325 MB binary | +| **localnet release** (`--workspace --profile=release --features fast-runtime`) | ✅ 9m28s | + +macOS note: the wasm build needs a WebAssembly-capable LLVM (Apple clang lacks the +target). Fix: `brew install llvm` and build with +`CC_wasm32v1_none=/opt/homebrew/opt/llvm/bin/clang +AR_wasm32v1_none=…/llvm-ar CFLAGS_wasm32v1_none="-DZSTD_DISABLE_ASM=1"`. + +## 2. Static analysis / style gates + +| Gate | Result | +|---|---| +| `cargo fmt --check` (all feature files) | ✅ clean | +| `cargo clippy -p pallet-subtensor --lib` | ✅ no warnings in feature code (only an unrelated `trie-db` dependency future-incompat note) | + +## 3. Tests + +- **Derivatives suite: 74 tests, 0 failed.** +- **Full pallet suite: `cargo test -p pallet-subtensor --lib` → 1258 passed / 0 failed / 9 ignored** (no regressions in adjacent staking / networks / weights / swap-hotkey / registration suites). + +Coverage by area (both short and long sides unless noted): + +- **Open**: quote↔open match; reject paths — disabled, stable-subnet, zero/min-input, + capacity (`κ·ref`), low-liquidity (`λ_eff ≤ 0`), cold-EMA fresh subnet; merge + + hotkey-mismatch; **execution-bound rejection** (`max_alpha_liability` / + `max_tao_liability`, both sides); validate-before-mutate (`assert_noop!` + strands-no-funds). +- **Atomicity**: `open_short_failed_pool_transfer_rolls_back_atomically` — forces the + pool→custody leg to fail after the floor moved and asserts full `#[transactional]` + rollback (would fail without the attribute). +- **Top-up / close**: partial + full close, alpha-mint guards, invalid fraction, + many-partial drain, close quote consistency. +- **Default**: dust + grace eligibility, permissionless-default anti-snipe, + per-side grace independence, recycle-exactly-the-floor proof. +- **Decay / dereg**: rate-vs-closed-form, restore, block-step, materialize-never- + inflates; full terminal matrix (in-the-money / underwater / cold-EMA) both sides; + dereg **full-`do_dissolve_network`-path** long-equity survival through the stake-wipe. +- **Views/RPC, governance clamps, capacity/anti-split, active-set tracking.** +- **Invariant proofs**: TAO+alpha conservation across the full mixed lifecycle; + **custody ≥ Σ materialized(P+R+E) under decay at the `DecayMax` clamp extreme, + checked every tick**; `ShortPositionCount == |ShortPositions[netuid]|` and + active-set ⟺ nonzero Σ through churn. + +## 4. Adversarial review (three independent lenses) + +| Lens | Verdict | +|---|---| +| Security skeptic | **CONVINCED** — atomicity (`#[transactional]`) + decay-restore ordering re-derived; no overflow / conservation / desync residue | +| Architect skeptic | **CONVINCED** — operand-order footgun resolved, dereg ordering contract guarded by an end-to-end test, `pEMA` dependency documented, decay-hook bound noted; M2/M3 now property-tested | +| Exploiter | **DEFEATED** — no profitable extraction across self-short-to-dereg, sandwich, capacity-split, decay-drift mint, EMA manipulation, cross-side, RPC abuse | + +Key hardening landed from review: caller-signed execution bounds; validate-before-mutate; +`#[transactional]` on all 8 money functions; terminal `K_D = max(K_spot, K_EMA)` as a +u128 ceiling CPMM buyback (fixes an `I64F64` rao² overflow) + short cold-EMA floor; +decay restore-then-commit ordering; cancellation-stable `solve_collateral`. + +## 5. Precision review & on-chain CPMM audit + +- **Precision vs house style**: the only rao² product (terminal buyback) uses u128 + ceiling math; all other `I64F64` use is price×rao / ratio×rao (rao-scale, no + overflow). `solve_collateral` uses the cancellation-stable root. Matches the + codebase's "no `I64F64` for rao² products" convention. +- **Live CPMM audit** (128 dynamic mainnet subnets): no cold/empty/tiny/over-1.0-price + anomalies; spot↔EMA drift handled in the safe direction by the `min`/`max` + reference design. Documented `pEMA` caveat (`min(spot,1.0)` clamp, ~30d half-life; + guarantees hold for price ≤ ~1.0, true for all subnets today). + +## 6. Live local-chain end-to-end (3-validator `fast-runtime` localnet) + +- Chain produced blocks; runtime metadata contains all derivative extrinsics + (incl. the `max_alpha_liability` bound param) and governance setters. +- **Governance**: `sudo_set_shorts_enabled`, `sudo_set_short_kappa`, + `sudo_set_subtoken_enabled`, `sudo_set_longs_enabled` — all applied on-chain. +- **Full SHORT lifecycle**: `add_stake` (fund pool) → `open_short` (`ShortOpened`; + SubnetTAO fell by exactly R+E — conservation) → `top_up_short` (R grew by the + exact amount) → `close_short` full (`ShortClosed`; position cleared, escrow + + repaid Q returned to pool). +- **Atomic rollback observed live**: an `open_short` against an unfunded pool failed + on the pool leg and rolled back completely (no position, `SubnetTAO` Δ=0, only the + tx fee). +- **LONG side**: enabled; `open_long` correctly rejected on the alpha-depleted pool + via both guards (`EffectiveLtvNonPositive`, `AmountTooLow`) — extrinsic + domain + checks live (a clean long open needs a pool with healthy alpha reserve). + +## 7. Residual / out-of-scope (tracked, not code-correctness gaps) + +- **Benchmarked weights** — *now implemented:* FRAME v2 benchmarks exist for all 8 + extrinsics plus the O(N) hooks (`run_short_decay`/`run_long_decay` with an + active-subnet component over `[0,128]`, `settle_shorts_on_dereg`/`settle_longs_on_dereg` + with a position component over `[0,1024]`); the 8 dispatches use `T::WeightInfo::*`, + `on_initialize` charges the per-block decay at `TotalNetworks`, and the dissolve + extrinsics charge terminal settlement at the *actual* per-subnet position count. + The remaining step is regenerating the weight constants on CI reference hardware + (`--extrinsic '*'`) before mainnet enablement — the harness/wiring is in place. +- A clean **successful long open on-chain** was subsequently demonstrated on a + mainnet-seeded localnet (`open_long` P=1.0α → D liability at spot, full close + clears); also covered by unit tests (`long_dereg_in_the_money_pays_bounded_equity`, + conservation proofs). + +### 7.1 Pre-enablement checklist (must clear before `ShortsEnabled`/`κ` ramp) + +These are integration dependencies and operational invariants, not code-correctness +gaps. They must be re-verified at enablement time because they depend on upstream +state or governance configuration that can drift after merge. + +1. **Adversarial trading-games matrix** on a mainnet-like replica — EMA half-life × + `κ` × pool depth × attacker capital × dereg distance × registration timing × + spot-buy defense. The short-to-dereg safety margin and the one-sided + reserve-accounting approximation (intentional divergence from fee/weighted spot + execution) are only valid once this passes. **Must include position-count stress** + (not just economics): drive a subnet to a high synthetic open-position count within + the `κ`/min-input bounds and confirm the immediate dereg sweep stays within block + weight/latency. +2. **`pEMA` dependency is load-bearing.** The safety math assumes + `SubnetMovingPrice = EMA(min(spot, 1.0))` with a slow half-life. If upstream + changes the `min(·,1.0)` clamp or the half-life, the derivative anti-suppression + math must be revalidated before enablement. +3. **High-price-subnet caveat.** Because `pEMA` is clamped around 1.0, the terminal + anti-suppression guarantee is stated only for subnets priced below ~1.0 (true for + all mainnet subnets today). Confirm this still holds at enablement. +4. **Benchmark validation boundaries.** Decay `WeightInfo` is benchmarked over + `[0,128]` (= `DefaultSubnetLimit`); `on_initialize` charges `run_*_decay(TotalNetworks)` + with the full count (linear, scaled — not under-charged — above 128, but only + *validated* to 128). Terminal settlement is benchmarked over `[0,1024]` and the + dissolve extrinsics charge `settle_*_on_dereg(actual position count)`; counts above + 1024 extrapolate the per-position slope (scaled, not under-charged). There is no + per-subnet open-position cap (parity with the uncapped alpha-stake unwind); position + count is bounded only by the κ capacity limit and min-input. If the subnet count is + raised above 128, or subnets routinely carry more than 1024 positions, regenerate the + corresponding weights to re-validate linearity. `[0,1024]` is a benchmark calibration + range, **not** a consensus-enforced cap — terminal settlement is an immediate + enumerate-and-settle sweep (see DESIGN.md §3.4), modelled on native alpha liquidation. +5. **CI reference-hardware weight regen** (`--extrinsic '*'`) — wiring is in place. The + `open_short`/`open_long` weights were regenerated locally after the cap removal (the stale + `Short/LongMaxPositions` storage-read annotation is gone); the final constants still need a + run on CI reference hardware before enablement. +6. **Dereg weight/latency monitoring** — on a mainnet-seeded localnet, exercise + `dissolve_network` against subnets carrying high synthetic short/long position counts and + record settlement weight + wall-clock, confirming the immediate sweep stays within block + limits at the counts the `κ`/min-input economics actually permit. + +### 7.2 Accepted tradeoffs (intentional, not blockers) + +- **Terminal transfer failure = log + sweep, not abort.** A custody-invariant breach + during dereg settlement logs loudly and the unpaid value stays in custody to be + swept (recycled), rather than aborting the dereg. No value is created/lost and the + emitted `equity` reflects only what was actually paid; the position is underpaid + rather than the subnet bricked on a dust shortfall. Intentional. diff --git a/pallets/admin-utils/src/lib.rs b/pallets/admin-utils/src/lib.rs index f972facca6..a8fb7764f2 100644 --- a/pallets/admin-utils/src/lib.rs +++ b/pallets/admin-utils/src/lib.rs @@ -2276,6 +2276,130 @@ pub mod pallet { Ok(()) } + + /// Enable or disable short-side covered derivatives (launch gate). + #[pallet::call_index(96)] + #[pallet::weight(::DbWeight::get().reads_writes(0, 1))] + pub fn sudo_set_shorts_enabled(origin: OriginFor, enabled: bool) -> DispatchResult { + ensure_root(origin)?; + pallet_subtensor::Pallet::::set_shorts_enabled(enabled); + Ok(()) + } + + /// Set the short footprint-cap factor `κ_S` (scaled by 1e9). + #[pallet::call_index(97)] + #[pallet::weight(::DbWeight::get().reads_writes(0, 1))] + pub fn sudo_set_short_kappa(origin: OriginFor, kappa_ppb: u64) -> DispatchResult { + ensure_root(origin)?; + pallet_subtensor::Pallet::::set_short_kappa_ppb(kappa_ppb); + Ok(()) + } + + /// Set the base short LTV `λ` (scaled by 1e9). + #[pallet::call_index(98)] + #[pallet::weight(::DbWeight::get().reads_writes(0, 1))] + pub fn sudo_set_short_base_ltv(origin: OriginFor, ltv_ppb: u64) -> DispatchResult { + ensure_root(origin)?; + pallet_subtensor::Pallet::::set_short_base_ltv_ppb(ltv_ppb); + Ok(()) + } + + /// Set the daily decay bounds `d_min`, `d_max` (scaled by 1e9). + #[pallet::call_index(99)] + #[pallet::weight(::DbWeight::get().reads_writes(0, 2))] + pub fn sudo_set_short_decay_bounds( + origin: OriginFor, + min_ppb: u64, + max_ppb: u64, + ) -> DispatchResult { + ensure_root(origin)?; + pallet_subtensor::Pallet::::set_decay_bounds_ppb(min_ppb, max_ppb); + Ok(()) + } + + /// Set the retained-buffer dust threshold `R_dust` (in rao). + #[pallet::call_index(100)] + #[pallet::weight(::DbWeight::get().reads_writes(0, 1))] + pub fn sudo_set_short_dust(origin: OriginFor, dust_rao: u64) -> DispatchResult { + ensure_root(origin)?; + pallet_subtensor::Pallet::::set_short_dust(dust_rao.into()); + Ok(()) + } + + /// Set the anti-snipe default grace period (in blocks). + #[pallet::call_index(101)] + #[pallet::weight(::DbWeight::get().reads_writes(0, 1))] + pub fn sudo_set_short_default_grace(origin: OriginFor, blocks: u64) -> DispatchResult { + ensure_root(origin)?; + pallet_subtensor::Pallet::::set_short_default_grace(blocks); + Ok(()) + } + + /// Set the minimum short open input (in rao). + #[pallet::call_index(102)] + #[pallet::weight(::DbWeight::get().reads_writes(0, 1))] + pub fn sudo_set_short_min_input( + origin: OriginFor, + min_input_rao: u64, + ) -> DispatchResult { + ensure_root(origin)?; + pallet_subtensor::Pallet::::set_short_min_input(min_input_rao.into()); + Ok(()) + } + + /// Enable or disable long-side covered derivatives (launch gate). + #[pallet::call_index(104)] + #[pallet::weight(::DbWeight::get().reads_writes(0, 1))] + pub fn sudo_set_longs_enabled(origin: OriginFor, enabled: bool) -> DispatchResult { + ensure_root(origin)?; + pallet_subtensor::Pallet::::set_longs_enabled(enabled); + Ok(()) + } + + /// Set the long footprint-cap factor `κ_L` (scaled by 1e9). + #[pallet::call_index(105)] + #[pallet::weight(::DbWeight::get().reads_writes(0, 1))] + pub fn sudo_set_long_kappa(origin: OriginFor, kappa_ppb: u64) -> DispatchResult { + ensure_root(origin)?; + pallet_subtensor::Pallet::::set_long_kappa_ppb(kappa_ppb); + Ok(()) + } + + /// Set the base long LTV `λ_L` (scaled by 1e9). + #[pallet::call_index(106)] + #[pallet::weight(::DbWeight::get().reads_writes(0, 1))] + pub fn sudo_set_long_base_ltv(origin: OriginFor, ltv_ppb: u64) -> DispatchResult { + ensure_root(origin)?; + pallet_subtensor::Pallet::::set_long_base_ltv_ppb(ltv_ppb); + Ok(()) + } + + /// Set the long retained-buffer dust threshold (in rao of Alpha). + #[pallet::call_index(107)] + #[pallet::weight(::DbWeight::get().reads_writes(0, 1))] + pub fn sudo_set_long_dust(origin: OriginFor, dust_rao: u64) -> DispatchResult { + ensure_root(origin)?; + pallet_subtensor::Pallet::::set_long_dust(dust_rao.into()); + Ok(()) + } + + /// Set the minimum long open input (in rao of Alpha). + #[pallet::call_index(108)] + #[pallet::weight(::DbWeight::get().reads_writes(0, 1))] + pub fn sudo_set_long_min_input(origin: OriginFor, min_input_rao: u64) -> DispatchResult { + ensure_root(origin)?; + pallet_subtensor::Pallet::::set_long_min_input(min_input_rao.into()); + Ok(()) + } + + /// Set the long-side anti-snipe default grace period (in blocks). + #[pallet::call_index(110)] + #[pallet::weight(::DbWeight::get().reads_writes(0, 1))] + pub fn sudo_set_long_default_grace(origin: OriginFor, blocks: u64) -> DispatchResult { + ensure_root(origin)?; + pallet_subtensor::Pallet::::set_long_default_grace(blocks); + Ok(()) + } } } diff --git a/pallets/subtensor/runtime-api/src/lib.rs b/pallets/subtensor/runtime-api/src/lib.rs index 0fb24d61c2..1027def5e5 100644 --- a/pallets/subtensor/runtime-api/src/lib.rs +++ b/pallets/subtensor/runtime-api/src/lib.rs @@ -3,6 +3,10 @@ extern crate alloc; use alloc::collections::BTreeMap; use alloc::vec::Vec; use codec::Compact; +use pallet_subtensor::derivatives::{ + CloseLongQuote, CloseShortQuote, LongMarketInfo, LongOpenQuote, LongPositionInfo, + ShortMarketInfo, ShortOpenQuote, ShortPositionInfo, +}; use pallet_subtensor::rpc_info::{ delegate_info::DelegateInfo, dynamic_info::DynamicInfo, @@ -81,4 +85,18 @@ sp_api::decl_runtime_apis! { fn get_proxy_types() -> Vec; fn get_proxy_filter(proxy_type: Option) -> Vec; } + + pub trait DerivativesRuntimeApi { + fn quote_open_short(netuid: NetUid, position_input: TaoBalance) -> Option; + fn quote_close_short(coldkey: AccountId32, netuid: NetUid, fraction_ppb: u64) -> Option; + fn get_short_position(coldkey: AccountId32, netuid: NetUid) -> Option>; + fn get_short_positions(coldkey: AccountId32) -> Vec>; + fn get_subnet_short_state(netuid: NetUid) -> Option; + + fn quote_open_long(netuid: NetUid, position_input: AlphaBalance) -> Option; + fn quote_close_long(coldkey: AccountId32, netuid: NetUid, fraction_ppb: u64) -> Option; + fn get_long_position(coldkey: AccountId32, netuid: NetUid) -> Option>; + fn get_long_positions(coldkey: AccountId32) -> Vec>; + fn get_subnet_long_state(netuid: NetUid) -> Option; + } } diff --git a/pallets/subtensor/src/benchmarks.rs b/pallets/subtensor/src/benchmarks.rs index 2a08e4b933..bb2ac6b02e 100644 --- a/pallets/subtensor/src/benchmarks.rs +++ b/pallets/subtensor/src/benchmarks.rs @@ -17,7 +17,7 @@ use sp_runtime::{ }; use sp_std::collections::btree_set::BTreeSet; use sp_std::vec; -use substrate_fixed::types::U64F64; +use substrate_fixed::types::{I64F64, I96F32, U64F64}; use subtensor_runtime_common::{AlphaBalance, NetUid, TaoBalance}; use subtensor_swap_interface::SwapHandler; @@ -2258,6 +2258,337 @@ mod pallet_benchmarks { ); } + // ---- covered derivatives (spec v3.6.1) ----------------------------- + + /// Build a dynamic subnet with warm EMA + deep reserves, and fund the per-subnet + /// pool account so pool→custody transfers at open succeed. + fn deriv_subnet(netuid: NetUid) { + Subtensor::::init_new_network(netuid, 1); + SubtokenEnabled::::insert(netuid, true); + SubnetMechanism::::insert(netuid, 1); + SubnetTAO::::insert(netuid, TaoBalance::from(1_000_000_000_000_000u64)); + SubnetAlphaIn::::insert(netuid, AlphaBalance::from(100_000_000_000_000_000u64)); + SubnetAlphaOut::::insert(netuid, AlphaBalance::from(100_000_000_000_000_000u64)); + SubnetMovingPrice::::insert(netuid, I96F32::from_num(0.01)); + // Fund the per-subnet pool account so pool→custody transfers at open + // succeed. Kept modest (1000 TAO) so the multi-subnet decay benchmark + // loop (up to 64 subnets) does not exhaust the mint path. + if let Some(sa) = Subtensor::::get_subnet_account_id(netuid) { + add_balance_to_coldkey_account::(&sa, TaoBalance::from(1_000_000_000_000u64)); + } + } + + /// A funded trader holding TAO and (for long/close) alpha collateral on `hotkey`. + fn deriv_trader(netuid: NetUid, seed: u32) -> (T::AccountId, T::AccountId) { + let coldkey: T::AccountId = account("DerivCold", 0, seed); + let hotkey: T::AccountId = account("DerivHot", 0, seed); + add_balance_to_coldkey_account::(&coldkey, TaoBalance::from(1_000_000_000_000_000u64)); + let alpha = AlphaBalance::from(10_000_000_000_000u64); + Subtensor::::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid, alpha, + ); + SubnetAlphaOut::::mutate(netuid, |o| *o = o.saturating_add(alpha)); + (coldkey, hotkey) + } + + #[benchmark] + fn open_short() { + let netuid = NetUid::from(1); + deriv_subnet::(netuid); + Subtensor::::set_shorts_enabled(true); + Subtensor::::set_short_kappa_ppb(900_000_000); + let (coldkey, hotkey) = deriv_trader::(netuid, 1); + #[extrinsic_call] + _( + RawOrigin::Signed(coldkey), + hotkey, + netuid, + TaoBalance::from(1_000_000_000u64), + AlphaBalance::MAX, + ); + } + + #[benchmark] + fn top_up_short() { + let netuid = NetUid::from(1); + deriv_subnet::(netuid); + Subtensor::::set_shorts_enabled(true); + Subtensor::::set_short_kappa_ppb(900_000_000); + let (coldkey, hotkey) = deriv_trader::(netuid, 1); + assert_ok!(Subtensor::::open_short( + RawOrigin::Signed(coldkey.clone()).into(), + hotkey, + netuid, + TaoBalance::from(1_000_000_000u64), + AlphaBalance::MAX + )); + #[extrinsic_call] + _( + RawOrigin::Signed(coldkey), + netuid, + TaoBalance::from(500_000_000u64), + ); + } + + #[benchmark] + fn close_short() { + let netuid = NetUid::from(1); + deriv_subnet::(netuid); + Subtensor::::set_shorts_enabled(true); + Subtensor::::set_short_kappa_ppb(900_000_000); + let (coldkey, hotkey) = deriv_trader::(netuid, 1); + assert_ok!(Subtensor::::open_short( + RawOrigin::Signed(coldkey.clone()).into(), + hotkey, + netuid, + TaoBalance::from(1_000_000_000u64), + AlphaBalance::MAX + )); + #[extrinsic_call] + _(RawOrigin::Signed(coldkey), netuid, 1_000_000_000u64); + } + + #[benchmark] + fn default_short() { + let netuid = NetUid::from(1); + deriv_subnet::(netuid); + Subtensor::::set_shorts_enabled(true); + Subtensor::::set_short_kappa_ppb(900_000_000); + Subtensor::::set_short_dust(TaoBalance::from(u64::MAX)); + Subtensor::::set_short_default_grace(0); + let (coldkey, hotkey) = deriv_trader::(netuid, 1); + assert_ok!(Subtensor::::open_short( + RawOrigin::Signed(coldkey.clone()).into(), + hotkey, + netuid, + TaoBalance::from(1_000_000_000u64), + AlphaBalance::MAX + )); + let caller: T::AccountId = account("Defaulter", 0, 9); + add_balance_to_coldkey_account::(&caller, TaoBalance::from(1_000_000_000u64)); + #[extrinsic_call] + _(RawOrigin::Signed(caller), coldkey, netuid); + } + + #[benchmark] + fn open_long() { + let netuid = NetUid::from(1); + deriv_subnet::(netuid); + Subtensor::::set_longs_enabled(true); + Subtensor::::set_long_kappa_ppb(900_000_000); + let (coldkey, hotkey) = deriv_trader::(netuid, 1); + #[extrinsic_call] + _( + RawOrigin::Signed(coldkey), + hotkey, + netuid, + AlphaBalance::from(1_000_000_000u64), + TaoBalance::MAX, + ); + } + + #[benchmark] + fn top_up_long() { + let netuid = NetUid::from(1); + deriv_subnet::(netuid); + Subtensor::::set_longs_enabled(true); + Subtensor::::set_long_kappa_ppb(900_000_000); + let (coldkey, hotkey) = deriv_trader::(netuid, 1); + assert_ok!(Subtensor::::open_long( + RawOrigin::Signed(coldkey.clone()).into(), + hotkey, + netuid, + AlphaBalance::from(1_000_000_000u64), + TaoBalance::MAX + )); + #[extrinsic_call] + _( + RawOrigin::Signed(coldkey), + netuid, + AlphaBalance::from(500_000_000u64), + ); + } + + #[benchmark] + fn close_long() { + let netuid = NetUid::from(1); + deriv_subnet::(netuid); + Subtensor::::set_longs_enabled(true); + Subtensor::::set_long_kappa_ppb(900_000_000); + let (coldkey, hotkey) = deriv_trader::(netuid, 1); + assert_ok!(Subtensor::::open_long( + RawOrigin::Signed(coldkey.clone()).into(), + hotkey, + netuid, + AlphaBalance::from(1_000_000_000u64), + TaoBalance::MAX + )); + #[extrinsic_call] + _(RawOrigin::Signed(coldkey), netuid, 1_000_000_000u64); + } + + #[benchmark] + fn default_long() { + let netuid = NetUid::from(1); + deriv_subnet::(netuid); + Subtensor::::set_longs_enabled(true); + Subtensor::::set_long_kappa_ppb(900_000_000); + Subtensor::::set_long_dust(AlphaBalance::from(u64::MAX)); + Subtensor::::set_long_default_grace(0); + let (coldkey, hotkey) = deriv_trader::(netuid, 1); + assert_ok!(Subtensor::::open_long( + RawOrigin::Signed(coldkey.clone()).into(), + hotkey, + netuid, + AlphaBalance::from(1_000_000_000u64), + TaoBalance::MAX + )); + let caller: T::AccountId = account("Defaulter", 0, 9); + add_balance_to_coldkey_account::(&caller, TaoBalance::from(1_000_000_000u64)); + #[extrinsic_call] + _(RawOrigin::Signed(caller), coldkey, netuid); + } + + /// Per-block short decay over `s` active subnets (O(s), the block-step hook cost). + #[benchmark] + fn run_short_decay(s: Linear<0, 128>) { + Subtensor::::set_shorts_enabled(true); + for i in 0..s { + let netuid = NetUid::from((i + 1) as u16); + deriv_subnet::(netuid); + ShortAggregate::::insert( + netuid, + crate::derivatives::ShortAgg { + r_sigma: TaoBalance::from(1_000_000_000u64), + e_sigma: TaoBalance::from(1_000_000_000u64), + b_sigma: TaoBalance::from(1_000_000_000u64), + q_sigma: AlphaBalance::from(1_000_000_000u64), + omega: I64F64::from_num(0), + }, + ); + ShortActiveSubnets::::insert(netuid, ()); + add_balance_to_coldkey_account::( + &Subtensor::::short_custody_account(netuid), + TaoBalance::from(1_000_000_000_000u64), + ); + } + #[block] + { + Subtensor::::run_short_decay(); + } + } + + /// Per-block long decay over `s` active subnets. + #[benchmark] + fn run_long_decay(s: Linear<0, 128>) { + Subtensor::::set_longs_enabled(true); + for i in 0..s { + let netuid = NetUid::from((i + 1) as u16); + deriv_subnet::(netuid); + LongAggregate::::insert( + netuid, + crate::derivatives::LongAgg { + r_sigma: AlphaBalance::from(1_000_000_000u64), + e_sigma: AlphaBalance::from(1_000_000_000u64), + b_sigma: AlphaBalance::from(1_000_000_000u64), + d_sigma: TaoBalance::from(1_000_000_000u64), + omega: I64F64::from_num(0), + }, + ); + LongActiveSubnets::::insert(netuid, ()); + } + #[block] + { + Subtensor::::run_long_decay(); + } + } + + /// Terminal short settlement over `p` positions on one subnet (dereg sweep, O(p)). + #[benchmark] + fn settle_shorts_on_dereg(p: Linear<0, 1024>) { + let netuid = NetUid::from(1); + deriv_subnet::(netuid); + add_balance_to_coldkey_account::( + &Subtensor::::short_custody_account(netuid), + TaoBalance::from(1_000_000_000_000_000u64), + ); + let unit = 1_000_000_000u64; + for i in 0..p { + let ck: T::AccountId = account("SetCold", 0, i); + let hk: T::AccountId = account("SetHot", 0, i); + ShortPositions::::insert( + netuid, + &ck, + crate::derivatives::ShortPosition { + hotkey: hk, + p_floor: TaoBalance::from(unit), + q_liability: AlphaBalance::from(unit), + r_stored: TaoBalance::from(unit), + e_stored: TaoBalance::from(unit), + b_stored: TaoBalance::from(unit), + omega_entry: I64F64::from_num(0), + last_active: 0, + }, + ); + } + ShortPositionCount::::insert(netuid, p); + ShortAggregate::::insert( + netuid, + crate::derivatives::ShortAgg { + r_sigma: TaoBalance::from(unit.saturating_mul(p as u64)), + e_sigma: TaoBalance::from(unit.saturating_mul(p as u64)), + b_sigma: TaoBalance::from(unit.saturating_mul(p as u64)), + q_sigma: AlphaBalance::from(unit.saturating_mul(p as u64)), + omega: I64F64::from_num(0), + }, + ); + #[block] + { + Subtensor::::settle_shorts_on_dereg(netuid); + } + } + + /// Terminal long settlement over `p` positions on one subnet. + #[benchmark] + fn settle_longs_on_dereg(p: Linear<0, 1024>) { + let netuid = NetUid::from(1); + deriv_subnet::(netuid); + let unit = 1_000_000_000u64; + for i in 0..p { + let ck: T::AccountId = account("SetCold", 0, i); + let hk: T::AccountId = account("SetHot", 0, i); + LongPositions::::insert( + netuid, + &ck, + crate::derivatives::LongPosition { + hotkey: hk, + p_floor: AlphaBalance::from(unit), + d_liability: TaoBalance::from(unit), + r_stored: AlphaBalance::from(unit), + e_stored: AlphaBalance::from(unit), + b_stored: AlphaBalance::from(unit), + omega_entry: I64F64::from_num(0), + last_active: 0, + }, + ); + } + LongPositionCount::::insert(netuid, p); + LongAggregate::::insert( + netuid, + crate::derivatives::LongAgg { + r_sigma: AlphaBalance::from(unit.saturating_mul(p as u64)), + e_sigma: AlphaBalance::from(unit.saturating_mul(p as u64)), + b_sigma: AlphaBalance::from(unit.saturating_mul(p as u64)), + d_sigma: TaoBalance::from(unit.saturating_mul(p as u64)), + omega: I64F64::from_num(0), + }, + ); + #[block] + { + Subtensor::::settle_longs_on_dereg(netuid); + } + } + impl_benchmark_test_suite!( Subtensor, crate::tests::mock::new_test_ext(1), diff --git a/pallets/subtensor/src/coinbase/block_step.rs b/pallets/subtensor/src/coinbase/block_step.rs index fac924ccf4..eebeccfb79 100644 --- a/pallets/subtensor/src/coinbase/block_step.rs +++ b/pallets/subtensor/src/coinbase/block_step.rs @@ -19,6 +19,10 @@ impl Pallet { Self::reveal_crv3_commits(); // --- 4. Run emission through network. Self::run_coinbase(block_emission); + // --- 4b. Decay covered derivative positions and restore unwound + // collateral to pools (shorts return TAO, longs return Alpha). + Self::run_short_decay(); + Self::run_long_decay(); // --- 5. Update moving prices AFTER using them for emissions. Self::update_moving_prices(); // --- 6. Update roop prop AFTER using them for emissions. diff --git a/pallets/subtensor/src/coinbase/root.rs b/pallets/subtensor/src/coinbase/root.rs index c61a71aa65..45ef9e7851 100644 --- a/pallets/subtensor/src/coinbase/root.rs +++ b/pallets/subtensor/src/coinbase/root.rs @@ -203,6 +203,13 @@ impl Pallet { /// * 'MechanismDoesNotExist': If the specified network does not exist. /// * 'NotSubnetOwner': If the caller does not own the specified subnet. /// + // Atomic: derivative terminal settlement (`settle_shorts/longs_on_dereg`) + // runs before the fallible `destroy_alpha_in_out_stakes` / + // `clear_protocol_liquidity` legs. Without a storage layer a failure in a + // later leg would leave derivative positions removed / equity paid / custody + // recycled while the subnet survives. `#[transactional]` rolls the whole + // dissolve back as a unit on any error. + #[frame_support::transactional] pub fn do_dissolve_network(netuid: NetUid) -> dispatch::DispatchResult { // --- The network exists? ensure!( @@ -212,6 +219,11 @@ impl Pallet { Self::finalize_all_subnet_root_dividends(netuid); + // --- Settle covered derivatives before the pool is drained, so restored + // escrow joins terminal distribution and liabilities are bounded. + Self::settle_shorts_on_dereg(netuid); + Self::settle_longs_on_dereg(netuid); + // --- Perform the cleanup before removing the network. Self::destroy_alpha_in_out_stakes(netuid)?; T::SwapInterface::clear_protocol_liquidity(netuid)?; diff --git a/pallets/subtensor/src/derivatives/long.rs b/pallets/subtensor/src/derivatives/long.rs new file mode 100644 index 0000000000..865a5ff0c6 --- /dev/null +++ b/pallets/subtensor/src/derivatives/long.rs @@ -0,0 +1,696 @@ +//! Covered continuous-unwind LONGS — the mirror of shorts with Alpha and TAO +//! swapped (spec §9). Collateral/buffer/escrow are Alpha; the fixed liability +//! `D` is TAO. +//! +//! Unlike shorts, longs need no TAO custody account: the parked Alpha is +//! tracked purely via issuance accounting (removed from `SubnetAlphaIn` / +//! `SubnetAlphaOut` at open, minted back on restoration/close, left burned = +//! recycled on default/cover). The only TAO movement is the trader paying the +//! `D` liability into the pool at close. Shared math (`solve_collateral`, +//! `solve_phi`, `neg_ln_one_minus`, `decay_curve`, conversions) is reused from +//! the parent module. + +use super::*; +use safe_math::FixedExt; +use substrate_fixed::types::I64F64; +use subtensor_runtime_common::Token; + +impl Pallet { + /// Conservative Alpha reference `A_ref = min(A_live, A_EMA)`, with + /// `A_EMA = T_live / pEMA` reconstructed from the price EMA. Cold EMA falls + /// back to the live reserve. + fn long_a_ref(netuid: NetUid) -> I64F64 { + let a_live = Self::alpha_f(SubnetAlphaIn::::get(netuid)); + let t_live = Self::tao_f(SubnetTAO::::get(netuid)); + let pema = I64F64::from_num(Self::get_moving_alpha_price(netuid)); + if pema <= I64F64::from_num(0) { + return a_live; + } + a_live.min(t_live.safe_div(pema)) + } + + /// Current long daily decay rate at the live long footprint. + fn long_daily_decay(netuid: NetUid, b_sigma: AlphaBalance) -> I64F64 { + let cap = LongKappa::::get().saturating_mul(Self::long_a_ref(netuid)); + Self::decay_curve(Self::utilization(Self::alpha_f(b_sigma), cap)) + } + + fn materialize_long(pos: &mut LongPosition, omega_now: I64F64) { + let arg = pos + .omega_entry + .saturating_sub(omega_now) + .min(I64F64::from_num(0)); + let f = arg.checked_exp().unwrap_or_else(|| I64F64::from_num(0)); + pos.r_stored = Self::mul_alpha(pos.r_stored, f); + pos.e_stored = Self::mul_alpha(pos.e_stored, f); + pos.b_stored = Self::mul_alpha(pos.b_stored, f); + pos.omega_entry = omega_now; + } + + /// Drop the aggregate + active-set entry once the last long position closes, + /// so the decay tick stops visiting a subnet that only holds rounding dust. + fn cleanup_long_if_empty(netuid: NetUid) { + if LongPositionCount::::get(netuid) == 0 { + LongAggregate::::remove(netuid); + LongActiveSubnets::::remove(netuid); + } + } + + fn sync_active_long(netuid: NetUid, agg: &LongAgg) { + if agg.r_sigma.is_zero() + && agg.e_sigma.is_zero() + && agg.b_sigma.is_zero() + && agg.d_sigma.is_zero() + { + LongActiveSubnets::::remove(netuid); + } else { + LongActiveSubnets::::insert(netuid, ()); + } + } + + // ---- user operations (spec §9, mirror of §8) ----------------------- + + /// Open (or merge into) a covered long. Trader posts `position_input` Alpha + /// (drawn from stake at `hotkey`). + #[frame_support::transactional] + pub fn do_open_long( + origin: OriginFor, + hotkey: T::AccountId, + netuid: NetUid, + position_input: AlphaBalance, + max_tao_liability: TaoBalance, + ) -> DispatchResult { + let coldkey = ensure_signed(origin)?; + ensure!(LongsEnabled::::get(), Error::::LongsDisabled); + ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); + ensure!( + SubnetMechanism::::get(netuid) == 1, + Error::::SubnetNotDynamic + ); + // Warm-EMA guard (mirror of the short side): block opens on a cold-`pEMA` + // subnet where the EMA risk reference / terminal anti-suppression leg are + // unavailable. + ensure!( + Self::get_moving_alpha_price(netuid) > 0, + Error::::ColdEmaNotAllowed + ); + ensure!( + position_input >= LongMinInput::::get(), + Error::::AmountTooLow + ); + + let mut agg = LongAggregate::::get(netuid); + let a_ref = Self::long_a_ref(netuid); + let p = Self::alpha_f(position_input); + let (c, n) = Self::solve_collateral( + p, + a_ref, + Self::alpha_f(agg.b_sigma), + LongBaseLtv::::get(), + ) + .ok_or(Error::::EffectiveLtvNonPositive)?; + let b = LongBaseLtv::::get().saturating_mul(c); + + ensure!( + Self::alpha_f(agg.b_sigma).saturating_add(b) + <= LongKappa::::get().saturating_mul(a_ref), + Error::::LongCapacityExceeded + ); + + let a_live = Self::alpha_f(SubnetAlphaIn::::get(netuid)); + let t_live = Self::tao_f(SubnetTAO::::get(netuid)); + let phi = Self::solve_phi(n, a_live).ok_or(Error::::ReserveDomainExceeded)?; + + let n_alpha = Self::to_alpha(n); + let e_alpha = Self::to_alpha(phi.saturating_mul(a_live)); + let b_alpha = Self::to_alpha(b); + let d_tao = Self::to_tao(phi.saturating_mul(t_live)); + ensure!(!n_alpha.is_zero(), Error::::RetainedProceedsNonPositive); + + // Caller-signed execution bound (anti-sandwich): the TAO liability derived + // from live reserves must not exceed the maximum the trader accepted. + // `TaoBalance::MAX` opts out of the bound. + ensure!(d_tao <= max_tao_liability, Error::::SlippageTooHigh); + + // Validate-before-mutate: the merge hotkey check runs before any + // stake/reserve mutation, so a rejected open never burns/strands Alpha. + if let Some(existing) = LongPositions::::get(netuid, &coldkey) { + ensure!(existing.hotkey == hotkey, Error::::LongHotkeyMismatch); + } + + // Trader posts P Alpha from stake; remove N+E Alpha from the pool. All + // of this leaves issuance (held off-chain in the position numbers). + ensure!( + Self::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid) + >= position_input, + Error::::InsufficientCollateral + ); + // Respect stake locks: collateral must be unlocked alpha, exactly as a + // normal unstake requires (otherwise a long open+close would free locked + // alpha and bypass the subnet-ownership lock). + Self::ensure_available_to_unstake(&coldkey, netuid, position_input)?; + // Symmetric to the short-close guard: never `saturating_sub` below the + // collateral, which would later let close mint alpha back unbacked. + ensure!( + SubnetAlphaOut::::get(netuid) >= position_input, + Error::::InsufficientCollateral + ); + Self::decrease_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + netuid, + position_input, + ); + SubnetAlphaOut::::mutate(netuid, |o| *o = o.saturating_sub(position_input)); + Self::decrease_provided_alpha_reserve(netuid, n_alpha.saturating_add(e_alpha)); + + let block = Self::get_current_block_as_u64(); + let pos = match LongPositions::::get(netuid, &coldkey) { + Some(mut existing) => { + // Hotkey match was validated before any mutation above. + Self::materialize_long(&mut existing, agg.omega); + existing.p_floor = existing.p_floor.saturating_add(position_input); + existing.d_liability = existing.d_liability.saturating_add(d_tao); + existing.r_stored = existing.r_stored.saturating_add(n_alpha); + existing.e_stored = existing.e_stored.saturating_add(e_alpha); + existing.b_stored = existing.b_stored.saturating_add(b_alpha); + existing.last_active = block; + existing + } + None => { + // No per-subnet position cap; maintain the denormalized count used + // for the dereg settlement weight charge and active-set detection. + let count = LongPositionCount::::get(netuid); + LongPositionCount::::insert(netuid, count.saturating_add(1)); + LongPosition { + hotkey, + p_floor: position_input, + d_liability: d_tao, + r_stored: n_alpha, + e_stored: e_alpha, + b_stored: b_alpha, + omega_entry: agg.omega, + last_active: block, + } + } + }; + LongPositions::::insert(netuid, &coldkey, pos); + + agg.r_sigma = agg.r_sigma.saturating_add(n_alpha); + agg.e_sigma = agg.e_sigma.saturating_add(e_alpha); + agg.b_sigma = agg.b_sigma.saturating_add(b_alpha); + agg.d_sigma = agg.d_sigma.saturating_add(d_tao); + LongAggregate::::insert(netuid, agg); + LongActiveSubnets::::insert(netuid, ()); + + Self::deposit_event(Event::LongOpened { + coldkey, + netuid, + position_input, + retained_proceeds: n_alpha, + tao_liability: d_tao, + escrow: e_alpha, + }); + Ok(()) + } + + /// Top up the carry buffer `R` with fresh Alpha (drawn from stake). + #[frame_support::transactional] + pub fn do_top_up_long( + origin: OriginFor, + netuid: NetUid, + amount: AlphaBalance, + ) -> DispatchResult { + let coldkey = ensure_signed(origin)?; + ensure!(!amount.is_zero(), Error::::AmountTooLow); + let mut pos = + LongPositions::::get(netuid, &coldkey).ok_or(Error::::LongPositionNotFound)?; + let mut agg = LongAggregate::::get(netuid); + Self::materialize_long(&mut pos, agg.omega); + + ensure!( + Self::get_stake_for_hotkey_and_coldkey_on_subnet(&pos.hotkey, &coldkey, netuid) + >= amount, + Error::::InsufficientCollateral + ); + Self::ensure_available_to_unstake(&coldkey, netuid, amount)?; + ensure!( + SubnetAlphaOut::::get(netuid) >= amount, + Error::::InsufficientCollateral + ); + Self::decrease_stake_for_hotkey_and_coldkey_on_subnet( + &pos.hotkey, + &coldkey, + netuid, + amount, + ); + SubnetAlphaOut::::mutate(netuid, |o| *o = o.saturating_sub(amount)); + + pos.r_stored = pos.r_stored.saturating_add(amount); + pos.last_active = Self::get_current_block_as_u64(); + agg.r_sigma = agg.r_sigma.saturating_add(amount); + LongPositions::::insert(netuid, &coldkey, pos); + LongAggregate::::insert(netuid, agg); + Self::deposit_event(Event::LongToppedUp { + coldkey, + netuid, + amount, + }); + Ok(()) + } + + /// Partial or full close. Trader repays `ρD` TAO into the pool and receives + /// `ρ(P+R)` Alpha back as stake. + #[frame_support::transactional] + pub fn do_close_long( + origin: OriginFor, + netuid: NetUid, + fraction_ppb: u64, + ) -> DispatchResult { + let coldkey = ensure_signed(origin)?; + ensure!( + fraction_ppb > 0 && fraction_ppb <= 1_000_000_000, + Error::::InvalidCloseFraction + ); + let rho = I64F64::from_num(fraction_ppb).safe_div(I64F64::from_num(1_000_000_000u64)); + let mut pos = + LongPositions::::get(netuid, &coldkey).ok_or(Error::::LongPositionNotFound)?; + let mut agg = LongAggregate::::get(netuid); + Self::materialize_long(&mut pos, agg.omega); + + let d_close = Self::mul_tao(pos.d_liability, rho); + let r_close = Self::mul_alpha(pos.r_stored, rho); + let e_close = Self::mul_alpha(pos.e_stored, rho); + let p_close = Self::mul_alpha(pos.p_floor, rho); + let b_close = Self::mul_alpha(pos.b_stored, rho); + + // Trader repays ρD TAO into the pool (strict transfer). + if !d_close.is_zero() { + let subnet_account = + Self::get_subnet_account_id(netuid).ok_or(Error::::SubnetNotExists)?; + Self::transfer_tao(&coldkey, &subnet_account, d_close.into())?; + Self::increase_provided_tao_reserve(netuid, d_close); + TotalStake::::mutate(|t| *t = t.saturating_add(d_close)); + } + // Settle escrow back to the pool; return floor+buffer as stake (mint). + Self::increase_provided_alpha_reserve(netuid, e_close); + let returned = p_close.saturating_add(r_close); + if !returned.is_zero() { + Self::increase_stake_for_hotkey_and_coldkey_on_subnet( + &pos.hotkey, + &coldkey, + netuid, + returned, + ); + SubnetAlphaOut::::mutate(netuid, |o| *o = o.saturating_add(returned)); + } + + pos.d_liability = pos.d_liability.saturating_sub(d_close); + pos.r_stored = pos.r_stored.saturating_sub(r_close); + pos.e_stored = pos.e_stored.saturating_sub(e_close); + pos.p_floor = pos.p_floor.saturating_sub(p_close); + pos.b_stored = pos.b_stored.saturating_sub(b_close); + + agg.d_sigma = agg.d_sigma.saturating_sub(d_close); + agg.r_sigma = agg.r_sigma.saturating_sub(r_close); + agg.e_sigma = agg.e_sigma.saturating_sub(e_close); + agg.b_sigma = agg.b_sigma.saturating_sub(b_close); + Self::sync_active_long(netuid, &agg); + LongAggregate::::insert(netuid, agg); + + if fraction_ppb == 1_000_000_000 || pos.p_floor.is_zero() { + LongPositions::::remove(netuid, &coldkey); + LongPositionCount::::mutate(netuid, |c| *c = c.saturating_sub(1)); + Self::cleanup_long_if_empty(netuid); + } else { + LongPositions::::insert(netuid, &coldkey, pos); + } + Self::deposit_event(Event::LongClosed { + coldkey, + netuid, + fraction_ppb, + repaid_tao: d_close, + returned, + }); + Ok(()) + } + + /// Permissionless default once the buffer is dust and the grace window has + /// elapsed. Restores residual Alpha, recycles the floor (left burned), + /// extinguishes `D`. + #[frame_support::transactional] + pub fn do_default_long( + origin: OriginFor, + coldkey: T::AccountId, + netuid: NetUid, + ) -> DispatchResult { + ensure_signed(origin)?; + let mut pos = + LongPositions::::get(netuid, &coldkey).ok_or(Error::::LongPositionNotFound)?; + let mut agg = LongAggregate::::get(netuid); + Self::materialize_long(&mut pos, agg.omega); + ensure!( + pos.r_stored <= LongDust::::get(), + Error::::PositionNotDefaultEligible + ); + ensure!( + Self::get_current_block_as_u64() + >= pos.last_active.saturating_add(LongDefaultGrace::::get()), + Error::::PositionNotDefaultEligible + ); + + // Restore residual R+E Alpha to the pool; floor stays burned (recycled). + Self::increase_provided_alpha_reserve(netuid, pos.r_stored.saturating_add(pos.e_stored)); + + agg.r_sigma = agg.r_sigma.saturating_sub(pos.r_stored); + agg.e_sigma = agg.e_sigma.saturating_sub(pos.e_stored); + agg.b_sigma = agg.b_sigma.saturating_sub(pos.b_stored); + agg.d_sigma = agg.d_sigma.saturating_sub(pos.d_liability); + Self::sync_active_long(netuid, &agg); + LongAggregate::::insert(netuid, agg); + LongPositions::::remove(netuid, &coldkey); + LongPositionCount::::mutate(netuid, |c| *c = c.saturating_sub(1)); + Self::cleanup_long_if_empty(netuid); + + Self::deposit_event(Event::LongDefaulted { coldkey, netuid }); + Ok(()) + } + + // ---- per-block decay + restoration --------------------------------- + + /// O(1)-per-subnet long decay tick; restores decayed Alpha to the pool by + /// minting it back into `SubnetAlphaIn`. + pub fn run_long_decay() { + let active: Vec = LongActiveSubnets::::iter_keys().collect(); + for netuid in active { + let mut agg = LongAggregate::::get(netuid); + if agg.r_sigma.is_zero() && agg.e_sigma.is_zero() && agg.b_sigma.is_zero() { + continue; + } + let delta = Self::long_daily_decay(netuid, agg.b_sigma) + .safe_div(I64F64::from_num(super::BLOCKS_PER_DAY)); + if delta <= I64F64::from_num(0) { + continue; + } + let dr = Self::mul_alpha(agg.r_sigma, delta); + let de = Self::mul_alpha(agg.e_sigma, delta); + let db = Self::mul_alpha(agg.b_sigma, delta); + agg.r_sigma = agg.r_sigma.saturating_sub(dr); + agg.e_sigma = agg.e_sigma.saturating_sub(de); + agg.b_sigma = agg.b_sigma.saturating_sub(db); + agg.omega = agg.omega.saturating_add(Self::neg_ln_one_minus(delta)); + LongAggregate::::insert(netuid, agg); + + // Restoration: mint decayed R+E Alpha back into the pool reserve. + // Unlike the short path (which transfers real TAO from custody and so + // must restore-then-commit to keep custody >= obligations on a failed + // leg), Alpha restoration is an infallible issuance-accounting mint, so + // committing the decayed aggregate first is safe — there is no transfer + // that can fail and leave Omega advanced ahead of restored value. + Self::increase_provided_alpha_reserve(netuid, dr.saturating_add(de)); + } + } + + // ---- terminal deregistration settlement (spec §11.5) --------------- + + /// Settle all longs on a subnet at deregistration: escrow Alpha rejoins the + /// pool; collateral is valued at the price EMA; the alpha covering the TAO + /// debt stays burned (recycled); the equity remainder returns as stake. + pub fn settle_longs_on_dereg(netuid: NetUid) { + let agg = LongAggregate::::get(netuid); + let price = I64F64::from_num(Self::get_moving_alpha_price(netuid)); + // Terminal settlement snapshot (spec §11.1): price every position's + // cover against ONE frozen reserve reference captured before any + // per-position escrow restoration, so per-position equity is independent + // of settlement (storage/key) order — mirror of the short-side fix. + let a_snap = u128::from(SubnetAlphaIn::::get(netuid).to_u64()); + let t_snap = u128::from(SubnetTAO::::get(netuid).to_u64()); + let t_ema_snap = price + .saturating_mul(Self::alpha_f(SubnetAlphaIn::::get(netuid))) + .max(I64F64::from_num(0)) + .saturating_to_num::(); + + let positions: Vec<(T::AccountId, LongPosition)> = + LongPositions::::iter_prefix(netuid).collect(); + + // Split-neutral aggregate pricing (mirror of the short side, spec §10.1). + // The cover CPMM cost `⌈A·d/(T−d)⌉` is convex in `d`, so price the + // aggregate TAO liability `D_Σ` once against the frozen snapshot and + // allocate each position's cover pro-rata by `d_i` (ceiling). `Σ cover_i ≥ + // cover_Σ`, so splitting a liability across coldkeys cannot reduce cover. + let d_sigma: u128 = positions + .iter() + .map(|(_, p)| u128::from(p.d_liability.to_u64())) + .sum(); + let cover_sigma = u128::from(Self::buyback_cost_rao(a_snap, t_snap, d_sigma)).max( + u128::from(Self::buyback_cost_rao(a_snap, t_ema_snap, d_sigma)), + ); + + for (coldkey, mut pos) in positions { + Self::materialize_long(&mut pos, agg.omega); + // Escrow rejoins the pool / terminal distribution. + Self::increase_provided_alpha_reserve(netuid, pos.e_stored); + + // Slippage-aware cover, mirroring the short terminal leg: the Alpha + // required to repay the `D` TAO debt on the CPMM is `⌈A·D/(T−D)⌉ = + // buyback_cost_rao(A, T, D)`. Take the larger of the live and the + // EMA-implied (`T_EMA = pEMA·A_live`) buyback so a suppressed live + // price cannot cheapen the cover (the EMA leg's infimum over `A` is the + // slow scalar `D/pEMA`). Integer rao + ceiling: never under-charges. + let c_l_rao = + u128::from(pos.p_floor.to_u64()).saturating_add(u128::from(pos.r_stored.to_u64())); + let d_rao = u128::from(pos.d_liability.to_u64()); + // Split-neutral allocation: pro-rata share (ceiling) of the aggregate + // cover `cover_Σ` priced on `D_Σ` against the frozen snapshot. Cold EMA + // (`t_ema_snap==0`) saturates `cover_Σ` to u64::MAX ⇒ `cover = c_l` ⇒ + // equity 0 (a cold long can never refund pool-origin R). + let cover_rao = c_l_rao.min(if d_sigma == 0 { + 0 + } else { + cover_sigma.saturating_mul(d_rao).div_ceil(d_sigma) + }); + let equity = AlphaBalance::from( + c_l_rao.saturating_sub(cover_rao).min(u128::from(u64::MAX)) as u64, + ); + if !equity.is_zero() { + Self::increase_stake_for_hotkey_and_coldkey_on_subnet( + &pos.hotkey, + &coldkey, + netuid, + equity, + ); + SubnetAlphaOut::::mutate(netuid, |o| *o = o.saturating_add(equity)); + } + // The cover portion of the collateral stays burned (recycled). + LongPositions::::remove(netuid, &coldkey); + Self::deposit_event(Event::LongTerminalSettled { + coldkey, + netuid, + equity, + }); + } + LongAggregate::::remove(netuid); + LongActiveSubnets::::remove(netuid); + LongPositionCount::::remove(netuid); + } + + // ---- governance setters -------------------------------------------- + + pub fn set_long_kappa_ppb(kappa_ppb: u64) { + let k = kappa_ppb.clamp(1, 2_000_000_000); + LongKappa::::put(I64F64::from_num(k).safe_div(I64F64::from_num(1_000_000_000u64))); + } + pub fn set_long_base_ltv_ppb(ltv_ppb: u64) { + let ltv = ltv_ppb.clamp(1, 999_999_999); + LongBaseLtv::::put(I64F64::from_num(ltv).safe_div(I64F64::from_num(1_000_000_000u64))); + } + pub fn set_long_dust(dust: AlphaBalance) { + LongDust::::put(dust); + } + pub fn set_long_min_input(min_input: AlphaBalance) { + LongMinInput::::put(min_input); + } + + // ---- read-only views (mirror of the short read layer) -------------- + + /// Free TAO balance a coldkey holds (the asset used to repay a long's `D`). + /// `BalanceOf` is u64 rao in this runtime, so `saturated_into::()` is a + /// lossless identity; the saturation is only a guard should the balance type + /// ever be widened. Read-only (used by views), so any clamp is non-consensus. + fn long_tao_held(coldkey: &T::AccountId) -> TaoBalance { + TaoBalance::from(sp_runtime::SaturatedConversion::saturated_into::( + Self::get_coldkey_balance(coldkey), + )) + } + + /// Pure pre-open quote for a covered long. `None` when longs are disabled or + /// the subnet is not a dynamic market. + pub fn quote_open_long(netuid: NetUid, position_input: AlphaBalance) -> Option { + if !LongsEnabled::::get() || SubnetMechanism::::get(netuid) != 1 { + return None; + } + // Mirror the open-time non-user-specific rejections (cold EMA, below-min + // input); capacity + reserve-domain are checked below via the same solves. + if Self::get_moving_alpha_price(netuid) == 0 || position_input < LongMinInput::::get() { + return None; + } + let agg = LongAggregate::::get(netuid); + let a_ref = Self::long_a_ref(netuid); + let p = Self::alpha_f(position_input); + let (c, n) = Self::solve_collateral( + p, + a_ref, + Self::alpha_f(agg.b_sigma), + LongBaseLtv::::get(), + )?; + // Capacity cap `S_L + B_L ≤ κ_L·A_ref` (`LongCapacityExceeded` at open). + if Self::alpha_f(agg.b_sigma).saturating_add(LongBaseLtv::::get().saturating_mul(c)) + > LongKappa::::get().saturating_mul(a_ref) + { + return None; + } + let a_live = Self::alpha_f(SubnetAlphaIn::::get(netuid)); + let t_live = Self::tao_f(SubnetTAO::::get(netuid)); + let phi = Self::solve_phi(n, a_live)?; + let d_tao = Self::to_tao(phi.saturating_mul(t_live)); + let scale = I64F64::from_num(1_000_000_000u64); + Some(LongOpenQuote { + gross_collateral: Self::to_alpha(c), + retained_proceeds: Self::to_alpha(n), + tao_liability: d_tao, + escrow: Self::to_alpha(phi.saturating_mul(a_live)), + effective_ltv: n + .safe_div(c) + .saturating_mul(scale) + .saturating_to_num::(), + daily_decay: Self::long_daily_decay(netuid, agg.b_sigma) + .saturating_mul(scale) + .saturating_to_num::(), + est_close_cost: d_tao, + }) + } + + /// Estimated blocks until `r_current` decays to dust at the current rate. + fn long_blocks_to_dust(netuid: NetUid, r_current: AlphaBalance, b_sigma: AlphaBalance) -> u64 { + let dust = LongDust::::get(); + if r_current <= dust || dust.is_zero() { + return if r_current <= dust { 0 } else { u64::MAX }; + } + let delta = Self::long_daily_decay(netuid, b_sigma) + .safe_div(I64F64::from_num(super::BLOCKS_PER_DAY)); + if delta <= I64F64::from_num(0) { + return u64::MAX; + } + let neg_ln_g = Self::neg_ln_one_minus(delta); + if neg_ln_g <= I64F64::from_num(0) { + return u64::MAX; + } + let ratio = Self::alpha_f(r_current).safe_div(Self::alpha_f(dust)); + match ratio.checked_ln() { + Some(ln_ratio) if ln_ratio > I64F64::from_num(0) => { + ln_ratio.safe_div(neg_ln_g).saturating_to_num::() + } + _ => 0, + } + } + + /// Materialized, health-rich view of one long position. + pub fn get_long_position( + coldkey: &T::AccountId, + netuid: NetUid, + ) -> Option> { + let mut pos = LongPositions::::get(netuid, coldkey)?; + let agg = LongAggregate::::get(netuid); + Self::materialize_long(&mut pos, agg.omega); + + let scale = I64F64::from_num(1_000_000_000u64); + let now = Self::get_current_block_as_u64(); + let defaultable_at_block = pos.last_active.saturating_add(LongDefaultGrace::::get()); + let default_eligible = pos.r_stored <= LongDust::::get() && now >= defaultable_at_block; + let tao_held = Self::long_tao_held(coldkey); + let d = pos.d_liability; + Some(LongPositionInfo { + netuid, + hotkey: pos.hotkey.clone(), + floor: pos.p_floor, + tao_liability: d, + buffer: pos.r_stored, + escrow: pos.e_stored, + collateral_claim: pos.p_floor.saturating_add(pos.r_stored), + daily_decay: Self::long_daily_decay(netuid, agg.b_sigma) + .saturating_mul(scale) + .saturating_to_num::(), + blocks_to_dust: Self::long_blocks_to_dust(netuid, pos.r_stored, agg.b_sigma), + default_eligible, + defaultable_at_block, + est_close_cost: d, + tao_held, + tao_needed: TaoBalance::from(d.to_u64().saturating_sub(tao_held.to_u64())), + }) + } + + /// All of a coldkey's long positions across subnets. + pub fn get_long_positions(coldkey: &T::AccountId) -> Vec> { + Self::get_all_subnet_netuids() + .into_iter() + .filter_map(|netuid| Self::get_long_position(coldkey, netuid)) + .collect() + } + + /// Per-subnet long market state for sizing and capacity decisions. + pub fn get_subnet_long_state(netuid: NetUid) -> Option { + if !Self::if_subnet_exist(netuid) { + return None; + } + let agg = LongAggregate::::get(netuid); + let a_ref = Self::long_a_ref(netuid); + let cap = LongKappa::::get().saturating_mul(a_ref); + let used = Self::alpha_f(agg.b_sigma); + let scale = I64F64::from_num(1_000_000_000u64); + let ppb = |x: I64F64| x.saturating_mul(scale).saturating_to_num::(); + Some(LongMarketInfo { + longs_enabled: LongsEnabled::::get(), + base_ltv: ppb(LongBaseLtv::::get()), + kappa: ppb(LongKappa::::get()), + decay_min: ppb(DecayMin::::get()), + decay_max: ppb(DecayMax::::get()), + current_daily_decay: ppb(Self::long_daily_decay(netuid, agg.b_sigma)), + a_ref: Self::to_alpha(a_ref), + footprint_used: agg.b_sigma, + footprint_cap: Self::to_alpha(cap), + footprint_remaining: Self::to_alpha(cap.saturating_sub(used)), + open_interest_tao: agg.d_sigma, + buffer_total: agg.r_sigma, + escrow_total: agg.e_sigma, + dust_threshold: LongDust::::get(), + min_input: LongMinInput::::get(), + default_grace: LongDefaultGrace::::get(), + }) + } + + /// Pre-close quote for `fraction_ppb / 1e9` of a long position. + pub fn quote_close_long( + coldkey: &T::AccountId, + netuid: NetUid, + fraction_ppb: u64, + ) -> Option { + if fraction_ppb == 0 || fraction_ppb > 1_000_000_000 { + return None; + } + let mut pos = LongPositions::::get(netuid, coldkey)?; + let agg = LongAggregate::::get(netuid); + Self::materialize_long(&mut pos, agg.omega); + let rho = I64F64::from_num(fraction_ppb).safe_div(I64F64::from_num(1_000_000_000u64)); + + let repay_tao = Self::mul_tao(pos.d_liability, rho); + let tao_held = Self::long_tao_held(coldkey); + Some(CloseLongQuote { + repay_tao, + returned_alpha: Self::mul_alpha(pos.p_floor, rho) + .saturating_add(Self::mul_alpha(pos.r_stored, rho)), + escrow_settled: Self::mul_alpha(pos.e_stored, rho), + tao_held, + tao_needed: TaoBalance::from(repay_tao.to_u64().saturating_sub(tao_held.to_u64())), + }) + } +} diff --git a/pallets/subtensor/src/derivatives/mod.rs b/pallets/subtensor/src/derivatives/mod.rs new file mode 100644 index 0000000000..c8ba5a04f6 --- /dev/null +++ b/pallets/subtensor/src/derivatives/mod.rs @@ -0,0 +1,1049 @@ +//! Fixed-liability covered continuous-unwind derivatives (spec v3.6.1). +//! +//! Both sides are implemented and independently gated (`ShortsEnabled` / +//! `LongsEnabled`, both default-off). Shorts live here; the long mirror is in +//! `long.rs`. The client/RPC read layer (`quote_*`, `get_*`) exists for both +//! sides (short views here, long views in `long.rs`). +//! +//! Custody model. Shorts park floor/buffer/escrow TAO in a dedicated per-subnet +//! custody account; longs have no custody account and instead track parked Alpha +//! via issuance accounting (burned at open, minted back on restore/close). Pool +//! reserves, `TotalStake`, and issuance move in lockstep and derivative legs +//! never write TaoFlow. +//! +//! Custody solvency invariant. `custody_balance(netuid)` (shorts) and the burned +//! Alpha (longs) equal `Σ materialized (P + R(t) + E(t))` **to within per-block +//! floor rounding**. The aggregate Σ-decay floors faster than the per-position +//! `exp` decay, so the drift is always in the safe direction (custody ≥ +//! obligations); residual dust is reclaimed by the terminal sweep at dereg. + +use super::*; +use frame_support::traits::tokens::{Fortitude, Precision, Preservation, fungible::Balanced}; +use safe_math::FixedExt; +use sp_runtime::traits::AccountIdConversion; +use substrate_fixed::types::I64F64; +use subtensor_runtime_common::Token; + +pub mod long; +pub mod types; +pub use types::*; + +/// 12s blocks → 7200 per day. Decay rates are pro-rated per block. +const BLOCKS_PER_DAY: u64 = 7200; +/// Bisection tolerance for fixed-point square roots. +fn sqrt_eps() -> I64F64 { + I64F64::from_num(0.000_000_001) +} + +impl Pallet { + // ---- conversions ---------------------------------------------------- + + fn tao_f(t: TaoBalance) -> I64F64 { + I64F64::from_num(t.to_u64()) + } + fn alpha_f(a: AlphaBalance) -> I64F64 { + I64F64::from_num(a.to_u64()) + } + fn to_tao(x: I64F64) -> TaoBalance { + TaoBalance::from(x.max(I64F64::from_num(0)).saturating_to_num::()) + } + fn to_alpha(x: I64F64) -> AlphaBalance { + AlphaBalance::from(x.max(I64F64::from_num(0)).saturating_to_num::()) + } + fn mul_tao(t: TaoBalance, f: I64F64) -> TaoBalance { + Self::to_tao(Self::tao_f(t).saturating_mul(f)) + } + fn mul_alpha(a: AlphaBalance, f: I64F64) -> AlphaBalance { + Self::to_alpha(Self::alpha_f(a).saturating_mul(f)) + } + + // ---- accounts ------------------------------------------------------- + + /// Per-subnet account holding parked derivative TAO (floor + buffer + escrow). + /// Distinct from the subnet pool account so pool reserves are never polluted. + pub fn short_custody_account(netuid: NetUid) -> T::AccountId { + T::SubtensorPalletId::get().into_sub_account_truncating(("shrt", u16::from(netuid))) + } + + /// Recycle TAO out of the protocol custody account (reduce issuance). Unlike + /// `recycle_tao`, this does not preserve an existential deposit, so the + /// custody account can be drained to zero. + fn recycle_custody_tao(custody: &T::AccountId, amount: TaoBalance) { + if amount.is_zero() { + return; + } + // Never recycle (and never reduce issuance by) more than is actually + // held: caps an `Exact` withdraw failure that would desync issuance. + let amt = Self::get_coldkey_balance(custody).min(amount.into()); + if amt.is_zero() { + return; + } + // Withdraw first; reduce issuance only after the funds are confirmed + // removed, so a (capped, Force) withdraw shortfall can never desync + // TotalIssuance. (Canonical `recycle_tao` reduces-then-withdraws but + // propagates the error via `?` for transactional rollback; this helper + // returns `()` and runs in the non-transactional dereg sweep, so it + // checks the withdraw result inline instead.) + if ::Currency::withdraw( + custody, + amt, + Precision::Exact, + Preservation::Expendable, + Fortitude::Force, + ) + .is_ok() + { + TotalIssuance::::mutate(|ti| *ti = ti.saturating_sub(amt)); + } + } + + // ---- references (spec §3, §4) -------------------------------------- + + /// Conservative TAO reference `T_ref = min(T_live, T_EMA)`, with + /// `T_EMA = pEMA · A_live` reconstructed from the existing price EMA. + fn short_t_ref(netuid: NetUid) -> I64F64 { + let t_live = Self::tao_f(SubnetTAO::::get(netuid)); + let a_live = Self::alpha_f(SubnetAlphaIn::::get(netuid)); + let pema = I64F64::from_num(Self::get_moving_alpha_price(netuid)); + // `pema` is the upstream `min(spot,1.0)`-clamped moving price, so `pema ≤ ~1` + // and `pema·a_live ≤ a_live (≤ ~2e16 rao)` stays well inside I64F64 — no + // saturation. (The guarantees here hold for price ≤ ~1.0; see DESIGN.md.) + let t_ema = pema.saturating_mul(a_live); + // A cold price EMA (`pema == 0`, e.g. a freshly created subnet) must not + // lock the market; fall back to the live reserve until it warms up. + if t_ema <= I64F64::from_num(0) { + t_live + } else { + t_live.min(t_ema) + } + } + + /// Convex decay curve `d(u) = d_min + (d_max − d_min)·u²` (spec §6.2), + /// shared by both sides (the rate is denomination-agnostic). + fn decay_curve(u: I64F64) -> I64F64 { + let dmin = DecayMin::::get(); + let dmax = DecayMax::::get(); + dmin.saturating_add( + dmax.saturating_sub(dmin) + .saturating_mul(u) + .saturating_mul(u), + ) + } + + /// Utilization ratio `min(1, S / cap)`. + fn utilization(s: I64F64, cap: I64F64) -> I64F64 { + if cap > I64F64::from_num(0) { + s.safe_div(cap).min(I64F64::from_num(1)) + } else { + I64F64::from_num(0) + } + } + + /// Current short daily decay rate at the live short footprint. + fn short_daily_decay(netuid: NetUid, b_sigma: TaoBalance) -> I64F64 { + let cap = ShortKappa::::get().saturating_mul(Self::short_t_ref(netuid)); + Self::decay_curve(Self::utilization(Self::tao_f(b_sigma), cap)) + } + + // ---- open-time math (spec §4.1–4.3, Appendix A.1) ------------------- + + /// Solve gross collateral `C` and retained proceeds `N` from input `P` + /// (spec §4.2). Side-agnostic: `ref_reserve` is `T_ref` for shorts / `A_ref` + /// for longs, `lambda` the per-side base LTV. Returns `None` if `N ≤ 0`. + fn solve_collateral( + p: I64F64, + ref_reserve: I64F64, + s: I64F64, + lambda: I64F64, + ) -> Option<(I64F64, I64F64)> { + let t_ref = ref_reserve; + if t_ref <= I64F64::from_num(0) || lambda <= I64F64::from_num(0) { + return None; + } + let one = I64F64::from_num(1); + let two = I64F64::from_num(2); + let four = I64F64::from_num(4); + // a = λ²/T_ref ; b = 1 − λ + 2λS/T_ref + let a = lambda.saturating_mul(lambda).safe_div(t_ref); + let b = one + .saturating_sub(lambda) + .saturating_add(two.saturating_mul(lambda).saturating_mul(s).safe_div(t_ref)); + // Positive root of `a·C² + b·C − P = 0`. Use the cancellation-stable form + // C = 2P / (b + √(b² + 4aP)) + // rather than the algebraically-equal `(√(b²+4aP) − b) / 2a`: the latter + // subtracts two nearly-equal positives when `4aP ≪ b²` (small `a` = large + // pool / small position) and then divides by the tiny `2a`, compounding the + // catastrophic cancellation; the stable form sums two positives and never + // divides by `a` (it also limits gracefully to `P/b` as `a → 0`). This + // follows the codebase's preference for numerically-robust fixed-point math. + let disc = b + .saturating_mul(b) + .saturating_add(four.saturating_mul(a).saturating_mul(p)); + let root = disc.checked_sqrt(sqrt_eps())?; + let c = two.saturating_mul(p).safe_div(b.saturating_add(root)); + let n = c.saturating_sub(p); + if n <= I64F64::from_num(0) || c <= I64F64::from_num(0) { + return None; + } + Some((c, n)) + } + + /// Pool fraction `ϕ = (1 − √(1 − 4N/T))/2` (spec §4.3). Returns `None` if the + /// remove-and-sell-back domain `4N ≤ T` fails. + fn solve_phi(n: I64F64, t_live: I64F64) -> Option { + if t_live <= I64F64::from_num(0) { + return None; + } + let one = I64F64::from_num(1); + let four = I64F64::from_num(4); + let frac = four.saturating_mul(n).safe_div(t_live); + if frac > one { + return None; + } + let root = one.saturating_sub(frac).checked_sqrt(sqrt_eps())?; + Some(one.saturating_sub(root).safe_div(I64F64::from_num(2))) + } + + /// Keep the active-short-subnet set in sync with the aggregate: a subnet is + /// tracked iff it still has any live short state. The per-block decay tick + /// iterates only this set instead of every subnet. + fn sync_active_short(netuid: NetUid, agg: &ShortAgg) { + if agg.r_sigma.is_zero() + && agg.e_sigma.is_zero() + && agg.b_sigma.is_zero() + && agg.q_sigma.is_zero() + { + ShortActiveSubnets::::remove(netuid); + } else { + ShortActiveSubnets::::insert(netuid, ()); + } + } + + /// `−ln(1 − δ) = δ + δ²/2 + δ³/3 + …` for the small per-block decay `δ`. + /// + /// Computed directly from the series rather than `checked_ln(1 − δ)`, which + /// is imprecise (and can return the wrong sign) for arguments just below 1. + /// This keeps the aggregate factor `g = 1 − δ` and the per-position factor + /// `exp(−ΔΩ) = Π g` consistent to within per-block floor rounding (the + /// 3-term series and `checked_exp`'s 7-term series are both truncations). + fn neg_ln_one_minus(delta: I64F64) -> I64F64 { + let d2 = delta.saturating_mul(delta); + let d3 = d2.saturating_mul(delta); + delta + .saturating_add(d2.saturating_mul(I64F64::from_num(0.5))) + .saturating_add(d3.saturating_mul(I64F64::from_num(1.0 / 3.0))) + } + + /// When the last position on a subnet closes, drop the aggregate and the + /// active-set entry so the per-block decay tick stops visiting it (otherwise + /// floor-rounding dust in `r_sigma` keeps the subnet "active" forever). Any + /// residual custody dust is reclaimed by the terminal sweep at dereg. + fn cleanup_short_if_empty(netuid: NetUid) { + if ShortPositionCount::::get(netuid) == 0 { + ShortAggregate::::remove(netuid); + ShortActiveSubnets::::remove(netuid); + } + } + + /// Materialize a position to the current accumulator: `f = exp(−(Ω − Ω_entry))`. + fn materialize_short(pos: &mut ShortPosition, omega_now: I64F64) { + // `Ω` only ever grows, so `arg ≤ 0` and `f ≤ 1` (decay never inflates). + // The `unwrap_or(0)` below is correct, not a silent failure: a large + // negative `arg` legitimately decays the buffer toward 0. Clamp `arg ≤ 0` + // defensively so an (impossible) positive `arg` can't yield `f > 1`. + let arg = pos + .omega_entry + .saturating_sub(omega_now) + .min(I64F64::from_num(0)); + let f = arg.checked_exp().unwrap_or_else(|| I64F64::from_num(0)); + pos.r_stored = Self::mul_tao(pos.r_stored, f); + pos.e_stored = Self::mul_tao(pos.e_stored, f); + pos.b_stored = Self::mul_tao(pos.b_stored, f); + pos.omega_entry = omega_now; + } + + // ---- user operations (spec §8) ------------------------------------- + + /// Open (or merge into) a covered short (spec §8.1, §8.6). + #[frame_support::transactional] + pub fn do_open_short( + origin: OriginFor, + hotkey: T::AccountId, + netuid: NetUid, + position_input: TaoBalance, + max_alpha_liability: AlphaBalance, + ) -> DispatchResult { + let coldkey = ensure_signed(origin)?; + ensure!(ShortsEnabled::::get(), Error::::ShortsDisabled); + ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); + ensure!( + SubnetMechanism::::get(netuid) == 1, + Error::::SubnetNotDynamic + ); + // Warm-EMA guard: a cold `pEMA` (freshly registered subnet, no price + // history) makes `short_t_ref` fall back to the live reserve and the + // terminal `K_EMA` anti-suppression leg unavailable, so opens are blocked + // until the EMA warms. (Profit on a cold-EMA dereg is already floored by + // the cold-EMA `K_D ≥ R` guard and subnet immunity; this also removes the + // griefing-pressure surface on fresh subnets.) + ensure!( + Self::get_moving_alpha_price(netuid) > 0, + Error::::ColdEmaNotAllowed + ); + ensure!( + position_input >= ShortMinInput::::get(), + Error::::AmountTooLow + ); + + let mut agg = ShortAggregate::::get(netuid); + let t_ref = Self::short_t_ref(netuid); + let p = Self::tao_f(position_input); + + let (c, n) = + Self::solve_collateral(p, t_ref, Self::tao_f(agg.b_sigma), ShortBaseLtv::::get()) + .ok_or(Error::::EffectiveLtvNonPositive)?; + let b = ShortBaseLtv::::get().saturating_mul(c); + + // Capacity: S + B ≤ κ_S · T_ref (also bounds same-block stacked opens). + ensure!( + Self::tao_f(agg.b_sigma).saturating_add(b) + <= ShortKappa::::get().saturating_mul(t_ref), + Error::::ShortCapacityExceeded + ); + + let t_live = Self::tao_f(SubnetTAO::::get(netuid)); + let a_live = Self::alpha_f(SubnetAlphaIn::::get(netuid)); + let phi = Self::solve_phi(n, t_live).ok_or(Error::::ReserveDomainExceeded)?; + + let n_tao = Self::to_tao(n); + let e_tao = Self::to_tao(phi.saturating_mul(t_live)); + let b_tao = Self::to_tao(b); + let q_alpha = Self::to_alpha(phi.saturating_mul(a_live)); + ensure!(!n_tao.is_zero(), Error::::RetainedProceedsNonPositive); + + // Caller-signed execution bound (anti-sandwich): the alpha liability + // derived from live reserves at inclusion must not exceed the maximum the + // trader accepted. `AlphaBalance::MAX` opts out of the bound. + ensure!(q_alpha <= max_alpha_liability, Error::::SlippageTooHigh); + + // Validate-before-mutate: all fallible eligibility checks that do not + // depend on the realized legs run BEFORE any funds move, so a rejected + // open never strands custody TAO or desyncs pool/`TotalStake` accounting. + if let Some(existing) = ShortPositions::::get(netuid, &coldkey) { + ensure!(existing.hotkey == hotkey, Error::::ShortHotkeyMismatch); + } + + let custody = Self::short_custody_account(netuid); + let subnet_account = + Self::get_subnet_account_id(netuid).ok_or(Error::::SubnetNotExists)?; + + // 1. Trader posts floor P into custody (fails early if underfunded). + Self::transfer_tao(&coldkey, &custody, position_input.into())?; + // 2. Remove N+E TAO from the pool into custody (the downward price impact). + let removed = n_tao.saturating_add(e_tao); + Self::transfer_tao(&subnet_account, &custody, removed.into())?; + Self::decrease_provided_tao_reserve(netuid, removed); + TotalStake::::mutate(|t| *t = t.saturating_sub(removed)); + + let block = Self::get_current_block_as_u64(); + let pos = match ShortPositions::::get(netuid, &coldkey) { + Some(mut existing) => { + // Hotkey match was validated before any mutation above. + Self::materialize_short(&mut existing, agg.omega); + existing.p_floor = existing.p_floor.saturating_add(position_input); + existing.q_liability = existing.q_liability.saturating_add(q_alpha); + existing.r_stored = existing.r_stored.saturating_add(n_tao); + existing.e_stored = existing.e_stored.saturating_add(e_tao); + existing.b_stored = existing.b_stored.saturating_add(b_tao); + existing.last_active = block; + existing + } + None => { + // No per-subnet position cap (terminal dereg is an immediate + // enumerate-and-settle sweep, like native alpha liquidation). Bump + // the denormalized count used for the dereg settlement weight charge + // and empty/active-set detection. + let count = ShortPositionCount::::get(netuid); + ShortPositionCount::::insert(netuid, count.saturating_add(1)); + ShortPosition { + hotkey, + p_floor: position_input, + q_liability: q_alpha, + r_stored: n_tao, + e_stored: e_tao, + b_stored: b_tao, + omega_entry: agg.omega, + last_active: block, + } + } + }; + ShortPositions::::insert(netuid, &coldkey, pos); + + agg.r_sigma = agg.r_sigma.saturating_add(n_tao); + agg.e_sigma = agg.e_sigma.saturating_add(e_tao); + agg.b_sigma = agg.b_sigma.saturating_add(b_tao); + agg.q_sigma = agg.q_sigma.saturating_add(q_alpha); + ShortAggregate::::insert(netuid, agg); + ShortActiveSubnets::::insert(netuid, ()); + + Self::deposit_event(Event::ShortOpened { + coldkey, + netuid, + position_input, + retained_proceeds: n_tao, + alpha_liability: q_alpha, + escrow: e_tao, + }); + Ok(()) + } + + /// Top up the carry buffer `R` with fresh capital (spec §8.2). + #[frame_support::transactional] + pub fn do_top_up_short( + origin: OriginFor, + netuid: NetUid, + amount: TaoBalance, + ) -> DispatchResult { + let coldkey = ensure_signed(origin)?; + ensure!(!amount.is_zero(), Error::::AmountTooLow); + let mut pos = + ShortPositions::::get(netuid, &coldkey).ok_or(Error::::ShortPositionNotFound)?; + let mut agg = ShortAggregate::::get(netuid); + Self::materialize_short(&mut pos, agg.omega); + + Self::transfer_tao( + &coldkey, + &Self::short_custody_account(netuid), + amount.into(), + )?; + pos.r_stored = pos.r_stored.saturating_add(amount); + pos.last_active = Self::get_current_block_as_u64(); + agg.r_sigma = agg.r_sigma.saturating_add(amount); + + ShortPositions::::insert(netuid, &coldkey, pos); + ShortAggregate::::insert(netuid, agg); + Self::deposit_event(Event::ShortToppedUp { + coldkey, + netuid, + amount, + }); + Ok(()) + } + + /// Partial (`fraction_ppb < 1e9`) or full (`= 1e9`) close (spec §8.3–8.5). + #[frame_support::transactional] + pub fn do_close_short( + origin: OriginFor, + netuid: NetUid, + fraction_ppb: u64, + ) -> DispatchResult { + let coldkey = ensure_signed(origin)?; + ensure!( + fraction_ppb > 0 && fraction_ppb <= 1_000_000_000, + Error::::InvalidCloseFraction + ); + let rho = I64F64::from_num(fraction_ppb).safe_div(I64F64::from_num(1_000_000_000u64)); + + let mut pos = + ShortPositions::::get(netuid, &coldkey).ok_or(Error::::ShortPositionNotFound)?; + let mut agg = ShortAggregate::::get(netuid); + Self::materialize_short(&mut pos, agg.omega); + + let q_close = Self::mul_alpha(pos.q_liability, rho); + let r_close = Self::mul_tao(pos.r_stored, rho); + let e_close = Self::mul_tao(pos.e_stored, rho); + let p_close = Self::mul_tao(pos.p_floor, rho); + let b_close = Self::mul_tao(pos.b_stored, rho); + + // Trader repays ρQ alpha from staked balance at the position hotkey. + ensure!( + Self::get_stake_for_hotkey_and_coldkey_on_subnet(&pos.hotkey, &coldkey, netuid) + >= q_close, + Error::::InsufficientAlphaToClose + ); + // Guard against minting alpha: the repaid `q_close` must come out of + // outstanding stake, never saturate `SubnetAlphaOut` to zero. + ensure!( + SubnetAlphaOut::::get(netuid) >= q_close, + Error::::InsufficientAlphaToClose + ); + // The repayment alpha must be unlocked (respect stake locks like unstake). + Self::ensure_available_to_unstake(&coldkey, netuid, q_close)?; + Self::decrease_stake_for_hotkey_and_coldkey_on_subnet( + &pos.hotkey, + &coldkey, + netuid, + q_close, + ); + SubnetAlphaOut::::mutate(netuid, |o| *o = o.saturating_sub(q_close)); + Self::increase_provided_alpha_reserve(netuid, q_close); + + let custody = Self::short_custody_account(netuid); + let subnet_account = + Self::get_subnet_account_id(netuid).ok_or(Error::::SubnetNotExists)?; + // Settle escrow ρE back to the pool, return ρ(P+R) to the trader. + if !e_close.is_zero() { + Self::transfer_tao(&custody, &subnet_account, e_close.into())?; + Self::increase_provided_tao_reserve(netuid, e_close); + TotalStake::::mutate(|t| *t = t.saturating_add(e_close)); + } + let returned = p_close.saturating_add(r_close); + if !returned.is_zero() { + Self::transfer_tao(&custody, &coldkey, returned.into())?; + } + + pos.q_liability = pos.q_liability.saturating_sub(q_close); + pos.r_stored = pos.r_stored.saturating_sub(r_close); + pos.e_stored = pos.e_stored.saturating_sub(e_close); + pos.p_floor = pos.p_floor.saturating_sub(p_close); + pos.b_stored = pos.b_stored.saturating_sub(b_close); + + agg.q_sigma = agg.q_sigma.saturating_sub(q_close); + agg.r_sigma = agg.r_sigma.saturating_sub(r_close); + agg.e_sigma = agg.e_sigma.saturating_sub(e_close); + agg.b_sigma = agg.b_sigma.saturating_sub(b_close); + Self::sync_active_short(netuid, &agg); + ShortAggregate::::insert(netuid, agg); + + if fraction_ppb == 1_000_000_000 || pos.p_floor.is_zero() { + ShortPositions::::remove(netuid, &coldkey); + ShortPositionCount::::mutate(netuid, |c| *c = c.saturating_sub(1)); + Self::cleanup_short_if_empty(netuid); + } else { + ShortPositions::::insert(netuid, &coldkey, pos); + } + Self::deposit_event(Event::ShortClosed { + coldkey, + netuid, + fraction_ppb, + repaid_alpha: q_close, + returned, + }); + Ok(()) + } + + /// Permissionless default once the buffer has decayed to dust (spec §7.4). + #[frame_support::transactional] + pub fn do_default_short( + origin: OriginFor, + coldkey: T::AccountId, + netuid: NetUid, + ) -> DispatchResult { + ensure_signed(origin)?; + let mut pos = + ShortPositions::::get(netuid, &coldkey).ok_or(Error::::ShortPositionNotFound)?; + let mut agg = ShortAggregate::::get(netuid); + Self::materialize_short(&mut pos, agg.omega); + ensure!( + pos.r_stored <= ShortDust::::get(), + Error::::PositionNotDefaultEligible + ); + // Anti-snipe: a third party cannot default within the grace window after + // the owner's last action, so the owner always has time to top up. + ensure!( + Self::get_current_block_as_u64() + >= pos + .last_active + .saturating_add(ShortDefaultGrace::::get()), + Error::::PositionNotDefaultEligible + ); + + let custody = Self::short_custody_account(netuid); + let subnet_account = + Self::get_subnet_account_id(netuid).ok_or(Error::::SubnetNotExists)?; + // Restore residual R+E to the pool; recycle the floor P; extinguish Q. + let residual = pos.r_stored.saturating_add(pos.e_stored); + if !residual.is_zero() { + Self::transfer_tao(&custody, &subnet_account, residual.into())?; + Self::increase_provided_tao_reserve(netuid, residual); + TotalStake::::mutate(|t| *t = t.saturating_add(residual)); + } + Self::recycle_custody_tao(&custody, pos.p_floor); + + agg.r_sigma = agg.r_sigma.saturating_sub(pos.r_stored); + agg.e_sigma = agg.e_sigma.saturating_sub(pos.e_stored); + agg.b_sigma = agg.b_sigma.saturating_sub(pos.b_stored); + agg.q_sigma = agg.q_sigma.saturating_sub(pos.q_liability); + Self::sync_active_short(netuid, &agg); + ShortAggregate::::insert(netuid, agg); + ShortPositions::::remove(netuid, &coldkey); + ShortPositionCount::::mutate(netuid, |c| *c = c.saturating_sub(1)); + Self::cleanup_short_if_empty(netuid); + + Self::deposit_event(Event::ShortDefaulted { coldkey, netuid }); + Ok(()) + } + + // ---- per-block decay + restoration (spec §6.4–6.5, §12.4) ---------- + + /// O(1)-per-subnet aggregate decay tick with one-sided TAO restoration zap. + /// Iterates only subnets with live short state (`ShortActiveSubnets`), whose + /// size is bounded by the total subnet count, so the per-block hook cost is + /// O(active subnets) with O(1) work each. Metered in `on_initialize` via + /// `WeightInfo::run_short_decay(TotalNetworks)` (benchmarked over [0,128]). + pub fn run_short_decay() { + let active: Vec = ShortActiveSubnets::::iter_keys().collect(); + for netuid in active { + let mut agg = ShortAggregate::::get(netuid); + if agg.r_sigma.is_zero() && agg.e_sigma.is_zero() && agg.b_sigma.is_zero() { + continue; + } + let d_day = Self::short_daily_decay(netuid, agg.b_sigma); + let delta = d_day.safe_div(I64F64::from_num(BLOCKS_PER_DAY)); + if delta <= I64F64::from_num(0) { + continue; + } + let dr = Self::mul_tao(agg.r_sigma, delta); + let de = Self::mul_tao(agg.e_sigma, delta); + let db = Self::mul_tao(agg.b_sigma, delta); + let restore = dr.saturating_add(de); + + // Restoration zap FIRST, then commit the decay. The decayed R+E is moved + // from custody into the pool; only if that transfer actually lands do we + // advance Ω, shrink the aggregates, and credit reserves. If it fails + // (e.g. a dust shortfall) we leave the aggregate AND Ω untouched and + // retry next block — so the per-position `exp(−ΔΩ)` materialization can + // never decay ahead of TAO that is still sitting in custody (the + // custody ≥ obligations invariant holds even on a failed transfer, and + // a short custody can never inflate `SubnetTAO` / `TotalStake`). + if !restore.is_zero() { + let subnet_account = match Self::get_subnet_account_id(netuid) { + Some(a) => a, + None => continue, + }; + if Self::transfer_tao( + &Self::short_custody_account(netuid), + &subnet_account, + restore.into(), + ) + .is_err() + { + continue; + } + Self::increase_provided_tao_reserve(netuid, restore); + TotalStake::::mutate(|t| *t = t.saturating_add(restore)); + } + + agg.r_sigma = agg.r_sigma.saturating_sub(dr); + agg.e_sigma = agg.e_sigma.saturating_sub(de); + agg.b_sigma = agg.b_sigma.saturating_sub(db); + // Ω ← Ω + (−ln(1−δ)), so a later exp(−ΔΩ) reproduces Π(1−δ) exactly. + agg.omega = agg.omega.saturating_add(Self::neg_ln_one_minus(delta)); + ShortAggregate::::insert(netuid, agg); + } + } + + // ---- terminal deregistration settlement (spec §11.4) --------------- + + /// Settle all shorts on a subnet at deregistration. Must run before the + /// pool is drained so restored escrow joins the terminal distribution. + pub fn settle_shorts_on_dereg(netuid: NetUid) { + let agg = ShortAggregate::::get(netuid); + let pema = I64F64::from_num(Self::get_moving_alpha_price(netuid)); + let custody = Self::short_custody_account(netuid); + let subnet_account = match Self::get_subnet_account_id(netuid) { + Some(a) => a, + None => return, + }; + + // Terminal settlement snapshot (spec §11.1): every position's K_D is + // priced against ONE frozen reserve reference captured before any + // per-position escrow restoration, so per-position equity is independent + // of settlement (storage/key) order. The escrow restorations in the loop + // move real TAO into the pool for terminal distribution but are NOT + // admitted into this pricing snapshot — otherwise a position settled + // later would be priced against the escrow already restored by earlier + // positions (order-dependent, unfair, grindable by coldkey hash). + let a_snap = u128::from(SubnetAlphaIn::::get(netuid).to_u64()); + let t_snap = u128::from(SubnetTAO::::get(netuid).to_u64()); + let t_ema_snap = pema + .saturating_mul(Self::alpha_f(SubnetAlphaIn::::get(netuid))) + .max(I64F64::from_num(0)) + .saturating_to_num::(); + + let positions: Vec<(T::AccountId, ShortPosition)> = + ShortPositions::::iter_prefix(netuid).collect(); + + // Split-neutral aggregate pricing (spec §10.1 anti-splitting). The CPMM + // buyback `K(q)=⌈T·q/(A−q)⌉` is CONVEX, so pricing each position + // independently would let a whale split one liability across many coldkeys + // to cut total terminal cover (`Σ K(q_i) < K(ΣQ)`). Instead price the + // buyback ONCE for the aggregate liability `Q_Σ` against the frozen + // snapshot, then allocate each position's cover pro-rata by `q_i` with + // ceiling rounding. Then `Σ k_i ≥ K_Σ` regardless of how the liability is + // split, so splitting never reduces total cover. `q_liability` is fixed + // (does not decay), so summing the pre-materialized positions is exact. + let q_sigma: u128 = positions + .iter() + .map(|(_, p)| u128::from(p.q_liability.to_u64())) + .sum(); + let k_sigma = u128::from(Self::buyback_cost_rao(t_snap, a_snap, q_sigma)).max(u128::from( + Self::buyback_cost_rao(t_ema_snap, a_snap, q_sigma), + )); + + for (coldkey, mut pos) in positions { + Self::materialize_short(&mut pos, agg.omega); + + // Escrow returns to the pool (joins terminal distribution). Credit + // reserves only on a successful transfer. The transfer cannot fail + // under the custody-≥-obligations invariant; if it ever does, log + // loudly (rather than silently) — the un-transferred escrow stays in + // custody and is reclaimed by the terminal sweep below, so accounting + // is preserved, but a failure means the invariant was violated. + if !pos.e_stored.is_zero() { + if Self::transfer_tao(&custody, &subnet_account, pos.e_stored.into()).is_ok() { + Self::increase_provided_tao_reserve(netuid, pos.e_stored); + TotalStake::::mutate(|t| *t = t.saturating_add(pos.e_stored)); + } else { + log::error!( + "derivatives: short terminal escrow restore failed (custody invariant breach) netuid={netuid:?} e={:?}", + pos.e_stored + ); + } + } + + // K_D(Q) = max(K_spot,last, K_EMA), both slippage-aware (spec §11.4, §13.6). + // + // K_spot uses live reserves; K_EMA prices the buyback against the + // EMA-implied reserve `T_EMA = pEMA·A_live`. Two reasons the EMA leg is + // a CPMM buyback (not the scalar `Q·pEMA`): + // 1. A scalar price understates the true cost of acquiring a large Q + // (spec §13.6) — slippage must be charged so terminal extraction is + // bounded by what closing actually costs. + // 2. An attacker who shorts a subnet to force its deregistration + // suppresses the *live* price, which would cheapen K_spot. Pricing + // the EMA leg off the slow `pEMA` keeps K_D high, so the carry paid + // while waiting for dereg is not refunded at settlement. Provided + // `pEMA` is slow enough (governance: SubnetMovingPrice half-life) + // and the max price lift is capped (κ), the attacker's carry + + // bounded equity recovery exceeds any forced-slot-acquisition gain. + let c_rao = + u128::from(pos.p_floor.to_u64()).saturating_add(u128::from(pos.r_stored.to_u64())); + let q_rao = u128::from(pos.q_liability.to_u64()); + // Split-neutral allocation: this position's cover is its pro-rata + // share of the aggregate buyback `K_Σ` (ceiling-rounded). Order- and + // split-independent, and `Σ k_i ≥ K_Σ`, so neither storage order nor + // wallet-splitting changes total cover. `K_Σ` already folds the + // `max(K_spot, K_EMA)` slippage-aware legs against the frozen snapshot. + let mut k_d = if q_sigma == 0 { + 0 + } else { + k_sigma.saturating_mul(q_rao).div_ceil(q_sigma) + }; + + // Cold-EMA guard. When `pEMA == 0` (fresh subnet, no trustworthy slow + // price), the EMA leg is 0 and only the suppressible live leg governs — + // which would let a trader who forced the dereg recover the pool-origin + // retained buffer `R` as equity. Floor `K_D` at `R` so equity can never + // exceed the trader's own floor `P` (`equity = C − K_D ≤ P`); the buffer + // is recycled rather than refunded. A warm EMA prices a genuine in-the- + // money close correctly, so legitimate profit is unaffected. + if pema <= I64F64::from_num(0) { + k_d = k_d.max(u128::from(pos.r_stored.to_u64())); + } + + let equity = + TaoBalance::from(c_rao.saturating_sub(k_d).min(u128::from(u64::MAX)) as u64); + let cover = TaoBalance::from(c_rao.min(k_d).min(u128::from(u64::MAX)) as u64); + // Pay equity; if the transfer fails the amount stays in custody and is + // recycled by the terminal sweep below, so the emitted `equity` reflects + // what was actually paid (never claims an unpaid amount). + // Pay equity from custody. `paid` (emitted in the event) reflects what + // actually moved — never an unpaid amount. A transfer failure is + // impossible under custody ≥ obligations; if it ever occurs, log loudly + // rather than silently under-paying (the unpaid value stays in custody + // and is recycled by the terminal sweep, so no TAO is created/lost). + let paid = if equity.is_zero() { + TaoBalance::from(0) + } else if Self::transfer_tao(&custody, &coldkey, equity.into()).is_ok() { + equity + } else { + log::error!( + "derivatives: short terminal equity payout failed (custody invariant breach) netuid={netuid:?} equity={equity:?}" + ); + TaoBalance::from(0) + }; + Self::recycle_custody_tao(&custody, cover); + + ShortPositions::::remove(netuid, &coldkey); + Self::deposit_event(Event::ShortTerminalSettled { + coldkey, + netuid, + equity: paid, + liability_cover: cover, + }); + } + // Sweep any residual custody dust (rounding drift) so no TAO is orphaned + // in the per-subnet custody account after the subnet is gone. + Self::recycle_custody_tao(&custody, TaoBalance::MAX); + ShortAggregate::::remove(netuid); + ShortActiveSubnets::::remove(netuid); + ShortPositionCount::::remove(netuid); + } + + /// Slippage-aware CPMM cost — in the **pay** asset, rao — to acquire + /// `recv_amount` of the **recv** asset from a pool with reserves + /// `(pay_reserve, recv_reserve)`: the exact constant-product amount + /// `⌈pay_reserve · recv_amount / (recv_reserve − recv_amount)⌉`. + /// + /// The CPMM is symmetric in its two assets, so the **caller selects the + /// denomination by operand order** (the params are intentionally asset-neutral): + /// - a short buying `Q` alpha with TAO → `(T_reserve, A_reserve, Q)` → TAO cost; + /// - a long repaying `D` TAO with alpha → `(A_reserve, T_reserve, D)` → alpha cost. + /// + /// Computed in u128 so the product (each operand up to ~2e16 rao) cannot + /// overflow, and **ceiling-rounded** so the terminal cover is never + /// under-charged (bounding the equity an attacker can recover at a forced + /// deregistration). Saturates to `u64::MAX` when un-buyable + /// (`recv_amount ≥ recv_reserve`), giving `cover = C, equity = 0`. + fn buyback_cost_rao(pay_reserve: u128, recv_reserve: u128, recv_amount: u128) -> u64 { + if recv_reserve <= recv_amount { + return u64::MAX; + } + let num = pay_reserve.saturating_mul(recv_amount); + let den = recv_reserve.saturating_sub(recv_amount); + num.div_ceil(den).min(u64::MAX as u128) as u64 + } + + /// Slippage-aware TAO cost (as I64F64) to buy `q` alpha on the live pool. + fn short_spot_close_cost(netuid: NetUid, q: AlphaBalance) -> I64F64 { + let cost = Self::buyback_cost_rao( + u128::from(SubnetTAO::::get(netuid).to_u64()), + u128::from(SubnetAlphaIn::::get(netuid).to_u64()), + u128::from(q.to_u64()), + ); + I64F64::from_num(cost) + } + + // ---- governance setters (spec §14.6) ------------------------------- + + pub fn set_shorts_enabled(enabled: bool) { + ShortsEnabled::::put(enabled); + } + pub fn set_longs_enabled(enabled: bool) { + LongsEnabled::::put(enabled); + } + /// `κ_S`, supplied scaled by 1e9. Clamped to `(0, 2.0]` so governance can't + /// freeze the market (`κ=0`) or remove the capacity guard entirely. + pub fn set_short_kappa_ppb(kappa_ppb: u64) { + let k = kappa_ppb.clamp(1, 2_000_000_000); + ShortKappa::::put(I64F64::from_num(k).safe_div(I64F64::from_num(1_000_000_000u64))); + } + /// `λ`, supplied scaled by 1e9. Clamped to `(0, 1)` so the open quadratic + /// stays well-formed. + pub fn set_short_base_ltv_ppb(ltv_ppb: u64) { + let ltv = ltv_ppb.clamp(1, 999_999_999); + ShortBaseLtv::::put(I64F64::from_num(ltv).safe_div(I64F64::from_num(1_000_000_000u64))); + } + /// `d_min`, `d_max`, supplied scaled by 1e9. Each is clamped to `[0, 1.0]` + /// per day (so the per-block factor `g = 1 − d/blocks_per_day` stays in + /// `(0, 1]`) and `d_min ≤ d_max` is enforced. + pub fn set_decay_bounds_ppb(min_ppb: u64, max_ppb: u64) { + let scale = I64F64::from_num(1_000_000_000u64); + let lo = min_ppb.min(1_000_000_000); + let hi = max_ppb.clamp(lo, 1_000_000_000); + DecayMin::::put(I64F64::from_num(lo).safe_div(scale)); + DecayMax::::put(I64F64::from_num(hi).safe_div(scale)); + } + pub fn set_short_dust(dust: TaoBalance) { + ShortDust::::put(dust); + } + pub fn set_short_default_grace(blocks: u64) { + ShortDefaultGrace::::put(blocks); + } + pub fn set_long_default_grace(blocks: u64) { + LongDefaultGrace::::put(blocks); + } + pub fn set_short_min_input(min_input: TaoBalance) { + ShortMinInput::::put(min_input); + } + + // ---- read-only quote (spec §1.2) ----------------------------------- + + /// Pure pre-open quote for a given input `P`. Returns `None` when shorts are + /// disabled or the subnet is not a dynamic market. + pub fn quote_open_short(netuid: NetUid, position_input: TaoBalance) -> Option { + if !ShortsEnabled::::get() || SubnetMechanism::::get(netuid) != 1 { + return None; + } + // Mirror the open-time non-user-specific rejection paths so the quote is + // unavailable exactly when an open would be rejected: cold EMA + // (`ColdEmaNotAllowed`) and below-minimum input (`AmountTooLow`). Capacity + // and the reserve-domain bound are checked below via the same solves. + if Self::get_moving_alpha_price(netuid) == 0 || position_input < ShortMinInput::::get() { + return None; + } + let agg = ShortAggregate::::get(netuid); + let t_ref = Self::short_t_ref(netuid); + let p = Self::tao_f(position_input); + let (c, n) = + Self::solve_collateral(p, t_ref, Self::tao_f(agg.b_sigma), ShortBaseLtv::::get())?; + // Capacity cap `S + B ≤ κ·T_ref` (`ShortCapacityExceeded` at open). + if Self::tao_f(agg.b_sigma).saturating_add(ShortBaseLtv::::get().saturating_mul(c)) + > ShortKappa::::get().saturating_mul(t_ref) + { + return None; + } + let t_live = Self::tao_f(SubnetTAO::::get(netuid)); + let a_live = Self::alpha_f(SubnetAlphaIn::::get(netuid)); + let phi = Self::solve_phi(n, t_live)?; + + let q_alpha = Self::to_alpha(phi.saturating_mul(a_live)); + let scale = I64F64::from_num(1_000_000_000u64); + let lambda_eff = n + .safe_div(c) + .saturating_mul(scale) + .saturating_to_num::(); + let daily_decay = Self::short_daily_decay(netuid, agg.b_sigma) + .saturating_mul(scale) + .saturating_to_num::(); + Some(ShortOpenQuote { + gross_collateral: Self::to_tao(c), + retained_proceeds: Self::to_tao(n), + alpha_liability: q_alpha, + escrow: Self::to_tao(phi.saturating_mul(t_live)), + effective_ltv: lambda_eff, + daily_decay, + est_close_cost: Self::to_tao(Self::short_spot_close_cost(netuid, q_alpha)), + }) + } + + /// Estimated blocks until `r_current` decays to dust at the current rate. + /// `u64::MAX` when decay is effectively zero. + fn short_blocks_to_dust(netuid: NetUid, r_current: TaoBalance, b_sigma: TaoBalance) -> u64 { + let dust = ShortDust::::get(); + if r_current <= dust || dust.is_zero() { + return if r_current <= dust { 0 } else { u64::MAX }; + } + let delta = + Self::short_daily_decay(netuid, b_sigma).safe_div(I64F64::from_num(BLOCKS_PER_DAY)); + if delta <= I64F64::from_num(0) { + return u64::MAX; + } + let neg_ln_g = Self::neg_ln_one_minus(delta); + if neg_ln_g <= I64F64::from_num(0) { + return u64::MAX; + } + let ratio = Self::tao_f(r_current).safe_div(Self::tao_f(dust)); + match ratio.checked_ln() { + Some(ln_ratio) if ln_ratio > I64F64::from_num(0) => { + ln_ratio.safe_div(neg_ln_g).saturating_to_num::() + } + _ => 0, + } + } + + /// Materialized, health-rich view of one position (decayed to the current block). + pub fn get_short_position( + coldkey: &T::AccountId, + netuid: NetUid, + ) -> Option> { + let mut pos = ShortPositions::::get(netuid, coldkey)?; + let agg = ShortAggregate::::get(netuid); + Self::materialize_short(&mut pos, agg.omega); + + let scale = I64F64::from_num(1_000_000_000u64); + let daily_decay = Self::short_daily_decay(netuid, agg.b_sigma) + .saturating_mul(scale) + .saturating_to_num::(); + let now = Self::get_current_block_as_u64(); + let defaultable_at_block = pos + .last_active + .saturating_add(ShortDefaultGrace::::get()); + let default_eligible = pos.r_stored <= ShortDust::::get() && now >= defaultable_at_block; + let alpha_held = + Self::get_stake_for_hotkey_and_coldkey_on_subnet(&pos.hotkey, coldkey, netuid); + + Some(ShortPositionInfo { + netuid, + hotkey: pos.hotkey.clone(), + floor: pos.p_floor, + alpha_liability: pos.q_liability, + buffer: pos.r_stored, + escrow: pos.e_stored, + collateral_claim: pos.p_floor.saturating_add(pos.r_stored), + daily_decay, + blocks_to_dust: Self::short_blocks_to_dust(netuid, pos.r_stored, agg.b_sigma), + default_eligible, + defaultable_at_block, + est_close_cost: Self::to_tao(Self::short_spot_close_cost(netuid, pos.q_liability)), + alpha_held, + alpha_needed: AlphaBalance::from( + pos.q_liability.to_u64().saturating_sub(alpha_held.to_u64()), + ), + }) + } + + /// All of a coldkey's short positions across subnets. + pub fn get_short_positions(coldkey: &T::AccountId) -> Vec> { + Self::get_all_subnet_netuids() + .into_iter() + .filter_map(|netuid| Self::get_short_position(coldkey, netuid)) + .collect() + } + + /// Per-subnet short market state for sizing and capacity decisions. + pub fn get_subnet_short_state(netuid: NetUid) -> Option { + if !Self::if_subnet_exist(netuid) { + return None; + } + let agg = ShortAggregate::::get(netuid); + let t_ref = Self::short_t_ref(netuid); + let cap = ShortKappa::::get().saturating_mul(t_ref); + let used = Self::tao_f(agg.b_sigma); + let scale = I64F64::from_num(1_000_000_000u64); + let ppb = |x: I64F64| x.saturating_mul(scale).saturating_to_num::(); + + Some(ShortMarketInfo { + shorts_enabled: ShortsEnabled::::get(), + base_ltv: ppb(ShortBaseLtv::::get()), + kappa: ppb(ShortKappa::::get()), + decay_min: ppb(DecayMin::::get()), + decay_max: ppb(DecayMax::::get()), + current_daily_decay: ppb(Self::short_daily_decay(netuid, agg.b_sigma)), + t_ref: Self::to_tao(t_ref), + footprint_used: agg.b_sigma, + footprint_cap: Self::to_tao(cap), + footprint_remaining: Self::to_tao(cap.saturating_sub(used)), + open_interest_alpha: agg.q_sigma, + buffer_total: agg.r_sigma, + escrow_total: agg.e_sigma, + dust_threshold: ShortDust::::get(), + min_input: ShortMinInput::::get(), + default_grace: ShortDefaultGrace::::get(), + }) + } + + /// Pre-close quote for `fraction_ppb / 1e9` of a position. + pub fn quote_close_short( + coldkey: &T::AccountId, + netuid: NetUid, + fraction_ppb: u64, + ) -> Option { + if fraction_ppb == 0 || fraction_ppb > 1_000_000_000 { + return None; + } + let mut pos = ShortPositions::::get(netuid, coldkey)?; + let agg = ShortAggregate::::get(netuid); + Self::materialize_short(&mut pos, agg.omega); + let rho = I64F64::from_num(fraction_ppb).safe_div(I64F64::from_num(1_000_000_000u64)); + + let repay_alpha = Self::mul_alpha(pos.q_liability, rho); + let returned_tao = + Self::mul_tao(pos.p_floor, rho).saturating_add(Self::mul_tao(pos.r_stored, rho)); + let escrow_settled = Self::mul_tao(pos.e_stored, rho); + let alpha_held = + Self::get_stake_for_hotkey_and_coldkey_on_subnet(&pos.hotkey, coldkey, netuid); + + Some(CloseShortQuote { + repay_alpha, + returned_tao, + escrow_settled, + est_buyback_cost: Self::to_tao(Self::short_spot_close_cost(netuid, repay_alpha)), + alpha_held, + alpha_needed: AlphaBalance::from( + repay_alpha.to_u64().saturating_sub(alpha_held.to_u64()), + ), + }) + } +} diff --git a/pallets/subtensor/src/derivatives/types.rs b/pallets/subtensor/src/derivatives/types.rs new file mode 100644 index 0000000000..93ded55320 --- /dev/null +++ b/pallets/subtensor/src/derivatives/types.rs @@ -0,0 +1,322 @@ +use codec::{Decode, DecodeWithMemTracking, Encode}; +use scale_info::TypeInfo; +use substrate_fixed::types::I64F64; +use subtensor_macros::freeze_struct; +use subtensor_runtime_common::{AlphaBalance, NetUid, TaoBalance, Token}; + +/// A merged covered short position for one `(coldkey, netuid)` (spec §2.2). +/// +/// `C`, `N`, `λ_eff`, and `ϕ` are open-time derivations and are deliberately not +/// persisted. `r/e/b_stored` are the values at the last materialization; current +/// values are recovered by multiplying by `exp(-(Ω_S − omega_entry))`. +#[freeze_struct("43ae40d25be019c8")] +#[derive(Encode, Decode, DecodeWithMemTracking, TypeInfo, Clone, PartialEq, Eq, Debug)] +pub struct ShortPosition { + /// Hotkey the liability alpha is repaid from on close. + pub hotkey: AccountId, + /// Non-decaying TAO floor supplied by the trader (spec `P`). + pub p_floor: TaoBalance, + /// Fixed alpha liability (spec `Q`); changes only on close/default/dereg. + pub q_liability: AlphaBalance, + /// Retained-proceeds buffer at last materialization (spec `R`). + pub r_stored: TaoBalance, + /// Linked TAO escrow at last materialization (spec `E`). + pub e_stored: TaoBalance, + /// Utilization footprint at last materialization (spec `B = λC`). + pub b_stored: TaoBalance, + /// Value of `Ω_S` at last materialization (spec `Ω_entry`). + pub omega_entry: I64F64, + /// Block of the last owner action (open / merge / top-up). Permissionless + /// default is gated to `last_active + grace`, so an owner always has a + /// window to top up before a third party can default them. + pub last_active: u64, +} + +/// A merged covered long position for one `(coldkey, netuid)` (spec §2.3). +/// +/// The mirror of `ShortPosition` with Alpha and TAO swapped: collateral/buffer/ +/// escrow/footprint are Alpha; the fixed liability `D` is TAO. +#[freeze_struct("fba4427847673b78")] +#[derive(Encode, Decode, DecodeWithMemTracking, TypeInfo, Clone, PartialEq, Eq, Debug)] +pub struct LongPosition { + /// Hotkey the collateral alpha is sourced from / returned to. + pub hotkey: AccountId, + /// Non-decaying Alpha floor supplied by the trader (spec `P`). + pub p_floor: AlphaBalance, + /// Fixed TAO liability (spec `D`); changes only on close/default/dereg. + pub d_liability: TaoBalance, + /// Retained Alpha-proceeds buffer at last materialization (spec `R`). + pub r_stored: AlphaBalance, + /// Linked Alpha escrow at last materialization (spec `E`). + pub e_stored: AlphaBalance, + /// Utilization footprint at last materialization (spec `B = λ_L·C`). + pub b_stored: AlphaBalance, + /// Value of `Ω_L` at last materialization. + pub omega_entry: I64F64, + /// Block of the last owner action (open / merge / top-up). + pub last_active: u64, +} + +/// Per-subnet long-side aggregate and decay accumulator. +#[freeze_struct("571ef9a642107d3b")] +#[derive(Encode, Decode, DecodeWithMemTracking, TypeInfo, Clone, PartialEq, Eq, Debug)] +pub struct LongAgg { + /// Σ current retained Alpha buffer. + pub r_sigma: AlphaBalance, + /// Σ current Alpha escrow. + pub e_sigma: AlphaBalance, + /// Σ current Alpha footprint == active utilization `S_L`. + pub b_sigma: AlphaBalance, + /// Σ fixed TAO liability (open interest `D_Σ`). + pub d_sigma: TaoBalance, + /// Cumulative monotone long-side decay accumulator `Ω_L`. + pub omega: I64F64, +} + +impl LongAgg { + /// Empty long-side aggregate. + pub fn zero() -> Self { + Self { + r_sigma: AlphaBalance::ZERO, + e_sigma: AlphaBalance::ZERO, + b_sigma: AlphaBalance::ZERO, + d_sigma: TaoBalance::ZERO, + omega: I64F64::from_num(0), + } + } +} + +/// Per-subnet short-side aggregate and decay accumulator (spec §2.4, §6.3). +#[freeze_struct("376a8ccf882d6dea")] +#[derive(Encode, Decode, DecodeWithMemTracking, TypeInfo, Clone, PartialEq, Eq, Debug)] +pub struct ShortAgg { + /// Σ current retained buffer. + pub r_sigma: TaoBalance, + /// Σ current escrow. + pub e_sigma: TaoBalance, + /// Σ current footprint == active utilization `S`. + pub b_sigma: TaoBalance, + /// Σ fixed alpha liability (open interest `Q_Σ`). + pub q_sigma: AlphaBalance, + /// Cumulative monotone decay accumulator `Ω_S` (`Ω ← Ω − ln g`). + pub omega: I64F64, +} + +impl ShortAgg { + /// Empty short-side aggregate. + pub fn zero() -> Self { + Self { + r_sigma: TaoBalance::ZERO, + e_sigma: TaoBalance::ZERO, + b_sigma: TaoBalance::ZERO, + q_sigma: AlphaBalance::ZERO, + omega: I64F64::from_num(0), + } + } +} + +/// Pre-open trader quote (spec §1.2). Pure derivation, no state change. +#[freeze_struct("54beac46977b1ec5")] +#[derive(Encode, Decode, TypeInfo, Clone, PartialEq, Eq, Debug)] +pub struct ShortOpenQuote { + /// Gross open-time collateral `C = P + N`. + pub gross_collateral: TaoBalance, + /// Retained proceeds `N` (becomes the initial buffer `R0`). + pub retained_proceeds: TaoBalance, + /// Fixed alpha liability `Q`. + pub alpha_liability: AlphaBalance, + /// Linked TAO escrow `E`. + pub escrow: TaoBalance, + /// Effective LTV `λ_eff`, scaled by 1e9. + pub effective_ltv: u64, + /// Current daily decay/carry rate, scaled by 1e9. + pub daily_decay: u64, + /// Estimated TAO cost to repay `Q` at the current pool (slippage-aware). + pub est_close_cost: TaoBalance, +} + +/// Live, materialized view of a trader's short position (decayed to the current +/// block) plus the health metrics a client needs to manage it. +#[freeze_struct("9f6810752569e314")] +#[derive(Encode, Decode, TypeInfo, Clone, PartialEq, Eq, Debug)] +pub struct ShortPositionInfo { + pub netuid: NetUid, + pub hotkey: AccountId, + /// Non-decaying floor `P`. + pub floor: TaoBalance, + /// Fixed alpha liability `Q`. + pub alpha_liability: AlphaBalance, + /// Current retained buffer `R(t)` after decay. + pub buffer: TaoBalance, + /// Current linked escrow `E(t)` after decay. + pub escrow: TaoBalance, + /// Current TAO collateral claim `C = P + R(t)`. + pub collateral_claim: TaoBalance, + /// Current daily carry/decay rate, scaled by 1e9. + pub daily_decay: u64, + /// Estimated blocks until `R` decays to dust at the current rate + /// (`u64::MAX` if decay is effectively zero). + pub blocks_to_dust: u64, + /// Whether the position can be defaulted right now. + pub default_eligible: bool, + /// Earliest block a third party could default once dusted (`last_active + grace`). + pub defaultable_at_block: u64, + /// Slippage-aware TAO cost to repay the full liability `Q` now. + pub est_close_cost: TaoBalance, + /// Alpha already staked at the position hotkey (counts toward `Q`). + pub alpha_held: AlphaBalance, + /// Incremental alpha still to acquire before a full close (spec §1.6). + pub alpha_needed: AlphaBalance, +} + +/// Per-subnet short market state for sizing and capacity decisions. +#[freeze_struct("b87648108ccb15")] +#[derive(Encode, Decode, TypeInfo, Clone, PartialEq, Eq, Debug)] +pub struct ShortMarketInfo { + pub shorts_enabled: bool, + /// Base LTV `λ`, scaled by 1e9. + pub base_ltv: u64, + /// Footprint-cap factor `κ_S`, scaled by 1e9. + pub kappa: u64, + /// Daily decay bounds, scaled by 1e9. + pub decay_min: u64, + pub decay_max: u64, + /// Current daily decay at the live utilization, scaled by 1e9. + pub current_daily_decay: u64, + /// Conservative TAO reference `T_ref`. + pub t_ref: TaoBalance, + /// Active footprint `S` (used capacity). + pub footprint_used: TaoBalance, + /// Footprint cap `κ_S · T_ref`. + pub footprint_cap: TaoBalance, + /// Remaining openable footprint. + pub footprint_remaining: TaoBalance, + /// Aggregate fixed alpha liability (open interest). + pub open_interest_alpha: AlphaBalance, + /// Aggregate retained buffer and escrow. + pub buffer_total: TaoBalance, + pub escrow_total: TaoBalance, + /// Dust threshold, minimum input, and default grace. + pub dust_threshold: TaoBalance, + pub min_input: TaoBalance, + pub default_grace: u64, +} + +/// Pre-close quote for a fraction of a position (spec §1.5–1.6). +#[freeze_struct("e5828d301fddd1a1")] +#[derive(Encode, Decode, TypeInfo, Clone, PartialEq, Eq, Debug)] +pub struct CloseShortQuote { + /// Alpha that must be repaid for this close fraction. + pub repay_alpha: AlphaBalance, + /// TAO returned to the trader (floor + buffer fraction). + pub returned_tao: TaoBalance, + /// Escrow settled back into the pool. + pub escrow_settled: TaoBalance, + /// Slippage-aware TAO cost to acquire `repay_alpha` now. + pub est_buyback_cost: TaoBalance, + /// Alpha already held toward the repayment. + pub alpha_held: AlphaBalance, + /// Incremental alpha still to acquire (`max(0, repay_alpha − held)`). + pub alpha_needed: AlphaBalance, +} + +/// Pre-open trader quote for a covered long (mirror of `ShortOpenQuote`, Alpha +/// and TAO swapped). Pure derivation, no state change. +#[freeze_struct("b921cfc5a27a3721")] +#[derive(Encode, Decode, TypeInfo, Clone, PartialEq, Eq, Debug)] +pub struct LongOpenQuote { + /// Gross open-time Alpha collateral `C = P + N`. + pub gross_collateral: AlphaBalance, + /// Retained Alpha proceeds `N` (becomes the initial buffer `R0`). + pub retained_proceeds: AlphaBalance, + /// Fixed TAO liability `D`. + pub tao_liability: TaoBalance, + /// Linked Alpha escrow `E`. + pub escrow: AlphaBalance, + /// Effective LTV `λ_eff`, scaled by 1e9. + pub effective_ltv: u64, + /// Current daily decay/carry rate, scaled by 1e9. + pub daily_decay: u64, + /// TAO required to close (repay `D` directly; deterministic, no slippage). + pub est_close_cost: TaoBalance, +} + +/// Live, materialized view of a trader's long position (mirror of +/// `ShortPositionInfo`, Alpha and TAO swapped). +#[freeze_struct("9d21c3852383a38d")] +#[derive(Encode, Decode, TypeInfo, Clone, PartialEq, Eq, Debug)] +pub struct LongPositionInfo { + pub netuid: NetUid, + pub hotkey: AccountId, + /// Non-decaying Alpha floor `P`. + pub floor: AlphaBalance, + /// Fixed TAO liability `D`. + pub tao_liability: TaoBalance, + /// Current retained Alpha buffer `R(t)` after decay. + pub buffer: AlphaBalance, + /// Current linked Alpha escrow `E(t)` after decay. + pub escrow: AlphaBalance, + /// Current Alpha collateral claim `C = P + R(t)`. + pub collateral_claim: AlphaBalance, + /// Current daily carry/decay rate, scaled by 1e9. + pub daily_decay: u64, + /// Estimated blocks until `R` decays to dust (`u64::MAX` if ~zero). + pub blocks_to_dust: u64, + /// Whether the position can be defaulted right now. + pub default_eligible: bool, + /// Earliest block a third party could default once dusted. + pub defaultable_at_block: u64, + /// TAO required to close (repay `D` directly; deterministic). + pub est_close_cost: TaoBalance, + /// Free TAO balance the trader holds toward repaying `D`. + pub tao_held: TaoBalance, + /// Incremental TAO still needed (`max(0, D − held)`). + pub tao_needed: TaoBalance, +} + +/// Per-subnet long market state for sizing and capacity decisions (mirror of +/// `ShortMarketInfo`, Alpha and TAO swapped). +#[freeze_struct("276293898546e74c")] +#[derive(Encode, Decode, TypeInfo, Clone, PartialEq, Eq, Debug)] +pub struct LongMarketInfo { + pub longs_enabled: bool, + /// Base LTV `λ_L`, scaled by 1e9. + pub base_ltv: u64, + /// Footprint-cap factor `κ_L`, scaled by 1e9. + pub kappa: u64, + pub decay_min: u64, + pub decay_max: u64, + pub current_daily_decay: u64, + /// Conservative Alpha reference `A_ref`. + pub a_ref: AlphaBalance, + /// Active Alpha footprint `S_L` (used capacity). + pub footprint_used: AlphaBalance, + /// Footprint cap `κ_L · A_ref`. + pub footprint_cap: AlphaBalance, + /// Remaining openable footprint. + pub footprint_remaining: AlphaBalance, + /// Aggregate fixed TAO liability (open interest `D_Σ`). + pub open_interest_tao: TaoBalance, + /// Aggregate retained Alpha buffer and escrow. + pub buffer_total: AlphaBalance, + pub escrow_total: AlphaBalance, + pub dust_threshold: AlphaBalance, + pub min_input: AlphaBalance, + pub default_grace: u64, +} + +/// Pre-close quote for a fraction of a long position (mirror of `CloseShortQuote`). +#[freeze_struct("d36159bde80f0a83")] +#[derive(Encode, Decode, TypeInfo, Clone, PartialEq, Eq, Debug)] +pub struct CloseLongQuote { + /// TAO that must be repaid for this close fraction. + pub repay_tao: TaoBalance, + /// Alpha returned to the trader (floor + buffer fraction). + pub returned_alpha: AlphaBalance, + /// Alpha escrow settled back into the pool. + pub escrow_settled: AlphaBalance, + /// Free TAO balance the trader holds toward the repayment. + pub tao_held: TaoBalance, + /// Incremental TAO still needed (`max(0, repay_tao − held)`). + pub tao_needed: TaoBalance, +} diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index 7ce25b65b6..e50774e7a5 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -34,6 +34,7 @@ mod benchmarks; // ==== Pallet Imports ===== // ========================= pub mod coinbase; +pub mod derivatives; pub mod epoch; pub mod extensions; pub mod guards; @@ -1381,6 +1382,197 @@ pub mod pallet { #[pallet::storage] pub type SubnetAlphaOut = StorageMap<_, Identity, NetUid, AlphaBalance, ValueQuery, DefaultZeroAlpha>; + + // ===== Covered continuous-unwind derivatives (spec v3.6.1) ===== + + #[pallet::type_value] + /// Shorts are gated off until the trading-games suite passes. + pub fn DefaultDisabled() -> bool { + false + } + #[pallet::type_value] + /// Base short LTV `λ` = 0.50. + pub fn DefaultShortBaseLtv() -> substrate_fixed::types::I64F64 { + substrate_fixed::types::I64F64::from_num(0.5) + } + #[pallet::type_value] + /// Conservative short footprint-cap factor `κ_S`. + pub fn DefaultShortKappa() -> substrate_fixed::types::I64F64 { + substrate_fixed::types::I64F64::from_num(0.05) + } + #[pallet::type_value] + /// `d_min` = 0.1%/day. + pub fn DefaultDecayMin() -> substrate_fixed::types::I64F64 { + substrate_fixed::types::I64F64::from_num(0.001) + } + #[pallet::type_value] + /// `d_max` = 1.5%/day. + pub fn DefaultDecayMax() -> substrate_fixed::types::I64F64 { + substrate_fixed::types::I64F64::from_num(0.015) + } + #[pallet::type_value] + /// Dust threshold `R_dust` = 1 TAO. + pub fn DefaultShortDust() -> TaoBalance { + TaoBalance::from(1_000_000_000u64) + } + #[pallet::type_value] + /// Anti-snipe grace: blocks after the last owner action during which a + /// permissionless default is rejected (~1.2h at 12s blocks). + pub fn DefaultShortDefaultGrace() -> u64 { + 360 + } + #[pallet::type_value] + /// Minimum short open input = 0.1 TAO. Bounds dust-spam and terminal load. + pub fn DefaultShortMinInput() -> TaoBalance { + TaoBalance::from(100_000_000u64) + } + #[pallet::type_value] + /// Empty short-side aggregate. + pub fn DefaultShortAgg() -> crate::derivatives::ShortAgg { + crate::derivatives::ShortAgg::zero() + } + + /// Short-side master enablement flag. + #[pallet::storage] + pub type ShortsEnabled = StorageValue<_, bool, ValueQuery, DefaultDisabled>; + + /// Long-side master enablement flag (gated; long mechanics not yet built). + #[pallet::storage] + pub type LongsEnabled = StorageValue<_, bool, ValueQuery, DefaultDisabled>; + + /// Base short LTV `λ`. + #[pallet::storage] + pub type ShortBaseLtv = + StorageValue<_, substrate_fixed::types::I64F64, ValueQuery, DefaultShortBaseLtv>; + + /// Short footprint-cap factor `κ_S`. + #[pallet::storage] + pub type ShortKappa = + StorageValue<_, substrate_fixed::types::I64F64, ValueQuery, DefaultShortKappa>; + + /// Minimum daily decay rate `d_min`. + #[pallet::storage] + pub type DecayMin = + StorageValue<_, substrate_fixed::types::I64F64, ValueQuery, DefaultDecayMin>; + + /// Maximum daily decay rate `d_max`. + #[pallet::storage] + pub type DecayMax = + StorageValue<_, substrate_fixed::types::I64F64, ValueQuery, DefaultDecayMax>; + + /// Retained-buffer dust threshold `R_dust`. + #[pallet::storage] + pub type ShortDust = StorageValue<_, TaoBalance, ValueQuery, DefaultShortDust>; + + /// Anti-snipe default grace period, in blocks. + #[pallet::storage] + pub type ShortDefaultGrace = + StorageValue<_, u64, ValueQuery, DefaultShortDefaultGrace>; + + /// Minimum short open input. + #[pallet::storage] + pub type ShortMinInput = + StorageValue<_, TaoBalance, ValueQuery, DefaultShortMinInput>; + + /// --- SET ( netuid ) of subnets with live short state, so the per-block + /// decay tick iterates only active subnets instead of all of them. + #[pallet::storage] + pub type ShortActiveSubnets = StorageMap<_, Identity, NetUid, (), OptionQuery>; + + /// --- MAP ( netuid ) --> count of open short positions on the subnet. + #[pallet::storage] + pub type ShortPositionCount = StorageMap<_, Identity, NetUid, u32, ValueQuery>; + + /// --- MAP ( netuid ) --> short-side aggregate + decay accumulator. + #[pallet::storage] + pub type ShortAggregate = StorageMap< + _, + Identity, + NetUid, + crate::derivatives::ShortAgg, + ValueQuery, + DefaultShortAgg, + >; + + /// --- DMAP ( netuid, coldkey ) --> merged covered short position. + #[pallet::storage] + pub type ShortPositions = StorageDoubleMap< + _, + Identity, + NetUid, + Blake2_128Concat, + T::AccountId, + crate::derivatives::ShortPosition, + OptionQuery, + >; + + // ===== Long side (mirror; Alpha collateral, TAO liability) ===== + + #[pallet::type_value] + /// Long dust threshold = 1 Alpha. + pub fn DefaultLongDust() -> AlphaBalance { + AlphaBalance::from(1_000_000_000u64) + } + #[pallet::type_value] + /// Minimum long open input = 0.1 Alpha. + pub fn DefaultLongMinInput() -> AlphaBalance { + AlphaBalance::from(100_000_000u64) + } + #[pallet::type_value] + /// Empty long-side aggregate. + pub fn DefaultLongAgg() -> crate::derivatives::LongAgg { + crate::derivatives::LongAgg::zero() + } + + /// Base long LTV `λ_L` (shares the short LTV default; ADR-adjustment TBD). + #[pallet::storage] + pub type LongBaseLtv = + StorageValue<_, substrate_fixed::types::I64F64, ValueQuery, DefaultShortBaseLtv>; + + /// Long footprint-cap factor `κ_L`. + #[pallet::storage] + pub type LongKappa = + StorageValue<_, substrate_fixed::types::I64F64, ValueQuery, DefaultShortKappa>; + + /// Long retained-buffer dust threshold (Alpha). + #[pallet::storage] + pub type LongDust = StorageValue<_, AlphaBalance, ValueQuery, DefaultLongDust>; + + /// Minimum long open input (Alpha). + #[pallet::storage] + pub type LongMinInput = + StorageValue<_, AlphaBalance, ValueQuery, DefaultLongMinInput>; + + /// Long-side anti-snipe default grace period, in blocks (independent of the + /// short grace so the two sides can be tuned separately). + #[pallet::storage] + pub type LongDefaultGrace = + StorageValue<_, u64, ValueQuery, DefaultShortDefaultGrace>; + + /// --- MAP ( netuid ) --> long-side aggregate + decay accumulator. + #[pallet::storage] + pub type LongAggregate = + StorageMap<_, Identity, NetUid, crate::derivatives::LongAgg, ValueQuery, DefaultLongAgg>; + + /// --- DMAP ( netuid, coldkey ) --> merged covered long position. + #[pallet::storage] + pub type LongPositions = StorageDoubleMap< + _, + Identity, + NetUid, + Blake2_128Concat, + T::AccountId, + crate::derivatives::LongPosition, + OptionQuery, + >; + + /// --- SET ( netuid ) of subnets with live long state. + #[pallet::storage] + pub type LongActiveSubnets = StorageMap<_, Identity, NetUid, (), OptionQuery>; + + /// --- MAP ( netuid ) --> count of open long positions on the subnet. + #[pallet::storage] + pub type LongPositionCount = StorageMap<_, Identity, NetUid, u32, ValueQuery>; /// --- MAP ( netuid ) --> protocol_alpha | Returns the protocol-owned alpha cached for the subnet. #[pallet::storage] pub type SubnetProtocolAlpha = diff --git a/pallets/subtensor/src/macros/dispatches.rs b/pallets/subtensor/src/macros/dispatches.rs index 7a64acba44..55c3b0131b 100644 --- a/pallets/subtensor/src/macros/dispatches.rs +++ b/pallets/subtensor/src/macros/dispatches.rs @@ -1229,11 +1229,19 @@ mod dispatches { } /// Remove a user's subnetwork - /// The caller must be the owner of the network + /// The caller must be root. #[pallet::call_index(61)] #[pallet::weight(Weight::from_parts(119_000_000, 0) .saturating_add(T::DbWeight::get().reads(6)) - .saturating_add(T::DbWeight::get().writes(31)))] + .saturating_add(T::DbWeight::get().writes(31)) + // Terminal derivative settlement (O(positions/subnet), like the alpha-stake + // unwind in do_dissolve_network); charge the benchmarked linear settlement + // weight at the actual per-subnet position counts. Root-only extrinsic. + // Linearity is benchmarked over [0,1024]; counts above that extrapolate the + // per-position slope (scaled, not under-charged) — regen if subnets routinely + // carry more than 1024 positions. + .saturating_add(::WeightInfo::settle_shorts_on_dereg(crate::ShortPositionCount::::get(netuid))) + .saturating_add(::WeightInfo::settle_longs_on_dereg(crate::LongPositionCount::::get(netuid))))] pub fn dissolve_network( origin: OriginFor, _coldkey: T::AccountId, @@ -2144,7 +2152,15 @@ mod dispatches { #[pallet::call_index(120)] #[pallet::weight(Weight::from_parts(119_000_000, 0) .saturating_add(T::DbWeight::get().reads(6)) - .saturating_add(T::DbWeight::get().writes(31)))] + .saturating_add(T::DbWeight::get().writes(31)) + // Terminal derivative settlement (O(positions/subnet), like the alpha-stake + // unwind in do_dissolve_network); charge the benchmarked linear settlement + // weight at the actual per-subnet position counts. Root-only extrinsic. + // Linearity is benchmarked over [0,1024]; counts above that extrapolate the + // per-position slope (scaled, not under-charged) — regen if subnets routinely + // carry more than 1024 positions. + .saturating_add(::WeightInfo::settle_shorts_on_dereg(crate::ShortPositionCount::::get(netuid))) + .saturating_add(::WeightInfo::settle_longs_on_dereg(crate::LongPositionCount::::get(netuid))))] pub fn root_dissolve_network(origin: OriginFor, netuid: NetUid) -> DispatchResult { ensure_root(origin)?; Self::do_dissolve_network(netuid) @@ -2593,5 +2609,97 @@ mod dispatches { let coldkey = ensure_signed(origin)?; Self::do_set_perpetual_lock(&coldkey, netuid, enabled) } + + /// Open (or merge into) a covered short with floor input `position_input`. + #[pallet::call_index(139)] + #[pallet::weight(::WeightInfo::open_short())] + pub fn open_short( + origin: OriginFor, + hotkey: T::AccountId, + netuid: NetUid, + position_input: TaoBalance, + max_alpha_liability: AlphaBalance, + ) -> DispatchResult { + Self::do_open_short(origin, hotkey, netuid, position_input, max_alpha_liability) + } + + /// Top up a covered short's carry buffer with fresh capital. + #[pallet::call_index(140)] + #[pallet::weight(::WeightInfo::top_up_short())] + pub fn top_up_short( + origin: OriginFor, + netuid: NetUid, + amount: TaoBalance, + ) -> DispatchResult { + Self::do_top_up_short(origin, netuid, amount) + } + + /// Close `fraction_ppb / 1e9` of a covered short (`1e9` = full close). + #[pallet::call_index(141)] + #[pallet::weight(::WeightInfo::close_short())] + pub fn close_short( + origin: OriginFor, + netuid: NetUid, + fraction_ppb: u64, + ) -> DispatchResult { + Self::do_close_short(origin, netuid, fraction_ppb) + } + + /// Permissionlessly default a covered short whose buffer reached dust. + #[pallet::call_index(142)] + #[pallet::weight(::WeightInfo::default_short())] + pub fn default_short( + origin: OriginFor, + coldkey: T::AccountId, + netuid: NetUid, + ) -> DispatchResult { + Self::do_default_short(origin, coldkey, netuid) + } + + /// Open (or merge into) a covered long with floor Alpha `position_input`. + #[pallet::call_index(143)] + #[pallet::weight(::WeightInfo::open_long())] + pub fn open_long( + origin: OriginFor, + hotkey: T::AccountId, + netuid: NetUid, + position_input: AlphaBalance, + max_tao_liability: TaoBalance, + ) -> DispatchResult { + Self::do_open_long(origin, hotkey, netuid, position_input, max_tao_liability) + } + + /// Top up a covered long's carry buffer with fresh Alpha. + #[pallet::call_index(144)] + #[pallet::weight(::WeightInfo::top_up_long())] + pub fn top_up_long( + origin: OriginFor, + netuid: NetUid, + amount: AlphaBalance, + ) -> DispatchResult { + Self::do_top_up_long(origin, netuid, amount) + } + + /// Close `fraction_ppb / 1e9` of a covered long (`1e9` = full close). + #[pallet::call_index(145)] + #[pallet::weight(::WeightInfo::close_long())] + pub fn close_long( + origin: OriginFor, + netuid: NetUid, + fraction_ppb: u64, + ) -> DispatchResult { + Self::do_close_long(origin, netuid, fraction_ppb) + } + + /// Permissionlessly default a covered long whose buffer reached dust. + #[pallet::call_index(146)] + #[pallet::weight(::WeightInfo::default_long())] + pub fn default_long( + origin: OriginFor, + coldkey: T::AccountId, + netuid: NetUid, + ) -> DispatchResult { + Self::do_default_long(origin, coldkey, netuid) + } } } diff --git a/pallets/subtensor/src/macros/errors.rs b/pallets/subtensor/src/macros/errors.rs index 46343b6ed1..99359c59e5 100644 --- a/pallets/subtensor/src/macros/errors.rs +++ b/pallets/subtensor/src/macros/errors.rs @@ -301,5 +301,42 @@ mod errors { CannotUseSystemAccount, /// Trying to unlock more than locked UnlockAmountTooHigh, + /// Short-side derivatives are disabled. + ShortsDisabled, + /// The subnet is not a dynamic (AMM) subnet. + SubnetNotDynamic, + /// No short position exists for this coldkey on the subnet. + ShortPositionNotFound, + /// Effective LTV is non-positive at current utilization. + EffectiveLtvNonPositive, + /// Retained proceeds would be non-positive. + RetainedProceedsNonPositive, + /// Open would exceed the active short footprint cap. + ShortCapacityExceeded, + /// Open violates the remove-and-sell-back square-root domain. + ReserveDomainExceeded, + /// Close fraction must be in (0, 1e9]. + InvalidCloseFraction, + /// Trader does not hold enough alpha to repay the liability. + InsufficientAlphaToClose, + /// Position has not decayed to dust and is not default-eligible. + PositionNotDefaultEligible, + /// Additional open targets a different hotkey than the existing position. + ShortHotkeyMismatch, + /// Long-side derivatives are disabled. + LongsDisabled, + /// No long position exists for this coldkey on the subnet. + LongPositionNotFound, + /// Open would exceed the active long footprint cap. + LongCapacityExceeded, + /// Additional open targets a different hotkey than the existing position. + LongHotkeyMismatch, + /// Trader does not hold enough alpha collateral to open/extend the long. + InsufficientCollateral, + /// Derivative open requires a warm price EMA (`pEMA > 0`). The subnet's + /// moving price is still cold (freshly registered / no price history), + /// where the EMA risk reference and terminal anti-suppression leg are + /// unavailable, so opens are blocked until the EMA warms. + ColdEmaNotAllowed, } } diff --git a/pallets/subtensor/src/macros/events.rs b/pallets/subtensor/src/macros/events.rs index 918baf1107..7ccebae210 100644 --- a/pallets/subtensor/src/macros/events.rs +++ b/pallets/subtensor/src/macros/events.rs @@ -631,5 +631,115 @@ mod events { /// Whether this coldkey's locks are now perpetual. enabled: bool, }, + + /// A covered short was opened (or merged). + ShortOpened { + /// Position owner coldkey. + coldkey: T::AccountId, + /// Subnet the short is on. + netuid: NetUid, + /// Floor TAO supplied by the trader. + position_input: TaoBalance, + /// Retained proceeds booked as the initial buffer. + retained_proceeds: TaoBalance, + /// Fixed alpha liability created. + alpha_liability: AlphaBalance, + /// Linked TAO escrow created. + escrow: TaoBalance, + }, + /// A covered short's carry buffer was topped up. + ShortToppedUp { + /// Position owner coldkey. + coldkey: T::AccountId, + /// Subnet the short is on. + netuid: NetUid, + /// TAO added to the buffer. + amount: TaoBalance, + }, + /// A covered short was (partially) closed. + ShortClosed { + /// Position owner coldkey. + coldkey: T::AccountId, + /// Subnet the short is on. + netuid: NetUid, + /// Closed fraction in parts-per-billion. + fraction_ppb: u64, + /// Alpha repaid to extinguish the liability slice. + repaid_alpha: AlphaBalance, + /// TAO (floor + buffer) returned to the trader. + returned: TaoBalance, + }, + /// A covered short defaulted after its buffer reached dust. + ShortDefaulted { + /// Position owner coldkey. + coldkey: T::AccountId, + /// Subnet the short was on. + netuid: NetUid, + }, + /// A covered short was settled at subnet deregistration. + ShortTerminalSettled { + /// Position owner coldkey. + coldkey: T::AccountId, + /// Subnet that deregistered. + netuid: NetUid, + /// Terminal equity paid to the trader. + equity: TaoBalance, + /// Liability-cover recycled outside terminal distribution. + liability_cover: TaoBalance, + }, + + /// A covered long was opened (or merged). + LongOpened { + /// Position owner coldkey. + coldkey: T::AccountId, + /// Subnet the long is on. + netuid: NetUid, + /// Floor Alpha supplied by the trader. + position_input: AlphaBalance, + /// Retained Alpha proceeds booked as the initial buffer. + retained_proceeds: AlphaBalance, + /// Fixed TAO liability created. + tao_liability: TaoBalance, + /// Linked Alpha escrow created. + escrow: AlphaBalance, + }, + /// A covered long's carry buffer was topped up. + LongToppedUp { + /// Position owner coldkey. + coldkey: T::AccountId, + /// Subnet the long is on. + netuid: NetUid, + /// Alpha added to the buffer. + amount: AlphaBalance, + }, + /// A covered long was (partially) closed. + LongClosed { + /// Position owner coldkey. + coldkey: T::AccountId, + /// Subnet the long is on. + netuid: NetUid, + /// Closed fraction in parts-per-billion. + fraction_ppb: u64, + /// TAO repaid to extinguish the liability slice. + repaid_tao: TaoBalance, + /// Alpha (floor + buffer) returned to the trader. + returned: AlphaBalance, + }, + /// A covered long defaulted after its buffer reached dust. + LongDefaulted { + /// Position owner coldkey. + coldkey: T::AccountId, + /// Subnet the long was on. + netuid: NetUid, + }, + /// A covered long was settled at subnet deregistration. + LongTerminalSettled { + /// Position owner coldkey. + coldkey: T::AccountId, + /// Subnet that deregistered. + netuid: NetUid, + /// Terminal equity (Alpha) returned to the trader. + equity: AlphaBalance, + }, } } diff --git a/pallets/subtensor/src/macros/hooks.rs b/pallets/subtensor/src/macros/hooks.rs index 869d6074da..3f0000edfb 100644 --- a/pallets/subtensor/src/macros/hooks.rs +++ b/pallets/subtensor/src/macros/hooks.rs @@ -18,6 +18,19 @@ mod hooks { let hotkey_swap_clean_up_weight = Self::clean_up_hotkey_swap_records(block_number); let block_step_result = Self::block_step(); + // Account the per-block derivative decay hooks (run_short/long_decay, + // invoked inside block_step). Cost is O(active derivative subnets) via + // the per-subnet aggregate + Ω index (NOT per-position); bound by the + // total subnet count. + // + // OPERATIONAL INVARIANT: the decay WeightInfo is benchmarked over the + // component range [0, 128] (the current DefaultSubnetLimit). If the + // subnet count is ever raised above 128, the decay weights must be + // regenerated at the new ceiling (or this hook must clamp/paginate) + // before that limit is lifted. + let n = TotalNetworks::::get() as u32; + let decay_weight = ::WeightInfo::run_short_decay(n) + .saturating_add(::WeightInfo::run_long_decay(n)); match block_step_result { Ok(_) => { // --- If the block step was successful, return the weight. @@ -26,6 +39,7 @@ mod hooks { .saturating_add(T::DbWeight::get().reads(8304_u64)) .saturating_add(T::DbWeight::get().writes(110_u64)) .saturating_add(hotkey_swap_clean_up_weight) + .saturating_add(decay_weight) } Err(e) => { // --- If the block step was unsuccessful, return the weight anyway. @@ -34,6 +48,7 @@ mod hooks { .saturating_add(T::DbWeight::get().reads(8304_u64)) .saturating_add(T::DbWeight::get().writes(110_u64)) .saturating_add(hotkey_swap_clean_up_weight) + .saturating_add(decay_weight) } } } diff --git a/pallets/subtensor/src/tests/derivatives.rs b/pallets/subtensor/src/tests/derivatives.rs new file mode 100644 index 0000000000..4420f3b94f --- /dev/null +++ b/pallets/subtensor/src/tests/derivatives.rs @@ -0,0 +1,3131 @@ +#![allow(clippy::arithmetic_side_effects, clippy::unwrap_used)] +//! Covered continuous-unwind short derivatives — edge-case suite. +//! +//! Covers subnet creation, low liquidity, capacity/anti-split, decay + +//! restoration, the full close/default/top-up lifecycle, value conservation, +//! and subnet deregistration (in-the-money and underwater terminal settlement). + +use super::mock::*; +use crate::*; +use frame_support::{assert_noop, assert_ok}; +use sp_core::U256; +use substrate_fixed::types::{I64F64, I96F32}; +use subtensor_runtime_common::{AlphaBalance, NetUid, TaoBalance, Token}; + +const TAO: u64 = 1_000_000_000; + +fn t(v: u64) -> TaoBalance { + TaoBalance::from(v) +} + +fn bal(acc: &U256) -> u64 { + Balances::free_balance(acc).into() +} + +fn custody_bal(netuid: NetUid) -> u64 { + bal(&SubtensorModule::short_custody_account(netuid)) +} + +fn assert_approx(a: u64, b: u64, tol: u64, what: &str) { + let d = a.abs_diff(b); + assert!(d <= tol, "{what}: {a} vs {b} (diff {d} > tol {tol})"); +} + +/// Dynamic subnet with balance-backed reserves, a warmed price EMA, shorts +/// enabled, and a generous footprint cap. Returns the netuid. +fn setup_market(tao_reserve: u64, alpha_reserve: u64, price: f64) -> NetUid { + let owner_c = U256::from(1); + let owner_h = U256::from(2); + let netuid = add_dynamic_network(&owner_h, &owner_c); + setup_reserves(netuid, t(tao_reserve), AlphaBalance::from(alpha_reserve)); + // Back the pool TAO with real balance so custody transfers can move it. + let sa = SubtensorModule::get_subnet_account_id(netuid).unwrap(); + add_balance_to_coldkey_account(&sa, t(tao_reserve)); + SubnetMovingPrice::::insert(netuid, I96F32::from_num(price)); + SubtensorModule::set_shorts_enabled(true); + SubtensorModule::set_short_kappa_ppb(900_000_000); // κ = 0.9, generous + netuid +} + +/// Credit `q` alpha as stake at `(hotkey, coldkey)` without touching the pool, +/// mirroring the `SubnetAlphaOut` bump a real stake performs. +fn give_alpha(hotkey: U256, coldkey: U256, netuid: NetUid, q: AlphaBalance) { + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid, q); + SubnetAlphaOut::::mutate(netuid, |o| *o = o.saturating_add(q)); +} + +// --------------------------------------------------------------------------- +// Gating & subnet-kind edges +// --------------------------------------------------------------------------- + +#[test] +fn open_short_rejected_when_disabled() { + new_test_ext(1).execute_with(|| { + let netuid = setup_market(1000 * TAO, 1000 * TAO, 1.0); + SubtensorModule::set_shorts_enabled(false); + let trader = U256::from(10); + add_balance_to_coldkey_account(&trader, t(1000 * TAO)); + assert_noop!( + SubtensorModule::open_short( + RuntimeOrigin::signed(trader), + U256::from(11), + netuid, + t(100 * TAO), + AlphaBalance::MAX + ), + Error::::ShortsDisabled + ); + }); +} + +#[test] +fn open_short_rejected_on_stable_subnet() { + new_test_ext(1).execute_with(|| { + let netuid = setup_market(1000 * TAO, 1000 * TAO, 1.0); + SubnetMechanism::::insert(netuid, 0); // stable + let trader = U256::from(10); + add_balance_to_coldkey_account(&trader, t(1000 * TAO)); + assert_noop!( + SubtensorModule::open_short( + RuntimeOrigin::signed(trader), + U256::from(11), + netuid, + t(100 * TAO), + AlphaBalance::MAX + ), + Error::::SubnetNotDynamic + ); + }); +} + +#[test] +fn open_short_rejects_zero_input() { + new_test_ext(1).execute_with(|| { + let netuid = setup_market(1000 * TAO, 1000 * TAO, 1.0); + let trader = U256::from(10); + add_balance_to_coldkey_account(&trader, t(1000 * TAO)); + assert_noop!( + SubtensorModule::open_short( + RuntimeOrigin::signed(trader), + U256::from(11), + netuid, + t(0), + AlphaBalance::MAX + ), + Error::::AmountTooLow + ); + }); +} + +// --------------------------------------------------------------------------- +// Open math vs spec worked example (§1.7–1.8) +// --------------------------------------------------------------------------- + +#[test] +fn quote_matches_spec_worked_example() { + new_test_ext(1).execute_with(|| { + // Pool 1000 TAO / 100_000 alpha, price 0.01, pre-trade S = 100 TAO. + let netuid = setup_market(1000 * TAO, 100_000 * TAO, 0.01); + let mut agg = ShortAggregate::::get(netuid); + agg.b_sigma = t(100 * TAO); + ShortAggregate::::insert(netuid, agg); + + let q = SubtensorModule::quote_open_short(netuid, t(62_500_000_000)).unwrap(); // P = 62.5 TAO + assert_approx(q.gross_collateral.to_u64(), 100 * TAO, TAO / 10, "C"); + assert_approx(q.retained_proceeds.to_u64(), 37_500_000_000, TAO / 10, "N"); + assert_approx(q.alpha_liability.to_u64(), 3902 * TAO, 10 * TAO, "Q"); + assert_approx(q.escrow.to_u64(), 39 * TAO, TAO / 2, "E"); + assert_approx(q.effective_ltv, 375_000_000, 2_000_000, "lambda_eff"); + }); +} + +#[test] +fn open_matches_quote_and_moves_pool() { + new_test_ext(1).execute_with(|| { + let netuid = setup_market(1000 * TAO, 1000 * TAO, 1.0); + let trader = U256::from(10); + let hotkey = U256::from(11); + add_balance_to_coldkey_account(&trader, t(1000 * TAO)); + let p = 100 * TAO; + + let quote = SubtensorModule::quote_open_short(netuid, t(p)).unwrap(); + let tao_before = SubnetTAO::::get(netuid).to_u64(); + let trader_before = bal(&trader); + + assert_ok!(SubtensorModule::open_short( + RuntimeOrigin::signed(trader), + hotkey, + netuid, + t(p), + AlphaBalance::MAX + )); + + let pos = ShortPositions::::get(netuid, trader).unwrap(); + // Position fields equal the pure quote (same code path). + assert_eq!(pos.r_stored, quote.retained_proceeds); + assert_eq!(pos.q_liability, quote.alpha_liability); + assert_eq!(pos.e_stored, quote.escrow); + assert_eq!(pos.p_floor, t(p)); + assert_eq!(pos.hotkey, hotkey); + assert!(pos.b_stored.to_u64() > 0); + + let n = quote.retained_proceeds.to_u64(); + let e = quote.escrow.to_u64(); + // Pool lost exactly N+E TAO; trader paid exactly P; custody holds P+N+E. + assert_eq!(SubnetTAO::::get(netuid).to_u64(), tao_before - n - e); + assert_eq!(bal(&trader), trader_before - p); + assert_eq!(custody_bal(netuid), p + n + e); + + // Aggregate reflects the single position. + let agg = ShortAggregate::::get(netuid); + assert_eq!(agg.r_sigma, pos.r_stored); + assert_eq!(agg.e_sigma, pos.e_stored); + assert_eq!(agg.b_sigma, pos.b_stored); + assert_eq!(agg.q_sigma, pos.q_liability); + }); +} + +// --------------------------------------------------------------------------- +// Capacity / anti-split (§5.1–5.2.1) +// --------------------------------------------------------------------------- + +#[test] +fn open_rejected_when_capacity_exceeded() { + new_test_ext(1).execute_with(|| { + let netuid = setup_market(1000 * TAO, 1000 * TAO, 1.0); + SubtensorModule::set_short_kappa_ppb(1_000_000); // κ = 0.001 → cap ≈ 1 TAO + let trader = U256::from(10); + add_balance_to_coldkey_account(&trader, t(1000 * TAO)); + assert_noop!( + SubtensorModule::open_short( + RuntimeOrigin::signed(trader), + U256::from(11), + netuid, + t(100 * TAO), + AlphaBalance::MAX + ), + Error::::ShortCapacityExceeded + ); + }); +} + +#[test] +fn stacked_opens_share_capacity() { + new_test_ext(1).execute_with(|| { + let netuid = setup_market(1000 * TAO, 1000 * TAO, 1.0); + // Cap ≈ 70 TAO: one P=50 open (B≈47 TAO) fits; a second does not. + SubtensorModule::set_short_kappa_ppb(70_000_000); + let a = U256::from(10); + let b = U256::from(20); + add_balance_to_coldkey_account(&a, t(1000 * TAO)); + add_balance_to_coldkey_account(&b, t(1000 * TAO)); + + assert_ok!(SubtensorModule::open_short( + RuntimeOrigin::signed(a), + U256::from(11), + netuid, + t(50 * TAO), + AlphaBalance::MAX + )); + assert_noop!( + SubtensorModule::open_short( + RuntimeOrigin::signed(b), + U256::from(21), + netuid, + t(50 * TAO), + AlphaBalance::MAX + ), + Error::::ShortCapacityExceeded + ); + }); +} + +// --------------------------------------------------------------------------- +// Execution bounds + validate-before-mutate (anti-sandwich, no fund stranding) +// --------------------------------------------------------------------------- + +#[test] +fn open_short_rejects_when_liability_exceeds_bound() { + new_test_ext(1).execute_with(|| { + let netuid = setup_market(1000 * TAO, 1000 * TAO, 1.0); + let trader = U256::from(10); + add_balance_to_coldkey_account(&trader, t(1000 * TAO)); + // A 1-rao liability cap is below any real Q. `assert_noop!` proves the + // bound is enforced before any transfer/reserve mutation (no state moves). + assert_noop!( + SubtensorModule::open_short( + RuntimeOrigin::signed(trader), + U256::from(11), + netuid, + t(100 * TAO), + AlphaBalance::from(1) + ), + Error::::SlippageTooHigh + ); + assert_eq!(custody_bal(netuid), 0); + assert!(ShortPositions::::get(netuid, trader).is_none()); + }); +} + +#[test] +fn open_short_wrong_hotkey_merge_strands_no_funds() { + new_test_ext(1).execute_with(|| { + let netuid = setup_market(1000 * TAO, 1000 * TAO, 1.0); + let trader = U256::from(10); + add_balance_to_coldkey_account(&trader, t(1000 * TAO)); + assert_ok!(SubtensorModule::open_short( + RuntimeOrigin::signed(trader), + U256::from(11), + netuid, + t(50 * TAO), + AlphaBalance::MAX + )); + // A merge against a different hotkey must reject BEFORE moving funds. + // `assert_noop!` fails if any balance/reserve mutated before the error. + assert_noop!( + SubtensorModule::open_short( + RuntimeOrigin::signed(trader), + U256::from(12), + netuid, + t(50 * TAO), + AlphaBalance::MAX + ), + Error::::ShortHotkeyMismatch + ); + }); +} + +#[test] +fn open_long_rejects_when_liability_exceeds_bound() { + new_test_ext(1).execute_with(|| { + let netuid = setup_long(1000 * TAO, 1000 * TAO, 1.0); + let trader = U256::from(10); + let hotkey = U256::from(11); + give_alpha(hotkey, trader, netuid, AlphaBalance::from(500 * TAO)); + // A 1-rao TAO-liability cap is below any real D ⇒ rejected before any + // mutation (long-side mirror of the short execution bound). + assert_noop!( + SubtensorModule::open_long( + RuntimeOrigin::signed(trader), + hotkey, + netuid, + AlphaBalance::from(100 * TAO), + TaoBalance::from(1) + ), + Error::::SlippageTooHigh + ); + assert!(LongPositions::::get(netuid, trader).is_none()); + }); +} + +// Directly exercises the #[transactional] rollback: the trader's floor moves to +// custody first, then the pool→custody transfer of N+E FAILS (subnet account +// drained below it). The whole open must roll back — trader keeps their floor, +// nothing lands in custody, and pool reserves are untouched. Without +// #[transactional] the first transfer would persist and the trader would lose P. +#[test] +fn open_short_failed_pool_transfer_rolls_back_atomically() { + new_test_ext(1).execute_with(|| { + let netuid = setup_market(1000 * TAO, 1000 * TAO, 1.0); + let trader = U256::from(10); + add_balance_to_coldkey_account(&trader, t(1000 * TAO)); + // Drain the subnet account so it cannot cover the N+E pool→custody leg + // (≈76 TAO for P=100), while SubnetTAO storage stays high for pricing. + let sa = SubtensorModule::get_subnet_account_id(netuid).unwrap(); + remove_balance_from_coldkey_account(&sa, t(995 * TAO)); + + let trader_before = bal(&trader); + let tao_before = SubnetTAO::::get(netuid); + + let r = SubtensorModule::open_short( + RuntimeOrigin::signed(trader), + U256::from(11), + netuid, + t(100 * TAO), + AlphaBalance::MAX, + ); + assert!( + r.is_err(), + "open must fail when the pool leg can't be funded" + ); + + // Atomic rollback: floor returned, custody empty, reserves unchanged, no position. + assert_eq!( + bal(&trader), + trader_before, + "floor must be rolled back to the trader" + ); + assert_eq!(custody_bal(netuid), 0, "nothing may remain in custody"); + assert_eq!( + SubnetTAO::::get(netuid), + tao_before, + "pool reserve must be untouched" + ); + assert!(ShortPositions::::get(netuid, trader).is_none()); + assert!(!ShortActiveSubnets::::contains_key(netuid)); + }); +} + +// --------------------------------------------------------------------------- +// Low liquidity (§4.1: λ_eff ≤ 0 rejects oversized opens) +// --------------------------------------------------------------------------- + +#[test] +fn low_liquidity_rejects_oversized_open() { + new_test_ext(1).execute_with(|| { + let netuid = setup_market(10 * TAO, 10 * TAO, 1.0); // tiny pool + let trader = U256::from(10); + add_balance_to_coldkey_account(&trader, t(1000 * TAO)); + // P far larger than the pool can collateralize → retained proceeds ≤ 0. + assert_noop!( + SubtensorModule::open_short( + RuntimeOrigin::signed(trader), + U256::from(11), + netuid, + t(100 * TAO), + AlphaBalance::MAX + ), + Error::::EffectiveLtvNonPositive + ); + }); +} + +// Warm-EMA guard: opening on a cold-`pEMA` (freshly registered, no price +// history) subnet is rejected, since the EMA risk reference and terminal +// anti-suppression leg are unavailable there. Opens are admitted only once the +// EMA warms. +#[test] +fn open_rejected_on_cold_ema_subnet() { + new_test_ext(1).execute_with(|| { + let owner_c = U256::from(1); + let owner_h = U256::from(2); + let netuid = add_dynamic_network(&owner_h, &owner_c); + setup_reserves(netuid, t(1000 * TAO), AlphaBalance::from(1000 * TAO)); + let sa = SubtensorModule::get_subnet_account_id(netuid).unwrap(); + add_balance_to_coldkey_account(&sa, t(1000 * TAO)); + SubtensorModule::set_shorts_enabled(true); + SubtensorModule::set_short_kappa_ppb(900_000_000); + assert_eq!(SubtensorModule::get_moving_alpha_price(netuid), 0); // cold + + let trader = U256::from(10); + add_balance_to_coldkey_account(&trader, t(1000 * TAO)); + assert_noop!( + SubtensorModule::open_short( + RuntimeOrigin::signed(trader), + U256::from(11), + netuid, + t(50 * TAO), + AlphaBalance::MAX + ), + Error::::ColdEmaNotAllowed + ); + assert!(ShortPositions::::get(netuid, trader).is_none()); + + // Once the EMA warms, the same open is admitted. + SubnetMovingPrice::::insert(netuid, I96F32::from_num(1.0)); + assert_ok!(SubtensorModule::open_short( + RuntimeOrigin::signed(trader), + U256::from(11), + netuid, + t(50 * TAO), + AlphaBalance::MAX + )); + assert!(ShortPositions::::get(netuid, trader).is_some()); + }); +} + +// --------------------------------------------------------------------------- +// Decay + restoration (§6) +// --------------------------------------------------------------------------- + +#[test] +fn decay_shrinks_buffer_and_restores_tao() { + new_test_ext(1).execute_with(|| { + let netuid = setup_market(1000 * TAO, 1000 * TAO, 1.0); + let trader = U256::from(10); + add_balance_to_coldkey_account(&trader, t(1000 * TAO)); + assert_ok!(SubtensorModule::open_short( + RuntimeOrigin::signed(trader), + U256::from(11), + netuid, + t(100 * TAO), + AlphaBalance::MAX + )); + + let r0 = ShortAggregate::::get(netuid).r_sigma.to_u64(); + let tao0 = SubnetTAO::::get(netuid).to_u64(); + let custody0 = custody_bal(netuid); + let omega0 = ShortAggregate::::get(netuid).omega; + + for _ in 0..200 { + SubtensorModule::run_short_decay(); + } + + let agg = ShortAggregate::::get(netuid); + let r1 = agg.r_sigma.to_u64(); + let tao1 = SubnetTAO::::get(netuid).to_u64(); + let custody1 = custody_bal(netuid); + + assert!(r1 < r0, "buffer must decay: {r1} !< {r0}"); + assert!(agg.omega > omega0, "omega must increase"); + let restored = tao1 - tao0; + let drained = custody0 - custody1; + assert!(restored > 0, "TAO must be restored to the pool"); + // Conservation of the restoration leg: custody out == pool in. + assert_eq!(restored, drained); + }); +} + +#[test] +fn block_step_runs_decay() { + new_test_ext(1).execute_with(|| { + let netuid = setup_market(1000 * TAO, 1000 * TAO, 1.0); + let trader = U256::from(10); + add_balance_to_coldkey_account(&trader, t(1000 * TAO)); + assert_ok!(SubtensorModule::open_short( + RuntimeOrigin::signed(trader), + U256::from(11), + netuid, + t(100 * TAO), + AlphaBalance::MAX + )); + let r0 = ShortAggregate::::get(netuid).r_sigma.to_u64(); + step_block(5); + assert!(ShortAggregate::::get(netuid).r_sigma.to_u64() < r0); + }); +} + +// --------------------------------------------------------------------------- +// Top-up (§8.2) +// --------------------------------------------------------------------------- + +#[test] +fn top_up_adds_buffer_only() { + new_test_ext(1).execute_with(|| { + let netuid = setup_market(1000 * TAO, 1000 * TAO, 1.0); + let trader = U256::from(10); + add_balance_to_coldkey_account(&trader, t(1000 * TAO)); + assert_ok!(SubtensorModule::open_short( + RuntimeOrigin::signed(trader), + U256::from(11), + netuid, + t(100 * TAO), + AlphaBalance::MAX + )); + + let pos0 = ShortPositions::::get(netuid, trader).unwrap(); + let custody0 = custody_bal(netuid); + assert_ok!(SubtensorModule::top_up_short( + RuntimeOrigin::signed(trader), + netuid, + t(10 * TAO) + )); + let pos1 = ShortPositions::::get(netuid, trader).unwrap(); + + assert_eq!(pos1.r_stored, pos0.r_stored + t(10 * TAO)); + assert_eq!(pos1.q_liability, pos0.q_liability); // unchanged + assert_eq!(pos1.e_stored, pos0.e_stored); + assert_eq!(pos1.b_stored, pos0.b_stored); + assert_eq!(custody_bal(netuid), custody0 + 10 * TAO); + }); +} + +#[test] +fn top_up_requires_position() { + new_test_ext(1).execute_with(|| { + let netuid = setup_market(1000 * TAO, 1000 * TAO, 1.0); + let trader = U256::from(10); + add_balance_to_coldkey_account(&trader, t(1000 * TAO)); + assert_noop!( + SubtensorModule::top_up_short(RuntimeOrigin::signed(trader), netuid, t(TAO)), + Error::::ShortPositionNotFound + ); + }); +} + +// --------------------------------------------------------------------------- +// Merge (§8.6) +// --------------------------------------------------------------------------- + +#[test] +fn additional_open_merges_into_position() { + new_test_ext(1).execute_with(|| { + let netuid = setup_market(1000 * TAO, 1000 * TAO, 1.0); + let trader = U256::from(10); + let hotkey = U256::from(11); + add_balance_to_coldkey_account(&trader, t(1000 * TAO)); + + assert_ok!(SubtensorModule::open_short( + RuntimeOrigin::signed(trader), + hotkey, + netuid, + t(50 * TAO), + AlphaBalance::MAX + )); + let p1 = ShortPositions::::get(netuid, trader).unwrap(); + assert_ok!(SubtensorModule::open_short( + RuntimeOrigin::signed(trader), + hotkey, + netuid, + t(50 * TAO), + AlphaBalance::MAX + )); + let p2 = ShortPositions::::get(netuid, trader).unwrap(); + + assert_eq!(p2.p_floor, t(100 * TAO)); + assert!(p2.q_liability > p1.q_liability); + assert!(p2.r_stored > p1.r_stored); + // Single merged position, not two entries. + assert_eq!(ShortPositions::::iter_prefix(netuid).count(), 1); + }); +} + +// --------------------------------------------------------------------------- +// Close (§8.3–8.5) + conservation +// --------------------------------------------------------------------------- + +#[test] +fn full_close_conserves_value() { + new_test_ext(1).execute_with(|| { + let netuid = setup_market(1000 * TAO, 1000 * TAO, 1.0); + let trader = U256::from(10); + let hotkey = U256::from(11); + add_balance_to_coldkey_account(&trader, t(1000 * TAO)); + let p = 100 * TAO; + assert_ok!(SubtensorModule::open_short( + RuntimeOrigin::signed(trader), + hotkey, + netuid, + t(p), + AlphaBalance::MAX + )); + + let pos = ShortPositions::::get(netuid, trader).unwrap(); + let (n, e, q) = ( + pos.r_stored.to_u64(), + pos.e_stored.to_u64(), + pos.q_liability, + ); + let tao_after_open = SubnetTAO::::get(netuid).to_u64(); + let alpha_after_open = SubnetAlphaIn::::get(netuid).to_u64(); + + // Trader acquires the liability alpha (seeded) and closes fully. + give_alpha( + hotkey, + trader, + netuid, + AlphaBalance::from(q.to_u64() + 10 * TAO), + ); + let trader_before_close = bal(&trader); + + assert_ok!(SubtensorModule::close_short( + RuntimeOrigin::signed(trader), + netuid, + 1_000_000_000 + )); + + // Position gone, aggregate empty. + assert!(ShortPositions::::get(netuid, trader).is_none()); + let agg = ShortAggregate::::get(netuid); + assert_eq!(agg.r_sigma.to_u64(), 0); + assert_eq!(agg.q_sigma.to_u64(), 0); + + // Custody fully drained; pool regained escrow + repaid alpha. + assert_eq!(custody_bal(netuid), 0); + assert_eq!(SubnetTAO::::get(netuid).to_u64(), tao_after_open + e); + assert_eq!( + SubnetAlphaIn::::get(netuid).to_u64(), + alpha_after_open + q.to_u64() + ); + // Trader received floor + remaining buffer = P + N. + assert_eq!(bal(&trader), trader_before_close + p + n); + }); +} + +#[test] +fn partial_close_reduces_prorata() { + new_test_ext(1).execute_with(|| { + let netuid = setup_market(1000 * TAO, 1000 * TAO, 1.0); + let trader = U256::from(10); + let hotkey = U256::from(11); + add_balance_to_coldkey_account(&trader, t(1000 * TAO)); + assert_ok!(SubtensorModule::open_short( + RuntimeOrigin::signed(trader), + hotkey, + netuid, + t(100 * TAO), + AlphaBalance::MAX + )); + + let pos0 = ShortPositions::::get(netuid, trader).unwrap(); + give_alpha( + hotkey, + trader, + netuid, + AlphaBalance::from(pos0.q_liability.to_u64()), + ); + + // Close half. + assert_ok!(SubtensorModule::close_short( + RuntimeOrigin::signed(trader), + netuid, + 500_000_000 + )); + let pos1 = ShortPositions::::get(netuid, trader).unwrap(); + + assert_approx(pos1.p_floor.to_u64(), pos0.p_floor.to_u64() / 2, 2, "p/2"); + assert_approx( + pos1.q_liability.to_u64(), + pos0.q_liability.to_u64() / 2, + 2, + "q/2", + ); + assert_approx(pos1.r_stored.to_u64(), pos0.r_stored.to_u64() / 2, 2, "r/2"); + assert_approx(pos1.e_stored.to_u64(), pos0.e_stored.to_u64() / 2, 2, "e/2"); + }); +} + +#[test] +fn close_without_alpha_rejected() { + new_test_ext(1).execute_with(|| { + let netuid = setup_market(1000 * TAO, 1000 * TAO, 1.0); + let trader = U256::from(10); + add_balance_to_coldkey_account(&trader, t(1000 * TAO)); + assert_ok!(SubtensorModule::open_short( + RuntimeOrigin::signed(trader), + U256::from(11), + netuid, + t(100 * TAO), + AlphaBalance::MAX + )); + // No alpha staked at the hotkey → cannot repay the liability. + assert_noop!( + SubtensorModule::close_short(RuntimeOrigin::signed(trader), netuid, 1_000_000_000), + Error::::InsufficientAlphaToClose + ); + }); +} + +#[test] +fn close_invalid_fraction_rejected() { + new_test_ext(1).execute_with(|| { + let netuid = setup_market(1000 * TAO, 1000 * TAO, 1.0); + let trader = U256::from(10); + add_balance_to_coldkey_account(&trader, t(1000 * TAO)); + assert_ok!(SubtensorModule::open_short( + RuntimeOrigin::signed(trader), + U256::from(11), + netuid, + t(100 * TAO), + AlphaBalance::MAX + )); + assert_noop!( + SubtensorModule::close_short(RuntimeOrigin::signed(trader), netuid, 0), + Error::::InvalidCloseFraction + ); + assert_noop!( + SubtensorModule::close_short(RuntimeOrigin::signed(trader), netuid, 1_000_000_001), + Error::::InvalidCloseFraction + ); + }); +} + +// --------------------------------------------------------------------------- +// Default (§7) +// --------------------------------------------------------------------------- + +#[test] +fn default_rejected_when_buffer_above_dust() { + new_test_ext(1).execute_with(|| { + let netuid = setup_market(1000 * TAO, 1000 * TAO, 1.0); + let trader = U256::from(10); + add_balance_to_coldkey_account(&trader, t(1000 * TAO)); + assert_ok!(SubtensorModule::open_short( + RuntimeOrigin::signed(trader), + U256::from(11), + netuid, + t(100 * TAO), + AlphaBalance::MAX + )); + let poker = U256::from(99); + assert_noop!( + SubtensorModule::default_short(RuntimeOrigin::signed(poker), trader, netuid), + Error::::PositionNotDefaultEligible + ); + }); +} + +#[test] +fn default_recycles_floor_and_restores_residual() { + new_test_ext(1).execute_with(|| { + let netuid = setup_market(1000 * TAO, 1000 * TAO, 1.0); + let trader = U256::from(10); + add_balance_to_coldkey_account(&trader, t(1000 * TAO)); + assert_ok!(SubtensorModule::open_short( + RuntimeOrigin::signed(trader), + U256::from(11), + netuid, + t(100 * TAO), + AlphaBalance::MAX + )); + + let pos = ShortPositions::::get(netuid, trader).unwrap(); + let (p, n, e) = ( + pos.p_floor.to_u64(), + pos.r_stored.to_u64(), + pos.e_stored.to_u64(), + ); + // Make the whole buffer dust so the position is default-eligible now. + SubtensorModule::set_short_dust(t(1000 * TAO)); + SubtensorModule::set_short_default_grace(0); // no anti-snipe delay for this test + + let tao0 = SubnetTAO::::get(netuid).to_u64(); + let ti0 = TotalIssuance::::get(); + let poker = U256::from(99); + assert_ok!(SubtensorModule::default_short( + RuntimeOrigin::signed(poker), + trader, + netuid + )); + + // Position removed; residual R+E restored to pool; floor P recycled (TI down). + assert!(ShortPositions::::get(netuid, trader).is_none()); + assert_eq!(SubnetTAO::::get(netuid).to_u64(), tao0 + n + e); + assert_eq!(custody_bal(netuid), 0); + assert_eq!(TotalIssuance::::get(), ti0 - t(p)); + let agg = ShortAggregate::::get(netuid); + assert_eq!(agg.r_sigma.to_u64(), 0); + }); +} + +#[test] +fn default_requires_position() { + new_test_ext(1).execute_with(|| { + let netuid = setup_market(1000 * TAO, 1000 * TAO, 1.0); + assert_noop!( + SubtensorModule::default_short( + RuntimeOrigin::signed(U256::from(99)), + U256::from(10), + netuid + ), + Error::::ShortPositionNotFound + ); + }); +} + +// --------------------------------------------------------------------------- +// Subnet deregistration terminal settlement (§11.4) +// --------------------------------------------------------------------------- + +#[test] +fn dereg_settles_in_the_money_short() { + new_test_ext(1).execute_with(|| { + let netuid = setup_market(1000 * TAO, 1000 * TAO, 1.0); + let trader = U256::from(10); + let hotkey = U256::from(11); + add_balance_to_coldkey_account(&trader, t(1000 * TAO)); + assert_ok!(SubtensorModule::open_short( + RuntimeOrigin::signed(trader), + hotkey, + netuid, + t(100 * TAO), + AlphaBalance::MAX + )); + + let pos = ShortPositions::::get(netuid, trader).unwrap(); + let c = pos.p_floor.to_u64() + pos.r_stored.to_u64(); // P + R + let trader_before = bal(&trader); + + // Settle terminal. With pEMA = 1 and a bounded liability, equity > 0. + SubtensorModule::settle_shorts_on_dereg(netuid); + + assert!(ShortPositions::::get(netuid, trader).is_none()); + assert_eq!(custody_bal(netuid), 0); + // Trader received positive equity, strictly less than the full claim. + let gained = bal(&trader) - trader_before; + assert!(gained > 0 && gained < c, "equity {gained} not in (0,{c})"); + }); +} + +#[test] +fn dereg_settles_underwater_short_with_zero_equity() { + new_test_ext(1).execute_with(|| { + let netuid = setup_market(1000 * TAO, 1000 * TAO, 1.0); + let trader = U256::from(10); + let hotkey = U256::from(11); + add_balance_to_coldkey_account(&trader, t(1000 * TAO)); + assert_ok!(SubtensorModule::open_short( + RuntimeOrigin::signed(trader), + hotkey, + netuid, + t(100 * TAO), + AlphaBalance::MAX + )); + + // Drive the EMA liability reference far above the collateral claim. + SubnetMovingPrice::::insert(netuid, I96F32::from_num(50.0)); + let trader_before = bal(&trader); + let ti0 = TotalIssuance::::get(); + + SubtensorModule::settle_shorts_on_dereg(netuid); + + assert!(ShortPositions::::get(netuid, trader).is_none()); + assert_eq!(custody_bal(netuid), 0); + // No equity paid; the full claim was recycled (issuance fell). + assert_eq!(bal(&trader), trader_before); + assert!(TotalIssuance::::get() < ti0); + }); +} + +#[test] +fn dereg_cold_ema_caps_equity_at_floor() { + new_test_ext(1).execute_with(|| { + let netuid = setup_market(1000 * TAO, 1000 * TAO, 1.0); + let trader = U256::from(10); + let hotkey = U256::from(11); + add_balance_to_coldkey_account(&trader, t(1000 * TAO)); + assert_ok!(SubtensorModule::open_short( + RuntimeOrigin::signed(trader), + hotkey, + netuid, + t(100 * TAO), + AlphaBalance::MAX + )); + let p_floor = ShortPositions::::get(netuid, trader) + .unwrap() + .p_floor + .to_u64(); + // Cold price EMA at settlement: no trustworthy slow reference. The cold-EMA + // guard must floor K_D at the retained buffer R, so the trader recovers at + // most their own floor P — never the pool-origin buffer. + SubnetMovingPrice::::insert(netuid, I96F32::from_num(0)); + let before = bal(&trader); + SubtensorModule::settle_shorts_on_dereg(netuid); + let gained = bal(&trader) - before; + assert!( + gained <= p_floor, + "cold-EMA equity {gained} must not exceed floor {p_floor}" + ); + assert_eq!(custody_bal(netuid), 0); + }); +} + +// Regression for the order-dependence fix: every position's terminal K_D is +// priced against ONE frozen pre-settlement reserve snapshot, so per-position +// equity does not depend on settlement (coldkey storage) order. Here we settle +// several positions and assert each paid equity equals the value computed +// against the snapshot captured before any escrow restoration. Pre-fix, later +// positions saw earlier positions' escrow already restored into SubnetTAO and +// were mispriced. +#[test] +fn terminal_settlement_order_independent() { + new_test_ext(1).execute_with(|| { + // price = 1.0 ⇒ pEMA warm; equal reserves ⇒ K_spot == K_EMA. + let netuid = setup_market(2000 * TAO, 2000 * TAO, 1.0); + let hotkey = U256::from(11); + let traders = [ + U256::from(21), + U256::from(22), + U256::from(23), + U256::from(24), + ]; + for tr in traders.iter() { + add_balance_to_coldkey_account(tr, t(1000 * TAO)); + assert_ok!(SubtensorModule::open_short( + RuntimeOrigin::signed(*tr), + hotkey, + netuid, + t(50 * TAO), + AlphaBalance::MAX + )); + } + + // Frozen snapshot, captured before settlement (no decay tick has run, so + // stored position values are the materialized values). + let t0 = SubnetTAO::::get(netuid).to_u64() as u128; + let a0 = SubnetAlphaIn::::get(netuid).to_u64() as u128; + let pema = SubtensorModule::get_moving_alpha_price(netuid); + let t_ema0 = (I96F32::from_num(pema) * I96F32::from_num(a0)).to_num::(); + // Byte-exact mirror of `buyback_cost_rao` (mod.rs), incl. the u64::MAX clamp. + let bb = |pay: u128, recv: u128, amt: u128| -> u128 { + if recv <= amt { + u64::MAX as u128 + } else { + pay.saturating_mul(amt) + .div_ceil(recv - amt) + .min(u64::MAX as u128) + } + }; + // Aggregate (split-neutral) pricing: K_Σ on the total liability, allocated + // pro-rata (ceiling). Order-independent because every position reads the + // same frozen snapshot + aggregate. + let q_sigma: u128 = traders + .iter() + .map(|tr| { + ShortPositions::::get(netuid, tr) + .unwrap() + .q_liability + .to_u64() as u128 + }) + .sum(); + let k_sigma = bb(t0, a0, q_sigma).max(bb(t_ema0, a0, q_sigma)); + let mut expected: Vec = vec![]; + let mut before: Vec = vec![]; + for tr in traders.iter() { + let pos = ShortPositions::::get(netuid, tr).unwrap(); + let c = pos.p_floor.to_u64() as u128 + pos.r_stored.to_u64() as u128; + let q = pos.q_liability.to_u64() as u128; + let k_i = k_sigma.saturating_mul(q).div_ceil(q_sigma); + expected.push(c.saturating_sub(k_i).min(u64::MAX as u128) as u64); + before.push(bal(tr)); + } + + SubtensorModule::settle_shorts_on_dereg(netuid); + + for (i, tr) in traders.iter().enumerate() { + let paid = bal(tr) - before[i]; + assert_eq!( + paid, expected[i], + "position {i} mispriced vs frozen snapshot (order-dependent settlement)" + ); + } + assert!( + expected[0] > 0, + "expected in-the-money equity to make the test meaningful" + ); + assert!(ShortPositions::::iter_prefix(netuid).next().is_none()); + }); +} + +// Long-side mirror of the order-independence regression: every long position's +// terminal cover is priced against the same frozen pre-settlement snapshot, so +// per-position equity (minted as stake) is independent of settlement order. +#[test] +fn long_terminal_settlement_order_independent() { + new_test_ext(1).execute_with(|| { + let netuid = setup_long(2000 * TAO, 2000 * TAO, 1.0); + let hotkey = U256::from(11); + let traders = [ + U256::from(31), + U256::from(32), + U256::from(33), + U256::from(34), + ]; + for tr in traders.iter() { + give_alpha(hotkey, *tr, netuid, AlphaBalance::from(500 * TAO)); + assert_ok!(SubtensorModule::open_long( + RuntimeOrigin::signed(*tr), + hotkey, + netuid, + AlphaBalance::from(50 * TAO), + TaoBalance::MAX + )); + } + + let a0 = SubnetAlphaIn::::get(netuid).to_u64() as u128; + let t0 = SubnetTAO::::get(netuid).to_u64() as u128; + let pema = SubtensorModule::get_moving_alpha_price(netuid); + let t_ema0 = (I96F32::from_num(pema) * I96F32::from_num(a0)).to_num::(); + let bb = |pay: u128, recv: u128, amt: u128| -> u128 { + if recv <= amt { + u64::MAX as u128 + } else { + pay.saturating_mul(amt) + .div_ceil(recv - amt) + .min(u64::MAX as u128) + } + }; + let stake = |tr: &U256| -> u64 { + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, tr, netuid) + .to_u64() + }; + // Aggregate (split-neutral) cover: cover_Σ on the total D, allocated + // pro-rata (ceiling) — order-independent against the frozen snapshot. + let d_sigma: u128 = traders + .iter() + .map(|tr| { + LongPositions::::get(netuid, tr) + .unwrap() + .d_liability + .to_u64() as u128 + }) + .sum(); + let cover_sigma = bb(a0, t0, d_sigma).max(bb(a0, t_ema0, d_sigma)); + let mut expected: Vec = vec![]; + let mut before: Vec = vec![]; + for tr in traders.iter() { + let pos = LongPositions::::get(netuid, tr).unwrap(); + let c_l = pos.p_floor.to_u64() as u128 + pos.r_stored.to_u64() as u128; + let d = pos.d_liability.to_u64() as u128; + let cover = c_l.min(cover_sigma.saturating_mul(d).div_ceil(d_sigma)); + expected.push(c_l.saturating_sub(cover).min(u64::MAX as u128) as u64); + before.push(stake(tr)); + } + + SubtensorModule::settle_longs_on_dereg(netuid); + + for (i, tr) in traders.iter().enumerate() { + let minted = stake(tr) - before[i]; + assert_eq!( + minted, expected[i], + "long position {i} mispriced vs frozen snapshot (order-dependent settlement)" + ); + } + assert!(expected[0] > 0, "expected in-the-money long equity"); + assert!(LongPositions::::iter_prefix(netuid).next().is_none()); + }); +} + +// Split-neutrality (spec §10.1): terminal cover is priced ONCE on the aggregate +// liability Q_Σ and allocated pro-rata, so wallet-splitting one liability across +// many coldkeys cannot reduce total cover. Because the CPMM buyback is convex, +// per-position pricing (the prior behavior) would give Σ K(q_i) < K(ΣQ); this +// test asserts the realized total cover tracks K(Q_Σ), not the smaller per-position sum. +#[test] +fn terminal_settlement_split_neutral() { + new_test_ext(1).execute_with(|| { + let netuid = setup_market(2000 * TAO, 2000 * TAO, 1.0); + let hotkey = U256::from(11); + let traders = [ + U256::from(41), + U256::from(42), + U256::from(43), + U256::from(44), + U256::from(45), + ]; + for tr in traders.iter() { + add_balance_to_coldkey_account(tr, t(1000 * TAO)); + assert_ok!(SubtensorModule::open_short( + RuntimeOrigin::signed(*tr), + hotkey, + netuid, + t(50 * TAO), + AlphaBalance::MAX + )); + } + let t0 = SubnetTAO::::get(netuid).to_u64() as u128; + let a0 = SubnetAlphaIn::::get(netuid).to_u64() as u128; + let pema = SubtensorModule::get_moving_alpha_price(netuid); + let t_ema0 = (I96F32::from_num(pema) * I96F32::from_num(a0)).to_num::(); + let bb = |pay: u128, recv: u128, amt: u128| -> u128 { + if recv <= amt { + u64::MAX as u128 + } else { + pay.saturating_mul(amt) + .div_ceil(recv - amt) + .min(u64::MAX as u128) + } + }; + let mut q_sigma = 0u128; + let mut c_sigma = 0u128; + let mut k_single_sum = 0u128; // the convex per-position sum (buggy behavior) + let mut before = vec![]; + for tr in traders.iter() { + let p = ShortPositions::::get(netuid, tr).unwrap(); + let q = p.q_liability.to_u64() as u128; + q_sigma += q; + c_sigma += p.p_floor.to_u64() as u128 + p.r_stored.to_u64() as u128; + k_single_sum += bb(t0, a0, q).max(bb(t_ema0, a0, q)); + before.push(bal(tr)); + } + let k_agg = bb(t0, a0, q_sigma).max(bb(t_ema0, a0, q_sigma)); + // Convexity must hold or the test is not discriminating. + assert!( + k_agg > k_single_sum, + "expected convex buyback: K(ΣQ) {k_agg} > Σ K(q_i) {k_single_sum}" + ); + + SubtensorModule::settle_shorts_on_dereg(netuid); + + let equity_sum: u128 = traders + .iter() + .zip(before.iter()) + .map(|(tr, b0)| (bal(tr) - b0) as u128) + .sum(); + // cover = collateral − equity. Split-neutral ⇒ total cover ≈ K(Q_Σ), + // (ceiling allocation makes it ≥ K_agg by < #positions rao), and strictly + // MORE than the convex per-position sum a splitter would have paid. + let cover_sum = c_sigma - equity_sum; + assert!( + cover_sum >= k_agg && cover_sum <= k_agg + traders.len() as u128, + "total cover {cover_sum} must track aggregate K(ΣQ) {k_agg} (split-neutral)" + ); + assert!( + cover_sum > k_single_sum, + "split-neutral cover {cover_sum} must exceed the convex per-position sum {k_single_sum}" + ); + }); +} + +// #4: during EMA warmup a tiny pEMA makes T_ref = min(T_live, pEMA·A_live) tiny, +// so the capacity cap κ·T_ref admits only negligible opens — the cold/near-cold +// window is self-limiting even past the pEMA>0 guard. +#[test] +fn tiny_pema_caps_open_size() { + new_test_ext(1).execute_with(|| { + let netuid = setup_market(1000 * TAO, 1000 * TAO, 1.0); + // Near-cold EMA: set pEMA to a tiny positive value (passes the warm guard). + SubnetMovingPrice::::insert(netuid, I96F32::from_num(0.00001)); + let trader = U256::from(10); + add_balance_to_coldkey_account(&trader, t(1000 * TAO)); + // A normal 100-TAO open is rejected — the tiny T_ref drives λ_eff≤0 / + // capacity well before any meaningful size can open. + let r = SubtensorModule::open_short( + RuntimeOrigin::signed(trader), + U256::from(11), + netuid, + t(100 * TAO), + AlphaBalance::MAX, + ); + assert!( + r == Err(Error::::EffectiveLtvNonPositive.into()) + || r == Err(Error::::ShortCapacityExceeded.into()) + || r == Err(Error::::RetainedProceedsNonPositive.into()), + "tiny pEMA must cap open size, got {r:?}" + ); + assert!(ShortPositions::::get(netuid, trader).is_none()); + }); +} + +// Atomicity: `do_dissolve_network` is `#[frame_support::transactional]` and runs +// derivative terminal settlement BEFORE the fallible `destroy_alpha_in_out_stakes` +// / `clear_protocol_liquidity` legs. If a later leg fails, the settlement must roll +// back as a unit. This exercises that exact mechanism (`#[transactional]` == +// `with_storage_layer`) on the real settlement fn: settle inside the layer, then a +// later step errors, and assert all derivative/custody/aggregate state is restored. +#[test] +fn dereg_settlement_rolls_back_on_later_failure() { + new_test_ext(1).execute_with(|| { + let netuid = setup_market(2000 * TAO, 2000 * TAO, 1.0); + let trader = U256::from(10); + let hotkey = U256::from(11); + add_balance_to_coldkey_account(&trader, t(1000 * TAO)); + assert_ok!(SubtensorModule::open_short( + RuntimeOrigin::signed(trader), + hotkey, + netuid, + t(50 * TAO), + AlphaBalance::MAX + )); + assert!(ShortPositions::::get(netuid, trader).is_some()); + let custody_before = custody_bal(netuid); + let q_before = ShortAggregate::::get(netuid).q_sigma; + + // Model do_dissolve_network: settle, then a later fallible leg returns Err. + let r = frame_support::storage::with_storage_layer(|| -> sp_runtime::DispatchResult { + SubtensorModule::settle_shorts_on_dereg(netuid); + // Inside the layer the position is settled/removed... + assert!(ShortPositions::::get(netuid, trader).is_none()); + // ...then a subsequent dissolve leg fails. + Err(Error::::SubnetNotExists.into()) + }); + assert!(r.is_err()); + + // The whole settlement rolled back: position, custody, and aggregate restored. + assert!( + ShortPositions::::get(netuid, trader).is_some(), + "position must survive a rolled-back dissolve" + ); + assert_eq!( + custody_bal(netuid), + custody_before, + "custody must be restored" + ); + assert_eq!( + ShortAggregate::::get(netuid).q_sigma, + q_before, + "aggregate must be restored" + ); + }); +} + +#[test] +fn dissolve_network_clears_shorts() { + new_test_ext(1).execute_with(|| { + let netuid = setup_market(1000 * TAO, 1000 * TAO, 1.0); + let trader = U256::from(10); + add_balance_to_coldkey_account(&trader, t(1000 * TAO)); + assert_ok!(SubtensorModule::open_short( + RuntimeOrigin::signed(trader), + U256::from(11), + netuid, + t(100 * TAO), + AlphaBalance::MAX + )); + assert!(ShortPositions::::get(netuid, trader).is_some()); + + assert_ok!(SubtensorModule::do_dissolve_network(netuid)); + + // Terminal hook fired: positions and aggregate cleared. + assert!(ShortPositions::::get(netuid, trader).is_none()); + assert!(!ShortAggregate::::contains_key(netuid)); + assert!(!ShortActiveSubnets::::contains_key(netuid)); + }); +} + +// --------------------------------------------------------------------------- +// Audit fixes +// --------------------------------------------------------------------------- + +// Fix: additional open must target the same hotkey (else close would repay from +// the wrong stake). +#[test] +fn merge_with_mismatched_hotkey_rejected() { + new_test_ext(1).execute_with(|| { + let netuid = setup_market(1000 * TAO, 1000 * TAO, 1.0); + let trader = U256::from(10); + add_balance_to_coldkey_account(&trader, t(1000 * TAO)); + assert_ok!(SubtensorModule::open_short( + RuntimeOrigin::signed(trader), + U256::from(11), + netuid, + t(50 * TAO), + AlphaBalance::MAX + )); + // Second open with a different hotkey must be rejected, leaving state intact. + assert_noop!( + SubtensorModule::open_short( + RuntimeOrigin::signed(trader), + U256::from(12), + netuid, + t(50 * TAO), + AlphaBalance::MAX + ), + Error::::ShortHotkeyMismatch + ); + let pos = ShortPositions::::get(netuid, trader).unwrap(); + assert_eq!(pos.hotkey, U256::from(11)); + assert_eq!(pos.p_floor, t(50 * TAO)); // unchanged by the rejected merge + }); +} + +// Fix: opens below the minimum input are rejected (dust-spam / terminal-load bound). +#[test] +fn open_below_min_input_rejected() { + new_test_ext(1).execute_with(|| { + let netuid = setup_market(1000 * TAO, 1000 * TAO, 1.0); + let trader = U256::from(10); + add_balance_to_coldkey_account(&trader, t(1000 * TAO)); + SubtensorModule::set_short_min_input(t(TAO)); // 1 TAO floor + + assert_noop!( + SubtensorModule::open_short( + RuntimeOrigin::signed(trader), + U256::from(11), + netuid, + t(TAO / 2), + AlphaBalance::MAX + ), + Error::::AmountTooLow + ); + // At/above the floor it succeeds. + assert_ok!(SubtensorModule::open_short( + RuntimeOrigin::signed(trader), + U256::from(11), + netuid, + t(TAO), + AlphaBalance::MAX + )); + }); +} + +// Fix: a third party cannot snipe a default within the grace window after the +// owner's last action; after the window it is allowed. +#[test] +fn permissionless_default_respects_grace_window() { + new_test_ext(1).execute_with(|| { + let netuid = setup_market(1000 * TAO, 1000 * TAO, 1.0); + let trader = U256::from(10); + add_balance_to_coldkey_account(&trader, t(1000 * TAO)); + assert_ok!(SubtensorModule::open_short( + RuntimeOrigin::signed(trader), + U256::from(11), + netuid, + t(100 * TAO), + AlphaBalance::MAX + )); + + // Make the buffer dust-eligible, set a short grace window. + SubtensorModule::set_short_dust(t(1000 * TAO)); + SubtensorModule::set_short_default_grace(5); + let poker = U256::from(99); + + // Within the grace window: rejected even though the buffer is dust. + assert_noop!( + SubtensorModule::default_short(RuntimeOrigin::signed(poker), trader, netuid), + Error::::PositionNotDefaultEligible + ); + + // After the grace window: allowed. + step_block(6); + assert_ok!(SubtensorModule::default_short( + RuntimeOrigin::signed(poker), + trader, + netuid + )); + assert!(ShortPositions::::get(netuid, trader).is_none()); + }); +} + +// Fix: the owner can defeat a snipe by topping up, which resets the grace window. +#[test] +fn top_up_resets_default_grace() { + new_test_ext(1).execute_with(|| { + let netuid = setup_market(1000 * TAO, 1000 * TAO, 1.0); + let trader = U256::from(10); + add_balance_to_coldkey_account(&trader, t(1000 * TAO)); + assert_ok!(SubtensorModule::open_short( + RuntimeOrigin::signed(trader), + U256::from(11), + netuid, + t(100 * TAO), + AlphaBalance::MAX + )); + SubtensorModule::set_short_dust(t(1000 * TAO)); + SubtensorModule::set_short_default_grace(5); + + step_block(6); // grace from open has elapsed + // Owner tops up, resetting last_active to the current block. + assert_ok!(SubtensorModule::top_up_short( + RuntimeOrigin::signed(trader), + netuid, + t(TAO) + )); + + // A snipe is now blocked again for another grace window. + let poker = U256::from(99); + assert_noop!( + SubtensorModule::default_short(RuntimeOrigin::signed(poker), trader, netuid), + Error::::PositionNotDefaultEligible + ); + }); +} + +// Fix: only subnets with live short state are tracked for the per-block decay +// tick; membership is added on open and removed when the last position closes. +#[test] +fn active_subnet_set_tracks_membership() { + new_test_ext(1).execute_with(|| { + let netuid = setup_market(1000 * TAO, 1000 * TAO, 1.0); + let trader = U256::from(10); + let hotkey = U256::from(11); + add_balance_to_coldkey_account(&trader, t(1000 * TAO)); + + // No shorts yet → not tracked. + assert!(!ShortActiveSubnets::::contains_key(netuid)); + + assert_ok!(SubtensorModule::open_short( + RuntimeOrigin::signed(trader), + hotkey, + netuid, + t(100 * TAO), + AlphaBalance::MAX + )); + assert!(ShortActiveSubnets::::contains_key(netuid)); + + let pos = ShortPositions::::get(netuid, trader).unwrap(); + give_alpha( + hotkey, + trader, + netuid, + AlphaBalance::from(pos.q_liability.to_u64() + 10 * TAO), + ); + assert_ok!(SubtensorModule::close_short( + RuntimeOrigin::signed(trader), + netuid, + 1_000_000_000 + )); + + // Fully closed → no longer tracked, so decay skips this subnet. + assert!(!ShortActiveSubnets::::contains_key(netuid)); + }); +} + +// --------------------------------------------------------------------------- +// Read / RPC layer +// --------------------------------------------------------------------------- + +// The position view materializes decay to the current block, while raw storage +// stays at the last materialization. +#[test] +fn position_view_materializes_decay() { + new_test_ext(1).execute_with(|| { + let netuid = setup_market(1000 * TAO, 1000 * TAO, 1.0); + let trader = U256::from(10); + add_balance_to_coldkey_account(&trader, t(1000 * TAO)); + assert_ok!(SubtensorModule::open_short( + RuntimeOrigin::signed(trader), + U256::from(11), + netuid, + t(100 * TAO), + AlphaBalance::MAX + )); + SubtensorModule::set_decay_bounds_ppb(1_000_000_000, 1_000_000_000); // strong decay + + let raw = ShortPositions::::get(netuid, trader) + .unwrap() + .r_stored + .to_u64(); + for _ in 0..2000 { + SubtensorModule::run_short_decay(); + } + + let info = SubtensorModule::get_short_position(&trader, netuid).unwrap(); + // View reflects decay; raw storage is still the last-materialized value. + assert!( + info.buffer.to_u64() < raw, + "view buffer {} !< raw {}", + info.buffer.to_u64(), + raw + ); + assert_eq!( + ShortPositions::::get(netuid, trader) + .unwrap() + .r_stored + .to_u64(), + raw + ); + assert_eq!( + info.collateral_claim.to_u64(), + info.floor.to_u64() + info.buffer.to_u64() + ); + assert!(info.daily_decay > 0); + assert!(info.blocks_to_dust > 0 && info.blocks_to_dust < u64::MAX); + assert_eq!(info.alpha_needed, info.alpha_liability); // holds none yet + }); +} + +// The view's default-eligibility tracks the grace window. +#[test] +fn position_view_reports_default_window() { + new_test_ext(1).execute_with(|| { + let netuid = setup_market(1000 * TAO, 1000 * TAO, 1.0); + let trader = U256::from(10); + add_balance_to_coldkey_account(&trader, t(1000 * TAO)); + assert_ok!(SubtensorModule::open_short( + RuntimeOrigin::signed(trader), + U256::from(11), + netuid, + t(100 * TAO), + AlphaBalance::MAX + )); + SubtensorModule::set_short_dust(t(1000 * TAO)); // buffer is dust + SubtensorModule::set_short_default_grace(5); + + let info = SubtensorModule::get_short_position(&trader, netuid).unwrap(); + assert!(!info.default_eligible, "within grace, not yet defaultable"); + + step_block(6); + let info2 = SubtensorModule::get_short_position(&trader, netuid).unwrap(); + assert!(info2.default_eligible, "after grace, defaultable"); + assert_eq!(info2.defaultable_at_block, info.defaultable_at_block); + }); +} + +// Market view exposes capacity and parameters. +#[test] +fn market_view_reports_capacity() { + new_test_ext(1).execute_with(|| { + let netuid = setup_market(1000 * TAO, 1000 * TAO, 1.0); + let trader = U256::from(10); + add_balance_to_coldkey_account(&trader, t(1000 * TAO)); + assert_ok!(SubtensorModule::open_short( + RuntimeOrigin::signed(trader), + U256::from(11), + netuid, + t(100 * TAO), + AlphaBalance::MAX + )); + + let pos = ShortPositions::::get(netuid, trader).unwrap(); + let m = SubtensorModule::get_subnet_short_state(netuid).unwrap(); + assert!(m.shorts_enabled); + assert!(m.footprint_used.to_u64() > 0); + assert!(m.footprint_cap.to_u64() > m.footprint_used.to_u64()); + assert_eq!( + m.footprint_remaining.to_u64(), + m.footprint_cap.to_u64() - m.footprint_used.to_u64() + ); + assert_eq!(m.open_interest_alpha, pos.q_liability); + assert_eq!(m.buffer_total, pos.r_stored); + assert!(m.current_daily_decay > 0); + }); +} + +// Close quote matches the amounts an actual full close moves. +#[test] +fn close_quote_matches_position() { + new_test_ext(1).execute_with(|| { + let netuid = setup_market(1000 * TAO, 1000 * TAO, 1.0); + let trader = U256::from(10); + add_balance_to_coldkey_account(&trader, t(1000 * TAO)); + assert_ok!(SubtensorModule::open_short( + RuntimeOrigin::signed(trader), + U256::from(11), + netuid, + t(100 * TAO), + AlphaBalance::MAX + )); + let pos = ShortPositions::::get(netuid, trader).unwrap(); + + let full = SubtensorModule::quote_close_short(&trader, netuid, 1_000_000_000).unwrap(); + assert_eq!(full.repay_alpha, pos.q_liability); + assert_eq!( + full.returned_tao.to_u64(), + pos.p_floor.to_u64() + pos.r_stored.to_u64() + ); + assert_eq!(full.alpha_needed, pos.q_liability); // holds none + assert!(full.est_buyback_cost.to_u64() > 0); + + let half = SubtensorModule::quote_close_short(&trader, netuid, 500_000_000).unwrap(); + assert_approx( + half.repay_alpha.to_u64(), + full.repay_alpha.to_u64() / 2, + 2, + "half repay", + ); + assert_approx( + half.returned_tao.to_u64(), + full.returned_tao.to_u64() / 2, + 2, + "half return", + ); + }); +} + +// Materialization can never inflate a position: even with a (impossible) +// entry accumulator above the aggregate, the factor is clamped to ≤ 1. +#[test] +fn materialize_never_inflates() { + new_test_ext(1).execute_with(|| { + let netuid = setup_market(1000 * TAO, 1000 * TAO, 1.0); + let trader = U256::from(10); + add_balance_to_coldkey_account(&trader, t(1000 * TAO)); + assert_ok!(SubtensorModule::open_short( + RuntimeOrigin::signed(trader), + U256::from(11), + netuid, + t(100 * TAO), + AlphaBalance::MAX + )); + + // Corrupt the invariant: set omega_entry far above the aggregate omega. + let mut pos = ShortPositions::::get(netuid, trader).unwrap(); + let buf = pos.r_stored; + pos.omega_entry = I64F64::from_num(1000); + ShortPositions::::insert(netuid, trader, pos); + + // The materialized view must not exceed the stored buffer (no inflation). + let info = SubtensorModule::get_short_position(&trader, netuid).unwrap(); + assert!( + info.buffer <= buf, + "materialize inflated: {} > {}", + info.buffer.to_u64(), + buf.to_u64() + ); + }); +} + +// Open immediately followed by full close cannot be a rounding-profit loop: the +// trader gets back at most floor + buffer and must repay the full liability. +#[test] +fn open_close_roundtrip_is_not_profitable() { + new_test_ext(1).execute_with(|| { + let netuid = setup_market(1000 * TAO, 1000 * TAO, 1.0); + let trader = U256::from(10); + let hotkey = U256::from(11); + add_balance_to_coldkey_account(&trader, t(1000 * TAO)); + + let before = bal(&trader); + assert_ok!(SubtensorModule::open_short( + RuntimeOrigin::signed(trader), + hotkey, + netuid, + t(100 * TAO), + AlphaBalance::MAX + )); + let pos = ShortPositions::::get(netuid, trader).unwrap(); + let n = pos.r_stored.to_u64(); + // Seed exactly the liability alpha so the round trip is self-contained. + give_alpha(hotkey, trader, netuid, pos.q_liability); + assert_ok!(SubtensorModule::close_short( + RuntimeOrigin::signed(trader), + netuid, + 1_000_000_000 + )); + + // TAO-only delta is +N (the retained proceeds); the trader still had to + // source Q alpha, whose pool buy-cost strictly exceeds N — so no free TAO. + assert_eq!(bal(&trader), before + n); + let buy_cost = SubtensorModule::get_subnet_short_state(netuid); // sanity: market still consistent + assert!(buy_cost.is_some()); + }); +} + +// Fix (L3): close must never mint alpha by saturating SubnetAlphaOut to zero. +#[test] +fn close_guards_against_alpha_mint() { + new_test_ext(1).execute_with(|| { + let netuid = setup_market(1000 * TAO, 1000 * TAO, 1.0); + let trader = U256::from(10); + let hotkey = U256::from(11); + add_balance_to_coldkey_account(&trader, t(1000 * TAO)); + assert_ok!(SubtensorModule::open_short( + RuntimeOrigin::signed(trader), + hotkey, + netuid, + t(100 * TAO), + AlphaBalance::MAX + )); + let pos = ShortPositions::::get(netuid, trader).unwrap(); + give_alpha(hotkey, trader, netuid, pos.q_liability); + + // Corrupt outstanding alpha below the liability: close must refuse rather + // than push SubnetAlphaIn up while SubnetAlphaOut saturates (a mint). + SubnetAlphaOut::::insert(netuid, AlphaBalance::from(0)); + let alpha_in_before = SubnetAlphaIn::::get(netuid); + assert_noop!( + SubtensorModule::close_short(RuntimeOrigin::signed(trader), netuid, 1_000_000_000), + Error::::InsufficientAlphaToClose + ); + assert_eq!(SubnetAlphaIn::::get(netuid), alpha_in_before); // no mint + }); +} + +// Fix (L2): the open quote is unavailable while shorts are disabled. +#[test] +fn open_quote_gated_by_enable_flag() { + new_test_ext(1).execute_with(|| { + let netuid = setup_market(1000 * TAO, 1000 * TAO, 1.0); + assert!(SubtensorModule::quote_open_short(netuid, t(100 * TAO)).is_some()); + SubtensorModule::set_shorts_enabled(false); + assert!(SubtensorModule::quote_open_short(netuid, t(100 * TAO)).is_none()); + }); +} + +// Long-side mirror of L2: the long open quote is unavailable while longs are disabled. +#[test] +fn long_open_quote_gated_by_enable_flag() { + new_test_ext(1).execute_with(|| { + let netuid = setup_long(1000 * TAO, 1000 * TAO, 1.0); + assert!(SubtensorModule::quote_open_long(netuid, AlphaBalance::from(100 * TAO)).is_some()); + SubtensorModule::set_longs_enabled(false); + assert!(SubtensorModule::quote_open_long(netuid, AlphaBalance::from(100 * TAO)).is_none()); + }); +} + +// The caller execution bound is an opt-out: `*::MAX` accepts any realized liability +// (so a normal open never reverts on slippage), while a tight bound rejects. Asserts +// both directions in one scenario for both sides. +#[test] +fn open_max_liability_bound_opts_out() { + new_test_ext(1).execute_with(|| { + // SHORT: MAX opts out (opens), 1-rao bound rejects. + let netuid = setup_market(1000 * TAO, 1000 * TAO, 1.0); + let trader = U256::from(10); + add_balance_to_coldkey_account(&trader, t(1000 * TAO)); + assert_ok!(SubtensorModule::open_short( + RuntimeOrigin::signed(trader), + U256::from(11), + netuid, + t(50 * TAO), + AlphaBalance::MAX + )); + assert!(ShortPositions::::get(netuid, trader).is_some()); + let trader2 = U256::from(20); + add_balance_to_coldkey_account(&trader2, t(1000 * TAO)); + assert_noop!( + SubtensorModule::open_short( + RuntimeOrigin::signed(trader2), + U256::from(21), + netuid, + t(50 * TAO), + AlphaBalance::from(1) + ), + Error::::SlippageTooHigh + ); + + // LONG: MAX opts out (opens), 1-rao bound rejects. + let lnet = setup_long(1000 * TAO, 1000 * TAO, 1.0); + let lt = U256::from(30); + let lhot = U256::from(31); + give_alpha(lhot, lt, lnet, AlphaBalance::from(500 * TAO)); + assert_ok!(SubtensorModule::open_long( + RuntimeOrigin::signed(lt), + lhot, + lnet, + AlphaBalance::from(100 * TAO), + TaoBalance::MAX + )); + assert!(LongPositions::::get(lnet, lt).is_some()); + let lt2 = U256::from(40); + let lhot2 = U256::from(41); + give_alpha(lhot2, lt2, lnet, AlphaBalance::from(500 * TAO)); + assert_noop!( + SubtensorModule::open_long( + RuntimeOrigin::signed(lt2), + lhot2, + lnet, + AlphaBalance::from(100 * TAO), + TaoBalance::from(1) + ), + Error::::SlippageTooHigh + ); + }); +} + +// Per-subnet open-position count is maintained (increment on open, decrement on +// close, no double-count on merge) — it feeds the dereg-settlement weight charge. +#[test] +fn position_count_maintained() { + new_test_ext(1).execute_with(|| { + let netuid = setup_market(1000 * TAO, 1000 * TAO, 1.0); + let (a, b, c) = (U256::from(10), U256::from(20), U256::from(30)); + for k in [a, b, c] { + add_balance_to_coldkey_account(&k, t(1000 * TAO)); + } + + assert_ok!(SubtensorModule::open_short( + RuntimeOrigin::signed(a), + U256::from(11), + netuid, + t(20 * TAO), + AlphaBalance::MAX + )); + assert_ok!(SubtensorModule::open_short( + RuntimeOrigin::signed(b), + U256::from(21), + netuid, + t(20 * TAO), + AlphaBalance::MAX + )); + assert_eq!(ShortPositionCount::::get(netuid), 2); + + // Closing one decrements the count; the slot is reusable. + let pos = ShortPositions::::get(netuid, a).unwrap(); + give_alpha(U256::from(11), a, netuid, pos.q_liability); + assert_ok!(SubtensorModule::close_short( + RuntimeOrigin::signed(a), + netuid, + 1_000_000_000 + )); + assert_eq!(ShortPositionCount::::get(netuid), 1); + assert_ok!(SubtensorModule::open_short( + RuntimeOrigin::signed(c), + U256::from(31), + netuid, + t(20 * TAO), + AlphaBalance::MAX + )); + assert_eq!(ShortPositionCount::::get(netuid), 2); + + // A merge (same coldkey, same hotkey) does not consume a new slot. + assert_ok!(SubtensorModule::open_short( + RuntimeOrigin::signed(c), + U256::from(31), + netuid, + t(20 * TAO), + AlphaBalance::MAX + )); + assert_eq!(ShortPositionCount::::get(netuid), 2); + }); +} + +// =========================================================================== +// PROOF: global value conservation across the full mixed lifecycle. +// +// Exercises the real dispatch path for both sides (open/top-up/partial+full +// close) plus continuous decay, and asserts that no TAO and no Alpha is minted +// or destroyed once every position is closed. Decay is driven directly (not via +// step_block) so coinbase emissions don't perturb issuance. +// =========================================================================== +#[test] +fn proof_full_lifecycle_conserves_tao_and_alpha() { + new_test_ext(1).execute_with(|| { + let netuid = setup_long(1000 * TAO, 1000 * TAO, 1.0); // both sides enabled + let (s_cold, s_hot) = (U256::from(10), U256::from(11)); + let (l_cold, l_hot) = (U256::from(20), U256::from(21)); + // Fund: short needs TAO (floor + top-up) and Alpha (repay Q); long needs + // Alpha (collateral) and TAO (repay D). + add_balance_to_coldkey_account(&s_cold, t(1000 * TAO)); + add_balance_to_coldkey_account(&l_cold, t(1000 * TAO)); + give_alpha(s_hot, s_cold, netuid, AlphaBalance::from(5000 * TAO)); + give_alpha(l_hot, l_cold, netuid, AlphaBalance::from(500 * TAO)); + + // Baseline after all seeding. + let tao0 = TotalIssuance::::get().to_u64(); + let alpha0 = alpha_issuance(netuid); + + assert_ok!(SubtensorModule::open_short( + RuntimeOrigin::signed(s_cold), + s_hot, + netuid, + t(100 * TAO), + AlphaBalance::MAX + )); + assert_ok!(SubtensorModule::open_long( + RuntimeOrigin::signed(l_cold), + l_hot, + netuid, + AlphaBalance::from(100 * TAO), + TaoBalance::MAX + )); + + // Continuous unwind on both sides. + for _ in 0..500 { + SubtensorModule::run_short_decay(); + SubtensorModule::run_long_decay(); + } + + // Mid-life owner actions. + assert_ok!(SubtensorModule::top_up_short( + RuntimeOrigin::signed(s_cold), + netuid, + t(10 * TAO) + )); + assert_ok!(SubtensorModule::close_short( + RuntimeOrigin::signed(s_cold), + netuid, + 500_000_000 + )); // half + + // Close everything out. + assert_ok!(SubtensorModule::close_short( + RuntimeOrigin::signed(s_cold), + netuid, + 1_000_000_000 + )); + assert_ok!(SubtensorModule::close_long( + RuntimeOrigin::signed(l_cold), + netuid, + 1_000_000_000 + )); + + // CONSERVATION. + // TAO only ever *moves* between accounts (no recycle on this all-close + // path), so total TAO supply is conserved exactly. + assert_eq!( + TotalIssuance::::get().to_u64(), + tao0, + "TAO supply not conserved" + ); + + // Alpha is burned/minted around the pool; fixed-point flooring means the + // restored amount is never ABOVE baseline (no value minted) and is below + // it only by bounded rounding dust. + let alpha1 = alpha_issuance(netuid); + const DUST_TOL: u64 = 1_000_000; // 0.001 Alpha; observed drift is ~5e2 rao + assert!(alpha1 <= alpha0, "Alpha was minted: {alpha1} > {alpha0}"); + assert!( + alpha0 - alpha1 <= DUST_TOL, + "Alpha loss {} exceeds dust tol", + alpha0 - alpha1 + ); + assert!( + custody_bal(netuid) <= DUST_TOL, + "short custody dust too large" + ); + + // Positions and counts are cleared exactly; fixed liabilities net to 0. + assert!(ShortPositions::::get(netuid, s_cold).is_none()); + assert!(LongPositions::::get(netuid, l_cold).is_none()); + assert_eq!(ShortPositionCount::::get(netuid), 0); + assert_eq!(LongPositionCount::::get(netuid), 0); + assert_eq!(ShortAggregate::::get(netuid).q_sigma.to_u64(), 0); + assert_eq!(LongAggregate::::get(netuid).d_sigma.to_u64(), 0); + // cleanup-on-empty evicts fully-closed subnets from the decay tick. + assert!(!ShortActiveSubnets::::contains_key(netuid)); + assert!(!LongActiveSubnets::::contains_key(netuid)); + }); +} + +// PROOF (invariant): custody TAO always covers the materialized obligations +// Σ(P + R(t) + E(t)) across decay — including at the DecayMax clamp extreme +// (1.0/day), where the aggregate Σ-decay's faster flooring vs the per-position +// exp decay is most stressed. Locks the "custody ≥ obligations" solvency claim +// against future edits (architect M2). +#[test] +fn proof_custody_geq_obligations_under_decay() { + new_test_ext(1).execute_with(|| { + let netuid = setup_market(1000 * TAO, 1000 * TAO, 1.0); + SubtensorModule::set_decay_bounds_ppb(100_000_000, 1_000_000_000); // 10%..100%/day + let traders = [ + (U256::from(10), U256::from(11)), + (U256::from(20), U256::from(21)), + (U256::from(30), U256::from(31)), + ]; + for (c, h) in traders.iter() { + add_balance_to_coldkey_account(c, t(1000 * TAO)); + give_alpha(*h, *c, netuid, AlphaBalance::from(1000 * TAO)); // alpha to repay Q on close + } + for (i, (c, h)) in traders.iter().enumerate() { + assert_ok!(SubtensorModule::open_short( + RuntimeOrigin::signed(*c), + *h, + netuid, + t((20 + 10 * i as u64) * TAO), + AlphaBalance::MAX + )); + } + // Σ materialized (floor + buffer + escrow) over every live position. + let obligations = |nid: NetUid| -> u64 { + traders + .iter() + .filter_map(|(c, _)| SubtensorModule::get_short_position(c, nid)) + .map(|p| p.floor.to_u64() + p.buffer.to_u64() + p.escrow.to_u64()) + .sum() + }; + assert!( + custody_bal(netuid) >= obligations(netuid), + "custody < obligations at open" + ); + for k in 0..3000 { + SubtensorModule::run_short_decay(); + // Check every tick (not sampled): a one-block transient breach can't hide. + assert!( + custody_bal(netuid) >= obligations(netuid), + "custody < obligations during decay (block {k})" + ); + } + // Mid-life partial close must preserve the invariant too. + assert_ok!(SubtensorModule::close_short( + RuntimeOrigin::signed(traders[0].0), + netuid, + 400_000_000 + )); + assert!( + custody_bal(netuid) >= obligations(netuid), + "custody < obligations after partial close" + ); + }); +} + +// PROOF (invariant): the three denormalized bookkeeping copies stay in sync — +// ShortPositionCount == |ShortPositions[netuid]|, and ShortActiveSubnets membership +// iff the aggregate has any nonzero Σ — through an open/partial/full-close churn. +// Guards the denormalized count/active-set bookkeeping the dereg settlement weight +// charge and empty/active detection rely on (there is no per-subnet position cap). +#[test] +fn proof_position_count_matches_map_through_churn() { + new_test_ext(1).execute_with(|| { + let netuid = setup_market(1000 * TAO, 1000 * TAO, 1.0); + let traders = [ + (U256::from(10), U256::from(11)), + (U256::from(20), U256::from(21)), + (U256::from(30), U256::from(31)), + ]; + for (c, h) in traders.iter() { + add_balance_to_coldkey_account(c, t(1000 * TAO)); + give_alpha(*h, *c, netuid, AlphaBalance::from(1000 * TAO)); // alpha to repay Q on close + } + let check = |nid: NetUid| { + let map_count = ShortPositions::::iter_prefix(nid).count() as u32; + assert_eq!( + ShortPositionCount::::get(nid), + map_count, + "count != map size" + ); + let agg = ShortAggregate::::get(nid); + let nonzero = !(agg.r_sigma.is_zero() + && agg.e_sigma.is_zero() + && agg.b_sigma.is_zero() + && agg.q_sigma.is_zero()); + assert_eq!( + ShortActiveSubnets::::contains_key(nid), + nonzero, + "active-set membership != nonzero aggregate" + ); + }; + check(netuid); + for (c, h) in traders.iter() { + assert_ok!(SubtensorModule::open_short( + RuntimeOrigin::signed(*c), + *h, + netuid, + t(30 * TAO), + AlphaBalance::MAX + )); + check(netuid); + } + // partial close (count unchanged), then full closes (count decrements). + assert_ok!(SubtensorModule::close_short( + RuntimeOrigin::signed(traders[0].0), + netuid, + 500_000_000 + )); + check(netuid); + assert_ok!(SubtensorModule::close_short( + RuntimeOrigin::signed(traders[1].0), + netuid, + 1_000_000_000 + )); + check(netuid); + assert_ok!(SubtensorModule::close_short( + RuntimeOrigin::signed(traders[0].0), + netuid, + 1_000_000_000 + )); + check(netuid); + assert_ok!(SubtensorModule::close_short( + RuntimeOrigin::signed(traders[2].0), + netuid, + 1_000_000_000 + )); + check(netuid); + assert_eq!(ShortPositionCount::::get(netuid), 0); + assert!(!ShortActiveSubnets::::contains_key(netuid)); + }); +} + +// PROOF: default reduces issuance by EXACTLY the recycled floor — no more, no +// less — on both sides. +#[test] +fn proof_default_recycles_exactly_the_floor() { + new_test_ext(1).execute_with(|| { + // Short side: TotalIssuance (TAO) drops by exactly the floor P. + let netuid = setup_long(1000 * TAO, 1000 * TAO, 1.0); + let (s_cold, s_hot) = (U256::from(10), U256::from(11)); + add_balance_to_coldkey_account(&s_cold, t(1000 * TAO)); + SubtensorModule::set_short_default_grace(0); + SubtensorModule::set_short_dust(t(10_000 * TAO)); + assert_ok!(SubtensorModule::open_short( + RuntimeOrigin::signed(s_cold), + s_hot, + netuid, + t(100 * TAO), + AlphaBalance::MAX + )); + let tao_before = TotalIssuance::::get().to_u64(); + assert_ok!(SubtensorModule::default_short( + RuntimeOrigin::signed(U256::from(99)), + s_cold, + netuid + )); + assert_eq!( + TotalIssuance::::get().to_u64(), + tao_before - 100 * TAO, + "short default must recycle exactly the floor" + ); + + // Long side: Alpha issuance drops by exactly the floor P. + let (l_cold, l_hot) = (U256::from(20), U256::from(21)); + give_alpha(l_hot, l_cold, netuid, AlphaBalance::from(500 * TAO)); + SubtensorModule::set_long_dust(AlphaBalance::from(10_000 * TAO)); + SubtensorModule::set_long_default_grace(0); + // Measure BEFORE open: long open burns alpha, default restores all but the + // floor, so the net effect of open+default is exactly −floor. + let alpha_before = alpha_issuance(netuid); + assert_ok!(SubtensorModule::open_long( + RuntimeOrigin::signed(l_cold), + l_hot, + netuid, + AlphaBalance::from(100 * TAO), + TaoBalance::MAX + )); + assert_ok!(SubtensorModule::default_long( + RuntimeOrigin::signed(U256::from(98)), + l_cold, + netuid + )); + assert_eq!( + alpha_issuance(netuid), + alpha_before - 100 * TAO, + "long default must recycle exactly the floor" + ); + }); +} + +// PROOF (multi-position): the aggregate Σ-decay and per-position lazy decay +// stay solvent across MANY heterogeneous positions on both sides through a long +// decay horizon — the configuration the single-position tests can't exercise. +#[test] +fn proof_multi_position_decay_conserves() { + new_test_ext(1).execute_with(|| { + let netuid = setup_long(10_000 * TAO, 10_000 * TAO, 1.0); + let shorts: [(U256, U256, u64); 3] = [ + (U256::from(10), U256::from(11), 50 * TAO), + (U256::from(12), U256::from(13), 100 * TAO), + (U256::from(14), U256::from(15), 30 * TAO), + ]; + let longs: [(U256, U256, u64); 2] = [ + (U256::from(20), U256::from(21), 40 * TAO), + (U256::from(22), U256::from(23), 60 * TAO), + ]; + for (c, h, _) in shorts { + add_balance_to_coldkey_account(&c, t(2000 * TAO)); + give_alpha(h, c, netuid, AlphaBalance::from(5000 * TAO)); // to repay Q + } + for (c, h, _) in longs { + add_balance_to_coldkey_account(&c, t(2000 * TAO)); // to repay D + give_alpha(h, c, netuid, AlphaBalance::from(1000 * TAO)); // collateral + } + + let tao0 = TotalIssuance::::get().to_u64(); + let alpha0 = alpha_issuance(netuid); + + for (c, h, p) in shorts { + assert_ok!(SubtensorModule::open_short( + RuntimeOrigin::signed(c), + h, + netuid, + t(p), + AlphaBalance::MAX + )); + } + for (c, h, p) in longs { + assert_ok!(SubtensorModule::open_long( + RuntimeOrigin::signed(c), + h, + netuid, + AlphaBalance::from(p), + TaoBalance::MAX + )); + } + + for _ in 0..300 { + SubtensorModule::run_short_decay(); + SubtensorModule::run_long_decay(); + } + + for (c, _, _) in shorts { + assert_ok!(SubtensorModule::close_short( + RuntimeOrigin::signed(c), + netuid, + 1_000_000_000 + )); + } + for (c, _, _) in longs { + assert_ok!(SubtensorModule::close_long( + RuntimeOrigin::signed(c), + netuid, + 1_000_000_000 + )); + } + + const TOL: u64 = 10_000_000; // 0.01 token + assert_eq!( + TotalIssuance::::get().to_u64(), + tao0, + "TAO supply not conserved" + ); + let alpha1 = alpha_issuance(netuid); + assert!(alpha1 <= alpha0, "Alpha minted across many positions"); + assert!( + alpha0 - alpha1 <= TOL, + "Alpha drift {} > tol", + alpha0 - alpha1 + ); + assert!( + custody_bal(netuid) <= TOL, + "custody not drained across many positions" + ); + assert_eq!(ShortPositionCount::::get(netuid), 0); + assert_eq!(LongPositionCount::::get(netuid), 0); + assert!(!ShortActiveSubnets::::contains_key(netuid)); + assert!(!LongActiveSubnets::::contains_key(netuid)); + }); +} + +// Many partial closes followed by a full close drain the position cleanly (the +// floor-rounding residue path), with TAO conserved and custody emptied. +#[test] +fn short_many_partial_closes_drain_cleanly() { + new_test_ext(1).execute_with(|| { + let netuid = setup_market(10_000 * TAO, 10_000 * TAO, 1.0); + let trader = U256::from(10); + let hotkey = U256::from(11); + add_balance_to_coldkey_account(&trader, t(1000 * TAO)); + give_alpha(hotkey, trader, netuid, AlphaBalance::from(5000 * TAO)); + + let tao0 = TotalIssuance::::get().to_u64(); + assert_ok!(SubtensorModule::open_short( + RuntimeOrigin::signed(trader), + hotkey, + netuid, + t(100 * TAO), + AlphaBalance::MAX + )); + for _ in 0..9 { + assert_ok!(SubtensorModule::close_short( + RuntimeOrigin::signed(trader), + netuid, + 100_000_000 + )); // 10% of remaining + } + assert_ok!(SubtensorModule::close_short( + RuntimeOrigin::signed(trader), + netuid, + 1_000_000_000 + )); + + assert!(ShortPositions::::get(netuid, trader).is_none()); + assert_eq!(TotalIssuance::::get().to_u64(), tao0); + assert!( + custody_bal(netuid) <= 10_000, + "custody dust after partial closes" + ); + assert!(!ShortActiveSubnets::::contains_key(netuid)); + }); +} + +// Governance setters clamp out-of-range inputs (kappa can't freeze the market +// or remove the cap; decay bounds stay ordered and ≤ 1.0/day). +#[test] +fn governance_setters_clamp_ranges() { + new_test_ext(1).execute_with(|| { + let one = I64F64::from_num(1); + let two = I64F64::from_num(2); + + SubtensorModule::set_short_kappa_ppb(0); + assert!( + ShortKappa::::get() > I64F64::from_num(0), + "kappa=0 must clamp above 0" + ); + SubtensorModule::set_short_kappa_ppb(10_000_000_000); // 10.0 + assert_eq!(ShortKappa::::get(), two, "kappa clamps to 2.0"); + SubtensorModule::set_long_kappa_ppb(0); + assert!(LongKappa::::get() > I64F64::from_num(0)); + + // min > max → enforced min ≤ max. + SubtensorModule::set_decay_bounds_ppb(500_000_000, 100_000_000); + assert!(DecayMax::::get() >= DecayMin::::get()); + // max > 1.0/day → clamped so per-block delta stays < 1. + SubtensorModule::set_decay_bounds_ppb(0, 5_000_000_000); + assert!( + DecayMax::::get() <= one, + "decay max clamps to 1.0/day" + ); + }); +} + +// Cleanup-on-empty only evicts a subnet from the decay tick once its LAST +// position closes — not while others remain. +#[test] +fn cleanup_evicts_only_after_last_short_closes() { + new_test_ext(1).execute_with(|| { + let netuid = setup_market(10_000 * TAO, 10_000 * TAO, 1.0); + let (a, b) = (U256::from(10), U256::from(20)); + for k in [a, b] { + add_balance_to_coldkey_account(&k, t(1000 * TAO)); + } + give_alpha(U256::from(11), a, netuid, AlphaBalance::from(5000 * TAO)); + give_alpha(U256::from(21), b, netuid, AlphaBalance::from(5000 * TAO)); + assert_ok!(SubtensorModule::open_short( + RuntimeOrigin::signed(a), + U256::from(11), + netuid, + t(50 * TAO), + AlphaBalance::MAX + )); + assert_ok!(SubtensorModule::open_short( + RuntimeOrigin::signed(b), + U256::from(21), + netuid, + t(50 * TAO), + AlphaBalance::MAX + )); + + assert_ok!(SubtensorModule::close_short( + RuntimeOrigin::signed(a), + netuid, + 1_000_000_000 + )); + assert!( + ShortActiveSubnets::::contains_key(netuid), + "still active while b open" + ); + + assert_ok!(SubtensorModule::close_short( + RuntimeOrigin::signed(b), + netuid, + 1_000_000_000 + )); + assert!( + !ShortActiveSubnets::::contains_key(netuid), + "evicted after last close" + ); + }); +} + +// Long capacity cap is enforced. +#[test] +fn long_capacity_cap_enforced() { + new_test_ext(1).execute_with(|| { + let netuid = setup_long(1000 * TAO, 1000 * TAO, 1.0); + SubtensorModule::set_long_kappa_ppb(1_000_000); // κ_L = 0.001 + let trader = U256::from(10); + let hotkey = U256::from(11); + give_alpha(hotkey, trader, netuid, AlphaBalance::from(500 * TAO)); + assert_noop!( + SubtensorModule::open_long( + RuntimeOrigin::signed(trader), + hotkey, + netuid, + AlphaBalance::from(100 * TAO), + TaoBalance::MAX + ), + Error::::LongCapacityExceeded + ); + }); +} + +// Long partial close reduces all legs pro-rata. +#[test] +fn long_partial_close_reduces_prorata() { + new_test_ext(1).execute_with(|| { + let netuid = setup_long(1000 * TAO, 1000 * TAO, 1.0); + let trader = U256::from(10); + let hotkey = U256::from(11); + give_alpha(hotkey, trader, netuid, AlphaBalance::from(500 * TAO)); + add_balance_to_coldkey_account(&trader, t(1000 * TAO)); + assert_ok!(SubtensorModule::open_long( + RuntimeOrigin::signed(trader), + hotkey, + netuid, + AlphaBalance::from(100 * TAO), + TaoBalance::MAX + )); + let p0 = LongPositions::::get(netuid, trader).unwrap(); + + assert_ok!(SubtensorModule::close_long( + RuntimeOrigin::signed(trader), + netuid, + 500_000_000 + )); + let p1 = LongPositions::::get(netuid, trader).unwrap(); + assert_approx(p1.p_floor.to_u64(), p0.p_floor.to_u64() / 2, 2, "p/2"); + assert_approx( + p1.d_liability.to_u64(), + p0.d_liability.to_u64() / 2, + 2, + "d/2", + ); + assert_approx(p1.r_stored.to_u64(), p0.r_stored.to_u64() / 2, 2, "r/2"); + }); +} + +// Long terminal settlement is underwater (equity 0) when the collateral can't +// cover the TAO debt at the terminal price. +#[test] +fn long_dereg_underwater_pays_zero_equity() { + new_test_ext(1).execute_with(|| { + let netuid = setup_long(1000 * TAO, 1000 * TAO, 1.0); + let trader = U256::from(10); + let hotkey = U256::from(11); + give_alpha(hotkey, trader, netuid, AlphaBalance::from(500 * TAO)); + assert_ok!(SubtensorModule::open_long( + RuntimeOrigin::signed(trader), + hotkey, + netuid, + AlphaBalance::from(100 * TAO), + TaoBalance::MAX + )); + + // Crash the price: D/price ≫ collateral ⇒ cover = C_L, equity = 0. + SubnetMovingPrice::::insert(netuid, I96F32::from_num(0.0001)); + let stake_before = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &trader, netuid) + .to_u64(); + + SubtensorModule::settle_longs_on_dereg(netuid); + + assert!(LongPositions::::get(netuid, trader).is_none()); + let stake_after = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &trader, netuid) + .to_u64(); + assert_eq!( + stake_after, stake_before, + "underwater long must return no equity" + ); + assert!(!LongActiveSubnets::::contains_key(netuid)); + }); +} + +#[test] +fn long_dereg_in_the_money_pays_bounded_equity() { + new_test_ext(1).execute_with(|| { + let netuid = setup_long(1000 * TAO, 1000 * TAO, 1.0); + let trader = U256::from(10); + let hotkey = U256::from(11); + give_alpha(hotkey, trader, netuid, AlphaBalance::from(500 * TAO)); + assert_ok!(SubtensorModule::open_long( + RuntimeOrigin::signed(trader), + hotkey, + netuid, + AlphaBalance::from(100 * TAO), + TaoBalance::MAX + )); + let pos = LongPositions::::get(netuid, trader).unwrap(); + let c_l = pos.p_floor.to_u64() + pos.r_stored.to_u64(); + let before = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &trader, netuid) + .to_u64(); + SubtensorModule::settle_longs_on_dereg(netuid); + let gained = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &trader, netuid) + .to_u64() + - before; + assert!( + gained > 0 && gained < c_l, + "long equity {gained} not in (0,{c_l})" + ); + }); +} + +#[test] +fn long_dereg_cold_ema_pays_zero_equity() { + new_test_ext(1).execute_with(|| { + let netuid = setup_long(1000 * TAO, 1000 * TAO, 1.0); + let trader = U256::from(10); + let hotkey = U256::from(11); + give_alpha(hotkey, trader, netuid, AlphaBalance::from(500 * TAO)); + assert_ok!(SubtensorModule::open_long( + RuntimeOrigin::signed(trader), + hotkey, + netuid, + AlphaBalance::from(100 * TAO), + TaoBalance::MAX + )); + // Cold price EMA: the `a_param=0` sentinel makes the cover saturate to the + // full collateral, so a cold long pays zero equity (no pool-origin refund) — + // the long analog of the short cold-EMA floor, here safe by construction. + SubnetMovingPrice::::insert(netuid, I96F32::from_num(0)); + let before = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &trader, netuid) + .to_u64(); + SubtensorModule::settle_longs_on_dereg(netuid); + let gained = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &trader, netuid) + .to_u64() + - before; + assert_eq!(gained, 0, "cold-EMA long must pay zero equity"); + }); +} + +#[test] +fn quote_open_long_matches_realized_open() { + new_test_ext(1).execute_with(|| { + let netuid = setup_long(1000 * TAO, 1000 * TAO, 1.0); + let trader = U256::from(10); + let hotkey = U256::from(11); + give_alpha(hotkey, trader, netuid, AlphaBalance::from(500 * TAO)); + let p = AlphaBalance::from(100 * TAO); + + let quote = SubtensorModule::quote_open_long(netuid, p).unwrap(); + assert_ok!(SubtensorModule::open_long( + RuntimeOrigin::signed(trader), + hotkey, + netuid, + p, + TaoBalance::MAX + )); + + let pos = LongPositions::::get(netuid, trader).unwrap(); + // Pure quote equals the realized open (same code path). + assert_eq!(pos.r_stored, quote.retained_proceeds); + assert_eq!(pos.d_liability, quote.tao_liability); + assert_eq!(pos.e_stored, quote.escrow); + assert_eq!(pos.p_floor, p); + assert_eq!(quote.est_close_cost, quote.tao_liability); // close repays D directly + }); +} + +#[test] +fn long_position_and_market_views() { + new_test_ext(1).execute_with(|| { + let netuid = setup_long(1000 * TAO, 1000 * TAO, 1.0); + let trader = U256::from(10); + let hotkey = U256::from(11); + give_alpha(hotkey, trader, netuid, AlphaBalance::from(500 * TAO)); + assert_ok!(SubtensorModule::open_long( + RuntimeOrigin::signed(trader), + hotkey, + netuid, + AlphaBalance::from(100 * TAO), + TaoBalance::MAX + )); + + let view = SubtensorModule::get_long_position(&trader, netuid).unwrap(); + let pos = LongPositions::::get(netuid, trader).unwrap(); + assert_eq!(view.floor, pos.p_floor); + assert_eq!(view.tao_liability, pos.d_liability); + assert_eq!( + view.collateral_claim, + pos.p_floor.saturating_add(pos.r_stored) + ); + assert_eq!(view.est_close_cost, pos.d_liability); + assert!(!view.default_eligible); + assert_eq!(SubtensorModule::get_long_positions(&trader).len(), 1); + + let market = SubtensorModule::get_subnet_long_state(netuid).unwrap(); + assert!(market.longs_enabled); + assert!(market.footprint_used > AlphaBalance::ZERO); + assert!(market.open_interest_tao > TaoBalance::ZERO); + // close quote is consistent with the position + let cq = SubtensorModule::quote_close_long(&trader, netuid, 1_000_000_000).unwrap(); + assert_eq!(cq.repay_tao, pos.d_liability); + }); +} + +// Fix (L1): long open won't mint alpha by saturating SubnetAlphaOut to zero. +#[test] +fn open_long_guards_against_alpha_mint() { + new_test_ext(1).execute_with(|| { + let netuid = setup_long(1000 * TAO, 1000 * TAO, 1.0); + let trader = U256::from(10); + let hotkey = U256::from(11); + give_alpha(hotkey, trader, netuid, AlphaBalance::from(500 * TAO)); + // Corrupt outstanding alpha below the collateral; open must refuse. + SubnetAlphaOut::::insert(netuid, AlphaBalance::from(0)); + assert_noop!( + SubtensorModule::open_long( + RuntimeOrigin::signed(trader), + hotkey, + netuid, + AlphaBalance::from(100 * TAO), + TaoBalance::MAX + ), + Error::::InsufficientCollateral + ); + }); +} + +// Long top-up adds Alpha buffer (from stake) and resets the grace clock. +#[test] +fn long_top_up_adds_buffer_and_resets_grace() { + new_test_ext(1).execute_with(|| { + let netuid = setup_long(1000 * TAO, 1000 * TAO, 1.0); + let trader = U256::from(10); + let hotkey = U256::from(11); + give_alpha(hotkey, trader, netuid, AlphaBalance::from(500 * TAO)); + assert_ok!(SubtensorModule::open_long( + RuntimeOrigin::signed(trader), + hotkey, + netuid, + AlphaBalance::from(100 * TAO), + TaoBalance::MAX + )); + let r0 = LongPositions::::get(netuid, trader).unwrap().r_stored; + let stake0 = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &trader, netuid); + + assert_ok!(SubtensorModule::top_up_long( + RuntimeOrigin::signed(trader), + netuid, + AlphaBalance::from(10 * TAO) + )); + let pos = LongPositions::::get(netuid, trader).unwrap(); + assert_eq!(pos.r_stored, r0 + AlphaBalance::from(10 * TAO)); + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &trader, netuid), + stake0 - AlphaBalance::from(10 * TAO) + ); + }); +} + +// Long merge must target the same hotkey. +#[test] +fn long_merge_mismatch() { + new_test_ext(1).execute_with(|| { + let netuid = setup_long(1000 * TAO, 1000 * TAO, 1.0); + let a = U256::from(10); + give_alpha(U256::from(11), a, netuid, AlphaBalance::from(500 * TAO)); + assert_ok!(SubtensorModule::open_long( + RuntimeOrigin::signed(a), + U256::from(11), + netuid, + AlphaBalance::from(20 * TAO), + TaoBalance::MAX + )); + // Same coldkey, different hotkey → rejected. + give_alpha(U256::from(12), a, netuid, AlphaBalance::from(100 * TAO)); + assert_noop!( + SubtensorModule::open_long( + RuntimeOrigin::signed(a), + U256::from(12), + netuid, + AlphaBalance::from(20 * TAO), + TaoBalance::MAX + ), + Error::::LongHotkeyMismatch + ); + }); +} + +// Long close rejects invalid fractions and below-min-input opens. +#[test] +fn long_close_invalid_fraction_and_min_input() { + new_test_ext(1).execute_with(|| { + let netuid = setup_long(1000 * TAO, 1000 * TAO, 1.0); + let trader = U256::from(10); + let hotkey = U256::from(11); + give_alpha(hotkey, trader, netuid, AlphaBalance::from(500 * TAO)); + SubtensorModule::set_long_min_input(AlphaBalance::from(TAO)); + assert_noop!( + SubtensorModule::open_long( + RuntimeOrigin::signed(trader), + hotkey, + netuid, + AlphaBalance::from(TAO / 2), + TaoBalance::MAX + ), + Error::::AmountTooLow + ); + assert_ok!(SubtensorModule::open_long( + RuntimeOrigin::signed(trader), + hotkey, + netuid, + AlphaBalance::from(100 * TAO), + TaoBalance::MAX + )); + assert_noop!( + SubtensorModule::close_long(RuntimeOrigin::signed(trader), netuid, 0), + Error::::InvalidCloseFraction + ); + assert_noop!( + SubtensorModule::close_long(RuntimeOrigin::signed(trader), netuid, 1_000_000_001), + Error::::InvalidCloseFraction + ); + }); +} + +// Short and long default-grace windows are governed independently. +#[test] +fn default_grace_independent_per_side() { + new_test_ext(1).execute_with(|| { + let netuid = setup_long(1000 * TAO, 1000 * TAO, 1.0); + let (sc, sh) = (U256::from(10), U256::from(11)); + let (lc, lh) = (U256::from(20), U256::from(21)); + add_balance_to_coldkey_account(&sc, t(1000 * TAO)); + give_alpha(lh, lc, netuid, AlphaBalance::from(500 * TAO)); + assert_ok!(SubtensorModule::open_short( + RuntimeOrigin::signed(sc), + sh, + netuid, + t(100 * TAO), + AlphaBalance::MAX + )); + assert_ok!(SubtensorModule::open_long( + RuntimeOrigin::signed(lc), + lh, + netuid, + AlphaBalance::from(100 * TAO), + TaoBalance::MAX + )); + + SubtensorModule::set_short_dust(t(10_000 * TAO)); + SubtensorModule::set_long_dust(AlphaBalance::from(10_000 * TAO)); + SubtensorModule::set_short_default_grace(0); // shorts: no grace + SubtensorModule::set_long_default_grace(5); // longs: still gated + + let poker = U256::from(99); + // Short is immediately defaultable; long is not (independent grace). + assert_ok!(SubtensorModule::default_short( + RuntimeOrigin::signed(poker), + sc, + netuid + )); + assert_noop!( + SubtensorModule::default_long(RuntimeOrigin::signed(poker), lc, netuid), + Error::::PositionNotDefaultEligible + ); + }); +} + +// Decay rate matches the closed form: one day at 1.0/day leaves ≈ e⁻¹, and the +// per-position materialized buffer stays consistent with the aggregate. +#[test] +fn decay_rate_matches_closed_form() { + new_test_ext(1).execute_with(|| { + let netuid = setup_market(1000 * TAO, 1000 * TAO, 1.0); + let trader = U256::from(10); + add_balance_to_coldkey_account(&trader, t(1000 * TAO)); + assert_ok!(SubtensorModule::open_short( + RuntimeOrigin::signed(trader), + U256::from(11), + netuid, + t(100 * TAO), + AlphaBalance::MAX + )); + SubtensorModule::set_decay_bounds_ppb(1_000_000_000, 1_000_000_000); // d = 1.0/day + + let r0 = ShortAggregate::::get(netuid).r_sigma.to_u64(); + for _ in 0..7200 { + SubtensorModule::run_short_decay(); // one day of blocks + } + let r1 = ShortAggregate::::get(netuid).r_sigma.to_u64(); + + // (1 − 1/7200)^7200 ≈ e⁻¹ ≈ 0.3679 of the original buffer. + let expected = (r0 as f64 * 0.3679) as u64; + assert_approx(r1, expected, r0 / 50, "one-day decay ≈ e^-1"); // within 2% + + // Per-position view (single position) matches the aggregate. + let info = SubtensorModule::get_short_position(&trader, netuid).unwrap(); + assert_approx(info.buffer.to_u64(), r1, r0 / 100, "position == aggregate"); + }); +} + +// --------------------------------------------------------------------------- +// Longs (mirror) + side independence +// --------------------------------------------------------------------------- + +fn setup_long(tao_reserve: u64, alpha_reserve: u64, price: f64) -> NetUid { + let netuid = setup_market(tao_reserve, alpha_reserve, price); + SubtensorModule::set_longs_enabled(true); + SubtensorModule::set_long_kappa_ppb(900_000_000); + netuid +} + +fn alpha_issuance(netuid: NetUid) -> u64 { + SubnetAlphaIn::::get(netuid).to_u64() + SubnetAlphaOut::::get(netuid).to_u64() +} + +#[test] +fn open_long_rejected_when_disabled() { + new_test_ext(1).execute_with(|| { + // setup_market enables shorts only; longs remain off by default. + let netuid = setup_market(1000 * TAO, 1000 * TAO, 1.0); + let trader = U256::from(10); + let hotkey = U256::from(11); + give_alpha(hotkey, trader, netuid, AlphaBalance::from(500 * TAO)); + assert_noop!( + SubtensorModule::open_long( + RuntimeOrigin::signed(trader), + hotkey, + netuid, + AlphaBalance::from(100 * TAO), + TaoBalance::MAX + ), + Error::::LongsDisabled + ); + }); +} + +#[test] +fn open_long_moves_alpha_off_issuance() { + new_test_ext(1).execute_with(|| { + let netuid = setup_long(1000 * TAO, 1000 * TAO, 1.0); + let trader = U256::from(10); + let hotkey = U256::from(11); + give_alpha(hotkey, trader, netuid, AlphaBalance::from(500 * TAO)); + + let alpha_in0 = SubnetAlphaIn::::get(netuid).to_u64(); + let stake0 = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &trader, netuid) + .to_u64(); + + assert_ok!(SubtensorModule::open_long( + RuntimeOrigin::signed(trader), + hotkey, + netuid, + AlphaBalance::from(100 * TAO), + TaoBalance::MAX + )); + let pos = LongPositions::::get(netuid, trader).unwrap(); + let (n, e, d) = ( + pos.r_stored.to_u64(), + pos.e_stored.to_u64(), + pos.d_liability.to_u64(), + ); + + assert!(n > 0 && e > 0 && d > 0); + assert_eq!(pos.p_floor.to_u64(), 100 * TAO); + // Pool alpha dropped by N+E; trader stake dropped by the floor P. + assert_eq!( + SubnetAlphaIn::::get(netuid).to_u64(), + alpha_in0 - n - e + ); + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &trader, netuid) + .to_u64(), + stake0 - 100 * TAO + ); + let agg = LongAggregate::::get(netuid); + assert_eq!(agg.d_sigma, pos.d_liability); + assert!(LongActiveSubnets::::contains_key(netuid)); + }); +} + +#[test] +fn full_close_long_conserves_value() { + new_test_ext(1).execute_with(|| { + let netuid = setup_long(1000 * TAO, 1000 * TAO, 1.0); + let trader = U256::from(10); + let hotkey = U256::from(11); + give_alpha(hotkey, trader, netuid, AlphaBalance::from(500 * TAO)); + add_balance_to_coldkey_account(&trader, t(1000 * TAO)); // TAO to repay D + + let iss0 = alpha_issuance(netuid); + assert_ok!(SubtensorModule::open_long( + RuntimeOrigin::signed(trader), + hotkey, + netuid, + AlphaBalance::from(100 * TAO), + TaoBalance::MAX + )); + let pos = LongPositions::::get(netuid, trader).unwrap(); + let d = pos.d_liability.to_u64(); + let tao0 = SubnetTAO::::get(netuid).to_u64(); + + assert_ok!(SubtensorModule::close_long( + RuntimeOrigin::signed(trader), + netuid, + 1_000_000_000 + )); + + assert!(LongPositions::::get(netuid, trader).is_none()); + assert!(!LongActiveSubnets::::contains_key(netuid)); + // Alpha issuance is fully restored (mint == earlier burn); TAO liability paid into pool. + assert_eq!(alpha_issuance(netuid), iss0); + assert_eq!(SubnetTAO::::get(netuid).to_u64(), tao0 + d); + let agg = LongAggregate::::get(netuid); + assert_eq!(agg.r_sigma.to_u64(), 0); + assert_eq!(agg.d_sigma.to_u64(), 0); + }); +} + +#[test] +fn long_decay_restores_alpha_to_pool() { + new_test_ext(1).execute_with(|| { + let netuid = setup_long(1000 * TAO, 1000 * TAO, 1.0); + let trader = U256::from(10); + let hotkey = U256::from(11); + give_alpha(hotkey, trader, netuid, AlphaBalance::from(500 * TAO)); + assert_ok!(SubtensorModule::open_long( + RuntimeOrigin::signed(trader), + hotkey, + netuid, + AlphaBalance::from(100 * TAO), + TaoBalance::MAX + )); + + let r0 = LongAggregate::::get(netuid).r_sigma.to_u64(); + let alpha_in0 = SubnetAlphaIn::::get(netuid).to_u64(); + for _ in 0..300 { + SubtensorModule::run_long_decay(); + } + assert!(LongAggregate::::get(netuid).r_sigma.to_u64() < r0); + assert!(SubnetAlphaIn::::get(netuid).to_u64() > alpha_in0); // alpha minted back to pool + }); +} + +#[test] +fn long_default_recycles_floor_and_restores_residual() { + new_test_ext(1).execute_with(|| { + let netuid = setup_long(1000 * TAO, 1000 * TAO, 1.0); + let trader = U256::from(10); + let hotkey = U256::from(11); + give_alpha(hotkey, trader, netuid, AlphaBalance::from(500 * TAO)); + assert_ok!(SubtensorModule::open_long( + RuntimeOrigin::signed(trader), + hotkey, + netuid, + AlphaBalance::from(100 * TAO), + TaoBalance::MAX + )); + let pos = LongPositions::::get(netuid, trader).unwrap(); + let (p, n, e) = ( + pos.p_floor.to_u64(), + pos.r_stored.to_u64(), + pos.e_stored.to_u64(), + ); + SubtensorModule::set_long_dust(AlphaBalance::from(1000 * TAO)); + SubtensorModule::set_long_default_grace(0); + + let alpha_in0 = SubnetAlphaIn::::get(netuid).to_u64(); + let iss0 = alpha_issuance(netuid); + assert_ok!(SubtensorModule::default_long( + RuntimeOrigin::signed(U256::from(99)), + trader, + netuid + )); + + assert!(LongPositions::::get(netuid, trader).is_none()); + // Residual R+E minted back to the pool; floor P stays burned (recycled). + assert_eq!( + SubnetAlphaIn::::get(netuid).to_u64(), + alpha_in0 + n + e + ); + assert_eq!(alpha_issuance(netuid), iss0 + n + e); // P remains out of issuance + assert_eq!(p, 100 * TAO); + }); +} + +#[test] +fn dereg_settles_longs() { + new_test_ext(1).execute_with(|| { + let netuid = setup_long(1000 * TAO, 1000 * TAO, 1.0); + let trader = U256::from(10); + let hotkey = U256::from(11); + give_alpha(hotkey, trader, netuid, AlphaBalance::from(500 * TAO)); + assert_ok!(SubtensorModule::open_long( + RuntimeOrigin::signed(trader), + hotkey, + netuid, + AlphaBalance::from(100 * TAO), + TaoBalance::MAX + )); + assert!(LongPositions::::get(netuid, trader).is_some()); + + assert_ok!(SubtensorModule::do_dissolve_network(netuid)); + assert!(LongPositions::::get(netuid, trader).is_none()); + assert!(!LongActiveSubnets::::contains_key(netuid)); + }); +} + +// Regression guard for the dereg ORDERING contract: long terminal equity is +// minted as alpha stake inside settle_longs_on_dereg and must be picked up by +// the immediately-following destroy_alpha_in_out_stakes (which converts stake to +// a TAO distribution). Here the trader's ONLY stake is the long collateral, so +// any TAO they receive across the FULL do_dissolve_network path proves the +// mint-before-stake-wipe ordering survives. If anyone reorders dissolve so the +// wipe runs before settlement, this test fails (equity would be silently lost). +#[test] +fn dereg_long_equity_survives_full_dissolve_path() { + new_test_ext(1).execute_with(|| { + let netuid = setup_long(1000 * TAO, 1000 * TAO, 1.0); + let trader = U256::from(10); + let hotkey = U256::from(11); + // Exactly the collateral as stake (no other stake to mask the equity). + give_alpha(hotkey, trader, netuid, AlphaBalance::from(100 * TAO)); + add_balance_to_coldkey_account(&trader, t(TAO)); // ED only + assert_ok!(SubtensorModule::open_long( + RuntimeOrigin::signed(trader), + hotkey, + netuid, + AlphaBalance::from(100 * TAO), + TaoBalance::MAX + )); + let bal_before = bal(&trader); + + assert_ok!(SubtensorModule::do_dissolve_network(netuid)); + + assert!(LongPositions::::get(netuid, trader).is_none()); + // Equity (minted as stake by settlement) reached the trader as a TAO + // distribution from the subsequent stake-wipe — proving the ordering. + assert!( + bal(&trader) > bal_before, + "long equity must survive the full dissolve path as a TAO distribution" + ); + }); +} + +// Fix: long collateral must be UNLOCKED alpha — opening a long against +// locked alpha (which a normal unstake would block) is rejected, so it can't +// be used to free locked stake. +#[test] +fn open_long_respects_stake_lock() { + new_test_ext(1).execute_with(|| { + let netuid = setup_long(1000 * TAO, 1000 * TAO, 1.0); + let cold = U256::from(10); + let hot = U256::from(11); + register_ok_neuron(netuid, hot, cold, 0); + give_alpha(hot, cold, netuid, AlphaBalance::from(200 * TAO)); + + // Lock almost all the staked alpha. + assert_ok!(SubtensorModule::do_lock_stake( + &cold, + netuid, + &hot, + AlphaBalance::from(195 * TAO) + )); + + // A long against the locked alpha is rejected (would otherwise free it). + assert_noop!( + SubtensorModule::open_long( + RuntimeOrigin::signed(cold), + hot, + netuid, + AlphaBalance::from(100 * TAO), + TaoBalance::MAX + ), + Error::::StakeUnavailable + ); + }); +} + +// Shorts and longs are independently flaggable on the same subnet. +#[test] +fn short_and_long_flags_are_independent() { + new_test_ext(1).execute_with(|| { + let netuid = setup_market(1000 * TAO, 1000 * TAO, 1.0); // shorts on, longs off + let trader = U256::from(10); + let hotkey = U256::from(11); + add_balance_to_coldkey_account(&trader, t(1000 * TAO)); + give_alpha(hotkey, trader, netuid, AlphaBalance::from(500 * TAO)); + + // Shorts enabled, longs disabled. + assert_ok!(SubtensorModule::open_short( + RuntimeOrigin::signed(trader), + hotkey, + netuid, + t(50 * TAO), + AlphaBalance::MAX + )); + assert_noop!( + SubtensorModule::open_long( + RuntimeOrigin::signed(trader), + hotkey, + netuid, + AlphaBalance::from(50 * TAO), + TaoBalance::MAX + ), + Error::::LongsDisabled + ); + + // Flip: longs enabled, shorts disabled. + SubtensorModule::set_shorts_enabled(false); + SubtensorModule::set_longs_enabled(true); + SubtensorModule::set_long_kappa_ppb(900_000_000); + assert_noop!( + SubtensorModule::open_short( + RuntimeOrigin::signed(U256::from(20)), + hotkey, + netuid, + t(50 * TAO), + AlphaBalance::MAX + ), + Error::::ShortsDisabled + ); + assert_ok!(SubtensorModule::open_long( + RuntimeOrigin::signed(trader), + hotkey, + netuid, + AlphaBalance::from(50 * TAO), + TaoBalance::MAX + )); + }); +} + +// Listing returns every position a coldkey holds across subnets. +#[test] +fn list_positions_across_subnets() { + new_test_ext(1).execute_with(|| { + let n1 = setup_market(1000 * TAO, 1000 * TAO, 1.0); + let n2 = setup_market(1000 * TAO, 1000 * TAO, 1.0); + let trader = U256::from(10); + add_balance_to_coldkey_account(&trader, t(1000 * TAO)); + assert_ok!(SubtensorModule::open_short( + RuntimeOrigin::signed(trader), + U256::from(11), + n1, + t(50 * TAO), + AlphaBalance::MAX + )); + assert_ok!(SubtensorModule::open_short( + RuntimeOrigin::signed(trader), + U256::from(12), + n2, + t(50 * TAO), + AlphaBalance::MAX + )); + + let all = SubtensorModule::get_short_positions(&trader); + assert_eq!(all.len(), 2); + let mut netuids: Vec<_> = all.iter().map(|p| p.netuid).collect(); + netuids.sort(); + let mut want = vec![n1, n2]; + want.sort(); + assert_eq!(netuids, want); + }); +} diff --git a/pallets/subtensor/src/tests/mod.rs b/pallets/subtensor/src/tests/mod.rs index be37a9227b..6bc29c2bab 100644 --- a/pallets/subtensor/src/tests/mod.rs +++ b/pallets/subtensor/src/tests/mod.rs @@ -5,6 +5,7 @@ mod claim_root; mod coinbase; mod consensus; mod delegate_info; +mod derivatives; mod emission; mod ensure; mod epoch; diff --git a/pallets/subtensor/src/weights.rs b/pallets/subtensor/src/weights.rs index 12622c4fad..06f9aa10c5 100644 --- a/pallets/subtensor/src/weights.rs +++ b/pallets/subtensor/src/weights.rs @@ -93,6 +93,18 @@ pub trait WeightInfo { fn lock_stake() -> Weight; fn move_lock() -> Weight; fn associate_evm_key() -> Weight; + fn open_short() -> Weight; + fn top_up_short() -> Weight; + fn close_short() -> Weight; + fn default_short() -> Weight; + fn open_long() -> Weight; + fn top_up_long() -> Weight; + fn close_long() -> Weight; + fn default_long() -> Weight; + fn run_short_decay(s: u32, ) -> Weight; + fn run_long_decay(s: u32, ) -> Weight; + fn settle_shorts_on_dereg(p: u32, ) -> Weight; + fn settle_longs_on_dereg(p: u32, ) -> Weight; } /// Weights for `pallet_subtensor` using the Substrate node and recommended hardware. @@ -2365,6 +2377,431 @@ impl WeightInfo for SubstrateWeight { .saturating_add(T::DbWeight::get().reads(3_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) } + /// Storage: `SubtensorModule::ShortsEnabled` (r:1 w:0) + /// Proof: `SubtensorModule::ShortsEnabled` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::NetworksAdded` (r:1 w:0) + /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetMechanism` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetMechanism` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetMovingPrice` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetMovingPrice` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::ShortMinInput` (r:1 w:0) + /// Proof: `SubtensorModule::ShortMinInput` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::ShortAggregate` (r:1 w:1) + /// Proof: `SubtensorModule::ShortAggregate` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetTAO` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetAlphaIn` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::ShortBaseLtv` (r:1 w:0) + /// Proof: `SubtensorModule::ShortBaseLtv` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::ShortKappa` (r:1 w:0) + /// Proof: `SubtensorModule::ShortKappa` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::ShortPositions` (r:1 w:1) + /// Proof: `SubtensorModule::ShortPositions` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `System::Account` (r:3 w:3) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(104), added: 2579, mode: `MaxEncodedLen`) + /// Storage: `SubtensorModule::TotalStake` (r:1 w:1) + /// Proof: `SubtensorModule::TotalStake` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::ShortPositionCount` (r:1 w:1) + /// Proof: `SubtensorModule::ShortPositionCount` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::ShortActiveSubnets` (r:0 w:1) + /// Proof: `SubtensorModule::ShortActiveSubnets` (`max_values`: None, `max_size`: None, mode: `Measured`) + fn open_short() -> Weight { + // Proof Size summary in bytes: + // Measured: `1210` + // Estimated: `8727` + // Minimum execution time: 141_000_000 picoseconds. + Weight::from_parts(141_000_000, 0) + .saturating_add(Weight::from_parts(0, 8727)) + .saturating_add(T::DbWeight::get().reads(16)) + .saturating_add(T::DbWeight::get().writes(9)) + } + /// Storage: `SubtensorModule::ShortPositions` (r:1 w:1) + /// Proof: `SubtensorModule::ShortPositions` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::ShortAggregate` (r:1 w:1) + /// Proof: `SubtensorModule::ShortAggregate` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(104), added: 2579, mode: `MaxEncodedLen`) + fn top_up_short() -> Weight { + // Proof Size summary in bytes: + // Measured: `1055` + // Estimated: `6148` + // Minimum execution time: 60_000_000 picoseconds. + Weight::from_parts(61_000_000, 0) + .saturating_add(Weight::from_parts(0, 6148)) + .saturating_add(T::DbWeight::get().reads(4)) + .saturating_add(T::DbWeight::get().writes(4)) + } + /// Storage: `SubtensorModule::ShortPositions` (r:1 w:1) + /// Proof: `SubtensorModule::ShortPositions` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::ShortAggregate` (r:1 w:1) + /// Proof: `SubtensorModule::ShortAggregate` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::Alpha` (r:1 w:0) + /// Proof: `SubtensorModule::Alpha` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::AlphaV2` (r:1 w:1) + /// Proof: `SubtensorModule::AlphaV2` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::TotalHotkeyAlpha` (r:1 w:1) + /// Proof: `SubtensorModule::TotalHotkeyAlpha` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::TotalHotkeyShares` (r:1 w:0) + /// Proof: `SubtensorModule::TotalHotkeyShares` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::TotalHotkeySharesV2` (r:1 w:1) + /// Proof: `SubtensorModule::TotalHotkeySharesV2` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetAlphaOut` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetAlphaOut` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::StakingHotkeys` (r:1 w:0) + /// Proof: `SubtensorModule::StakingHotkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::Lock` (r:1 w:0) + /// Proof: `SubtensorModule::Lock` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetAlphaIn` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::NetworksAdded` (r:1 w:0) + /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `System::Account` (r:3 w:3) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(104), added: 2579, mode: `MaxEncodedLen`) + /// Storage: `SubtensorModule::SubnetTAO` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::TotalStake` (r:1 w:1) + /// Proof: `SubtensorModule::TotalStake` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::ShortPositionCount` (r:1 w:1) + /// Proof: `SubtensorModule::ShortPositionCount` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::ShortActiveSubnets` (r:0 w:1) + /// Proof: `SubtensorModule::ShortActiveSubnets` (`max_values`: None, `max_size`: None, mode: `Measured`) + fn close_short() -> Weight { + // Proof Size summary in bytes: + // Measured: `2077` + // Estimated: `8727` + // Minimum execution time: 234_000_000 picoseconds. + Weight::from_parts(237_000_000, 0) + .saturating_add(Weight::from_parts(0, 8727)) + .saturating_add(T::DbWeight::get().reads(18)) + .saturating_add(T::DbWeight::get().writes(14)) + } + /// Storage: `SubtensorModule::ShortPositions` (r:1 w:1) + /// Proof: `SubtensorModule::ShortPositions` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::ShortAggregate` (r:1 w:1) + /// Proof: `SubtensorModule::ShortAggregate` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::ShortDust` (r:1 w:0) + /// Proof: `SubtensorModule::ShortDust` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::ShortDefaultGrace` (r:1 w:0) + /// Proof: `SubtensorModule::ShortDefaultGrace` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::NetworksAdded` (r:1 w:0) + /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(104), added: 2579, mode: `MaxEncodedLen`) + /// Storage: `SubtensorModule::SubnetTAO` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::TotalStake` (r:1 w:1) + /// Proof: `SubtensorModule::TotalStake` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::TotalIssuance` (r:1 w:1) + /// Proof: `SubtensorModule::TotalIssuance` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::ShortPositionCount` (r:1 w:1) + /// Proof: `SubtensorModule::ShortPositionCount` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::ShortActiveSubnets` (r:0 w:1) + /// Proof: `SubtensorModule::ShortActiveSubnets` (`max_values`: None, `max_size`: None, mode: `Measured`) + fn default_short() -> Weight { + // Proof Size summary in bytes: + // Measured: `1552` + // Estimated: `6148` + // Minimum execution time: 112_000_000 picoseconds. + Weight::from_parts(114_000_000, 0) + .saturating_add(Weight::from_parts(0, 6148)) + .saturating_add(T::DbWeight::get().reads(11)) + .saturating_add(T::DbWeight::get().writes(9)) + } + /// Storage: `SubtensorModule::LongsEnabled` (r:1 w:0) + /// Proof: `SubtensorModule::LongsEnabled` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::NetworksAdded` (r:1 w:0) + /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetMechanism` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetMechanism` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetMovingPrice` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetMovingPrice` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LongMinInput` (r:1 w:0) + /// Proof: `SubtensorModule::LongMinInput` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LongAggregate` (r:1 w:1) + /// Proof: `SubtensorModule::LongAggregate` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetAlphaIn` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetTAO` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LongBaseLtv` (r:1 w:0) + /// Proof: `SubtensorModule::LongBaseLtv` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LongKappa` (r:1 w:0) + /// Proof: `SubtensorModule::LongKappa` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LongPositions` (r:1 w:1) + /// Proof: `SubtensorModule::LongPositions` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::Alpha` (r:1 w:0) + /// Proof: `SubtensorModule::Alpha` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::AlphaV2` (r:1 w:1) + /// Proof: `SubtensorModule::AlphaV2` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::TotalHotkeyAlpha` (r:1 w:1) + /// Proof: `SubtensorModule::TotalHotkeyAlpha` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::TotalHotkeyShares` (r:1 w:0) + /// Proof: `SubtensorModule::TotalHotkeyShares` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::TotalHotkeySharesV2` (r:1 w:1) + /// Proof: `SubtensorModule::TotalHotkeySharesV2` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::StakingHotkeys` (r:1 w:0) + /// Proof: `SubtensorModule::StakingHotkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::Lock` (r:1 w:0) + /// Proof: `SubtensorModule::Lock` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetAlphaOut` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetAlphaOut` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LongPositionCount` (r:1 w:1) + /// Proof: `SubtensorModule::LongPositionCount` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LongActiveSubnets` (r:0 w:1) + /// Proof: `SubtensorModule::LongActiveSubnets` (`max_values`: None, `max_size`: None, mode: `Measured`) + fn open_long() -> Weight { + // Proof Size summary in bytes: + // Measured: `1762` + // Estimated: `5227` + // Minimum execution time: 167_000_000 picoseconds. + Weight::from_parts(169_000_000, 0) + .saturating_add(Weight::from_parts(0, 5227)) + .saturating_add(T::DbWeight::get().reads(20)) + .saturating_add(T::DbWeight::get().writes(9)) + } + /// Storage: `SubtensorModule::LongPositions` (r:1 w:1) + /// Proof: `SubtensorModule::LongPositions` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LongAggregate` (r:1 w:1) + /// Proof: `SubtensorModule::LongAggregate` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::Alpha` (r:1 w:0) + /// Proof: `SubtensorModule::Alpha` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::AlphaV2` (r:1 w:1) + /// Proof: `SubtensorModule::AlphaV2` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::TotalHotkeyAlpha` (r:1 w:1) + /// Proof: `SubtensorModule::TotalHotkeyAlpha` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::TotalHotkeyShares` (r:1 w:0) + /// Proof: `SubtensorModule::TotalHotkeyShares` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::TotalHotkeySharesV2` (r:1 w:1) + /// Proof: `SubtensorModule::TotalHotkeySharesV2` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::StakingHotkeys` (r:1 w:0) + /// Proof: `SubtensorModule::StakingHotkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::Lock` (r:1 w:0) + /// Proof: `SubtensorModule::Lock` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetAlphaOut` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetAlphaOut` (`max_values`: None, `max_size`: None, mode: `Measured`) + fn top_up_long() -> Weight { + // Proof Size summary in bytes: + // Measured: `1706` + // Estimated: `5171` + // Minimum execution time: 133_000_000 picoseconds. + Weight::from_parts(136_000_000, 0) + .saturating_add(Weight::from_parts(0, 5171)) + .saturating_add(T::DbWeight::get().reads(10)) + .saturating_add(T::DbWeight::get().writes(6)) + } + /// Storage: `SubtensorModule::LongPositions` (r:1 w:1) + /// Proof: `SubtensorModule::LongPositions` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LongAggregate` (r:1 w:1) + /// Proof: `SubtensorModule::LongAggregate` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::NetworksAdded` (r:1 w:0) + /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(104), added: 2579, mode: `MaxEncodedLen`) + /// Storage: `SubtensorModule::SubnetTAO` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::TotalStake` (r:1 w:1) + /// Proof: `SubtensorModule::TotalStake` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetAlphaIn` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::StakingHotkeys` (r:1 w:0) + /// Proof: `SubtensorModule::StakingHotkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::TotalHotkeyAlpha` (r:1 w:1) + /// Proof: `SubtensorModule::TotalHotkeyAlpha` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::Alpha` (r:1 w:0) + /// Proof: `SubtensorModule::Alpha` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::AlphaV2` (r:1 w:1) + /// Proof: `SubtensorModule::AlphaV2` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::TotalHotkeyShares` (r:1 w:0) + /// Proof: `SubtensorModule::TotalHotkeyShares` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::TotalHotkeySharesV2` (r:1 w:1) + /// Proof: `SubtensorModule::TotalHotkeySharesV2` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetAlphaOut` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetAlphaOut` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LongPositionCount` (r:1 w:1) + /// Proof: `SubtensorModule::LongPositionCount` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LongActiveSubnets` (r:0 w:1) + /// Proof: `SubtensorModule::LongActiveSubnets` (`max_values`: None, `max_size`: None, mode: `Measured`) + fn close_long() -> Weight { + // Proof Size summary in bytes: + // Measured: `2066` + // Estimated: `6148` + // Minimum execution time: 131_000_000 picoseconds. + Weight::from_parts(137_000_000, 0) + .saturating_add(Weight::from_parts(0, 6148)) + .saturating_add(T::DbWeight::get().reads(16)) + .saturating_add(T::DbWeight::get().writes(13)) + } + /// Storage: `SubtensorModule::LongPositions` (r:1 w:1) + /// Proof: `SubtensorModule::LongPositions` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LongAggregate` (r:1 w:1) + /// Proof: `SubtensorModule::LongAggregate` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LongDust` (r:1 w:0) + /// Proof: `SubtensorModule::LongDust` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LongDefaultGrace` (r:1 w:0) + /// Proof: `SubtensorModule::LongDefaultGrace` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetAlphaIn` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LongPositionCount` (r:1 w:1) + /// Proof: `SubtensorModule::LongPositionCount` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LongActiveSubnets` (r:0 w:1) + /// Proof: `SubtensorModule::LongActiveSubnets` (`max_values`: None, `max_size`: None, mode: `Measured`) + fn default_long() -> Weight { + // Proof Size summary in bytes: + // Measured: `1112` + // Estimated: `4577` + // Minimum execution time: 36_000_000 picoseconds. + Weight::from_parts(37_000_000, 0) + .saturating_add(Weight::from_parts(0, 4577)) + .saturating_add(T::DbWeight::get().reads(6)) + .saturating_add(T::DbWeight::get().writes(5)) + } + /// Storage: `SubtensorModule::ShortActiveSubnets` (r:129 w:0) + /// Proof: `SubtensorModule::ShortActiveSubnets` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::ShortAggregate` (r:128 w:128) + /// Proof: `SubtensorModule::ShortAggregate` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::ShortKappa` (r:1 w:0) + /// Proof: `SubtensorModule::ShortKappa` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetTAO` (r:128 w:128) + /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetAlphaIn` (r:128 w:0) + /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetMechanism` (r:128 w:0) + /// Proof: `SubtensorModule::SubnetMechanism` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetMovingPrice` (r:128 w:0) + /// Proof: `SubtensorModule::SubnetMovingPrice` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::DecayMin` (r:1 w:0) + /// Proof: `SubtensorModule::DecayMin` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::DecayMax` (r:1 w:0) + /// Proof: `SubtensorModule::DecayMax` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::NetworksAdded` (r:128 w:0) + /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `System::Account` (r:256 w:256) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(104), added: 2579, mode: `MaxEncodedLen`) + /// Storage: `SubtensorModule::TotalStake` (r:1 w:1) + /// Proof: `SubtensorModule::TotalStake` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// The range of component `s` is `[0, 128]`. + fn run_short_decay(s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `608 + s * (343 ±0)` + // Estimated: `4085 + s * (5158 ±0)` + // Minimum execution time: 3_000_000 picoseconds. + Weight::from_parts(3_000_000, 0) + .saturating_add(Weight::from_parts(0, 4085)) + // Standard Error: 221_103 + .saturating_add(Weight::from_parts(56_028_094, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(4)) + .saturating_add(T::DbWeight::get().reads((9_u64).saturating_mul(s.into()))) + .saturating_add(T::DbWeight::get().writes(1)) + .saturating_add(T::DbWeight::get().writes((4_u64).saturating_mul(s.into()))) + .saturating_add(Weight::from_parts(0, 5158).saturating_mul(s.into())) + } + /// Storage: `SubtensorModule::LongActiveSubnets` (r:129 w:0) + /// Proof: `SubtensorModule::LongActiveSubnets` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LongAggregate` (r:128 w:128) + /// Proof: `SubtensorModule::LongAggregate` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LongKappa` (r:1 w:0) + /// Proof: `SubtensorModule::LongKappa` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetAlphaIn` (r:128 w:128) + /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetTAO` (r:128 w:0) + /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetMechanism` (r:128 w:0) + /// Proof: `SubtensorModule::SubnetMechanism` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetMovingPrice` (r:128 w:0) + /// Proof: `SubtensorModule::SubnetMovingPrice` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::DecayMin` (r:1 w:0) + /// Proof: `SubtensorModule::DecayMin` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::DecayMax` (r:1 w:0) + /// Proof: `SubtensorModule::DecayMax` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// The range of component `s` is `[0, 128]`. + fn run_long_decay(s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `904 + s * (111 ±0)` + // Estimated: `4309 + s * (2587 ±0)` + // Minimum execution time: 3_000_000 picoseconds. + Weight::from_parts(48_978_054, 0) + .saturating_add(Weight::from_parts(0, 4309)) + // Standard Error: 667_326 + .saturating_add(Weight::from_parts(13_368_821, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(3)) + .saturating_add(T::DbWeight::get().reads((6_u64).saturating_mul(s.into()))) + .saturating_add(T::DbWeight::get().writes((2_u64).saturating_mul(s.into()))) + .saturating_add(Weight::from_parts(0, 2587).saturating_mul(s.into())) + } + /// Storage: `SubtensorModule::ShortAggregate` (r:1 w:1) + /// Proof: `SubtensorModule::ShortAggregate` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetMechanism` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetMechanism` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetMovingPrice` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetMovingPrice` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::NetworksAdded` (r:1 w:0) + /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetAlphaIn` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetTAO` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::ShortPositions` (r:1025 w:1024) + /// Proof: `SubtensorModule::ShortPositions` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `System::Account` (r:1026 w:1026) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(104), added: 2579, mode: `MaxEncodedLen`) + /// Storage: `SubtensorModule::TotalStake` (r:1 w:1) + /// Proof: `SubtensorModule::TotalStake` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::TotalIssuance` (r:1 w:1) + /// Proof: `SubtensorModule::TotalIssuance` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::ShortActiveSubnets` (r:0 w:1) + /// Proof: `SubtensorModule::ShortActiveSubnets` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::ShortPositionCount` (r:0 w:1) + /// Proof: `SubtensorModule::ShortPositionCount` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// The range of component `p` is `[0, 1024]`. + fn settle_shorts_on_dereg(p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1365 + p * (152 ±0)` + // Estimated: `5670 + p * (2628 ±0)` + // Minimum execution time: 62_000_000 picoseconds. + Weight::from_parts(62_000_000, 0) + .saturating_add(Weight::from_parts(0, 5670)) + // Standard Error: 157_395 + .saturating_add(Weight::from_parts(96_135_273, 0).saturating_mul(p.into())) + .saturating_add(T::DbWeight::get().reads(11)) + .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(p.into()))) + .saturating_add(T::DbWeight::get().writes(7)) + .saturating_add(T::DbWeight::get().writes((2_u64).saturating_mul(p.into()))) + .saturating_add(Weight::from_parts(0, 2628).saturating_mul(p.into())) + } + /// Storage: `SubtensorModule::LongAggregate` (r:1 w:1) + /// Proof: `SubtensorModule::LongAggregate` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetMechanism` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetMechanism` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetMovingPrice` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetMovingPrice` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetAlphaIn` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetTAO` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LongPositions` (r:1025 w:1024) + /// Proof: `SubtensorModule::LongPositions` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LongActiveSubnets` (r:0 w:1) + /// Proof: `SubtensorModule::LongActiveSubnets` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LongPositionCount` (r:0 w:1) + /// Proof: `SubtensorModule::LongPositionCount` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// The range of component `p` is `[0, 1024]`. + fn settle_longs_on_dereg(p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `927 + p * (152 ±0)` + // Estimated: `4407 + p * (2628 ±0)` + // Minimum execution time: 28_000_000 picoseconds. + Weight::from_parts(29_000_000, 0) + .saturating_add(Weight::from_parts(0, 4407)) + // Standard Error: 32_540 + .saturating_add(Weight::from_parts(8_248_039, 0).saturating_mul(p.into())) + .saturating_add(T::DbWeight::get().reads(6)) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(p.into()))) + .saturating_add(T::DbWeight::get().writes(4)) + .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(p.into()))) + .saturating_add(Weight::from_parts(0, 2628).saturating_mul(p.into())) + } } // For backwards compatibility and tests. @@ -4636,4 +5073,429 @@ impl WeightInfo for () { .saturating_add(RocksDbWeight::get().reads(3_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } + /// Storage: `SubtensorModule::ShortsEnabled` (r:1 w:0) + /// Proof: `SubtensorModule::ShortsEnabled` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::NetworksAdded` (r:1 w:0) + /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetMechanism` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetMechanism` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetMovingPrice` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetMovingPrice` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::ShortMinInput` (r:1 w:0) + /// Proof: `SubtensorModule::ShortMinInput` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::ShortAggregate` (r:1 w:1) + /// Proof: `SubtensorModule::ShortAggregate` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetTAO` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetAlphaIn` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::ShortBaseLtv` (r:1 w:0) + /// Proof: `SubtensorModule::ShortBaseLtv` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::ShortKappa` (r:1 w:0) + /// Proof: `SubtensorModule::ShortKappa` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::ShortPositions` (r:1 w:1) + /// Proof: `SubtensorModule::ShortPositions` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `System::Account` (r:3 w:3) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(104), added: 2579, mode: `MaxEncodedLen`) + /// Storage: `SubtensorModule::TotalStake` (r:1 w:1) + /// Proof: `SubtensorModule::TotalStake` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::ShortPositionCount` (r:1 w:1) + /// Proof: `SubtensorModule::ShortPositionCount` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::ShortActiveSubnets` (r:0 w:1) + /// Proof: `SubtensorModule::ShortActiveSubnets` (`max_values`: None, `max_size`: None, mode: `Measured`) + fn open_short() -> Weight { + // Proof Size summary in bytes: + // Measured: `1210` + // Estimated: `8727` + // Minimum execution time: 141_000_000 picoseconds. + Weight::from_parts(141_000_000, 0) + .saturating_add(Weight::from_parts(0, 8727)) + .saturating_add(RocksDbWeight::get().reads(16)) + .saturating_add(RocksDbWeight::get().writes(9)) + } + /// Storage: `SubtensorModule::ShortPositions` (r:1 w:1) + /// Proof: `SubtensorModule::ShortPositions` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::ShortAggregate` (r:1 w:1) + /// Proof: `SubtensorModule::ShortAggregate` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(104), added: 2579, mode: `MaxEncodedLen`) + fn top_up_short() -> Weight { + // Proof Size summary in bytes: + // Measured: `1055` + // Estimated: `6148` + // Minimum execution time: 60_000_000 picoseconds. + Weight::from_parts(61_000_000, 0) + .saturating_add(Weight::from_parts(0, 6148)) + .saturating_add(RocksDbWeight::get().reads(4)) + .saturating_add(RocksDbWeight::get().writes(4)) + } + /// Storage: `SubtensorModule::ShortPositions` (r:1 w:1) + /// Proof: `SubtensorModule::ShortPositions` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::ShortAggregate` (r:1 w:1) + /// Proof: `SubtensorModule::ShortAggregate` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::Alpha` (r:1 w:0) + /// Proof: `SubtensorModule::Alpha` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::AlphaV2` (r:1 w:1) + /// Proof: `SubtensorModule::AlphaV2` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::TotalHotkeyAlpha` (r:1 w:1) + /// Proof: `SubtensorModule::TotalHotkeyAlpha` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::TotalHotkeyShares` (r:1 w:0) + /// Proof: `SubtensorModule::TotalHotkeyShares` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::TotalHotkeySharesV2` (r:1 w:1) + /// Proof: `SubtensorModule::TotalHotkeySharesV2` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetAlphaOut` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetAlphaOut` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::StakingHotkeys` (r:1 w:0) + /// Proof: `SubtensorModule::StakingHotkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::Lock` (r:1 w:0) + /// Proof: `SubtensorModule::Lock` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetAlphaIn` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::NetworksAdded` (r:1 w:0) + /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `System::Account` (r:3 w:3) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(104), added: 2579, mode: `MaxEncodedLen`) + /// Storage: `SubtensorModule::SubnetTAO` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::TotalStake` (r:1 w:1) + /// Proof: `SubtensorModule::TotalStake` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::ShortPositionCount` (r:1 w:1) + /// Proof: `SubtensorModule::ShortPositionCount` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::ShortActiveSubnets` (r:0 w:1) + /// Proof: `SubtensorModule::ShortActiveSubnets` (`max_values`: None, `max_size`: None, mode: `Measured`) + fn close_short() -> Weight { + // Proof Size summary in bytes: + // Measured: `2077` + // Estimated: `8727` + // Minimum execution time: 234_000_000 picoseconds. + Weight::from_parts(237_000_000, 0) + .saturating_add(Weight::from_parts(0, 8727)) + .saturating_add(RocksDbWeight::get().reads(18)) + .saturating_add(RocksDbWeight::get().writes(14)) + } + /// Storage: `SubtensorModule::ShortPositions` (r:1 w:1) + /// Proof: `SubtensorModule::ShortPositions` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::ShortAggregate` (r:1 w:1) + /// Proof: `SubtensorModule::ShortAggregate` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::ShortDust` (r:1 w:0) + /// Proof: `SubtensorModule::ShortDust` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::ShortDefaultGrace` (r:1 w:0) + /// Proof: `SubtensorModule::ShortDefaultGrace` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::NetworksAdded` (r:1 w:0) + /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(104), added: 2579, mode: `MaxEncodedLen`) + /// Storage: `SubtensorModule::SubnetTAO` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::TotalStake` (r:1 w:1) + /// Proof: `SubtensorModule::TotalStake` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::TotalIssuance` (r:1 w:1) + /// Proof: `SubtensorModule::TotalIssuance` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::ShortPositionCount` (r:1 w:1) + /// Proof: `SubtensorModule::ShortPositionCount` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::ShortActiveSubnets` (r:0 w:1) + /// Proof: `SubtensorModule::ShortActiveSubnets` (`max_values`: None, `max_size`: None, mode: `Measured`) + fn default_short() -> Weight { + // Proof Size summary in bytes: + // Measured: `1552` + // Estimated: `6148` + // Minimum execution time: 112_000_000 picoseconds. + Weight::from_parts(114_000_000, 0) + .saturating_add(Weight::from_parts(0, 6148)) + .saturating_add(RocksDbWeight::get().reads(11)) + .saturating_add(RocksDbWeight::get().writes(9)) + } + /// Storage: `SubtensorModule::LongsEnabled` (r:1 w:0) + /// Proof: `SubtensorModule::LongsEnabled` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::NetworksAdded` (r:1 w:0) + /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetMechanism` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetMechanism` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetMovingPrice` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetMovingPrice` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LongMinInput` (r:1 w:0) + /// Proof: `SubtensorModule::LongMinInput` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LongAggregate` (r:1 w:1) + /// Proof: `SubtensorModule::LongAggregate` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetAlphaIn` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetTAO` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LongBaseLtv` (r:1 w:0) + /// Proof: `SubtensorModule::LongBaseLtv` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LongKappa` (r:1 w:0) + /// Proof: `SubtensorModule::LongKappa` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LongPositions` (r:1 w:1) + /// Proof: `SubtensorModule::LongPositions` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::Alpha` (r:1 w:0) + /// Proof: `SubtensorModule::Alpha` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::AlphaV2` (r:1 w:1) + /// Proof: `SubtensorModule::AlphaV2` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::TotalHotkeyAlpha` (r:1 w:1) + /// Proof: `SubtensorModule::TotalHotkeyAlpha` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::TotalHotkeyShares` (r:1 w:0) + /// Proof: `SubtensorModule::TotalHotkeyShares` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::TotalHotkeySharesV2` (r:1 w:1) + /// Proof: `SubtensorModule::TotalHotkeySharesV2` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::StakingHotkeys` (r:1 w:0) + /// Proof: `SubtensorModule::StakingHotkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::Lock` (r:1 w:0) + /// Proof: `SubtensorModule::Lock` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetAlphaOut` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetAlphaOut` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LongPositionCount` (r:1 w:1) + /// Proof: `SubtensorModule::LongPositionCount` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LongActiveSubnets` (r:0 w:1) + /// Proof: `SubtensorModule::LongActiveSubnets` (`max_values`: None, `max_size`: None, mode: `Measured`) + fn open_long() -> Weight { + // Proof Size summary in bytes: + // Measured: `1762` + // Estimated: `5227` + // Minimum execution time: 167_000_000 picoseconds. + Weight::from_parts(169_000_000, 0) + .saturating_add(Weight::from_parts(0, 5227)) + .saturating_add(RocksDbWeight::get().reads(20)) + .saturating_add(RocksDbWeight::get().writes(9)) + } + /// Storage: `SubtensorModule::LongPositions` (r:1 w:1) + /// Proof: `SubtensorModule::LongPositions` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LongAggregate` (r:1 w:1) + /// Proof: `SubtensorModule::LongAggregate` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::Alpha` (r:1 w:0) + /// Proof: `SubtensorModule::Alpha` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::AlphaV2` (r:1 w:1) + /// Proof: `SubtensorModule::AlphaV2` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::TotalHotkeyAlpha` (r:1 w:1) + /// Proof: `SubtensorModule::TotalHotkeyAlpha` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::TotalHotkeyShares` (r:1 w:0) + /// Proof: `SubtensorModule::TotalHotkeyShares` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::TotalHotkeySharesV2` (r:1 w:1) + /// Proof: `SubtensorModule::TotalHotkeySharesV2` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::StakingHotkeys` (r:1 w:0) + /// Proof: `SubtensorModule::StakingHotkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::Lock` (r:1 w:0) + /// Proof: `SubtensorModule::Lock` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetAlphaOut` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetAlphaOut` (`max_values`: None, `max_size`: None, mode: `Measured`) + fn top_up_long() -> Weight { + // Proof Size summary in bytes: + // Measured: `1706` + // Estimated: `5171` + // Minimum execution time: 133_000_000 picoseconds. + Weight::from_parts(136_000_000, 0) + .saturating_add(Weight::from_parts(0, 5171)) + .saturating_add(RocksDbWeight::get().reads(10)) + .saturating_add(RocksDbWeight::get().writes(6)) + } + /// Storage: `SubtensorModule::LongPositions` (r:1 w:1) + /// Proof: `SubtensorModule::LongPositions` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LongAggregate` (r:1 w:1) + /// Proof: `SubtensorModule::LongAggregate` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::NetworksAdded` (r:1 w:0) + /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(104), added: 2579, mode: `MaxEncodedLen`) + /// Storage: `SubtensorModule::SubnetTAO` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::TotalStake` (r:1 w:1) + /// Proof: `SubtensorModule::TotalStake` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetAlphaIn` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::StakingHotkeys` (r:1 w:0) + /// Proof: `SubtensorModule::StakingHotkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::TotalHotkeyAlpha` (r:1 w:1) + /// Proof: `SubtensorModule::TotalHotkeyAlpha` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::Alpha` (r:1 w:0) + /// Proof: `SubtensorModule::Alpha` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::AlphaV2` (r:1 w:1) + /// Proof: `SubtensorModule::AlphaV2` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::TotalHotkeyShares` (r:1 w:0) + /// Proof: `SubtensorModule::TotalHotkeyShares` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::TotalHotkeySharesV2` (r:1 w:1) + /// Proof: `SubtensorModule::TotalHotkeySharesV2` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetAlphaOut` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetAlphaOut` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LongPositionCount` (r:1 w:1) + /// Proof: `SubtensorModule::LongPositionCount` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LongActiveSubnets` (r:0 w:1) + /// Proof: `SubtensorModule::LongActiveSubnets` (`max_values`: None, `max_size`: None, mode: `Measured`) + fn close_long() -> Weight { + // Proof Size summary in bytes: + // Measured: `2066` + // Estimated: `6148` + // Minimum execution time: 131_000_000 picoseconds. + Weight::from_parts(137_000_000, 0) + .saturating_add(Weight::from_parts(0, 6148)) + .saturating_add(RocksDbWeight::get().reads(16)) + .saturating_add(RocksDbWeight::get().writes(13)) + } + /// Storage: `SubtensorModule::LongPositions` (r:1 w:1) + /// Proof: `SubtensorModule::LongPositions` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LongAggregate` (r:1 w:1) + /// Proof: `SubtensorModule::LongAggregate` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LongDust` (r:1 w:0) + /// Proof: `SubtensorModule::LongDust` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LongDefaultGrace` (r:1 w:0) + /// Proof: `SubtensorModule::LongDefaultGrace` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetAlphaIn` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LongPositionCount` (r:1 w:1) + /// Proof: `SubtensorModule::LongPositionCount` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LongActiveSubnets` (r:0 w:1) + /// Proof: `SubtensorModule::LongActiveSubnets` (`max_values`: None, `max_size`: None, mode: `Measured`) + fn default_long() -> Weight { + // Proof Size summary in bytes: + // Measured: `1112` + // Estimated: `4577` + // Minimum execution time: 36_000_000 picoseconds. + Weight::from_parts(37_000_000, 0) + .saturating_add(Weight::from_parts(0, 4577)) + .saturating_add(RocksDbWeight::get().reads(6)) + .saturating_add(RocksDbWeight::get().writes(5)) + } + /// Storage: `SubtensorModule::ShortActiveSubnets` (r:129 w:0) + /// Proof: `SubtensorModule::ShortActiveSubnets` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::ShortAggregate` (r:128 w:128) + /// Proof: `SubtensorModule::ShortAggregate` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::ShortKappa` (r:1 w:0) + /// Proof: `SubtensorModule::ShortKappa` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetTAO` (r:128 w:128) + /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetAlphaIn` (r:128 w:0) + /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetMechanism` (r:128 w:0) + /// Proof: `SubtensorModule::SubnetMechanism` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetMovingPrice` (r:128 w:0) + /// Proof: `SubtensorModule::SubnetMovingPrice` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::DecayMin` (r:1 w:0) + /// Proof: `SubtensorModule::DecayMin` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::DecayMax` (r:1 w:0) + /// Proof: `SubtensorModule::DecayMax` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::NetworksAdded` (r:128 w:0) + /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `System::Account` (r:256 w:256) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(104), added: 2579, mode: `MaxEncodedLen`) + /// Storage: `SubtensorModule::TotalStake` (r:1 w:1) + /// Proof: `SubtensorModule::TotalStake` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// The range of component `s` is `[0, 128]`. + fn run_short_decay(s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `608 + s * (343 ±0)` + // Estimated: `4085 + s * (5158 ±0)` + // Minimum execution time: 3_000_000 picoseconds. + Weight::from_parts(3_000_000, 0) + .saturating_add(Weight::from_parts(0, 4085)) + // Standard Error: 221_103 + .saturating_add(Weight::from_parts(56_028_094, 0).saturating_mul(s.into())) + .saturating_add(RocksDbWeight::get().reads(4)) + .saturating_add(RocksDbWeight::get().reads((9_u64).saturating_mul(s.into()))) + .saturating_add(RocksDbWeight::get().writes(1)) + .saturating_add(RocksDbWeight::get().writes((4_u64).saturating_mul(s.into()))) + .saturating_add(Weight::from_parts(0, 5158).saturating_mul(s.into())) + } + /// Storage: `SubtensorModule::LongActiveSubnets` (r:129 w:0) + /// Proof: `SubtensorModule::LongActiveSubnets` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LongAggregate` (r:128 w:128) + /// Proof: `SubtensorModule::LongAggregate` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LongKappa` (r:1 w:0) + /// Proof: `SubtensorModule::LongKappa` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetAlphaIn` (r:128 w:128) + /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetTAO` (r:128 w:0) + /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetMechanism` (r:128 w:0) + /// Proof: `SubtensorModule::SubnetMechanism` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetMovingPrice` (r:128 w:0) + /// Proof: `SubtensorModule::SubnetMovingPrice` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::DecayMin` (r:1 w:0) + /// Proof: `SubtensorModule::DecayMin` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::DecayMax` (r:1 w:0) + /// Proof: `SubtensorModule::DecayMax` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// The range of component `s` is `[0, 128]`. + fn run_long_decay(s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `904 + s * (111 ±0)` + // Estimated: `4309 + s * (2587 ±0)` + // Minimum execution time: 3_000_000 picoseconds. + Weight::from_parts(48_978_054, 0) + .saturating_add(Weight::from_parts(0, 4309)) + // Standard Error: 667_326 + .saturating_add(Weight::from_parts(13_368_821, 0).saturating_mul(s.into())) + .saturating_add(RocksDbWeight::get().reads(3)) + .saturating_add(RocksDbWeight::get().reads((6_u64).saturating_mul(s.into()))) + .saturating_add(RocksDbWeight::get().writes((2_u64).saturating_mul(s.into()))) + .saturating_add(Weight::from_parts(0, 2587).saturating_mul(s.into())) + } + /// Storage: `SubtensorModule::ShortAggregate` (r:1 w:1) + /// Proof: `SubtensorModule::ShortAggregate` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetMechanism` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetMechanism` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetMovingPrice` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetMovingPrice` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::NetworksAdded` (r:1 w:0) + /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetAlphaIn` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetTAO` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::ShortPositions` (r:1025 w:1024) + /// Proof: `SubtensorModule::ShortPositions` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `System::Account` (r:1026 w:1026) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(104), added: 2579, mode: `MaxEncodedLen`) + /// Storage: `SubtensorModule::TotalStake` (r:1 w:1) + /// Proof: `SubtensorModule::TotalStake` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::TotalIssuance` (r:1 w:1) + /// Proof: `SubtensorModule::TotalIssuance` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::ShortActiveSubnets` (r:0 w:1) + /// Proof: `SubtensorModule::ShortActiveSubnets` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::ShortPositionCount` (r:0 w:1) + /// Proof: `SubtensorModule::ShortPositionCount` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// The range of component `p` is `[0, 1024]`. + fn settle_shorts_on_dereg(p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1365 + p * (152 ±0)` + // Estimated: `5670 + p * (2628 ±0)` + // Minimum execution time: 62_000_000 picoseconds. + Weight::from_parts(62_000_000, 0) + .saturating_add(Weight::from_parts(0, 5670)) + // Standard Error: 157_395 + .saturating_add(Weight::from_parts(96_135_273, 0).saturating_mul(p.into())) + .saturating_add(RocksDbWeight::get().reads(11)) + .saturating_add(RocksDbWeight::get().reads((2_u64).saturating_mul(p.into()))) + .saturating_add(RocksDbWeight::get().writes(7)) + .saturating_add(RocksDbWeight::get().writes((2_u64).saturating_mul(p.into()))) + .saturating_add(Weight::from_parts(0, 2628).saturating_mul(p.into())) + } + /// Storage: `SubtensorModule::LongAggregate` (r:1 w:1) + /// Proof: `SubtensorModule::LongAggregate` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetMechanism` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetMechanism` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetMovingPrice` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetMovingPrice` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetAlphaIn` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetTAO` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LongPositions` (r:1025 w:1024) + /// Proof: `SubtensorModule::LongPositions` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LongActiveSubnets` (r:0 w:1) + /// Proof: `SubtensorModule::LongActiveSubnets` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LongPositionCount` (r:0 w:1) + /// Proof: `SubtensorModule::LongPositionCount` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// The range of component `p` is `[0, 1024]`. + fn settle_longs_on_dereg(p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `927 + p * (152 ±0)` + // Estimated: `4407 + p * (2628 ±0)` + // Minimum execution time: 28_000_000 picoseconds. + Weight::from_parts(29_000_000, 0) + .saturating_add(Weight::from_parts(0, 4407)) + // Standard Error: 32_540 + .saturating_add(Weight::from_parts(8_248_039, 0).saturating_mul(p.into())) + .saturating_add(RocksDbWeight::get().reads(6)) + .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(p.into()))) + .saturating_add(RocksDbWeight::get().writes(4)) + .saturating_add(RocksDbWeight::get().writes((1_u64).saturating_mul(p.into()))) + .saturating_add(Weight::from_parts(0, 2628).saturating_mul(p.into())) + } } diff --git a/primitives/safe-math/src/lib.rs b/primitives/safe-math/src/lib.rs index 966dbb4de4..841c211fd5 100644 --- a/primitives/safe-math/src/lib.rs +++ b/primitives/safe-math/src/lib.rs @@ -172,6 +172,46 @@ pub trait FixedExt: Fixed { ln_y.checked_add(exp_ln2) } + /// Exponential (base e). + /// + /// Range reduction `exp(x) = 2^k · exp(r)` with `k = round(x / ln 2)` and + /// `r = x − k·ln 2`, so `|r| ≤ ln 2 / 2` and the Taylor series converges fast. + /// Mirrors `checked_ln` (its inverse); valid for positive and negative `x`. + fn checked_exp(&self) -> Option { + let one = Self::from_num(1); + let ln2 = Self::from_num(LN_2); + + // k = round(x / ln 2) = floor(x / ln 2 + 0.5) + let k_fixed = self + .checked_div(ln2)? + .checked_add(Self::from_num(0.5))? + .checked_floor()?; + let k_int: i32 = k_fixed.saturating_to_num::(); + + // r = x − k·ln 2, with |r| ≤ ln 2 / 2 ≈ 0.347 + let r = self.checked_sub(k_fixed.checked_mul(ln2)?)?; + + // exp(r) = 1 + r + r²/2 + r³/6 + r⁴/24 + r⁵/120 + r⁶/720 + r⁷/5040 + let r2 = r.checked_mul(r)?; + let r3 = r2.checked_mul(r)?; + let r4 = r3.checked_mul(r)?; + let r5 = r4.checked_mul(r)?; + let r6 = r5.checked_mul(r)?; + let r7 = r6.checked_mul(r)?; + let exp_r = one + .checked_add(r)? + .checked_add(r2.checked_mul(Self::from_num(0.5))?)? + .checked_add(r3.checked_mul(Self::from_num(1.0 / 6.0))?)? + .checked_add(r4.checked_mul(Self::from_num(1.0 / 24.0))?)? + .checked_add(r5.checked_mul(Self::from_num(1.0 / 120.0))?)? + .checked_add(r6.checked_mul(Self::from_num(1.0 / 720.0))?)? + .checked_add(r7.checked_mul(Self::from_num(1.0 / 5040.0))?)?; + + // 2^k (checked_pow handles negative k as 1 / 2^|k|) + let two_k = Self::from_num(2).checked_pow(k_int)?; + exp_r.checked_mul(two_k) + } + /// Logarithm with arbitrary base fn checked_log(&self, base: Self) -> Option { // Check for invalid base @@ -334,6 +374,41 @@ mod tests { assert!(I64F64::from_num(0.0).checked_ln().is_none()); } + #[test] + fn test_checked_exp() { + // exp(0) == 1 + assert_eq!(I64F64::from_num(0).checked_exp(), Some(I64F64::from_num(1))); + + // exp(1) ≈ e + assert!( + I64F64::from_num(1) + .checked_exp() + .unwrap() + .abs_diff(I64F64::from_num(core::f64::consts::E)) + < I64F64::from_num(0.0001) + ); + + // Direct accuracy vs f64::exp across positive and negative args. + for x in [-5.0_f64, -1.0, 0.5, 2.0, 5.0] { + let got = I64F64::from_num(x).checked_exp().unwrap(); + let want = x.exp(); + assert!( + got.abs_diff(I64F64::from_num(want)) < I64F64::from_num(want * 0.0001 + 0.0001), + "exp({x}) = {got}, want {want}" + ); + } + + // Negative argument: 0 < exp(x) <= 1, and exp(-1) ≈ 1/e. + let neg = I64F64::from_num(-1).checked_exp().unwrap(); + assert!(neg > I64F64::from_num(0) && neg <= I64F64::from_num(1)); + assert!( + neg.abs_diff(I64F64::from_num(1.0 / core::f64::consts::E)) < I64F64::from_num(0.0001) + ); + + // Large negative argument underflows toward 0 without panicking. + assert!(I64F64::from_num(-50).checked_exp().unwrap() < I64F64::from_num(0.0001)); + } + #[test] fn test_checked_log() { let x = I64F64::from_num(10.0); diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 5cf4d7aadb..b99e38060f 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -2556,6 +2556,76 @@ impl_runtime_apis! { } } + impl subtensor_custom_rpc_runtime_api::DerivativesRuntimeApi for Runtime { + fn quote_open_short( + netuid: NetUid, + position_input: TaoBalance, + ) -> Option { + SubtensorModule::quote_open_short(netuid, position_input) + } + + fn quote_close_short( + coldkey: AccountId32, + netuid: NetUid, + fraction_ppb: u64, + ) -> Option { + SubtensorModule::quote_close_short(&coldkey, netuid, fraction_ppb) + } + + fn get_short_position( + coldkey: AccountId32, + netuid: NetUid, + ) -> Option> { + SubtensorModule::get_short_position(&coldkey, netuid) + } + + fn get_short_positions( + coldkey: AccountId32, + ) -> Vec> { + SubtensorModule::get_short_positions(&coldkey) + } + + fn get_subnet_short_state( + netuid: NetUid, + ) -> Option { + SubtensorModule::get_subnet_short_state(netuid) + } + + fn quote_open_long( + netuid: NetUid, + position_input: AlphaBalance, + ) -> Option { + SubtensorModule::quote_open_long(netuid, position_input) + } + + fn quote_close_long( + coldkey: AccountId32, + netuid: NetUid, + fraction_ppb: u64, + ) -> Option { + SubtensorModule::quote_close_long(&coldkey, netuid, fraction_ppb) + } + + fn get_long_position( + coldkey: AccountId32, + netuid: NetUid, + ) -> Option> { + SubtensorModule::get_long_position(&coldkey, netuid) + } + + fn get_long_positions( + coldkey: AccountId32, + ) -> Vec> { + SubtensorModule::get_long_positions(&coldkey) + } + + fn get_subnet_long_state( + netuid: NetUid, + ) -> Option { + SubtensorModule::get_subnet_long_state(netuid) + } + } + impl subtensor_custom_rpc_runtime_api::ProxyFilterRuntimeApi for Runtime { fn get_proxy_types() -> Vec { get_all_proxy_type_infos()