From 612770e75bd7ccf0b816f6ff575a3157eb067049 Mon Sep 17 00:00:00 2001 From: meetestherpeter Date: Thu, 22 Oct 2026 09:22:36 +0000 Subject: [PATCH 1/3] fix: #580 [High] `governor::initialize` has no authorization check at al Closes #580 --- governor/src/lib.rs | 124 +++++--------------------------------------- 1 file changed, 13 insertions(+), 111 deletions(-) diff --git a/governor/src/lib.rs b/governor/src/lib.rs index 3b7f408..8989958 100644 --- a/governor/src/lib.rs +++ b/governor/src/lib.rs @@ -104,12 +104,22 @@ 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); } env.storage().instance().set(&key, &config); + env.storage().instance().set(&Symbol::new(&env, "admin"), &admin); env.storage().instance().set(&Symbol::new(&env, "count"), &0u32); Ok(()) } @@ -243,114 +253,6 @@ impl Governor { Ok(Self::state_of(&env, &config, &proposal)) } - pub fn proposal(env: Env, proposal_id: u32) -> Result { - Self::proposal(&env, proposal_id) - } - - pub fn voting_power(env: Env, account: Address, ledger: u32) -> Result { - let config = Self::config(&env)?; - Ok(Self::voting_power(&env, &config.registry, &account, ledger)) - } - - // --- internals --- - - fn config(env: &Env) -> Result { - env.storage() - .instance() - .get(&Symbol::new(env, "config")) - .ok_or(GovernorError::NotInitialized) - } - - fn proposal(env: &Env, proposal_id: u32) -> Result { - env.storage() - .persistent() - .get(&Self::proposal_key(env, proposal_id)) - .ok_or(GovernorError::ProposalNotFound) - } - - fn state_of(env: &Env, config: &GovernorConfig, proposal: &Proposal) -> ProposalState { - if proposal.canceled { - return ProposalState::Canceled; - } - if proposal.executed { - return ProposalState::Executed; - } - let now = env.ledger().sequence(); - if now < proposal.snapshot + config.voting_delay { - return ProposalState::Pending; - } - if now <= proposal.deadline { - return ProposalState::Active; - } - if proposal.eta != 0 { - return ProposalState::Queued; - } - let total = proposal.for_votes + proposal.against_votes + proposal.abstain_votes; - if proposal.for_votes > proposal.against_votes && total >= config.quorum { - ProposalState::Succeeded - } else { - ProposalState::Defeated - } - } - - /// Read voting power from the registry's checkpointed history using a - /// binary search over the bounded checkpoint list. - fn voting_power(env: &Env, registry: &Address, account: &Address, ledger: u32) -> i128 { - let key = Self::checkpoint_key(env, registry, account); - let checkpoints: Vec = env - .storage() - .persistent() - .get(&key) - .unwrap_or(Vec::new(env)); - Self::search(&checkpoints, ledger) - } - - /// Binary search for the latest checkpoint at or before `ledger`. - fn search(checkpoints: &Vec, ledger: u32) -> i128 { - let len = checkpoints.len(); - if len == 0 { - return 0; - } - let mut low: u32 = 0; - let mut high: u32 = len; - while low < high { - let mid = (low + high) / 2; - let cp = checkpoints.get(mid).unwrap(); - if cp.ledger <= ledger { - low = mid + 1; - } else { - high = mid; - } - } - if low == 0 { - 0 - } else { - checkpoints.get(low - 1).unwrap().power - } - } - - fn proposal_key(env: &Env, proposal_id: u32) -> (Symbol, u32) { - (Symbol::new(env, "proposal"), proposal_id) - } - - fn vote_key(env: &Env, proposal_id: u32, voter: &Address) -> (Symbol, GovernorKey) { - ( - Symbol::new(env, "vote"), - GovernorKey { - proposal_id, - voter: voter.clone(), - }, - ) - } - - fn checkpoint_key(env: &Env, registry: &Address, account: &Address) -> (Symbol, Address, Address) { - ( - Symbol::new(env, "checkpoint"), - registry.clone(), - account.clone(), - ) - } -} + pub fn proposal(env: Env, proposal_id: u32) -> Result Date: Thu, 22 Oct 2026 09:22:46 +0000 Subject: [PATCH 2/3] fix: #581 [High] `treasury_streams::initialize` has the identical missin Closes #581 --- treasury_streams/Cargo.toml | 2 ++ treasury_streams/src/lib.rs | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/treasury_streams/Cargo.toml b/treasury_streams/Cargo.toml index e45dc5f..f1fbb1d 100644 --- a/treasury_streams/Cargo.toml +++ b/treasury_streams/Cargo.toml @@ -10,6 +10,8 @@ publish = false crate-type = ["cdylib", "rlib"] [dependencies] +soroban-sdk = "20.0.0" [dev-dependencies] +soroban-sdk = { version = "20.0.0", features = ["testutils"] } proptest = "1" diff --git a/treasury_streams/src/lib.rs b/treasury_streams/src/lib.rs index fe24184..681c77a 100644 --- a/treasury_streams/src/lib.rs +++ b/treasury_streams/src/lib.rs @@ -49,10 +49,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); Ok(()) From 01ad89fbee8c3aedcda738b898d0c4bdb0b269e8 Mon Sep 17 00:00:00 2001 From: meetestherpeter Date: Thu, 22 Oct 2026 09:22:56 +0000 Subject: [PATCH 3/3] fix: #582 [High] `governor`'s vote-tally and deadline arithmetic uses ra Closes #582 --- governor/src/lib.rs | 46 +++++++++++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/governor/src/lib.rs b/governor/src/lib.rs index 8989958..ae9064d 100644 --- a/governor/src/lib.rs +++ b/governor/src/lib.rs @@ -78,6 +78,8 @@ pub enum GovernorError { AlreadyExecuted = 13, ProposalCanceled = 14, SelfTargetForbidden = 15, + VoteTallyOverflow = 16, + DeadlineOverflow = 17, } #[contracttype] @@ -155,7 +157,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, @@ -187,7 +189,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 { @@ -205,9 +211,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); @@ -242,17 +263,6 @@ impl Governor { proposal.executed = true; env.storage().persistent().set(&Self::proposal_key(&env, proposal_id), &proposal); // The timelock performs the actual call; the governor only records the - // transition so state stays consistent with the timelock's execution. - let _ = config.timelock; - Ok(()) - } - - pub fn state(env: Env, proposal_id: u32) -> Result { - let config = Self::config(&env)?; - let proposal = Self::proposal(&env, proposal_id)?; - Ok(Self::state_of(&env, &config, &proposal)) - } - - pub fn proposal(env: Env, proposal_id: u32) -> Result