Skip to content

[Governance 3/4] Introduce pallet-referenda#2807

Open
l0r1s wants to merge 1 commit into
governance-signed-votingfrom
governance-referenda
Open

[Governance 3/4] Introduce pallet-referenda#2807
l0r1s wants to merge 1 commit into
governance-signed-votingfrom
governance-referenda

Conversation

@l0r1s

@l0r1s l0r1s commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator

Summary

Part of #2804.

Adds pallet-referenda, a generic track-based referendum state machine. It owns proposal lifecycle, scheduling, status transitions, and governed dispatch, while delegating voting to a backend such as pallet-signed-voting.

What Changed

  • Added pallet-referenda.
  • Added track configuration for proposer sets, voter sets, voting schemes, and decision strategies.
  • Added submit, kill, advance_referendum, and enact extrinsics.
  • Added PassOrFail referenda for approve/reject-by-deadline decisions.
  • Added Adjustable referenda for scheduled calls that can be fast-tracked, delayed, or cancelled.
  • Added scheduler wrapper dispatch so the governed call and Enacted status update happen atomically.
  • Added global and per-proposer active referendum limits.
  • Added strategy snapshotting so runtime track changes only affect new referenda.
  • Added benchmarks, generated weights, README documentation, mock runtime support, and unit tests.

Detailed Reference

See pallets/referenda/README.md for the full design reference. It covers the track model, decision strategies, state machines, scheduler wrapper, tally-update deferral, track-config snapshotting, per-proposer quotas, adjustment curve, integrity checks, and runtime configuration.

Reviewers may want to focus on the dispatch safety path: governed calls are scheduled through referenda.enact(index, call), so dispatch and the Enacted status transition happen in the same root call, and stale scheduled wrappers cannot execute terminal-no-dispatch referenda.

Behavioral Impact

No live runtime behavior changes in this PR. The referenda pallet is added to the codebase but is not wired into the Subtensor runtime until the governance wiring PR.

Once wired, this pallet becomes the governance state machine that opens polls, reacts to voting tallies, schedules execution, and records final referendum status.

Migration / Spec Version

The pallet defines storage, but this PR does not add it to the runtime. No live storage migration or spec-version bump is required here.

Testing

Suggested local verification:

cargo test -p pallet-referenda

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Comment thread pallets/referenda/src/lib.rs
@github-actions

github-actions Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

🛡️ AI Review — Skeptic (security review)

VERDICT: VULNERABLE

BASELINE scrutiny: l0r1s is an established contributor with write permission; no Gittensor allowlist hit found. Branch is governance-referenda -> governance-signed-voting.

