feat(governance): RV16 — el override del veto se decide al cerrar la ventana, con 4/5 de quien participa
Cierra L-11 (override real del veto). David aprobó el 4/5 el 2026-10-01: «exigir una mayoría calificada de 4/5 en el consejo para anular un veto de emergencia es el cerrojo adecuado antes de retirar la llave Sudo». Qué cambia - La ventana de override tiene dos lados: revertir (override_council_veto) y sostener (uphold_council_veto, nuevo). Quien votó Nay puede sostener el veto; antes no tenía voz. - Nada se decide al cruzar un umbral. finalize_override (nuevo) decide UNA vez, al cerrar la ventana: se revierte si override >= ceil(4/5 * max(quórum, participación)). - El quórum se fija al vetar: max(Aye + Nay del voto original, MinOverrideQuorum). Abstain no cuenta. Así el bond que el veto devuelve no basta para revertirlo y un upgrade no lo mueve. - MinOverrideQuorum en el runtime = 100M OGC (el 10% de PILAR_4 §6.6); integrity_test exige que supere MIN_PROPOSAL_STAKE. - El periodo de voto ordinario dura al menos 14 días (MIN = DEFAULT) y el veto solo cabe dentro de él. Un voto de peso 0 en la ventana se rechaza. - spec_version 16. Pesos de uphold_council_veto y finalize_override medidos (frame-omni-bencher 0.15.0, 50 steps x 20 repeats); los demás re-medidos en el mismo banco. La revisión adversarial (nivel 1 de la directiva) tumbó la primera versión - Bloqueante: la regla de quórum no era monótona y votar «sostener» podía REVERTIR el veto. Ahora sostener nunca ayuda a revertir (test con la frontera exacta 2250/2251). - El mínimo de 48 h regalaba la vía de emergencia a cualquiera; ahora 14 días. - Las abstenciones inflaban el quórum; el quórum podía cambiar con un upgrade; la spec no subía. - 18 contrafactuales (mutaciones V1..V18), todos en rojo sin su pieza. Divergencia que queda abierta (D1 del roadmap V2): PILAR_4 §6.6 ratificó «override por 2/3» y el código implementa 4/5, que es lo que David confirmó. La constitución del DAO lo fija. Tests: gobernanza 78 (94 con runtime-benchmarks), runtime 44. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a694cbd9f0
commit
3771a15165
8 files changed
+1081
-173
No files matched your search
@@ -23,7 +23,10 @@
|
||||
//! - 5.2: `pallet_collective` integration (Technical Council)
|
||||
//! - 5.3: OIP stake minimum 1,000 OGC canonical
|
||||
//! - 5.4: Stake-weighted voting (1 OGC = 1 vote, 14-day period)
|
||||
//! - 5.5: Technical Council veto + override (2/3 voters)
|
||||
//! - 5.5: Technical Council veto (2/3 del council) + override comunitario. PILAR_4 §6.6
|
||||
//! ratificó «overridable por 2/3 voters»; el código implementa 4/5 de la participación
|
||||
//! en la ventana, con quórum (RV16), y David confirmó el 4/5 el 2026-10-01. Lo que
|
||||
//! el código hace y lo que dice la doctrina difieren hasta que la constitución lo fije.
|
||||
//! - 5.6: AI governance agent interfaces (5 agents advise-only)
|
||||
//! - 5.7: Emergency referendum (48h, 5/7 Technical Council)
|
||||
//! - 5.8: Treasury disbursement extrinsics
|
||||
@@ -208,6 +211,17 @@ pub mod pallet {
|
||||
/// de quien votara en ella (demostrado por la revisión adversaria de RV15).
|
||||
pub const MAX_VOTING_PERIOD_BLOCKS: u64 = 2 * DEFAULT_VOTING_PERIOD_BLOCKS;
|
||||
|
||||
/// RV16: periodo de voto mínimo de una propuesta ORDINARIA: 14 días, el que
|
||||
/// ratifica PILAR_4 §6.6 («14-day period»).
|
||||
///
|
||||
/// Antes podía ser 0: el proponente enviaba, votaba en el mismo bloque y
|
||||
/// finalizaba en el siguiente, aprobando con su único voto. La primera versión de
|
||||
/// RV16 lo subió a 48 h y la revisión adversaria mostró por qué no basta: era la
|
||||
/// duración de la vía de emergencia sin sus 5/7 endosos del council, y como el
|
||||
/// veto solo cabe dentro del periodo, el proponente elegía cuánto tiempo tenía el
|
||||
/// council para reaccionar. Las 48 h quedan solo para `submit_emergency_proposal`.
|
||||
pub const MIN_VOTING_PERIOD_BLOCKS: u64 = DEFAULT_VOTING_PERIOD_BLOCKS;
|
||||
|
||||
/// Emergency voting period canonical canonical 48 hours per CHN-4 5.7.
|
||||
/// 48 hours × 600 blocks/hour (@ 6s slot Aura) = 28,800 blocks.
|
||||
pub const EMERGENCY_VOTING_PERIOD_BLOCKS: u64 = 48 * 600;
|
||||
@@ -309,6 +323,14 @@ pub mod pallet {
|
||||
#[pallet::constant]
|
||||
type MaxActiveVotes: Get<u32>;
|
||||
|
||||
/// RV16: participación mínima (en OGC) para que un override pueda revertir
|
||||
/// un veto. El quórum efectivo, fijado en el veto, es el MAYOR entre esto y la
|
||||
/// participación del voto original (Aye + Nay): revertir al council exige al
|
||||
/// menos tanta comunidad como la que votó. Es un parámetro de la constitución,
|
||||
/// y `integrity_test` exige que supere el bond de una propuesta.
|
||||
#[pallet::constant]
|
||||
type MinOverrideQuorum: Get<u128>;
|
||||
|
||||
/// FIX-6: PalletId del que se deriva la cuenta-tesorería (vault). Cuenta
|
||||
/// determinística sin private key — nadie la controla por wallet, solo la
|
||||
/// governance vía treasury_disbursement (Root). Honor Inmutable #16: la
|
||||
@@ -330,6 +352,19 @@ pub mod pallet {
|
||||
#[pallet::storage_version(STORAGE_VERSION)]
|
||||
pub struct Pallet<T>(_);
|
||||
|
||||
#[pallet::hooks]
|
||||
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
|
||||
/// RV16: el quórum de override tiene que superar el bond de una propuesta. Si
|
||||
/// no, el proponente revertiría solo el veto con el bond que el propio veto le
|
||||
/// devuelve (hallazgo C3), y con 0 bastaría una unidad base.
|
||||
fn integrity_test() {
|
||||
assert!(
|
||||
T::MinOverrideQuorum::get() > MIN_PROPOSAL_STAKE,
|
||||
"MinOverrideQuorum debe superar MIN_PROPOSAL_STAKE (hallazgo C3)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// RV15: por qué este pallet congela OGC.
|
||||
#[pallet::composite_enum]
|
||||
pub enum FreezeReason {
|
||||
@@ -339,6 +374,25 @@ pub mod pallet {
|
||||
GovernanceVote,
|
||||
}
|
||||
|
||||
/// RV16: votos que SOSTIENEN el veto del council durante su ventana de
|
||||
/// override (la otra mitad del recuento; antes no existía y quien votó Nay no
|
||||
/// podía defender el veto).
|
||||
#[pallet::storage]
|
||||
pub type UpholdVotes<T: Config> = StorageDoubleMap<
|
||||
_,
|
||||
Blake2_128Concat,
|
||||
ProposalId,
|
||||
Blake2_128Concat,
|
||||
T::AccountId,
|
||||
u128,
|
||||
OptionQuery,
|
||||
>;
|
||||
|
||||
/// RV16: OGC total que sostiene el veto, por propuesta.
|
||||
#[pallet::storage]
|
||||
pub type UpholdStakeTotal<T: Config> =
|
||||
StorageMap<_, Blake2_128Concat, ProposalId, u128, ValueQuery>;
|
||||
|
||||
/// RV15: los votos vivos que respalda cada cuenta (ver [`VoteBacking`]). El
|
||||
/// freeze `GovernanceVote` de la cuenta es el máximo de sus `amount`.
|
||||
#[pallet::storage]
|
||||
@@ -357,6 +411,16 @@ pub mod pallet {
|
||||
pub type OverrideDeadline<T: Config> =
|
||||
StorageMap<_, Blake2_128Concat, ProposalId, u64, OptionQuery>;
|
||||
|
||||
/// RV16: quórum del override de cada propuesta vetada, FIJADO en el veto:
|
||||
/// max(Aye + Nay del voto original, `MinOverrideQuorum`). Se guarda y no se
|
||||
/// recalcula al decidir: un runtime upgrade que cambie el parámetro no mueve la
|
||||
/// meta de una ventana ya abierta (hallazgo de la revisión de RV16). La
|
||||
/// abstención no cuenta: si contara, el mismo OGC abstenido en muchas propuestas
|
||||
/// a la vez inflaría el quórum de todas sin aparecer en ninguna ventana.
|
||||
#[pallet::storage]
|
||||
pub type OverrideQuorum<T: Config> =
|
||||
StorageMap<_, Blake2_128Concat, ProposalId, u128, OptionQuery>;
|
||||
|
||||
/// CHN-4 canonical: `Proposals<StorageMap<ProposalId, Proposal>>` canonical.
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn proposal_of)]
|
||||
@@ -595,15 +659,16 @@ pub mod pallet {
|
||||
},
|
||||
/// Council veto canonical overridden por community supermajority (Phase 5.5 v0.0.35).
|
||||
///
|
||||
/// Emitted canonical cuando override votes reach 4/5 total stake canonical canonical.
|
||||
/// Proposal status canonical flipped from Rejected back to Voting OR Approved
|
||||
/// (depending canonical en tally pre-veto).
|
||||
/// RV16: lo emite `finalize_override` cuando, cerrada la ventana, revertir
|
||||
/// reunió 4/5 del mayor entre el quórum y la participación. La propuesta pasa
|
||||
/// a `Approved`. (Antes se emitía al cruzar el umbral en mitad de la ventana.)
|
||||
CouncilVetoOverridden {
|
||||
/// ProposalId canonical overridden.
|
||||
/// Propuesta cuyo veto se revierte.
|
||||
id: ProposalId,
|
||||
/// Total override stake canonical at moment of override.
|
||||
/// OGC que votó revertir.
|
||||
override_stake: u128,
|
||||
/// Total community stake canonical baseline (proposal.stake at submit).
|
||||
/// RV16: participación total en la ventana (revertir + sostener). Antes era
|
||||
/// la base del umbral viejo (stake + yes + nay + abstain de la propuesta).
|
||||
total_community_stake: u128,
|
||||
},
|
||||
/// Emergency proposal submitted canonical (Phase 5.7 v0.0.35).
|
||||
@@ -654,6 +719,30 @@ pub mod pallet {
|
||||
/// OGC que sigue congelado por votos todavía vivos.
|
||||
still_frozen: u128,
|
||||
},
|
||||
/// RV16: un voto en la ventana de override de un veto (para el indexer del
|
||||
/// portal: antes un voto de override no emitía nada hasta el desenlace).
|
||||
OverrideVoteCast {
|
||||
/// Propuesta vetada.
|
||||
id: ProposalId,
|
||||
/// Quien vota.
|
||||
voter: T::AccountId,
|
||||
/// `true` = sostiene el veto; `false` = vota revertirlo.
|
||||
uphold: bool,
|
||||
/// OGC que respalda el voto (congelado hasta el cierre de la ventana).
|
||||
weight: u128,
|
||||
},
|
||||
/// RV16: se cerró la ventana y el veto se SOSTIENE (el override no alcanzó
|
||||
/// 4/5 de la participación o no hubo quórum).
|
||||
CouncilVetoUpheld {
|
||||
/// Propuesta vetada.
|
||||
id: ProposalId,
|
||||
/// OGC que votó revertir el veto.
|
||||
override_stake: u128,
|
||||
/// OGC que votó sostenerlo.
|
||||
uphold_stake: u128,
|
||||
/// Quórum que se exigía.
|
||||
quorum: u128,
|
||||
},
|
||||
}
|
||||
|
||||
/// Pallet errors canonical.
|
||||
@@ -729,6 +818,15 @@ pub mod pallet {
|
||||
VotingPeriodTooLong,
|
||||
/// RV15: la cuenta ya respalda `MaxActiveVotes` votos vivos.
|
||||
TooManyActiveVotes,
|
||||
/// RV16: el periodo de voto pedido es menor que `MIN_VOTING_PERIOD_BLOCKS`.
|
||||
VotingPeriodTooShort,
|
||||
/// RV16: la ventana de override sigue abierta; el resultado se decide al
|
||||
/// cerrarse, no al cruzar un umbral a mitad de camino.
|
||||
OverrideWindowOpen,
|
||||
/// RV16: esta propuesta no tiene un override pendiente de decidir.
|
||||
OverrideNotPending,
|
||||
/// RV16: un voto de la ventana de override tiene que pesar más de 0.
|
||||
ZeroWeightWindowVote,
|
||||
}
|
||||
|
||||
/// Minimum stake canonical proposal submission per CHN-4 5.3.
|
||||
@@ -763,6 +861,10 @@ pub mod pallet {
|
||||
voting_period_blocks <= MAX_VOTING_PERIOD_BLOCKS,
|
||||
Error::<T>::VotingPeriodTooLong
|
||||
);
|
||||
ensure!(
|
||||
voting_period_blocks >= MIN_VOTING_PERIOD_BLOCKS,
|
||||
Error::<T>::VotingPeriodTooShort
|
||||
);
|
||||
|
||||
let proposer_acct = AccountId::new(account_bytes::<T>(&who));
|
||||
let id = crate::derive_proposal_id(&proposer_acct, &title_hash, &body_hash);
|
||||
@@ -1044,6 +1146,14 @@ pub mod pallet {
|
||||
Error::<T>::ProposalAlreadyFinalized
|
||||
);
|
||||
|
||||
// RV16: el veto solo cabe DURANTE el periodo de voto. Antes se podía vetar
|
||||
// entre end_block y el finalize, y el resultado dependía de quién llamara
|
||||
// primero (carrera entre el council y cualquiera que finalizara).
|
||||
ensure!(
|
||||
current_block_u64::<T>() <= proposal.end_block,
|
||||
Error::<T>::VotingPeriodClosed
|
||||
);
|
||||
|
||||
// Step 4: council size canonical > 0
|
||||
let council_size = CouncilSize::<T>::get();
|
||||
ensure!(council_size > 0, Error::<T>::InsufficientCouncilSize);
|
||||
@@ -1070,6 +1180,10 @@ pub mod pallet {
|
||||
|
||||
let required = council_veto_threshold(council_size);
|
||||
if live_count >= required {
|
||||
// RV16: el quórum del override se fija AQUÍ, con la participación del
|
||||
// voto original (Aye + Nay; la abstención no cuenta).
|
||||
let turnout = proposal.yes_weight.saturating_add(proposal.nay_weight);
|
||||
OverrideQuorum::<T>::insert(proposal_id, turnout.max(T::MinOverrideQuorum::get()));
|
||||
// Veto threshold reached canonical → flip status canonical
|
||||
proposal.status = ProposalStatus::Rejected;
|
||||
Proposals::<T>::insert(proposal_id, proposal);
|
||||
@@ -1142,21 +1256,21 @@ pub mod pallet {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// CHN-4 5.5 v0.0.35 canonical: `override_council_veto` Phase 5.5 dispatchable.
|
||||
/// CHN-4 5.5 + RV16: votar REVERTIR un veto del council durante su ventana.
|
||||
///
|
||||
/// Community supermajority canonical override de council veto. 4/5 total stake
|
||||
/// canonical required canonical per CHN-4 5.5. Proposal status canonical flipped
|
||||
/// desde Rejected canonical back to Approved canonical (community canonical >
|
||||
/// council canonical veto on supermajority).
|
||||
/// Doctrina: el council veta con 2/3 y la comunidad revierte con 4/5
|
||||
/// (`docs/ORIGIN_DAO_DONDE_VIVE_V1.md`). RV16 hace real ese 4/5 (hallazgo C3 de
|
||||
/// SUPER_AUDIT_V1, demostrado otra vez por la revisión de RV15):
|
||||
/// - el voto NO voltea nada al cruzar un umbral; el resultado se decide al
|
||||
/// cerrarse la ventana, en [`Pallet::finalize_override`];
|
||||
/// - el 4/5 es de la PARTICIPACIÓN en la ventana (revertir + sostener), no de
|
||||
/// los recuentos de la propia propuesta: antes contaba el bond del proponente,
|
||||
/// que el veto le devuelve y que luego podía votar;
|
||||
/// - quien quiere el veto puede defenderlo con [`Pallet::uphold_council_veto`];
|
||||
/// - hace falta quórum.
|
||||
///
|
||||
/// 7-step canonical flow:
|
||||
/// 1. ensure_signed canonical
|
||||
/// 2. Get proposal (must canonical be Rejected previamente vía veto)
|
||||
/// 3. Validate proposal.stake > 0 (baseline canonical)
|
||||
/// 4. !AdvisorFlags canonical (humans-only canonical canonical)
|
||||
/// 5. !OverrideVotes contains canonical (double-vote prevention)
|
||||
/// 6. Insert vote + increment total stake
|
||||
/// 7. Check threshold canonical 4/5: if >= → flip to Approved + emit CouncilVetoOverridden
|
||||
/// El peso sale del saldo y queda congelado hasta el cierre de la ventana (RV15).
|
||||
/// Una cuenta vota una vez en la ventana, en un solo lado.
|
||||
#[pallet::call_index(8)]
|
||||
#[pallet::weight(T::WeightInfo::override_council_veto())]
|
||||
pub fn override_council_veto(
|
||||
@@ -1165,73 +1279,7 @@ pub mod pallet {
|
||||
stake_weight: u128,
|
||||
) -> DispatchResult {
|
||||
let who = ensure_signed(origin)?;
|
||||
|
||||
let mut proposal = Proposals::<T>::get(proposal_id)
|
||||
.ok_or(Error::<T>::ProposalNotFound)?;
|
||||
|
||||
ensure!(
|
||||
matches!(proposal.status, ProposalStatus::Rejected),
|
||||
Error::<T>::ProposalNotRejected
|
||||
);
|
||||
|
||||
// Audit 2026-07-04: el override SOLO revierte un VETO del council, no
|
||||
// un rechazo por voto popular. Sin esta marca, una propuesta que la
|
||||
// comunidad votó EN CONTRA (Rejected vía finalize_proposal) podía
|
||||
// resucitarse a Approved, invirtiendo el voto. Solo las vetadas aplican.
|
||||
ensure!(
|
||||
VetoRejected::<T>::contains_key(proposal_id),
|
||||
Error::<T>::ProposalNotVetoRejected
|
||||
);
|
||||
|
||||
ensure!(
|
||||
!AdvisorFlags::<T>::contains_key(&who),
|
||||
Error::<T>::AdvisorMayNotVote
|
||||
);
|
||||
|
||||
ensure!(
|
||||
!OverrideVotes::<T>::contains_key(proposal_id, &who),
|
||||
Error::<T>::AlreadyOverrideVoted
|
||||
);
|
||||
|
||||
// RV15: el override tiene ventana, y su peso sale del saldo y queda
|
||||
// congelado hasta que la ventana cierre.
|
||||
let deadline =
|
||||
OverrideDeadline::<T>::get(proposal_id).ok_or(Error::<T>::OverrideWindowClosed)?;
|
||||
ensure!(
|
||||
current_block_u64::<T>() <= deadline,
|
||||
Error::<T>::OverrideWindowClosed
|
||||
);
|
||||
Self::back_vote_with_funds(
|
||||
&who,
|
||||
proposal_id,
|
||||
BackingKind::Override,
|
||||
stake_weight,
|
||||
deadline,
|
||||
)?;
|
||||
|
||||
OverrideVotes::<T>::insert(proposal_id, &who, stake_weight);
|
||||
let new_total = OverrideStakeTotal::<T>::get(proposal_id)
|
||||
.saturating_add(stake_weight);
|
||||
OverrideStakeTotal::<T>::insert(proposal_id, new_total);
|
||||
|
||||
let baseline = proposal.stake.saturating_add(proposal.yes_weight)
|
||||
.saturating_add(proposal.nay_weight)
|
||||
.saturating_add(proposal.abstain_weight);
|
||||
|
||||
let required = override_supermajority_threshold(baseline);
|
||||
|
||||
if new_total >= required {
|
||||
proposal.status = ProposalStatus::Approved;
|
||||
Proposals::<T>::insert(proposal_id, proposal);
|
||||
|
||||
Self::deposit_event(Event::CouncilVetoOverridden {
|
||||
id: proposal_id,
|
||||
override_stake: new_total,
|
||||
total_community_stake: baseline,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Self::cast_override_window_vote(who, proposal_id, stake_weight, false)
|
||||
}
|
||||
|
||||
/// CHN-4 5.7 v0.0.35 canonical: `submit_emergency_proposal` Phase 5.7 dispatchable.
|
||||
@@ -1473,6 +1521,89 @@ pub mod pallet {
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// RV16: votar SOSTENER un veto del council durante su ventana de override.
|
||||
/// Es la otra mitad del recuento: sin ella, quien votó Nay no podía defender
|
||||
/// el veto y cualquier minoría con OGC suficiente lo revertía sola.
|
||||
#[pallet::call_index(14)]
|
||||
#[pallet::weight(T::WeightInfo::uphold_council_veto())]
|
||||
pub fn uphold_council_veto(
|
||||
origin: OriginFor<T>,
|
||||
proposal_id: ProposalId,
|
||||
stake_weight: u128,
|
||||
) -> DispatchResult {
|
||||
let who = ensure_signed(origin)?;
|
||||
Self::cast_override_window_vote(who, proposal_id, stake_weight, true)
|
||||
}
|
||||
|
||||
/// RV16: decide un override cuando su ventana ya cerró. Lo puede llamar
|
||||
/// cualquiera.
|
||||
///
|
||||
/// El veto se revierte (la propuesta pasa a `Approved`) solo si:
|
||||
/// 1. la participación (revertir + sostener) alcanza el quórum, que es el
|
||||
/// MAYOR entre `MinOverrideQuorum` y la participación del voto original
|
||||
/// (Aye + Nay + Abstain); y
|
||||
/// 2. revertir reúne al menos 4/5 de esa participación.
|
||||
///
|
||||
/// En cualquier otro caso el veto se sostiene. Se decide una sola vez.
|
||||
#[pallet::call_index(15)]
|
||||
#[pallet::weight(T::WeightInfo::finalize_override())]
|
||||
pub fn finalize_override(origin: OriginFor<T>, proposal_id: ProposalId) -> DispatchResult {
|
||||
ensure_signed(origin)?;
|
||||
let mut proposal =
|
||||
Proposals::<T>::get(proposal_id).ok_or(Error::<T>::ProposalNotFound)?;
|
||||
ensure!(
|
||||
matches!(proposal.status, ProposalStatus::Rejected),
|
||||
Error::<T>::ProposalNotRejected
|
||||
);
|
||||
ensure!(
|
||||
VetoRejected::<T>::contains_key(proposal_id),
|
||||
Error::<T>::ProposalNotVetoRejected
|
||||
);
|
||||
let deadline =
|
||||
OverrideDeadline::<T>::get(proposal_id).ok_or(Error::<T>::OverrideNotPending)?;
|
||||
ensure!(
|
||||
current_block_u64::<T>() > deadline,
|
||||
Error::<T>::OverrideWindowOpen
|
||||
);
|
||||
|
||||
let override_stake = OverrideStakeTotal::<T>::get(proposal_id);
|
||||
let uphold_stake = UpholdStakeTotal::<T>::get(proposal_id);
|
||||
let participation = override_stake.saturating_add(uphold_stake);
|
||||
let quorum =
|
||||
OverrideQuorum::<T>::get(proposal_id).ok_or(Error::<T>::OverrideNotPending)?;
|
||||
|
||||
// Se decide una sola vez: sin ventana, no hay override pendiente.
|
||||
OverrideDeadline::<T>::remove(proposal_id);
|
||||
OverrideQuorum::<T>::remove(proposal_id);
|
||||
|
||||
// Regla MONÓTONA: revertir necesita 4/5 del MAYOR entre el quórum y la
|
||||
// participación. La primera versión exigía `participation >= quorum` y
|
||||
// aparte el 4/5 de la participación, y la revisión adversaria demostró que
|
||||
// así SOSTENER podía revertir el veto: un voto de sostener completaba el
|
||||
// quórum que el lado revertir no alcanzaba solo, y no votar era la manera
|
||||
// de defender el veto. Con max(quorum, participación), cada OGC que sostiene
|
||||
// solo puede subir el listón de revertir, nunca bajarlo.
|
||||
let overridden = override_stake > 0
|
||||
&& override_stake >= override_supermajority_threshold(quorum.max(participation));
|
||||
if overridden {
|
||||
proposal.status = ProposalStatus::Approved;
|
||||
Proposals::<T>::insert(proposal_id, proposal);
|
||||
Self::deposit_event(Event::CouncilVetoOverridden {
|
||||
id: proposal_id,
|
||||
override_stake,
|
||||
total_community_stake: participation,
|
||||
});
|
||||
} else {
|
||||
Self::deposit_event(Event::CouncilVetoUpheld {
|
||||
id: proposal_id,
|
||||
override_stake,
|
||||
uphold_stake,
|
||||
quorum,
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// El mayor OGC respaldado por una lista de votos (0 si está vacía).
|
||||
@@ -1485,6 +1616,74 @@ pub mod pallet {
|
||||
<frame_system::Pallet<T>>::block_number().saturated_into::<u64>()
|
||||
}
|
||||
|
||||
/// RV16 — un voto en la ventana de override de un veto, de cualquiera de los dos
|
||||
/// lados. Comparte TODAS las comprobaciones: que la propuesta esté vetada (no
|
||||
/// rechazada por voto popular), que la ventana esté abierta, que la cuenta no sea
|
||||
/// advisor, que no haya votado ya en la ventana (en ningún lado), y que el peso
|
||||
/// salga de su saldo y quede congelado hasta el cierre.
|
||||
impl<T: Config> Pallet<T> {
|
||||
fn cast_override_window_vote(
|
||||
who: T::AccountId,
|
||||
proposal_id: ProposalId,
|
||||
stake_weight: u128,
|
||||
uphold: bool,
|
||||
) -> DispatchResult {
|
||||
// Un voto de peso 0 en la ventana no decide nada y dejaría una entrada
|
||||
// permanente (y a la cuenta sin poder votar después en esa ventana).
|
||||
ensure!(stake_weight > 0, Error::<T>::ZeroWeightWindowVote);
|
||||
let proposal = Proposals::<T>::get(proposal_id).ok_or(Error::<T>::ProposalNotFound)?;
|
||||
ensure!(
|
||||
matches!(proposal.status, ProposalStatus::Rejected),
|
||||
Error::<T>::ProposalNotRejected
|
||||
);
|
||||
// Audit 2026-07-04: el override SOLO revierte un VETO del council, no
|
||||
// un rechazo por voto popular.
|
||||
ensure!(
|
||||
VetoRejected::<T>::contains_key(proposal_id),
|
||||
Error::<T>::ProposalNotVetoRejected
|
||||
);
|
||||
ensure!(
|
||||
!AdvisorFlags::<T>::contains_key(&who),
|
||||
Error::<T>::AdvisorMayNotVote
|
||||
);
|
||||
ensure!(
|
||||
!OverrideVotes::<T>::contains_key(proposal_id, &who)
|
||||
&& !UpholdVotes::<T>::contains_key(proposal_id, &who),
|
||||
Error::<T>::AlreadyOverrideVoted
|
||||
);
|
||||
let deadline =
|
||||
OverrideDeadline::<T>::get(proposal_id).ok_or(Error::<T>::OverrideWindowClosed)?;
|
||||
ensure!(
|
||||
current_block_u64::<T>() <= deadline,
|
||||
Error::<T>::OverrideWindowClosed
|
||||
);
|
||||
Self::back_vote_with_funds(
|
||||
&who,
|
||||
proposal_id,
|
||||
BackingKind::Override,
|
||||
stake_weight,
|
||||
deadline,
|
||||
)?;
|
||||
|
||||
if uphold {
|
||||
UpholdVotes::<T>::insert(proposal_id, &who, stake_weight);
|
||||
UpholdStakeTotal::<T>::mutate(proposal_id, |t| *t = t.saturating_add(stake_weight));
|
||||
} else {
|
||||
OverrideVotes::<T>::insert(proposal_id, &who, stake_weight);
|
||||
OverrideStakeTotal::<T>::mutate(proposal_id, |t| {
|
||||
*t = t.saturating_add(stake_weight)
|
||||
});
|
||||
}
|
||||
Self::deposit_event(Event::OverrideVoteCast {
|
||||
id: proposal_id,
|
||||
voter: who,
|
||||
uphold,
|
||||
weight: stake_weight,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// RV15 — el OGC que respalda un voto.
|
||||
impl<T: Config> Pallet<T> {
|
||||
/// Exige que `weight` no supere el OGC LIBRE de `who` y lo deja congelado
|
||||
|
||||
Reference in new issue
Block a user