diff --git a/governor/src/lib.rs b/governor/src/lib.rs index 7d41c62..eae9cad 100644 --- a/governor/src/lib.rs +++ b/governor/src/lib.rs @@ -1,25 +1,3 @@ -//! Stake-weighted Governor for protocol parameter changes. -//! -//! Voting power is derived from solver bond plus delegated stake recorded in -//! `solver_registry` checkpoints. Power is snapshotted at proposal creation so -//! stake acquired after the snapshot cannot influence an in-flight vote -//! (anti-flash-stake). Passed proposals are queued into the -//! `TimelockController` and executed after the timelock delay. -//! -//! ## Voting power source (v1) -//! -//! The governor maintains its own checkpoint history per account, fed by an -//! explicit `sync_power` call. `sync_power` reads the account's current bond -//! from `solver_registry` (the v1 source of truth) and appends a `Checkpoint` -//! at the current ledger. This keeps the governor self-contained (no -//! cross-contract read at vote time) while still deriving power from the -//! registry's bond data. -//! -//! Delegation (bond + delegated stake) is a documented follow-up and is out of -//! scope for v1: only the account's own bond contributes to its power. - -#![no_std] - use soroban_sdk::{ contract, contracterror, contractimpl, contracttype, symbol_short, Address, BytesN, Env, IntoVal, Symbol, Val, Vec, @@ -114,12 +92,14 @@ pub enum GovernorError { AlreadyExecuted = 13, ProposalCanceled = 14, SelfTargetForbidden = 15, - InvalidVotingDelay = 16, - InvalidVotingPeriod = 17, - InvalidProposalThreshold = 18, - InvalidQuorum = 19, - Unauthorized = 20, - NotAuthorized = 21, + VoteTallyOverflow = 16, + DeadlineOverflow = 17, + InvalidVotingDelay = 18, + InvalidVotingPeriod = 19, + InvalidProposalThreshold = 20, + InvalidQuorum = 21, + Unauthorized = 22, + NotAuthorized = 23, } #[contracttype] @@ -146,13 +126,23 @@ pub struct Governor; #[contractimpl] impl Governor { /// One-time configuration of the governor. - pub fn initialize(env: Env, config: GovernorConfig) -> Result<(), GovernorError> { + /// + /// `admin` must authorize the call, so a freshly-deployed governor cannot + /// be front-run and configured by an arbitrary party. The `admin` address + /// is recorded so the deployment authority is auditable on-chain. + pub fn initialize( + env: Env, + admin: Address, + config: GovernorConfig, + ) -> Result<(), GovernorError> { + admin.require_auth(); let key = Symbol::new(&env, "config"); if env.storage().instance().has(&key) { return Err(GovernorError::AlreadyInitialized); } Self::validate_config(&config)?; env.storage().instance().set(&key, &config); + env.storage().instance().set(&Symbol::new(&env, "admin"), &admin); env.storage().instance().set(&Symbol::new(&env, "count"), &0u32); env.events().publish( (symbol_short!("init"),), @@ -243,7 +233,7 @@ impl Governor { let proposal = Proposal { proposer, snapshot, - deadline: snapshot + config.voting_delay + config.voting_period, + deadline: Self::deadline(&env, snapshot, &config)?, eta: 0, for_votes: 0, against_votes: 0, @@ -310,7 +300,11 @@ impl Governor { return Err(GovernorError::ProposalCanceled); } let now = env.ledger().sequence(); - if now < proposal.snapshot + config.voting_delay { + let voting_start = proposal + .snapshot + .checked_add(config.voting_delay) + .ok_or(GovernorError::DeadlineOverflow)?; + if now < voting_start { return Err(GovernorError::NotActive); } if now > proposal.deadline { @@ -328,9 +322,24 @@ impl Governor { } match support { - VoteType::For => proposal.for_votes += power, - VoteType::Against => proposal.against_votes += power, - VoteType::Abstain => proposal.abstain_votes += power, + VoteType::For => { + proposal.for_votes = proposal + .for_votes + .checked_add(power) + .ok_or(GovernorError::VoteTallyOverflow)?; + } + VoteType::Against => { + proposal.against_votes = proposal + .against_votes + .checked_add(power) + .ok_or(GovernorError::VoteTallyOverflow)?; + } + VoteType::Abstain => { + proposal.abstain_votes = proposal + .abstain_votes + .checked_add(power) + .ok_or(GovernorError::VoteTallyOverflow)?; + } } env.storage().persistent().set(&vote_key, &power); env.storage().persistent().set(&Self::proposal_key(&env, proposal_id), &proposal); @@ -617,6 +626,15 @@ impl Governor { } } + /// Compute a proposal's deadline, guarding against ledger overflow. + fn deadline(env: &Env, snapshot: u32, config: &GovernorConfig) -> Result { + let _ = env; + snapshot + .checked_add(config.voting_delay) + .and_then(|v| v.checked_add(config.voting_period)) + .ok_or(GovernorError::DeadlineOverflow) + } + fn proposal_key(env: &Env, proposal_id: u32) -> (Symbol, u32) { (Symbol::new(env, "proposal"), proposal_id) } diff --git a/treasury_streams/src/lib.rs b/treasury_streams/src/lib.rs index 995b216..7b43252 100644 --- a/treasury_streams/src/lib.rs +++ b/treasury_streams/src/lib.rs @@ -85,10 +85,14 @@ pub struct TreasuryStreams; #[contractimpl] impl TreasuryStreams { /// One-time initialization setting the governor (timelock/governor) address. + /// + /// The `governor` must authorize this call so that an attacker cannot + /// front-run initialization and install themselves as governor. pub fn initialize(env: Env, governor: Address) -> Result<(), Error> { if env.storage().instance().has(&DataKey::Governor) { return Err(Error::AlreadyInitialized); } + governor.require_auth(); env.storage().instance().set(&DataKey::Governor, &governor); env.storage().instance().set(&DataKey::NextId, &0u64); env.storage().instance().set(&DataKey::Paused, &false);