No .github/ai-review/* or .github/copilot-instructions.md changes are present. The dependency change adds a local workspace pallet, not an external supply-chain dependency.

Findings

Sev File Finding
HIGH pallets/referenda/src/lib.rs:526 submit leaks queue slots when setup fails inline

Prior-comment reconciliation

  • 803dc7e7: not addressed — The current submit implementation still mutates ReferendumCount, ActiveCount, and ActivePerProposer before fallible scheduler/preimage setup, so failed setup can still leak queue capacity.

Conclusion

The prior queue-slot DoS in submit remains present: failed setup can permanently consume active referendum capacity without creating a referendum that can later be concluded or killed.


📜 Previous run (superseded)
Sev File Finding Status
HIGH pallets/referenda/src/lib.rs:529 submit leaks queue slots when setup fails ➡️ Carried forward to current findings
The current submit implementation still mutates ReferendumCount, ActiveCount, and ActivePerProposer before fallible scheduler/preimage setup, so failed setup can still leak queue capacity.

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

@github-actions

Copy link
Copy Markdown
Contributor

🔄 AI review updated — Skeptic: VULNERABLE

@l0r1s l0r1s force-pushed the governance-signed-voting branch from 51a033f to 0ebd213 Compare June 30, 2026 23:27
@l0r1s l0r1s force-pushed the governance-referenda branch from 6b00274 to f11a3bb Compare June 30, 2026 23:28

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Comment on lines +510 to +529
let index = ReferendumCount::<T>::get();
ReferendumCount::<T>::put(index.saturating_add(1));
ActiveCount::<T>::put(active.saturating_add(1));
ActivePerProposer::<T>::insert(&proposer, active_per_proposer.saturating_add(1));

let proposal = match &track_info.decision_strategy {
DecisionStrategy::PassOrFail {
decision_period, ..
} => {
let when = now.saturating_add(*decision_period);
Self::set_alarm(index, when)?;
let bounded_call = T::Preimages::bound(*call)?;
Proposal::Action(bounded_call)
}
DecisionStrategy::Adjustable { initial_delay, .. } => {
let when = now.saturating_add(*initial_delay);
Self::schedule_enactment(index, DispatchTime::At(when), call)?;
Proposal::Review
}
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[HIGH] submit leaks queue slots when setup fails

submit increments ReferendumCount, ActiveCount, and ActivePerProposer before the fallible setup work. If set_alarm, T::Preimages::bound, or schedule_enactment returns an error, the extrinsic aborts without inserting ReferendumStatusFor, so there is no referendum that kill/conclude can use to release the claimed capacity. A proposer can turn scheduler/preimage setup failures into a permanent governance queue DoS. Build the proposal and scheduler/preimage state first, then claim the counters only after no fallible setup remains; also drop the already-bound proposal if alarm setup fails.

Suggested change
let index = ReferendumCount::<T>::get();
ReferendumCount::<T>::put(index.saturating_add(1));
ActiveCount::<T>::put(active.saturating_add(1));
ActivePerProposer::<T>::insert(&proposer, active_per_proposer.saturating_add(1));
let proposal = match &track_info.decision_strategy {
DecisionStrategy::PassOrFail {
decision_period, ..
} => {
let when = now.saturating_add(*decision_period);
Self::set_alarm(index, when)?;
let bounded_call = T::Preimages::bound(*call)?;
Proposal::Action(bounded_call)
}
DecisionStrategy::Adjustable { initial_delay, .. } => {
let when = now.saturating_add(*initial_delay);
Self::schedule_enactment(index, DispatchTime::At(when), call)?;
Proposal::Review
}
};
let index = ReferendumCount::<T>::get();
let proposal = match &track_info.decision_strategy {
DecisionStrategy::PassOrFail {
decision_period, ..
} => {
let bounded_call = T::Preimages::bound(*call)?;
let when = now.saturating_add(*decision_period);
if let Err(err) = Self::set_alarm(index, when) {
T::Preimages::drop(&bounded_call);
return Err(err);
}
Proposal::Action(bounded_call)
}
DecisionStrategy::Adjustable { initial_delay, .. } => {
let when = now.saturating_add(*initial_delay);
Self::schedule_enactment(index, DispatchTime::At(when), call)?;
Proposal::Review
}
};
ReferendumCount::<T>::put(index.saturating_add(1));
ActiveCount::<T>::put(active.saturating_add(1));
ActivePerProposer::<T>::insert(&proposer, active_per_proposer.saturating_add(1));

@github-actions

Copy link
Copy Markdown
Contributor

🔄 AI review updated — Skeptic: VULNERABLE

@l0r1s l0r1s mentioned this pull request Jul 1, 2026
@l0r1s l0r1s marked this pull request as ready for review July 1, 2026 13:44

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Comment on lines +511 to +526
ReferendumCount::<T>::put(index.saturating_add(1));
ActiveCount::<T>::put(active.saturating_add(1));
ActivePerProposer::<T>::insert(&proposer, active_per_proposer.saturating_add(1));

let proposal = match &track_info.decision_strategy {
DecisionStrategy::PassOrFail {
decision_period, ..
} => {
let when = now.saturating_add(*decision_period);
Self::set_alarm(index, when)?;
let bounded_call = T::Preimages::bound(*call)?;
Proposal::Action(bounded_call)
}
DecisionStrategy::Adjustable { initial_delay, .. } => {
let when = now.saturating_add(*initial_delay);
Self::schedule_enactment(index, DispatchTime::At(when), call)?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[HIGH] submit leaks queue slots when setup fails

submit increments ReferendumCount, ActiveCount, and ActivePerProposer before the fallible setup calls at set_alarm, Preimages::bound, and schedule_enactment. Without making the extrinsic transactional or deferring these writes until after setup succeeds, an attacker can repeatedly submit calls that fail in scheduler/preimage setup and still consume global and per-proposer active capacity. No ReferendumStatusFor entry is inserted on these error paths, so there is no referendum that kill, expiry, or completion can later use to release the leaked slots. Move the capacity mutations after all fallible setup succeeds, add explicit rollback/cleanup, or wrap the dispatchable in a transactional boundary.

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

🔄 AI review updated — Skeptic: VULNERABLE

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant