From 3771a151656d6c0d49c1c74f70d3b39d4ffeda3e Mon Sep 17 00:00:00 2001 From: Charon Date: Wed, 7 Oct 2026 10:30:18 -0500 Subject: [PATCH] =?UTF-8?q?feat(governance):=20RV16=20=E2=80=94=20el=20ove?= =?UTF-8?q?rride=20del=20veto=20se=20decide=20al=20cerrar=20la=20ventana,?= =?UTF-8?q?=20con=204/5=20de=20quien=20participa?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../src/benchmarking.rs | 57 +- crates/origin-chain-governance/src/lib.rs | 373 ++++++++--- crates/origin-chain-governance/src/mock.rs | 5 + .../src/tests_integration.rs | 594 +++++++++++++++++- crates/origin-chain-governance/src/weights.rs | 194 ++++-- crates/origin-chain-runtime/src/configs.rs | 5 + .../src/genesis_config_presets.rs | 22 +- crates/origin-chain-runtime/src/lib.rs | 4 +- 8 files changed, 1081 insertions(+), 173 deletions(-) diff --git a/crates/origin-chain-governance/src/benchmarking.rs b/crates/origin-chain-governance/src/benchmarking.rs index a60c0e5..80a9c4f 100644 --- a/crates/origin-chain-governance/src/benchmarking.rs +++ b/crates/origin-chain-governance/src/benchmarking.rs @@ -94,7 +94,13 @@ mod benchmarks { fund::(&caller); #[extrinsic_call] - submit_proposal(RawOrigin::Signed(caller), [1u8; 32], [2u8; 32], 1_000u64, MIN_PROPOSAL_STAKE); + submit_proposal( + RawOrigin::Signed(caller), + [1u8; 32], + [2u8; 32], + MIN_VOTING_PERIOD_BLOCKS, + MIN_PROPOSAL_STAKE, + ); assert_eq!(Proposals::::iter().count(), 1); } @@ -230,7 +236,7 @@ mod benchmarks { seed_proposal::(id, ProposalStatus::Rejected, 0u64); VetoRejected::::insert(id, ()); // RV15: ventana abierta, peso respaldado por saldo y la lista de respaldo casi - // llena (peor caso). Y rama cara: el override ALCANZA el umbral y aprueba. + // llena (peor caso). RV16: el voto ya no decide nada; eso es finalize_override. OverrideDeadline::::insert(id, 1_000_000u64); fund::(&caller); seed_backing::( @@ -243,10 +249,6 @@ mod benchmarks { override_council_veto(RawOrigin::Signed(caller.clone()), id, MIN_PROPOSAL_STAKE); assert!(OverrideVotes::::contains_key(id, &caller)); - assert!(matches!( - Proposals::::get(id).unwrap().status, - ProposalStatus::Approved - )); } #[benchmark] @@ -332,5 +334,48 @@ mod benchmarks { assert_eq!(VoteLocks::::get(&votante).len() as u32, max - vencidos); } + #[benchmark] + fn uphold_council_veto() { + // Mismo peor caso que override_council_veto, por el otro lado. + let caller: T::AccountId = whitelisted_caller(); + let id = ProposalId([7u8; 32]); + seed_proposal::(id, ProposalStatus::Rejected, 0u64); + VetoRejected::::insert(id, ()); + OverrideDeadline::::insert(id, 1_000_000u64); + fund::(&caller); + seed_backing::( + &caller, + T::MaxActiveVotes::get().saturating_sub(1), + 1_000_000u64, + ); + + #[extrinsic_call] + uphold_council_veto(RawOrigin::Signed(caller.clone()), id, MIN_PROPOSAL_STAKE); + + assert!(UpholdVotes::::contains_key(id, &caller)); + } + + #[benchmark] + fn finalize_override() { + // Rama cara: el override REVIERTE el veto (escribe la propuesta y el evento). + let caller: T::AccountId = whitelisted_caller(); + let id = ProposalId([7u8; 32]); + seed_proposal::(id, ProposalStatus::Rejected, 0u64); + VetoRejected::::insert(id, ()); + OverrideDeadline::::insert(id, 1u64); + OverrideQuorum::::insert(id, T::MinOverrideQuorum::get()); + OverrideStakeTotal::::insert(id, T::MinOverrideQuorum::get().max(1)); + frame_system::Pallet::::set_block_number(2u32.into()); + + #[extrinsic_call] + finalize_override(RawOrigin::Signed(caller), id); + + assert!(matches!( + Proposals::::get(id).unwrap().status, + ProposalStatus::Approved + )); + assert!(!OverrideDeadline::::contains_key(id)); + } + impl_benchmark_test_suite!(Pallet, crate::mock::new_test_ext(), crate::mock::Test); } diff --git a/crates/origin-chain-governance/src/lib.rs b/crates/origin-chain-governance/src/lib.rs index 48830c3..d0b95af 100644 --- a/crates/origin-chain-governance/src/lib.rs +++ b/crates/origin-chain-governance/src/lib.rs @@ -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; + /// 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; + /// 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(_); + #[pallet::hooks] + impl Hooks> for Pallet { + /// 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 = StorageDoubleMap< + _, + Blake2_128Concat, + ProposalId, + Blake2_128Concat, + T::AccountId, + u128, + OptionQuery, + >; + + /// RV16: OGC total que sostiene el veto, por propuesta. + #[pallet::storage] + pub type UpholdStakeTotal = + 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 = 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 = + StorageMap<_, Blake2_128Concat, ProposalId, u128, OptionQuery>; + /// CHN-4 canonical: `Proposals>` 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::::VotingPeriodTooLong ); + ensure!( + voting_period_blocks >= MIN_VOTING_PERIOD_BLOCKS, + Error::::VotingPeriodTooShort + ); let proposer_acct = AccountId::new(account_bytes::(&who)); let id = crate::derive_proposal_id(&proposer_acct, &title_hash, &body_hash); @@ -1044,6 +1146,14 @@ pub mod pallet { Error::::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::() <= proposal.end_block, + Error::::VotingPeriodClosed + ); + // Step 4: council size canonical > 0 let council_size = CouncilSize::::get(); ensure!(council_size > 0, Error::::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::::insert(proposal_id, turnout.max(T::MinOverrideQuorum::get())); // Veto threshold reached canonical → flip status canonical proposal.status = ProposalStatus::Rejected; Proposals::::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::::get(proposal_id) - .ok_or(Error::::ProposalNotFound)?; - - ensure!( - matches!(proposal.status, ProposalStatus::Rejected), - Error::::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::::contains_key(proposal_id), - Error::::ProposalNotVetoRejected - ); - - ensure!( - !AdvisorFlags::::contains_key(&who), - Error::::AdvisorMayNotVote - ); - - ensure!( - !OverrideVotes::::contains_key(proposal_id, &who), - Error::::AlreadyOverrideVoted - ); - - // RV15: el override tiene ventana, y su peso sale del saldo y queda - // congelado hasta que la ventana cierre. - let deadline = - OverrideDeadline::::get(proposal_id).ok_or(Error::::OverrideWindowClosed)?; - ensure!( - current_block_u64::() <= deadline, - Error::::OverrideWindowClosed - ); - Self::back_vote_with_funds( - &who, - proposal_id, - BackingKind::Override, - stake_weight, - deadline, - )?; - - OverrideVotes::::insert(proposal_id, &who, stake_weight); - let new_total = OverrideStakeTotal::::get(proposal_id) - .saturating_add(stake_weight); - OverrideStakeTotal::::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::::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, + 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, proposal_id: ProposalId) -> DispatchResult { + ensure_signed(origin)?; + let mut proposal = + Proposals::::get(proposal_id).ok_or(Error::::ProposalNotFound)?; + ensure!( + matches!(proposal.status, ProposalStatus::Rejected), + Error::::ProposalNotRejected + ); + ensure!( + VetoRejected::::contains_key(proposal_id), + Error::::ProposalNotVetoRejected + ); + let deadline = + OverrideDeadline::::get(proposal_id).ok_or(Error::::OverrideNotPending)?; + ensure!( + current_block_u64::() > deadline, + Error::::OverrideWindowOpen + ); + + let override_stake = OverrideStakeTotal::::get(proposal_id); + let uphold_stake = UpholdStakeTotal::::get(proposal_id); + let participation = override_stake.saturating_add(uphold_stake); + let quorum = + OverrideQuorum::::get(proposal_id).ok_or(Error::::OverrideNotPending)?; + + // Se decide una sola vez: sin ventana, no hay override pendiente. + OverrideDeadline::::remove(proposal_id); + OverrideQuorum::::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::::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 { >::block_number().saturated_into::() } + /// 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 Pallet { + 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::::ZeroWeightWindowVote); + let proposal = Proposals::::get(proposal_id).ok_or(Error::::ProposalNotFound)?; + ensure!( + matches!(proposal.status, ProposalStatus::Rejected), + Error::::ProposalNotRejected + ); + // Audit 2026-07-04: el override SOLO revierte un VETO del council, no + // un rechazo por voto popular. + ensure!( + VetoRejected::::contains_key(proposal_id), + Error::::ProposalNotVetoRejected + ); + ensure!( + !AdvisorFlags::::contains_key(&who), + Error::::AdvisorMayNotVote + ); + ensure!( + !OverrideVotes::::contains_key(proposal_id, &who) + && !UpholdVotes::::contains_key(proposal_id, &who), + Error::::AlreadyOverrideVoted + ); + let deadline = + OverrideDeadline::::get(proposal_id).ok_or(Error::::OverrideWindowClosed)?; + ensure!( + current_block_u64::() <= deadline, + Error::::OverrideWindowClosed + ); + Self::back_vote_with_funds( + &who, + proposal_id, + BackingKind::Override, + stake_weight, + deadline, + )?; + + if uphold { + UpholdVotes::::insert(proposal_id, &who, stake_weight); + UpholdStakeTotal::::mutate(proposal_id, |t| *t = t.saturating_add(stake_weight)); + } else { + OverrideVotes::::insert(proposal_id, &who, stake_weight); + OverrideStakeTotal::::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 Pallet { /// Exige que `weight` no supere el OGC LIBRE de `who` y lo deja congelado diff --git a/crates/origin-chain-governance/src/mock.rs b/crates/origin-chain-governance/src/mock.rs index ae4ded8..785388b 100644 --- a/crates/origin-chain-governance/src/mock.rs +++ b/crates/origin-chain-governance/src/mock.rs @@ -48,6 +48,11 @@ impl origin_chain_governance::Config for Test { type RuntimeFreezeReason = RuntimeFreezeReason; // Bajo a propósito: el límite de votos vivos tiene que poder probarse. type MaxActiveVotes = frame_support::traits::ConstU32<4>; + // RV16: quórum mínimo de override del mock = DOS bonds (2.000 OGC). Por encima del + // bond a propósito, como en el runtime (100M OGC frente a 1.000): con el quórum + // igual al bond, el proponente revertía solo con el bond devuelto (revisión de RV16). + type MinOverrideQuorum = + frame_support::traits::ConstU128<{ 2 * crate::pallet::MIN_PROPOSAL_STAKE }>; type TreasuryPalletId = TestTreasuryPalletId; type WeightInfo = (); } diff --git a/crates/origin-chain-governance/src/tests_integration.rs b/crates/origin-chain-governance/src/tests_integration.rs index 647c832..5c1a941 100644 --- a/crates/origin-chain-governance/src/tests_integration.rs +++ b/crates/origin-chain-governance/src/tests_integration.rs @@ -27,7 +27,13 @@ use origin_chain_types::{AccountId, VoteStance}; const TITLE_HASH: [u8; 32] = [10u8; 32]; const BODY_HASH: [u8; 32] = [20u8; 32]; -const VOTING_PERIOD: u64 = 100; +/// RV16: el periodo de los tests es el mínimo legal (48 h). Antes era 100 bloques, +/// que hoy rechaza `submit_proposal`. +const VOTING_PERIOD: u64 = crate::pallet::MIN_VOTING_PERIOD_BLOCKS; +/// Un bloque claramente posterior al fin del periodo de `submit_default` (inicio +/// en el bloque 1). Sustituye a los `run_to_block(200)` que suponían un periodo +/// de 100 bloques. +const TRAS_EL_PERIODO: u64 = 1 + VOTING_PERIOD + 100; fn submit_default(origin: frame_system::pallet_prelude::OriginFor) -> ProposalId { let alice_bytes = alice_account_bytes(); @@ -187,7 +193,7 @@ fn scenario_7_finalize_approved_yes_majority_canonical() { assert_ok!(Governance::cast_vote(charlie_origin(), id, VoteStance::Nay, 500)); // Advance past voting period - run_to_block(200); + run_to_block(TRAS_EL_PERIODO); assert_ok!(Governance::finalize_proposal(alice_origin(), id)); @@ -214,7 +220,7 @@ fn scenario_8_finalize_rejected_nay_majority_canonical() { assert_ok!(Governance::cast_vote(bob_origin(), id, VoteStance::Aye, 300)); assert_ok!(Governance::cast_vote(charlie_origin(), id, VoteStance::Nay, 700)); - run_to_block(200); + run_to_block(TRAS_EL_PERIODO); assert_ok!(Governance::finalize_proposal(alice_origin(), id)); @@ -239,7 +245,7 @@ fn scenario_8b_finalize_tie_rejected_canonical() { assert_ok!(Governance::cast_vote(bob_origin(), id, VoteStance::Aye, 500)); assert_ok!(Governance::cast_vote(charlie_origin(), id, VoteStance::Nay, 500)); - run_to_block(200); + run_to_block(TRAS_EL_PERIODO); assert_ok!(Governance::finalize_proposal(alice_origin(), id)); @@ -262,7 +268,7 @@ fn scenario_9_vote_closed_period_rejected_canonical() { let id = submit_default(alice_origin()); // Advance past voting period - run_to_block(200); + run_to_block(TRAS_EL_PERIODO); // Vote after end_block → VotingPeriodClosed assert_noop!( @@ -650,21 +656,29 @@ fn override_scenario_2_successful_override_canonical() { let proposal = Proposals::::get(id).unwrap(); assert!(matches!(proposal.status, ProposalStatus::Rejected)); - // Community supermajority override canonical - // baseline = stake + yes + nay + abstain = MIN_PROPOSAL_STAKE (no votes cast) - // required = ceil(baseline × 4 / 5) - let baseline = MIN_PROPOSAL_STAKE; - let required = (baseline * 4 + 4) / 5; - - // Bob casts override con stake = required (enough) + // RV16: Bob vota revertir con el quórum mínimo (sin votos previos, el quórum + // es MinOverrideQuorum = 2 × MIN_PROPOSAL_STAKE en el mock) y nadie sostiene. assert_ok!(Governance::override_council_veto( bob_origin(), id, - required, + 2 * MIN_PROPOSAL_STAKE, )); + // El voto no voltea nada: el resultado se decide al cerrar la ventana. + assert!(matches!( + Proposals::::get(id).unwrap().status, + ProposalStatus::Rejected + )); + let cierre = crate::OverrideDeadline::::get(id).expect("ventana"); + run_to_block(cierre + 1); + assert_ok!(Governance::finalize_override(charlie_origin(), id)); let proposal_after = Proposals::::get(id).unwrap(); assert!(matches!(proposal_after.status, ProposalStatus::Approved)); + System::assert_last_event(RuntimeEvent::Governance(Event::CouncilVetoOverridden { + id, + override_stake: 2 * MIN_PROPOSAL_STAKE, + total_community_stake: 2 * MIN_PROPOSAL_STAKE, + })); }); } @@ -881,7 +895,7 @@ fn treasury_scenario_3_insufficient_balance_rejected_canonical() { // Setup approved proposal let id = submit_default(alice_origin()); assert_ok!(Governance::cast_vote(bob_origin(), id, VoteStance::Aye, 1000)); - run_to_block(200); + run_to_block(TRAS_EL_PERIODO); assert_ok!(Governance::finalize_proposal(alice_origin(), id)); // Try disburse 200 (exceeds 100 balance) @@ -908,7 +922,7 @@ fn treasury_scenario_4_successful_disbursement_canonical() { // Setup approved proposal let id = submit_default(alice_origin()); assert_ok!(Governance::cast_vote(bob_origin(), id, VoteStance::Aye, 1000)); - run_to_block(200); + run_to_block(TRAS_EL_PERIODO); assert_ok!(Governance::finalize_proposal(alice_origin(), id)); // Disburse 300_000 canonical @@ -951,7 +965,7 @@ fn override_no_puede_revertir_un_rechazo_por_voto_popular() { // La comunidad la rechaza en las urnas (Nay > Aye). assert_ok!(Governance::cast_vote(bob_origin(), id, VoteStance::Aye, 100)); assert_ok!(Governance::cast_vote(charlie_origin(), id, VoteStance::Nay, 900)); - run_to_block(200); + run_to_block(TRAS_EL_PERIODO); assert_ok!(Governance::finalize_proposal(alice_origin(), id)); let p = Proposals::::get(id).unwrap(); assert!(matches!(p.status, ProposalStatus::Rejected), "rechazada por voto"); @@ -977,7 +991,7 @@ fn treasury_disbursement_es_single_use_anti_replay() { assert_ok!(Governance::treasury_deposit(alice_origin(), 1_000_000)); let id = submit_default(alice_origin()); assert_ok!(Governance::cast_vote(bob_origin(), id, VoteStance::Aye, 1000)); - run_to_block(200); + run_to_block(TRAS_EL_PERIODO); assert_ok!(Governance::finalize_proposal(alice_origin(), id)); // Primer desembolso: OK. @@ -1054,7 +1068,7 @@ fn bond_se_retiene_al_someter_y_se_devuelve_al_finalizar() { assert_eq!(bal(&alice_account()), before - MIN_PROPOSAL_STAKE, "bond retenido"); assert_eq!(bal(&bond_acct), MIN_PROPOSAL_STAKE, "og/props tiene el bond"); // Finaliza (sin votos → Rejected) → bond devuelto íntegro. - run_to_block(200); + run_to_block(TRAS_EL_PERIODO); assert_ok!(Governance::finalize_proposal(alice_origin(), id)); assert_eq!(bal(&alice_account()), before, "bond devuelto al finalizar"); assert_eq!(bal(&bond_acct), 0, "cuenta-bond vacía"); @@ -1090,7 +1104,13 @@ fn submit_rechazado_si_no_alcanza_para_el_bond() { ) .into(); assert_noop!( - Governance::submit_proposal(pobre, [7u8; 32], [8u8; 32], 100, MIN_PROPOSAL_STAKE), + Governance::submit_proposal( + pobre, + [7u8; 32], + [8u8; 32], + VOTING_PERIOD, + MIN_PROPOSAL_STAKE + ), Error::::StakeInsufficientBalance ); }); @@ -1133,7 +1153,7 @@ fn scenario_10_lifecycle_walkthrough_canonical() { assert_eq!(proposal.nay_weight, 1000); // Step 3: advance + finalize canonical - run_to_block(200); + run_to_block(TRAS_EL_PERIODO); assert_ok!(Governance::finalize_proposal(alice_origin(), id)); // Verify final status canonical @@ -1252,7 +1272,7 @@ fn el_ogc_que_respalda_un_voto_no_sale_de_la_cuenta_mientras_el_voto_vive() { fn el_freeze_real_es_el_mayor_de_los_votos_vivos_no_el_ultimo() { new_test_ext().execute_with(|| { let corta = submit_default(alice_origin()); - let larga = propuesta_con_periodo(11, 3 * VOTING_PERIOD); + let larga = propuesta_con_periodo(11, 2 * VOTING_PERIOD); let saldo = Balances::free_balance(bob_account()); assert_ok!(Governance::cast_vote( bob_origin(), @@ -1292,7 +1312,7 @@ fn el_freeze_real_es_el_mayor_de_los_votos_vivos_no_el_ultimo() { fn al_vencer_el_voto_grande_solo_queda_congelado_el_pequeno_que_sigue_vivo() { new_test_ext().execute_with(|| { let corta = submit_default(alice_origin()); - let larga = propuesta_con_periodo(11, 3 * VOTING_PERIOD); + let larga = propuesta_con_periodo(11, 2 * VOTING_PERIOD); assert_ok!(Governance::cast_vote( bob_origin(), larga, @@ -1462,7 +1482,7 @@ fn un_voto_vencido_no_revive_con_el_siguiente() { fn el_mismo_ogc_entero_vota_en_dos_propuestas() { new_test_ext().execute_with(|| { let p1 = submit_default(alice_origin()); - let p2 = propuesta_con_periodo(11, 3 * VOTING_PERIOD); + let p2 = propuesta_con_periodo(11, 2 * VOTING_PERIOD); let saldo = Balances::free_balance(bob_account()); assert_ok!(Governance::cast_vote( bob_origin(), @@ -1615,7 +1635,7 @@ fn el_limite_es_de_votos_vivos_y_los_vencidos_no_ocupan_sitio() { let corta = submit_default(alice_origin()); let mut largas = Vec::new(); for semilla in 11..15u8 { - largas.push(propuesta_con_periodo(semilla, 3 * VOTING_PERIOD)); + largas.push(propuesta_con_periodo(semilla, 2 * VOTING_PERIOD)); } assert_ok!(Governance::cast_vote( bob_origin(), @@ -1646,3 +1666,529 @@ fn el_limite_es_de_votos_vivos_y_los_vencidos_no_ocupan_sitio() { assert_eq!(crate::VoteLocks::::get(bob_account()).len(), 4); }); } + +// ═══════════════════════════════════════════════════════════════ +// RV16 — el override de un veto del council es un 4/5 REAL de la comunidad. +// Doctrina: council veta con 2/3, la comunidad revierte con 4/5. Hallazgo C3 +// de SUPER_AUDIT_V1, demostrado otra vez por la revisión adversaria de RV15. +// ═══════════════════════════════════════════════════════════════ + +/// 1 OGC en unidades base (MIN_PROPOSAL_STAKE son 1.000 OGC). Los importes de los +/// tests de RV16 van en OGC reales: el quórum del mock es 1.000 OGC, y con unidades +/// base sueltas ningún override lo alcanzaría (me pasó al escribirlos). +const OGC: u128 = MIN_PROPOSAL_STAKE / 1_000; + +/// Council de 1 (Alice) y propuesta de Alice vetada en el acto (bloque 1). +fn propuesta_vetada() -> (ProposalId, u64) { + use crate::pallet::CouncilRole; + use frame_support::dispatch::RawOrigin; + assert_ok!(Governance::add_council_member( + RawOrigin::Root.into(), + alice_account(), + CouncilRole::Chair, + )); + let id = submit_default(alice_origin()); + assert_ok!(Governance::veto_proposal(alice_origin(), id)); + let cierre = crate::OverrideDeadline::::get(id).expect("el veto abre la ventana"); + (id, cierre) +} + +fn estado(id: ProposalId) -> ProposalStatus { + Proposals::::get(id).expect("propuesta").status +} + +/// C3: el bond que el veto devuelve no basta para revertirlo. El quórum (aquí 2.000 +/// OGC; 100M en el runtime) está por encima del bond (1.000) a propósito, y lo exige +/// `integrity_test`. La primera versión de este test usaba 800 OGC y pasaba porque +/// 800 no llegaba a un quórum IGUAL al bond: con el bond entero, revertía (revisión +/// de RV16). Ahora el bond entero no basta, y el doble sí, si nadie sostiene. +#[test] +fn el_bond_devuelto_no_basta_para_revertir_el_veto() { + for (revertir, revierte) in [(MIN_PROPOSAL_STAKE, false), (2 * MIN_PROPOSAL_STAKE, true)] { + new_test_ext().execute_with(|| { + let (id, cierre) = propuesta_vetada(); + assert_ok!(Governance::override_council_veto( + alice_origin(), + id, + revertir + )); + run_to_block(cierre + 1); + assert_ok!(Governance::finalize_override(bob_origin(), id)); + let aprobada = matches!(estado(id), ProposalStatus::Approved); + assert_eq!(aprobada, revierte, "el proponente revierte con {revertir}"); + }); + } +} + +/// La regla es MONÓTONA: sostener el veto nunca ayuda a revertirlo. La primera +/// versión de RV16 contaba sostener para el quórum y la revisión demostró que, con +/// Aye 9.000 / Nay 1.000, Charlie revertía el veto votando SOSTENER con 1.000, y lo +/// defendía no votando. Ahora: revertir 9.000 contra quórum 10.000 pasa sin nadie +/// enfrente, y cada OGC que sostiene solo sube el listón (2.250 aún no, 2.251 sí). +#[test] +fn sostener_el_veto_nunca_ayuda_a_revertirlo() { + let casos = [ + (0u128, true), + (1_000, true), + (2_000, true), + (2_250, true), + (2_251, false), + ]; + let mut anterior = true; + for (sostener, esperado) in casos { + let aprobada = new_test_ext().execute_with(|| { + use crate::pallet::CouncilRole; + use frame_support::dispatch::RawOrigin; + assert_ok!(Governance::add_council_member( + RawOrigin::Root.into(), + alice_account(), + CouncilRole::Chair, + )); + let id = submit_default(alice_origin()); + assert_ok!(Governance::cast_vote( + bob_origin(), + id, + VoteStance::Aye, + 9_000 * OGC + )); + assert_ok!(Governance::cast_vote( + charlie_origin(), + id, + VoteStance::Nay, + 1_000 * OGC + )); + assert_ok!(Governance::veto_proposal(alice_origin(), id)); + let cierre = crate::OverrideDeadline::::get(id).expect("ventana"); + assert_ok!(Governance::override_council_veto( + bob_origin(), + id, + 9_000 * OGC + )); + if sostener > 0 { + assert_ok!(Governance::uphold_council_veto( + charlie_origin(), + id, + sostener * OGC + )); + } + run_to_block(cierre + 1); + assert_ok!(Governance::finalize_override(alice_origin(), id)); + matches!(estado(id), ProposalStatus::Approved) + }); + assert_eq!(aprobada, esperado, "sostener {sostener} OGC"); + assert!( + anterior || !aprobada, + "sostener {sostener} OGC ayudó a revertir" + ); + anterior = aprobada; + } +} + +/// Quien quiere el veto puede DEFENDERLO, y el 4/5 se mide sobre la participación: +/// 4/5 exacto revierte; un poco menos lo sostiene. +#[test] +fn quien_voto_nay_puede_sostener_el_veto_y_el_umbral_es_4_de_5_exacto() { + for (sostener, revierte) in [(2_000 * OGC, true), (2_000 * OGC + 1, false)] { + new_test_ext().execute_with(|| { + let (id, cierre) = propuesta_vetada(); + // 8.000 OGC revertir (Bob) contra `sostener` (Charlie): 8.000/10.000 = 4/5 exacto. + assert_ok!(Governance::override_council_veto( + bob_origin(), + id, + 8_000 * OGC + )); + assert_ok!(Governance::uphold_council_veto( + charlie_origin(), + id, + sostener + )); + run_to_block(cierre + 1); + assert_ok!(Governance::finalize_override(alice_origin(), id)); + let aprobada = matches!(estado(id), ProposalStatus::Approved); + assert_eq!( + aprobada, revierte, + "revertir 8.000 contra sostener {sostener}" + ); + }); + } +} + +/// Un voto no voltea nada a mitad de ventana: el resultado se decide al cerrarla. +/// Antes, cruzar el umbral aprobaba en el acto, antes de que nadie pudiera sostener. +#[test] +fn el_override_se_decide_al_cerrar_la_ventana_no_al_cruzar_un_umbral() { + new_test_ext().execute_with(|| { + let (id, cierre) = propuesta_vetada(); + let saldo = Balances::free_balance(bob_account()); + assert_ok!(Governance::override_council_veto(bob_origin(), id, saldo)); + assert!( + matches!(estado(id), ProposalStatus::Rejected), + "100% y sigue vetada" + ); + + run_to_block(cierre); + assert_noop!( + Governance::finalize_override(alice_origin(), id), + Error::::OverrideWindowOpen + ); + run_to_block(cierre + 1); + assert_ok!(Governance::finalize_override(alice_origin(), id)); + assert!(matches!(estado(id), ProposalStatus::Approved)); + }); +} + +/// El quórum, fijado en el veto, es el MAYOR entre MinOverrideQuorum y la +/// participación del voto original contando Aye + Nay (la abstención NO cuenta: el +/// mismo OGC abstenido en muchas propuestas inflaría el quórum de todas sin aparecer +/// en ninguna ventana). Con Aye 1.000 + Nay 2.000 + Abstain 2.000, el quórum es +/// 3.000, y revertir solo, sin nadie que sostenga, necesita 4/5 de él: 2.400. +#[test] +fn el_quorum_es_aye_mas_nay_del_voto_original_y_se_fija_en_el_veto() { + for (revertir, revierte) in [(2_400 * OGC - 1, false), (2_400 * OGC, true)] { + new_test_ext().execute_with(|| { + use crate::pallet::CouncilRole; + use frame_support::dispatch::RawOrigin; + assert_ok!(Governance::add_council_member( + RawOrigin::Root.into(), + alice_account(), + CouncilRole::Chair, + )); + let id = submit_default(alice_origin()); + assert_ok!(Governance::cast_vote( + alice_origin(), + id, + VoteStance::Aye, + 1_000 * OGC + )); + assert_ok!(Governance::cast_vote( + bob_origin(), + id, + VoteStance::Nay, + 2_000 * OGC + )); + assert_ok!(Governance::cast_vote( + charlie_origin(), + id, + VoteStance::Abstain, + 2_000 * OGC + )); + assert_ok!(Governance::veto_proposal(alice_origin(), id)); + assert_eq!(crate::OverrideQuorum::::get(id), Some(3_000 * OGC)); + let cierre = crate::OverrideDeadline::::get(id).expect("ventana"); + assert_ok!(Governance::override_council_veto( + bob_origin(), + id, + revertir + )); + run_to_block(cierre + 1); + assert_ok!(Governance::finalize_override(alice_origin(), id)); + let aprobada = matches!(estado(id), ProposalStatus::Approved); + assert_eq!( + aprobada, revierte, + "revertir {revertir} con quórum 3.000 OGC" + ); + if !revierte { + System::assert_last_event(RuntimeEvent::Governance(Event::CouncilVetoUpheld { + id, + override_stake: revertir, + uphold_stake: 0, + quorum: 3_000 * OGC, + })); + } + }); + } +} + +/// El quórum que decide es el GUARDADO en el veto, no uno recalculado al final: un +/// upgrade que cambie el parámetro no mueve la meta de una ventana abierta. +#[test] +fn finalize_override_usa_el_quorum_fijado_en_el_veto() { + new_test_ext().execute_with(|| { + let (id, cierre) = propuesta_vetada(); + assert_eq!( + crate::OverrideQuorum::::get(id), + Some(2 * MIN_PROPOSAL_STAKE) + ); + assert_ok!(Governance::override_council_veto( + bob_origin(), + id, + MIN_PROPOSAL_STAKE + )); + // Simula un quórum guardado más bajo: si finalize recalculara, no lo vería. + crate::OverrideQuorum::::insert(id, MIN_PROPOSAL_STAKE); + run_to_block(cierre + 1); + assert_ok!(Governance::finalize_override(alice_origin(), id)); + assert!(matches!(estado(id), ProposalStatus::Approved)); + assert!(crate::OverrideQuorum::::get(id).is_none()); + }); +} + +/// Los votos del mismo lado se SUMAN, en los dos lados. +#[test] +fn los_votos_del_mismo_lado_se_suman() { + new_test_ext().execute_with(|| { + let (id, cierre) = propuesta_vetada(); + assert_ok!(Governance::override_council_veto( + bob_origin(), + id, + 4_000 * OGC + )); + assert_ok!(Governance::override_council_veto( + alice_origin(), + id, + 4_000 * OGC + )); + assert_ok!(Governance::uphold_council_veto( + charlie_origin(), + id, + 2_000 * OGC + )); + assert_eq!(crate::OverrideStakeTotal::::get(id), 8_000 * OGC); + run_to_block(cierre + 1); + assert_ok!(Governance::finalize_override(alice_origin(), id)); + assert!( + matches!(estado(id), ProposalStatus::Approved), + "8.000 de 10.000" + ); + }); + new_test_ext().execute_with(|| { + let (id, cierre) = propuesta_vetada(); + assert_ok!(Governance::override_council_veto( + bob_origin(), + id, + 8_000 * OGC + )); + assert_ok!(Governance::uphold_council_veto( + charlie_origin(), + id, + 1_000 * OGC + )); + assert_ok!(Governance::uphold_council_veto( + alice_origin(), + id, + 1_000 * OGC + 1 + )); + assert_eq!(crate::UpholdStakeTotal::::get(id), 2_000 * OGC + 1); + run_to_block(cierre + 1); + assert_ok!(Governance::finalize_override(alice_origin(), id)); + assert!( + matches!(estado(id), ProposalStatus::Rejected), + "8.000 de 10.000+1" + ); + }); +} + +/// Quien vota revertir no vuelve a votar en la ventana, ni en el mismo lado ni en el +/// otro: sin esto, el mismo OGC revertiría hasta MaxActiveVotes veces. +#[test] +fn quien_revierte_no_vuelve_a_votar_en_la_ventana() { + new_test_ext().execute_with(|| { + let (id, _) = propuesta_vetada(); + let saldo = Balances::free_balance(bob_account()); + assert_ok!(Governance::override_council_veto(bob_origin(), id, saldo)); + System::assert_last_event(RuntimeEvent::Governance(Event::OverrideVoteCast { + id, + voter: bob_account(), + uphold: false, + weight: saldo, + })); + assert_noop!( + Governance::override_council_veto(bob_origin(), id, saldo), + Error::::AlreadyOverrideVoted + ); + assert_noop!( + Governance::uphold_council_veto(bob_origin(), id, 1), + Error::::AlreadyOverrideVoted + ); + assert_eq!(crate::OverrideStakeTotal::::get(id), saldo); + assert_eq!(crate::VoteLocks::::get(bob_account()).len(), 1); + }); +} + +/// SOSTENER pasa por las mismas puertas que revertir: advisor, voto popular, +/// propuesta aún en votación, sin ventana, ventana cerrada y peso 0. +#[test] +fn sostener_pasa_por_las_mismas_puertas_que_revertir() { + new_test_ext().execute_with(|| { + use frame_support::dispatch::RawOrigin; + let (id, cierre) = propuesta_vetada(); + assert_noop!( + Governance::uphold_council_veto(charlie_origin(), id, 0), + Error::::ZeroWeightWindowVote + ); + assert_noop!( + Governance::override_council_veto(charlie_origin(), id, 0), + Error::::ZeroWeightWindowVote + ); + assert_ok!(Governance::mark_as_advisor( + RawOrigin::Root.into(), + charlie_account() + )); + assert_noop!( + Governance::uphold_council_veto(charlie_origin(), id, 1), + Error::::AdvisorMayNotVote + ); + // Propuesta todavía en votación. + let viva = propuesta_con_periodo(12, VOTING_PERIOD); + assert_noop!( + Governance::uphold_council_veto(bob_origin(), viva, 1), + Error::::ProposalNotRejected + ); + // Rechazada por voto popular: no es un veto. + run_to_block(fin_de(viva) + 1); + assert_ok!(Governance::finalize_proposal(bob_origin(), viva)); + assert_noop!( + Governance::uphold_council_veto(bob_origin(), viva, 1), + Error::::ProposalNotVetoRejected + ); + // Ventana cerrada. + run_to_block(cierre + 1); + assert_noop!( + Governance::uphold_council_veto(bob_origin(), id, 1), + Error::::OverrideWindowClosed + ); + }); + new_test_ext().execute_with(|| { + // Veto sin ventana registrada: falla cerrado también al sostener. + let id = submit_default(alice_origin()); + let mut p = Proposals::::get(id).expect("propuesta"); + p.status = ProposalStatus::Rejected; + Proposals::::insert(id, p); + crate::VetoRejected::::insert(id, ()); + assert_noop!( + Governance::uphold_council_veto(bob_origin(), id, 1), + Error::::OverrideWindowClosed + ); + }); +} + +/// Una cuenta vota una vez en la ventana, en un solo lado; el peso de sostener +/// también sale del saldo y queda congelado hasta el cierre. +#[test] +fn una_cuenta_vota_una_vez_en_la_ventana_y_sostener_tambien_congela() { + new_test_ext().execute_with(|| { + let (id, cierre) = propuesta_vetada(); + let saldo = Balances::free_balance(charlie_account()); + assert_noop!( + Governance::uphold_council_veto(charlie_origin(), id, saldo + 1), + Error::::VoteWeightExceedsBalance + ); + assert_ok!(Governance::uphold_council_veto(charlie_origin(), id, 500)); + assert_eq!(congelado_por_votos(&charlie_account()), 500); + assert_eq!( + crate::VoteLocks::::get(charlie_account())[0].until, + cierre + ); + System::assert_last_event(RuntimeEvent::Governance(Event::OverrideVoteCast { + id, + voter: charlie_account(), + uphold: true, + weight: 500, + })); + assert_noop!( + Governance::override_council_veto(charlie_origin(), id, 1), + Error::::AlreadyOverrideVoted + ); + assert_noop!( + Governance::uphold_council_veto(charlie_origin(), id, 1), + Error::::AlreadyOverrideVoted + ); + // Control positivo: otra cuenta sí vota. + assert_ok!(Governance::override_council_veto(bob_origin(), id, 1)); + }); +} + +/// El override se decide una sola vez, y solo sobre un veto. +#[test] +fn el_override_se_decide_una_sola_vez_y_solo_sobre_un_veto() { + new_test_ext().execute_with(|| { + let (id, cierre) = propuesta_vetada(); + run_to_block(cierre + 1); + assert_ok!(Governance::finalize_override(bob_origin(), id)); + // Decidido: no queda ventana ni quórum guardados (estado limpio, no basura). + assert!(crate::OverrideDeadline::::get(id).is_none()); + assert!(crate::OverrideQuorum::::get(id).is_none()); + assert_noop!( + Governance::finalize_override(bob_origin(), id), + Error::::OverrideNotPending + ); + // Una propuesta rechazada por VOTO POPULAR no tiene override que decidir. + let otra = propuesta_con_periodo(12, VOTING_PERIOD); + assert_ok!(Governance::cast_vote( + charlie_origin(), + otra, + VoteStance::Nay, + 10 + )); + run_to_block(fin_de(otra) + 1); + assert_ok!(Governance::finalize_proposal(bob_origin(), otra)); + assert_noop!( + Governance::finalize_override(bob_origin(), otra), + Error::::ProposalNotVetoRejected + ); + }); +} + +/// El veto solo cabe DURANTE el periodo de voto: antes podía llegar entre el fin +/// del periodo y el finalize, y ganaba quien llamara primero. +#[test] +fn el_veto_solo_cabe_durante_el_periodo_de_voto() { + new_test_ext().execute_with(|| { + use crate::pallet::CouncilRole; + use frame_support::dispatch::RawOrigin; + assert_ok!(Governance::add_council_member( + RawOrigin::Root.into(), + alice_account(), + CouncilRole::Chair, + )); + let tarde = submit_default(alice_origin()); + let a_tiempo = propuesta_con_periodo(12, VOTING_PERIOD); + run_to_block(fin_de(tarde) + 1); + assert_noop!( + Governance::veto_proposal(alice_origin(), tarde), + Error::::VotingPeriodClosed + ); + // Control positivo: en el último bloque de su periodo, sí. + run_to_block(fin_de(a_tiempo)); + assert_ok!(Governance::veto_proposal(alice_origin(), a_tiempo)); + assert!(matches!(estado(a_tiempo), ProposalStatus::Rejected)); + }); +} + +/// Una propuesta ORDINARIA dura al menos 14 días (PILAR_4 §6.6). La primera versión +/// de RV16 ponía 48 h, que es la vía de emergencia sin sus 5/7 endosos: cualquiera la +/// obtenía y elegía cuánto tiempo tenía el council para vetar (revisión de RV16). +#[test] +fn una_propuesta_ordinaria_dura_al_menos_14_dias() { + new_test_ext().execute_with(|| { + use crate::pallet::{ + DEFAULT_VOTING_PERIOD_BLOCKS, EMERGENCY_VOTING_PERIOD_BLOCKS, MIN_VOTING_PERIOD_BLOCKS, + }; + assert_eq!(MIN_VOTING_PERIOD_BLOCKS, DEFAULT_VOTING_PERIOD_BLOCKS); + assert_eq!(MIN_VOTING_PERIOD_BLOCKS, 14 * 24 * 600); + for corto in [ + 0u64, + EMERGENCY_VOTING_PERIOD_BLOCKS, + MIN_VOTING_PERIOD_BLOCKS - 1, + ] { + assert_noop!( + Governance::submit_proposal( + alice_origin(), + TITLE_HASH, + BODY_HASH, + corto, + MIN_PROPOSAL_STAKE, + ), + Error::::VotingPeriodTooShort + ); + } + // Control positivo: el mínimo exacto se acepta. + assert_ok!(Governance::submit_proposal( + alice_origin(), + TITLE_HASH, + BODY_HASH, + MIN_VOTING_PERIOD_BLOCKS, + MIN_PROPOSAL_STAKE, + )); + }); +} diff --git a/crates/origin-chain-governance/src/weights.rs b/crates/origin-chain-governance/src/weights.rs index e86182d..ea4f2c9 100644 --- a/crates/origin-chain-governance/src/weights.rs +++ b/crates/origin-chain-governance/src/weights.rs @@ -14,7 +14,7 @@ //! procedimiento completo y el hardware al que hay que creerle). //! //! GENERADO POR SUBSTRATE BENCHMARK CLI VERSION 51.0.0 -//! FECHA: 2026-09-24, STEPS: `50`, REPEAT: `20` +//! FECHA: 2026-10-07, STEPS: `50`, REPEAT: `20` //! HOSTNAME: `MacBookProM3Max.local`, CPU: `` //! WASM-EXECUTION: `Compiled`, DB CACHE: 1024 @@ -80,6 +80,10 @@ pub trait WeightInfo { fn endorse_emergency_proposal() -> Weight; /// Peso de `unlock_vote_funds` — MEDIDO sobre el WASM, no estimado a mano. fn unlock_vote_funds() -> Weight; + /// Peso de `uphold_council_veto` — MEDIDO sobre el WASM, no estimado a mano. + fn uphold_council_veto() -> Weight; + /// Peso de `finalize_override` — MEDIDO sobre el WASM, no estimado a mano. + fn finalize_override() -> Weight; } /// Weights parametrizados por el `DbWeight` real del runtime. @@ -105,8 +109,8 @@ impl WeightInfo for SubstrateWeight { // Proof size en bytes: // Medido: `107` // Estimado: `3722` - // Tiempo minimo de ejecucion: 54_000_000 picosegundos. - Weight::from_parts(55_000_000, 0) + // Tiempo minimo de ejecucion: 44_000_000 picosegundos. + Weight::from_parts(45_000_000, 0) .saturating_add(Weight::from_parts(0, 3722)) .saturating_add(T::DbWeight::get().reads(7_u64)) .saturating_add(T::DbWeight::get().writes(5_u64)) @@ -135,8 +139,8 @@ impl WeightInfo for SubstrateWeight { // Proof size en bytes: // Medido: `2320` // Estimado: `5338` - // Tiempo minimo de ejecucion: 46_000_000 picosegundos. - Weight::from_parts(48_000_000, 0) + // Tiempo minimo de ejecucion: 38_000_000 picosegundos. + Weight::from_parts(42_000_000, 0) .saturating_add(Weight::from_parts(0, 5338)) .saturating_add(T::DbWeight::get().reads(10_u64)) .saturating_add(T::DbWeight::get().writes(6_u64)) @@ -161,8 +165,8 @@ impl WeightInfo for SubstrateWeight { // Proof size en bytes: // Medido: `614` // Estimado: `6196` - // Tiempo minimo de ejecucion: 61_000_000 picosegundos. - Weight::from_parts(62_000_000, 0) + // Tiempo minimo de ejecucion: 52_000_000 picosegundos. + Weight::from_parts(59_000_000, 0) .saturating_add(Weight::from_parts(0, 6196)) .saturating_add(T::DbWeight::get().reads(9_u64)) .saturating_add(T::DbWeight::get().writes(6_u64)) @@ -183,8 +187,8 @@ impl WeightInfo for SubstrateWeight { // Proof size en bytes: // Medido: `25` // Estimado: `3554` - // Tiempo minimo de ejecucion: 12_000_000 picosegundos. - Weight::from_parts(12_000_000, 0) + // Tiempo minimo de ejecucion: 10_000_000 picosegundos. + Weight::from_parts(10_000_000, 0) .saturating_add(Weight::from_parts(0, 3554)) .saturating_add(T::DbWeight::get().reads(6_u64)) .saturating_add(T::DbWeight::get().writes(4_u64)) @@ -205,8 +209,8 @@ impl WeightInfo for SubstrateWeight { // Proof size en bytes: // Medido: `153` // Estimado: `3554` - // Tiempo minimo de ejecucion: 13_000_000 picosegundos. - Weight::from_parts(14_000_000, 0) + // Tiempo minimo de ejecucion: 11_000_000 picosegundos. + Weight::from_parts(12_000_000, 0) .saturating_add(Weight::from_parts(0, 3554)) .saturating_add(T::DbWeight::get().reads(6_u64)) .saturating_add(T::DbWeight::get().writes(4_u64)) @@ -215,12 +219,12 @@ impl WeightInfo for SubstrateWeight { /// Proof: `Governance::Council` (`max_values`: None, `max_size`: Some(89), added: 2564, mode: `MaxEncodedLen`) /// Storage: `Governance::Proposals` (r:1 w:1) /// Proof: `Governance::Proposals` (`max_values`: None, `max_size`: Some(257), added: 2732, mode: `MaxEncodedLen`) + /// Storage: `System::Number` (r:1 w:0) + /// Proof: `System::Number` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `Governance::CouncilSize` (r:1 w:0) /// Proof: `Governance::CouncilSize` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `Governance::VetoVotes` (r:6 w:1) /// Proof: `Governance::VetoVotes` (`max_values`: None, `max_size`: Some(96), added: 2571, mode: `MaxEncodedLen`) - /// Storage: `System::Number` (r:1 w:0) - /// Proof: `System::Number` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `Governance::ProposalBond` (r:1 w:1) /// Proof: `Governance::ProposalBond` (`max_values`: None, `max_size`: Some(96), added: 2571, mode: `MaxEncodedLen`) /// Storage: `Balances::TotalIssuance` (r:1 w:0) @@ -235,6 +239,8 @@ impl WeightInfo for SubstrateWeight { /// Proof: `System::Events` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) /// Storage: `Governance::OverrideDeadline` (r:0 w:1) /// Proof: `Governance::OverrideDeadline` (`max_values`: None, `max_size`: Some(56), added: 2531, mode: `MaxEncodedLen`) + /// Storage: `Governance::OverrideQuorum` (r:0 w:1) + /// Proof: `Governance::OverrideQuorum` (`max_values`: None, `max_size`: Some(64), added: 2539, mode: `MaxEncodedLen`) /// Storage: `Governance::VetoRejected` (r:0 w:1) /// Proof: `Governance::VetoRejected` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) /// Storage: `Governance::VetoCount` (r:0 w:1) @@ -243,11 +249,11 @@ impl WeightInfo for SubstrateWeight { // Proof size en bytes: // Medido: `1440` // Estimado: `16416` - // Tiempo minimo de ejecucion: 109_000_000 picosegundos. - Weight::from_parts(112_000_000, 0) + // Tiempo minimo de ejecucion: 91_000_000 picosegundos. + Weight::from_parts(95_000_000, 0) .saturating_add(Weight::from_parts(0, 16416)) .saturating_add(T::DbWeight::get().reads(21_u64)) - .saturating_add(T::DbWeight::get().writes(10_u64)) + .saturating_add(T::DbWeight::get().writes(11_u64)) } /// Storage: `Governance::AdvisorFlags` (r:1 w:1) /// Proof: `Governance::AdvisorFlags` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) @@ -263,8 +269,8 @@ impl WeightInfo for SubstrateWeight { // Proof size en bytes: // Medido: `25` // Estimado: `3513` - // Tiempo minimo de ejecucion: 9_000_000 picosegundos. - Weight::from_parts(10_000_000, 0) + // Tiempo minimo de ejecucion: 8_000_000 picosegundos. + Weight::from_parts(9_000_000, 0) .saturating_add(Weight::from_parts(0, 3513)) .saturating_add(T::DbWeight::get().reads(5_u64)) .saturating_add(T::DbWeight::get().writes(3_u64)) @@ -283,13 +289,13 @@ impl WeightInfo for SubstrateWeight { // Proof size en bytes: // Medido: `98` // Estimado: `3513` - // Tiempo minimo de ejecucion: 11_000_000 picosegundos. - Weight::from_parts(11_000_000, 0) + // Tiempo minimo de ejecucion: 10_000_000 picosegundos. + Weight::from_parts(10_000_000, 0) .saturating_add(Weight::from_parts(0, 3513)) .saturating_add(T::DbWeight::get().reads(5_u64)) .saturating_add(T::DbWeight::get().writes(3_u64)) } - /// Storage: `Governance::Proposals` (r:1 w:1) + /// Storage: `Governance::Proposals` (r:1 w:0) /// Proof: `Governance::Proposals` (`max_values`: None, `max_size`: Some(257), added: 2732, mode: `MaxEncodedLen`) /// Storage: `Governance::VetoRejected` (r:1 w:0) /// Proof: `Governance::VetoRejected` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) @@ -297,6 +303,8 @@ impl WeightInfo for SubstrateWeight { /// Proof: `Governance::AdvisorFlags` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) /// Storage: `Governance::OverrideVotes` (r:1 w:1) /// Proof: `Governance::OverrideVotes` (`max_values`: None, `max_size`: Some(112), added: 2587, mode: `MaxEncodedLen`) + /// Storage: `Governance::UpholdVotes` (r:1 w:0) + /// Proof: `Governance::UpholdVotes` (`max_values`: None, `max_size`: Some(112), added: 2587, mode: `MaxEncodedLen`) /// Storage: `Governance::OverrideDeadline` (r:1 w:0) /// Proof: `Governance::OverrideDeadline` (`max_values`: None, `max_size`: Some(56), added: 2531, mode: `MaxEncodedLen`) /// Storage: `System::Number` (r:1 w:0) @@ -319,11 +327,11 @@ impl WeightInfo for SubstrateWeight { // Proof size en bytes: // Medido: `2468` // Estimado: `5338` - // Tiempo minimo de ejecucion: 56_000_000 picosegundos. - Weight::from_parts(58_000_000, 0) + // Tiempo minimo de ejecucion: 47_000_000 picosegundos. + Weight::from_parts(49_000_000, 0) .saturating_add(Weight::from_parts(0, 5338)) - .saturating_add(T::DbWeight::get().reads(13_u64)) - .saturating_add(T::DbWeight::get().writes(7_u64)) + .saturating_add(T::DbWeight::get().reads(14_u64)) + .saturating_add(T::DbWeight::get().writes(6_u64)) } /// Storage: `Governance::Council` (r:1 w:0) /// Proof: `Governance::Council` (`max_values`: None, `max_size`: Some(89), added: 2564, mode: `MaxEncodedLen`) @@ -351,8 +359,8 @@ impl WeightInfo for SubstrateWeight { // Proof size en bytes: // Medido: `353` // Estimado: `6132` - // Tiempo minimo de ejecucion: 68_000_000 picosegundos. - Weight::from_parts(69_000_000, 0) + // Tiempo minimo de ejecucion: 58_000_000 picosegundos. + Weight::from_parts(60_000_000, 0) .saturating_add(Weight::from_parts(0, 6132)) .saturating_add(T::DbWeight::get().reads(11_u64)) .saturating_add(T::DbWeight::get().writes(6_u64)) @@ -377,8 +385,8 @@ impl WeightInfo for SubstrateWeight { // Proof size en bytes: // Medido: `494` // Estimado: `6196` - // Tiempo minimo de ejecucion: 56_000_000 picosegundos. - Weight::from_parts(58_000_000, 0) + // Tiempo minimo de ejecucion: 48_000_000 picosegundos. + Weight::from_parts(49_000_000, 0) .saturating_add(Weight::from_parts(0, 6196)) .saturating_add(T::DbWeight::get().reads(9_u64)) .saturating_add(T::DbWeight::get().writes(5_u64)) @@ -399,8 +407,8 @@ impl WeightInfo for SubstrateWeight { // Proof size en bytes: // Medido: `101` // Estimado: `3593` - // Tiempo minimo de ejecucion: 43_000_000 picosegundos. - Weight::from_parts(43_000_000, 0) + // Tiempo minimo de ejecucion: 37_000_000 picosegundos. + Weight::from_parts(38_000_000, 0) .saturating_add(Weight::from_parts(0, 3593)) .saturating_add(T::DbWeight::get().reads(6_u64)) .saturating_add(T::DbWeight::get().writes(3_u64)) @@ -423,8 +431,8 @@ impl WeightInfo for SubstrateWeight { // Proof size en bytes: // Medido: `153` // Estimado: `6132` - // Tiempo minimo de ejecucion: 22_000_000 picosegundos. - Weight::from_parts(23_000_000, 0) + // Tiempo minimo de ejecucion: 19_000_000 picosegundos. + Weight::from_parts(20_000_000, 0) .saturating_add(Weight::from_parts(0, 6132)) .saturating_add(T::DbWeight::get().reads(8_u64)) .saturating_add(T::DbWeight::get().writes(3_u64)) @@ -449,100 +457,180 @@ impl WeightInfo for SubstrateWeight { // Proof size en bytes: // Medido: `2197` // Estimado: `5338` - // Tiempo minimo de ejecucion: 34_000_000 picosegundos. - Weight::from_parts(40_000_000, 0) + // Tiempo minimo de ejecucion: 29_000_000 picosegundos. + Weight::from_parts(30_000_000, 0) .saturating_add(Weight::from_parts(0, 5338)) .saturating_add(T::DbWeight::get().reads(8_u64)) .saturating_add(T::DbWeight::get().writes(5_u64)) } + /// Storage: `Governance::Proposals` (r:1 w:0) + /// Proof: `Governance::Proposals` (`max_values`: None, `max_size`: Some(257), added: 2732, mode: `MaxEncodedLen`) + /// Storage: `Governance::VetoRejected` (r:1 w:0) + /// Proof: `Governance::VetoRejected` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) + /// Storage: `Governance::AdvisorFlags` (r:1 w:0) + /// Proof: `Governance::AdvisorFlags` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) + /// Storage: `Governance::OverrideVotes` (r:1 w:0) + /// Proof: `Governance::OverrideVotes` (`max_values`: None, `max_size`: Some(112), added: 2587, mode: `MaxEncodedLen`) + /// Storage: `Governance::UpholdVotes` (r:1 w:1) + /// Proof: `Governance::UpholdVotes` (`max_values`: None, `max_size`: Some(112), added: 2587, mode: `MaxEncodedLen`) + /// Storage: `Governance::OverrideDeadline` (r:1 w:0) + /// Proof: `Governance::OverrideDeadline` (`max_values`: None, `max_size`: Some(56), added: 2531, mode: `MaxEncodedLen`) + /// Storage: `System::Number` (r:1 w:0) + /// Proof: `System::Number` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Governance::VoteLocks` (r:1 w:1) + /// Proof: `Governance::VoteLocks` (`max_values`: None, `max_size`: Some(1873), added: 4348, mode: `MaxEncodedLen`) + /// Storage: `Balances::Freezes` (r:1 w:1) + /// Proof: `Balances::Freezes` (`max_values`: None, `max_size`: Some(67), added: 2542, mode: `MaxEncodedLen`) + /// Storage: `Balances::Locks` (r:1 w:0) + /// Proof: `Balances::Locks` (`max_values`: None, `max_size`: Some(1299), added: 3774, mode: `MaxEncodedLen`) + /// Storage: `System::ExecutionPhase` (r:1 w:0) + /// Proof: `System::ExecutionPhase` (`max_values`: Some(1), `max_size`: Some(5), added: 500, mode: `MaxEncodedLen`) + /// Storage: `System::EventCount` (r:1 w:1) + /// Proof: `System::EventCount` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `System::Events` (r:1 w:1) + /// Proof: `System::Events` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `Governance::UpholdStakeTotal` (r:1 w:1) + /// Proof: `Governance::UpholdStakeTotal` (`max_values`: None, `max_size`: Some(64), added: 2539, mode: `MaxEncodedLen`) + fn uphold_council_veto() -> Weight { + // Proof size en bytes: + // Medido: `2468` + // Estimado: `5338` + // Tiempo minimo de ejecucion: 47_000_000 picosegundos. + Weight::from_parts(50_000_000, 0) + .saturating_add(Weight::from_parts(0, 5338)) + .saturating_add(T::DbWeight::get().reads(14_u64)) + .saturating_add(T::DbWeight::get().writes(6_u64)) + } + /// Storage: `Governance::Proposals` (r:1 w:1) + /// Proof: `Governance::Proposals` (`max_values`: None, `max_size`: Some(257), added: 2732, mode: `MaxEncodedLen`) + /// Storage: `Governance::VetoRejected` (r:1 w:0) + /// Proof: `Governance::VetoRejected` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) + /// Storage: `Governance::OverrideDeadline` (r:1 w:1) + /// Proof: `Governance::OverrideDeadline` (`max_values`: None, `max_size`: Some(56), added: 2531, mode: `MaxEncodedLen`) + /// Storage: `System::Number` (r:1 w:0) + /// Proof: `System::Number` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Governance::OverrideStakeTotal` (r:1 w:0) + /// Proof: `Governance::OverrideStakeTotal` (`max_values`: None, `max_size`: Some(64), added: 2539, mode: `MaxEncodedLen`) + /// Storage: `Governance::UpholdStakeTotal` (r:1 w:0) + /// Proof: `Governance::UpholdStakeTotal` (`max_values`: None, `max_size`: Some(64), added: 2539, mode: `MaxEncodedLen`) + /// Storage: `Governance::OverrideQuorum` (r:1 w:1) + /// Proof: `Governance::OverrideQuorum` (`max_values`: None, `max_size`: Some(64), added: 2539, mode: `MaxEncodedLen`) + /// Storage: `System::ExecutionPhase` (r:1 w:0) + /// Proof: `System::ExecutionPhase` (`max_values`: Some(1), `max_size`: Some(5), added: 500, mode: `MaxEncodedLen`) + /// Storage: `System::EventCount` (r:1 w:1) + /// Proof: `System::EventCount` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `System::Events` (r:1 w:1) + /// Proof: `System::Events` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn finalize_override() -> Weight { + // Proof size en bytes: + // Medido: `636` + // Estimado: `3722` + // Tiempo minimo de ejecucion: 25_000_000 picosegundos. + Weight::from_parts(26_000_000, 0) + .saturating_add(Weight::from_parts(0, 3722)) + .saturating_add(T::DbWeight::get().reads(10_u64)) + .saturating_add(T::DbWeight::get().writes(5_u64)) + } } /// Impl para mocks y tests — `RocksDbWeight` directo, sin necesitar `T`. impl WeightInfo for () { fn submit_proposal() -> Weight { - Weight::from_parts(55_000_000, 0) + Weight::from_parts(45_000_000, 0) .saturating_add(Weight::from_parts(0, 3722)) .saturating_add(RocksDbWeight::get().reads(7_u64)) .saturating_add(RocksDbWeight::get().writes(5_u64)) } fn cast_vote() -> Weight { - Weight::from_parts(48_000_000, 0) + Weight::from_parts(42_000_000, 0) .saturating_add(Weight::from_parts(0, 5338)) .saturating_add(RocksDbWeight::get().reads(10_u64)) .saturating_add(RocksDbWeight::get().writes(6_u64)) } fn finalize_proposal() -> Weight { - Weight::from_parts(62_000_000, 0) + Weight::from_parts(59_000_000, 0) .saturating_add(Weight::from_parts(0, 6196)) .saturating_add(RocksDbWeight::get().reads(9_u64)) .saturating_add(RocksDbWeight::get().writes(6_u64)) } fn add_council_member() -> Weight { - Weight::from_parts(12_000_000, 0) + Weight::from_parts(10_000_000, 0) .saturating_add(Weight::from_parts(0, 3554)) .saturating_add(RocksDbWeight::get().reads(6_u64)) .saturating_add(RocksDbWeight::get().writes(4_u64)) } fn remove_council_member() -> Weight { - Weight::from_parts(14_000_000, 0) + Weight::from_parts(12_000_000, 0) .saturating_add(Weight::from_parts(0, 3554)) .saturating_add(RocksDbWeight::get().reads(6_u64)) .saturating_add(RocksDbWeight::get().writes(4_u64)) } fn veto_proposal() -> Weight { - Weight::from_parts(112_000_000, 0) + Weight::from_parts(95_000_000, 0) .saturating_add(Weight::from_parts(0, 16416)) .saturating_add(RocksDbWeight::get().reads(21_u64)) - .saturating_add(RocksDbWeight::get().writes(10_u64)) + .saturating_add(RocksDbWeight::get().writes(11_u64)) } fn mark_as_advisor() -> Weight { - Weight::from_parts(10_000_000, 0) + Weight::from_parts(9_000_000, 0) .saturating_add(Weight::from_parts(0, 3513)) .saturating_add(RocksDbWeight::get().reads(5_u64)) .saturating_add(RocksDbWeight::get().writes(3_u64)) } fn unmark_as_advisor() -> Weight { - Weight::from_parts(11_000_000, 0) + Weight::from_parts(10_000_000, 0) .saturating_add(Weight::from_parts(0, 3513)) .saturating_add(RocksDbWeight::get().reads(5_u64)) .saturating_add(RocksDbWeight::get().writes(3_u64)) } fn override_council_veto() -> Weight { - Weight::from_parts(58_000_000, 0) + Weight::from_parts(49_000_000, 0) .saturating_add(Weight::from_parts(0, 5338)) - .saturating_add(RocksDbWeight::get().reads(13_u64)) - .saturating_add(RocksDbWeight::get().writes(7_u64)) + .saturating_add(RocksDbWeight::get().reads(14_u64)) + .saturating_add(RocksDbWeight::get().writes(6_u64)) } fn submit_emergency_proposal() -> Weight { - Weight::from_parts(69_000_000, 0) + Weight::from_parts(60_000_000, 0) .saturating_add(Weight::from_parts(0, 6132)) .saturating_add(RocksDbWeight::get().reads(11_u64)) .saturating_add(RocksDbWeight::get().writes(6_u64)) } fn treasury_disbursement() -> Weight { - Weight::from_parts(58_000_000, 0) + Weight::from_parts(49_000_000, 0) .saturating_add(Weight::from_parts(0, 6196)) .saturating_add(RocksDbWeight::get().reads(9_u64)) .saturating_add(RocksDbWeight::get().writes(5_u64)) } fn treasury_deposit() -> Weight { - Weight::from_parts(43_000_000, 0) + Weight::from_parts(38_000_000, 0) .saturating_add(Weight::from_parts(0, 3593)) .saturating_add(RocksDbWeight::get().reads(6_u64)) .saturating_add(RocksDbWeight::get().writes(3_u64)) } fn endorse_emergency_proposal() -> Weight { - Weight::from_parts(23_000_000, 0) + Weight::from_parts(20_000_000, 0) .saturating_add(Weight::from_parts(0, 6132)) .saturating_add(RocksDbWeight::get().reads(8_u64)) .saturating_add(RocksDbWeight::get().writes(3_u64)) } fn unlock_vote_funds() -> Weight { - Weight::from_parts(40_000_000, 0) + Weight::from_parts(30_000_000, 0) .saturating_add(Weight::from_parts(0, 5338)) .saturating_add(RocksDbWeight::get().reads(8_u64)) .saturating_add(RocksDbWeight::get().writes(5_u64)) } + fn uphold_council_veto() -> Weight { + Weight::from_parts(50_000_000, 0) + .saturating_add(Weight::from_parts(0, 5338)) + .saturating_add(RocksDbWeight::get().reads(14_u64)) + .saturating_add(RocksDbWeight::get().writes(6_u64)) + } + fn finalize_override() -> Weight { + Weight::from_parts(26_000_000, 0) + .saturating_add(Weight::from_parts(0, 3722)) + .saturating_add(RocksDbWeight::get().reads(10_u64)) + .saturating_add(RocksDbWeight::get().writes(5_u64)) + } } #[cfg(test)] @@ -584,6 +672,8 @@ mod guard { ("treasury_deposit", <() as WeightInfo>::treasury_deposit()), ("endorse_emergency_proposal", <() as WeightInfo>::endorse_emergency_proposal()), ("unlock_vote_funds", <() as WeightInfo>::unlock_vote_funds()), + ("uphold_council_veto", <() as WeightInfo>::uphold_council_veto()), + ("finalize_override", <() as WeightInfo>::finalize_override()), ]; for (nombre, w) in casos { assert!( diff --git a/crates/origin-chain-runtime/src/configs.rs b/crates/origin-chain-runtime/src/configs.rs index c0ca154..28e2e54 100644 --- a/crates/origin-chain-runtime/src/configs.rs +++ b/crates/origin-chain-runtime/src/configs.rs @@ -536,6 +536,11 @@ impl origin_chain_governance::Config for Runtime { type RuntimeFreezeReason = RuntimeFreezeReason; // RV15: votos VIVOS simultáneos por cuenta (los vencidos se descartan solos). type MaxActiveVotes = frame_support::traits::ConstU32<32>; + // RV16: participación mínima para revertir un veto del council: 100M OGC, el 10% + // del suministro que ratifica PILAR_4 §6.6 como quórum («10% quorum»). Es un + // parámetro de la constitución del DAO y lo fija David; mientras, este. Lo pinna + // `quorum_de_override_del_runtime` (genesis_config_presets.rs). + type MinOverrideQuorum = frame_support::traits::ConstU128<{ 100_000_000 * OGC }>; type TreasuryPalletId = GovernanceTreasuryPalletId; // Weights INTERIM hand-derived DB-aware (ver weights.rs). Regenerable en HW. type WeightInfo = origin_chain_governance::weights::SubstrateWeight; diff --git a/crates/origin-chain-runtime/src/genesis_config_presets.rs b/crates/origin-chain-runtime/src/genesis_config_presets.rs index 4f94329..7999f00 100644 --- a/crates/origin-chain-runtime/src/genesis_config_presets.rs +++ b/crates/origin-chain-runtime/src/genesis_config_presets.rs @@ -615,7 +615,7 @@ mod tests { use crate::{Balances, Governance, RuntimeFreezeReason, RuntimeOrigin, System}; use frame_support::assert_ok; use frame_support::traits::fungible::{Inspect, InspectFreeze}; - use origin_chain_governance::{FreezeReason, MIN_PROPOSAL_STAKE}; + use origin_chain_governance::{FreezeReason, MIN_PROPOSAL_STAKE, MIN_VOTING_PERIOD_BLOCKS}; use origin_chain_types::VoteStance; let json = get_preset(&PresetId::from(PRESET_LOCAL_TESTNET)).expect("preset"); @@ -628,7 +628,7 @@ mod tests { let bob = account_from_sr25519(BOB_SR25519); let charlie = account_from_sr25519(CHARLIE_SR25519); - let (titulo, cuerpo, periodo) = ([1u8; 32], [2u8; 32], 100u64); + let (titulo, cuerpo, periodo) = ([1u8; 32], [2u8; 32], MIN_VOTING_PERIOD_BLOCKS); assert_ok!(Governance::submit_proposal( RuntimeOrigin::signed(alice.clone()), titulo, @@ -676,4 +676,22 @@ mod tests { )); }); } + + /// RV16: el quórum de override del runtime es el 10% del suministro que + /// ratifica PILAR_4 §6.6 (100M OGC). Sin este test, un typo como `{ 100_000_000 }` + /// sin `* OGC` (10^-10 OGC) o `ConstU128<0>` pasaba toda la suite (revisión de + /// RV16). `integrity_test` exige además que supere el bond de una propuesta. + #[test] + fn quorum_de_override_del_runtime() { + use frame_support::traits::Get; + let quorum: u128 = + <::MinOverrideQuorum as Get>::get(); + assert_eq!(quorum, 100_000_000 * crate::configs::OGC); + assert_eq!( + quorum, + crate::configs::OGC * 1_000_000_000 / 10, + "10% del suministro" + ); + assert!(quorum > origin_chain_governance::MIN_PROPOSAL_STAKE); + } } diff --git a/crates/origin-chain-runtime/src/lib.rs b/crates/origin-chain-runtime/src/lib.rs index cb32739..8295b5b 100644 --- a/crates/origin-chain-runtime/src/lib.rs +++ b/crates/origin-chain-runtime/src/lib.rs @@ -1019,7 +1019,7 @@ pub const VERSION: sp_version::RuntimeVersion = sp_version::RuntimeVersion { spec_name: alloc::borrow::Cow::Borrowed("origin-chain"), impl_name: alloc::borrow::Cow::Borrowed("origin-chain-charon"), authoring_version: 1, - spec_version: 15, + spec_version: 16, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1, @@ -1087,7 +1087,7 @@ mod tests { /// mismo commit en que la version subio a 12. #[test] fn spec_version_vigente_y_transaction_version_intacta() { - assert_eq!(RUNTIME_VERSION, 15); + assert_eq!(RUNTIME_VERSION, 16); assert_eq!(TRANSACTION_VERSION, 1); }