Skip to content
Merged
86 changes: 52 additions & 34 deletions governor/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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]
Expand All @@ -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"),),
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand All @@ -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);
Expand Down Expand Up @@ -617,6 +626,15 @@ impl Governor {
}
}

/// Compute a proposal's deadline, guarding against ledger overflow.
fn deadline(env: &Env, snapshot: u32, config: &GovernorConfig) -> Result<u32, GovernorError> {
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)
}
Expand Down
4 changes: 4 additions & 0 deletions treasury_streams/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down