pallet-referenda extracted from governance umbrella PR#2714
Conversation
| let now = T::BlockNumberProvider::current_block_number(); | ||
| 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)); |
There was a problem hiding this comment.
[HIGH] Submit can leak active referendum slots on scheduler/preimage failure
submit increments ReferendumCount, ActiveCount, and ActivePerProposer before fallible work runs below (set_alarm, Preimages::bound, or schedule_enactment). This call is not transactional, so if any of those operations returns Err the extrinsic leaves an active slot counted with no ReferendumStatusFor entry to kill or complete. A proposer that can trigger scheduler failure, for example by hitting MaxScheduledPerBlock, can permanently consume their quota and eventually the global governance queue. Move all fallible preimage/scheduler work before these counters are committed, or make the operation transactional and explicitly clean up any scheduled/preimage state on failure.
| fn set_alarm(index: ReferendumIndex, when: BlockNumberFor<T>) -> Result<(), DispatchError> { | ||
| let call = T::Preimages::bound(CallOf::<T>::from(Call::advance_referendum { index }))?; | ||
| let _ = T::Scheduler::cancel_named(alarm_name(index)); |
There was a problem hiding this comment.
[MEDIUM] Replacing an alarm can delete the only wake-up if scheduling fails
set_alarm cancels the existing named alarm before attempting to schedule the replacement. If schedule_named then fails, the referendum is left ongoing with no pending alarm, so it will not auto-advance or expire until another vote or privileged manual recovery happens. This is reachable from on_tally_updated, where a normal vote attempts to replace the deadline alarm with a next-block alarm. Use reschedule_named for existing alarms, or otherwise preserve/restore the old alarm unless the replacement has been scheduled successfully.
| return Err(Error::<T>::Unreachable.into()); | ||
| }; | ||
|
|
||
| if tally.approval >= *approve_threshold { |
There was a problem hiding this comment.
[MEDIUM] Pass-or-fail tracks approve when both thresholds are crossed
The state machine checks approval before rejection, but TracksInfo::check_integrity only rejects zero pass/fail thresholds and does not require approve_threshold + reject_threshold > 100%. A valid-looking track with both thresholds at or below 50% can therefore execute a proposal on a split vote where the rejection threshold was also met. Add the same non-overlap invariant used for adjustable tracks, or define an explicit rejection-precedence rule.
🛡️ AI Review — Skeptic (security review)VERDICT: VULNERABLE Baseline scrutiny: l0r1s has write permission, an established account, substantial prior subtensor history, and no Gittensor allowlist/index hit; branch is governance-referenda -> governance-signed-voting. The PR does not modify Findings
Prior-comment reconciliation
ConclusionThis looks like a legitimate governance-pallet extraction, but the current implementation still has governance liveness/state-accounting vulnerabilities around fallible scheduler/preimage operations and ambiguous threshold resolution. 📜 Previous run (superseded)
# 🔍 AI Review — Auditor (domain review) has not yet run on this PR. |
|
🔄 AI review updated — Skeptic: VULNERABLE |
| ReferendumCount::<T>::put(index.saturating_add(1)); | ||
| ActiveCount::<T>::put(active.saturating_add(1)); | ||
| ActivePerProposer::<T>::insert(&proposer, active_per_proposer.saturating_add(1)); |
There was a problem hiding this comment.
[HIGH] Submit can leak active referendum slots on scheduler/preimage failure
submit increments ReferendumCount, ActiveCount, and ActivePerProposer before the fallible setup at lines 517, 518, and 523. Returning Err from an extrinsic does not automatically roll back these storage writes, so a scheduler/preimage failure can consume global and per-proposer active slots without inserting an ongoing referendum that can later conclude and release them. Move these mutations after all fallible scheduling/preimage work succeeds, or explicitly undo them on every error path before returning.
| /// do not need to track whether one is currently pending. | ||
| fn set_alarm(index: ReferendumIndex, when: BlockNumberFor<T>) -> Result<(), DispatchError> { | ||
| let call = T::Preimages::bound(CallOf::<T>::from(Call::advance_referendum { index }))?; | ||
| let _ = T::Scheduler::cancel_named(alarm_name(index)); |
There was a problem hiding this comment.
[MEDIUM] Replacing an alarm can delete the only wake-up if scheduling fails
set_alarm cancels the existing alarm before it knows the replacement can be scheduled. Any call site that re-arms a live referendum can therefore lose the only automatic wake-up if schedule_named fails after this line; the referendum then requires privileged/manual recovery to progress. Schedule the replacement without first deleting the old alarm where possible, or restore/retry the previous alarm if replacement scheduling fails.
| if tally.approval >= *approve_threshold { | ||
| Self::do_approve(index, &info, on_approval, *decision_period); | ||
| } else if tally.rejection >= *reject_threshold { | ||
| Self::do_reject(index); |
There was a problem hiding this comment.
[MEDIUM] Pass-or-fail tracks approve when both thresholds are crossed
The pass-or-fail branch gives approval priority whenever both approval >= approve_threshold and rejection >= reject_threshold are true. VoteTally stores approval and rejection independently, and this PR only prevents overlapping fast-track/cancel thresholds for Adjustable tracks, not PassOrFail tracks. A misconfigured track with thresholds whose sum is at or below 100% can approve even when the reject threshold is also met. Add an integrity rule that prevents overlapping pass/fail thresholds, or define and enforce an explicit tie/dual-threshold policy.
|
🔄 AI review updated — Skeptic: VULNERABLE |
DO NOT MERGE