Exposing SNS Voting-Participation Shares After Reward Settlement

Heads up, I’m planning to implement and raise a PR for a small SNS Governance change to make each SNS neuron’s voting-participation weighting available after a reward event settles.

The planned implementation simply preserves the exact per-neuron reward shares SNS governance already calculates, tags them with the latest reward-event timestamp, and will expose them through the governance canister’s get_neuron and list_neurons endpoints. It will work even when native SNS maturity based voting rewards are set to zero and without the need to retain ballots beyond settlement.

This has been a recurring unmet need for several years. Ballots are cleared at settlement, and the window for dapp canisters to capture them can be as short as zero seconds. The feature enhancement I’m proposing will unblock a bunch of different ways in which the community should be able to extend SNS governance rewards system (rather than chucking away the work the governance canister is already performing).

Related context:

2025: https://forum.dfinity.org/t/maintaining-sns-proposal-ballot-records/43714/22

2024: https://forum.dfinity.org/t/sns-governance-feature-proposal-show-the-neuron-reward-portion-of-each-reward-event-so-that-snss-can-pay-stakeholders-in-proportion-to-the-voting-participation/32837

2022: https://forum.dfinity.org/t/sns-proposal-ended-ballot-information/17300


Having review the related code I don’t think this requires a big change at all. Hoping to have a PR to raise in the next day or two, along with tests.

@bjoern, please let me know if there’s anyone in DFINITY who will be willing to review the PR, offer feedback, and conditionally merge the PR in.

Thanks :victory_hand:

cc @Snassy-icp

This sounds like a very worthwhile change, Alex. Preserving the calculation at settlement inside Governance is much stronger than requiring downstream canisters to race ballot deletion or infer participation from later maturity changes.

A few details seem important for making the exposed evidence dependable:

  • The stored value should be the exact participation weight calculated before applying the native reward purse, rather than only the resulting maturity amount. Otherwise an SNS with zero native rewards would expose zero for every neuron and lose the relative allocation.

  • It would be useful to expose the event’s total participation weight so consumers can verify that the sum of the neuron weights reconciles exactly.

  • Please consider retaining a small bounded history, perhaps the latest five reward events as previously proposed. A latest-only value leaves consumers with a short collection window and means one missed event is permanently unrecoverable.

  • The behavior for zero participation, newly created neurons, and neurons disbursed or removed after settlement should be explicit. In particular, settled evidence should not disappear before it can be collected.

  • Using reward_event_end_timestamp_seconds as the canonical event identity sounds right, especially when list_neurons pagination could overlap settlement.

This would be immediately useful to ic-query. We could collect an API-exhausted neuron snapshot for one event, bracket the Governance version and reward event, reject mixed-event pagination, reconcile the weights against the event total, and publish an atomic cached allocation report. That would be much stronger than the maturity-delta reconciliation currently required.

I agree this does not need to preserve ballots beyond settlement. It provides the narrower artifact required for participation-proportional distributions. Full ballot history remains a separate governance-transparency question.

Very supportive of the direction and happy to test the resulting public API from ic-query once a PR is available.

Please loop in @daniel-wong for the PR review and possibly deeper technical discussions.

A concrete mainnet run may help make the integration case precise.

1. Select an SNS

ic-query resolves the deployed SNS catalog through SNS-W:

icq sns list --verbose

Abbreviated live output from 4 August 2026:

network: ic
sns_wasm_canister_id: qaa6y-5yaaa-aaaaa-aaafa-cai
sns_count: 53
source_endpoint: https://icp-api.io

ID   NAME            ROOT                          GOVERNANCE                    LEDGER
--   --------------  ---------------------------   ---------------------------   ---------------------------
 1   Dragginz        zxeu2-7aaaa-aaaaq-aaafa-cai   zqfso-syaaa-aaaaq-aaafq-cai   zfcdd-tqaaa-aaaaq-aaaga-cai
 2   OpenChat        3e3x2-xyaaa-aaaaq-aaalq-cai   2jvtu-yqaaa-aaaaq-aaama-cai   2ouva-viaaa-aaaaq-aaamq-cai
 3   unnamed-23ten   23ten-uaaaa-aaaaq-aaapa-cai   24scz-zyaaa-aaaaq-aaapq-cai   7ajy4-sqaaa-aaaaq-aaaqa-cai
...

For convenience the following commands select Dragginz by catalog ID:

SNS=1

The ID is only lookup/display metadata. The resulting evidence records and compares the stable Root, Governance, ledger, swap, index and SNS-W principals.

2. Exhaust the current neuron API

The existing command brackets a strict list_neurons walk with complete reads of:

  • get_running_sns_version;
  • the nervous-system parameters; and
  • get_latest_reward_event.

It then reads all neuron pages and repeats those three reads. Any changed bracket, duplicate neuron ID, overlapping page, cursor regression, row-ceiling violation or missing API-exhaustion evidence rejects the checkpoint.

icq sns reward checkpoint "$SNS"

The live Dragginz result was:

network: ic
sns_id: 1
name: Dragginz
root_canister_id: zxeu2-7aaaa-aaaaq-aaafa-cai
governance_canister_id: zqfso-syaaa-aaaaq-aaafq-cai
collection_status: api_exhausted_observed
point_in_time_guaranteed: no
collection_started_at: 2026-08-04T10:56:30Z
collection_completed_at: 2026-08-04T10:58:21Z
page_count: 126
row_count: 12530
collection_row_ceiling: 200000
client_query_count: 134
reward_event_end_timestamp_seconds: 1785801600
reward_event_round: 20749
reward_event_distributed: 0.00
aggregate_unstaked_maturity: 0.00
aggregate_staked_maturity: 0.00
aggregate_combined_maturity: 0.00

This is a useful real example of the missing evidence:

  • Governance successfully settled a specific reward event.
  • The complete neuron API contained 12,530 neurons.
  • Native maturity distribution was zero.
  • Consequently, all observed maturity totals were zero.
  • The existing API therefore provides no way to reconstruct the relative participation allocation after settlement.

The ballots have gone, and maturity deltas contain no information from which the per-neuron proportions can be recovered.

3. Existing positive-reward reconciliation

For an appropriately configured SNS with a positive native reward distribution, the current workaround is:

SNS=1
BEFORE=dragginz-before.json

icq sns reward checkpoint "$SNS" --json > "$BEFORE"

BEFORE_EVENT="$(
  jq -er '.reward_event_after.end_timestamp_seconds' "$BEFORE"
)"

printf 'before reward event: %s\n' "$BEFORE_EVENT"

# Run after the immediate next reward event has settled.
icq sns reward checkpoint "$SNS" --json > "$AFTER"

AFTER_EVENT="$(
  jq -er '.reward_event_after.end_timestamp_seconds' "$AFTER"
)"

printf 'after reward event: %s\n' "$AFTER_EVENT"
test "$AFTER_EVENT" -gt "$BEFORE_EVENT"

icq sns reward diff "$BEFORE" "$AFTER" --json > "$DIFF"

jq '{
  allocation_status,
  before_event: .before.reward_event_end_timestamp_seconds,
  after_event: .after.reward_event_end_timestamp_seconds,
  distributed_e8s_equivalent,
  aggregate_maturity_delta_e8s_equivalent,
  summed_neuron_maturity_delta_e8s_equivalent,
  aggregate_reconciled,
  per_neuron_reconciled,
  row_count: (.rows | length),
  invalid_reasons
}' "$DIFF"

A positive allocation is accepted only if:

sum(per-neuron maturity deltas)
    == aggregate maturity delta
    == reward_event.distributed_e8s_equivalent

It also requires non-negative explained deltas, stable canister identity, an immediate-successor reward event, no missing neurons, and no maturity conversion during the observation interval.

This is useful verification, but it is necessarily an inference from minted maturity. It cannot recover participation when the native reward purse is zero, as the live Dragginz event demonstrates.

4. Flow enabled by the proposed Governance evidence

The proposed field would replace that inference with the exact value Governance already computes during settlement.

For reward event E and neuron i, retain:

E   = reward_event_end_timestamp_seconds
w_i = exact pre-purse participation weight calculated for neuron i

An API-exhausted collector can then:

  1. Read and retain the complete latest reward event and running Governance version.
  2. Exhaust list_neurons.
  3. Select each neuron’s weight tagged with exactly E.
  4. Reject a missing weight, duplicate neuron, mixed event timestamp or unstable bracket.
  5. Calculate with checked integer arithmetic:
W = sum(w_i)
  1. Reconcile W with an authoritative event-level total participation weight, if that total is exposed.
  2. Atomically publish rows of:
reward_event_end_timestamp_seconds
neuron_id
participation_weight
total_participation_weight
  1. Let an external reward system distribute a separate pool P proportionally:
neuron_payout_i = floor(P * w_i / W)

The payout system must separately define deterministic remainder handling and an authenticated beneficiary or claim mechanism. Neuron permissions do not identify a canonical payment beneficiary.

Most importantly, this works when:

reward_event.distributed_e8s_equivalent = 0

because w_i and W describe voting participation before the native reward purse is applied.

The live Dragginz example would therefore change from “12,530 zero maturity rows with no recoverable allocation” into a directly collectable, event-labelled participation allocation—without retaining any ballots and without polling proposal deadlines.

Tagging every value with reward_event_end_timestamp_seconds also makes mixed-event pagination detectable. A small bounded history would make collection more operationally robust: the Dragginz walk above required 126 pages and approximately 111 seconds, while a latest-only value must always be collected before the following reward event replaces it.

This is a narrow settlement artifact rather than permanent ballot history. It directly addresses the zero-to-round_duration_seconds ballot collection window documented in the [2022 discussion](https://forum.dfinity.org/t/sns-proposal-ended-ballot-information/17300), while avoiding the storage and governance-transparency questions involved in retaining every ballot.

(going through it manually to make sure it all works, it was a lot of code and already getting some freaky errors)

Thanks. Give me some time to look into this. Sounds interesting.