Skip to content

fix(leasing): floor per-contributor dividend share so beneficiary gets the leftover#2610

Open
boskodev790 wants to merge 26 commits intoopentensor:mainfrom
boskodev790:fix/leasing-dividend-floor-per-contributor
Open

fix(leasing): floor per-contributor dividend share so beneficiary gets the leftover#2610
boskodev790 wants to merge 26 commits intoopentensor:mainfrom
boskodev790:fix/leasing-dividend-floor-per-contributor

Conversation

@boskodev790
Copy link
Copy Markdown

## Summary

`distribute_leased_network_dividends` rounded each contributor's alpha
share UP with `.ceil()` before accumulating into `alpha_distributed`.
Summed across contributors the ceilings can exceed
`total_contributors_cut_alpha`, zeroing the beneficiary's `saturating_sub`
leftover on line ~327 even though `raised - deposit > 0` always
entitles them to some dust.

In the degenerate case where cumulative over-allocation exceeds the
lease coldkey's actual subnet stake, `transfer_stake_within_subnet`
fails mid-loop, the whole `with_storage_layer` block rolls back, and
dividends re-accumulate for the next interval — breaking the
distribute-at-each-interval invariant.

The top-level `lease.emissions_share.mul_ceil(owner_cut_alpha.to_u64())`
on line ~273 still rounds UP in favour of the contributors group as a
whole (deliberate, called out by the comment there). This PR only
changes the per-contributor rounding inside the distribution loop.

## Changes

- `pallets/subtensor/src/subnets/leasing.rs`: switch the per-contributor
  `.ceil()` on line 305 to `.floor()` so the sum of all per-contributor
  shares is always `<= total_contributors_cut_alpha`. Expand the
  accompanying comment to explain the invariant and why the higher-up
  `mul_ceil` is still correct.
- `pallets/subtensor/src/tests/leasing.rs`: update the two matching
  `.ceil()` expected-value computations in
  `test_distribute_lease_network_dividends_multiple_contributors_works`
  so the expected per-contributor alpha tracks the production rounding.
  The `distributed_alpha == bene + c1 + c2` assertion now holds
  unconditionally (previously only when U64F64 happened to round
  cleanly for the specific fixture shares).

## Testing

- [x] Existing test suite for leasing dividends unchanged in structure;
      the two `expected_contributorN_alpha` computations are updated
      in lock-step with production.
- [ ] `cargo check -p pallet-subtensor` / `cargo test -p pallet-subtensor --test leasing`
      — not run locally because the Substrate toolchain + native
      dependencies are not installed in my environment. Happy to run
      locally and paste output if the reviewer prefers.

## Related Issues

N/A — code review. No existing issue or PR addresses this rounding
asymmetry; verified via GitHub search for
`distribute_leased_network_dividends`, `SubnetLeaseShares + ceil`,
`alpha_for_contributor`.

## Checklist

- [x] Code follows project style guidelines (mirrors the existing
      `saturating_to_num::<u64>()` idiom in the same function)
- [x] Self-review completed
- [x] No runtime storage / public API change — only rounding semantics
      inside an existing hook

open-junius and others added 26 commits April 13, 2026 16:07
…bnet

Introduce netuid generation for storage cleaning


Port alpha and address mapping precompiles tests to rust
…s the leftover

distribute_leased_network_dividends in pallets/subtensor/src/subnets/leasing.rs
rounded each contributor's share UP with `.ceil()` before accumulating into
`alpha_distributed`. Summed across contributors, the ceilings can accumulate
to MORE than `total_contributors_cut_alpha`, at which point
`total_contributors_cut_alpha.saturating_sub(alpha_distributed)` on line ~327
clamps to zero — the beneficiary receives nothing, even when they were
legitimately entitled to the leftover dust between the contributors' cut
and the beneficiary's implicit share (`raised - deposit`).

Concrete example: deposit = 1 TAO, raised = 1_000 TAO (999 TAO from many
small contributors). Beneficiary's intended share per dividend event is
~`deposit / raised = 0.1 %` of `total_contributors_cut_alpha`. With enough
contributors each rounding UP by ~1 rao, the cumulative rounding can exceed
the beneficiary's intended slice, zeroing their payout.

In the degenerate case where `alpha_distributed` exceeds the lease
coldkey's actual stake on the subnet (the sum of previously-accumulated
dividends plus the current owner cut), `transfer_stake_within_subnet` will
fail mid-loop and the whole `with_storage_layer` block rolls back — those
dividends then re-accumulate silently and the pattern repeats on the next
distribution, breaking the invariant that dividends are actually
distributed at each `LeaseDividendsDistributionInterval` boundary.

Switch the per-contributor rounding to `.floor()` so the sum is always
`<= total_contributors_cut_alpha`, letting the beneficiary's
`saturating_sub` on line ~327 always produce the true leftover. The
top-level `lease.emissions_share.mul_ceil(owner_cut_alpha.to_u64())`
higher up (line ~273) still rounds UP in favour of the contributors, which
is the deliberate intent called out by the comment there.

Update the matching `.ceil()` calls in
`tests/leasing.rs::test_distribute_lease_network_dividends_multiple_contributors_works`
so the expected per-contributor alpha is computed with the same rounding
as production; the `distributed_alpha == bene + c1 + c2` assertion now
holds unconditionally rather than only for fractional shares that happen
to round cleanly in U64F64.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants