diff --git a/contracts/factory/src/errors.rs b/contracts/factory/src/errors.rs index 3d50fe1..fca8941 100644 --- a/contracts/factory/src/errors.rs +++ b/contracts/factory/src/errors.rs @@ -67,4 +67,7 @@ pub enum Error { DuplicateStreamId = 33, /// The optional stream description exceeds 128 bytes (#845). DescriptionTooLong = 34, + /// Invalid factory initialization parameters (Issue #770). + /// Indicates misconfigured factory setup (e.g., invalid fee recipient, bounds). + InvalidFactoryConfiguration = 35, } diff --git a/contracts/factory/src/events.rs b/contracts/factory/src/events.rs index ff9584f..617b73b 100644 --- a/contracts/factory/src/events.rs +++ b/contracts/factory/src/events.rs @@ -60,6 +60,20 @@ pub fn protocol_fee_charged(env: &Env, fee_recipient: &Address, fee: i128) { .publish((symbol_short!("fee"), fee_recipient.clone()), fee); } +/// Protocol fee collection event (Issue #769). +/// +/// Emitted when protocol fees are collected and routed to the recipient. +/// Allows indexers to track fee accumulation without scanning balance changes. +/// +/// Topics: `("pfee_collected", recipient)` — the address that received the collected fee. +/// Data: `(token, amount)` — the token address and amount collected (in stroops). +pub fn protocol_fee_collected(env: &Env, token: &Address, recipient: &Address, amount: i128) { + env.events().publish( + (symbol_short!("pfee_collected"), recipient.clone()), + (token.clone(), amount), + ); +} + /// StreamCreated event payload metadata (Issue #684). #[soroban_sdk::contracttype] #[derive(Clone, Debug, Eq, PartialEq)] diff --git a/contracts/governor/src/config.rs b/contracts/governor/src/config.rs index ebb4a51..5477cc7 100644 --- a/contracts/governor/src/config.rs +++ b/contracts/governor/src/config.rs @@ -3,6 +3,10 @@ use soroban_sdk::{contracttype, Address, Env}; use crate::storage::DataKey; use crate::Error; +/// Minimum voting period: 17280 ledgers (~1 day on Soroban) (Issue #772). +/// Prevents flash-loan voting attacks and allows DAO participants time to react. +pub const MIN_VOTING_PERIOD_LEDGERS: u32 = 17280; + #[contracttype] #[derive(Clone)] pub struct GovernorConfig { @@ -19,6 +23,8 @@ pub struct GovernorConfig { pub force_cancel_pause_secs: u64, /// Minimum quorum threshold in basis points (10,000 = 100%). pub min_quorum_bps: u32, + /// Minimum voting period in ledger sequences (Issue #772). + pub voting_period_ledgers: u32, } /// Load the governor configuration from instance storage. @@ -46,5 +52,26 @@ pub fn load(env: &Env) -> Result { factory_address, force_cancel_pause_secs: s.get(&DataKey::ForceCancelPauseSecs).unwrap_or(2_592_000), // 30 days min_quorum_bps: s.get(&DataKey::MinQuorumBps).unwrap_or(0), + voting_period_ledgers: s.get(&DataKey::VotingPeriodLedgers).unwrap_or(MIN_VOTING_PERIOD_LEDGERS), }) } + +/// Validate and set the minimum voting period (Issue #772). +/// Ensures voting period is at least MIN_VOTING_PERIOD_LEDGERS to prevent attacks. +pub fn set_voting_period(env: &Env, voting_period_ledgers: u32) -> Result<(), Error> { + if voting_period_ledgers < MIN_VOTING_PERIOD_LEDGERS { + return Err(Error::InvalidParam); + } + env.storage() + .instance() + .set(&DataKey::VotingPeriodLedgers, &voting_period_ledgers); + Ok(()) +} + +/// Get the current voting period in ledgers. +pub fn get_voting_period(env: &Env) -> u32 { + env.storage() + .instance() + .get(&DataKey::VotingPeriodLedgers) + .unwrap_or(MIN_VOTING_PERIOD_LEDGERS) +} diff --git a/contracts/governor/src/role.rs b/contracts/governor/src/role.rs index 1e499c2..c4a6597 100644 --- a/contracts/governor/src/role.rs +++ b/contracts/governor/src/role.rs @@ -150,6 +150,15 @@ pub fn require_role_or_admin(env: &Env, caller: &Address, role: Role) -> Result< .map_err(|_| Error::NotAuthorized) } +/// Require that `caller` authorized the transaction and holds `Pauser` **or** `Admin` role. +/// Emergency responders (pausers) can execute pause/unpause operations (Issue #771). +/// This separates regular parameter tuning from emergency pause authority. +/// +/// Bumps instance TTL on success. +pub fn require_emergency_responder(env: &Env, caller: &Address) -> Result<(), Error> { + require_role_or_admin(env, caller, Role::Pauser) +} + /// Propose transferring the admin role to a new address (step 1 of 2) (Issue #682). pub fn propose_admin(env: &Env, proposer: &Address, new_admin: &Address) -> Result<(), Error> { require_role(env, proposer, Role::Admin)?; diff --git a/contracts/governor/src/storage.rs b/contracts/governor/src/storage.rs index 8a28b7f..560c81b 100644 --- a/contracts/governor/src/storage.rs +++ b/contracts/governor/src/storage.rs @@ -148,6 +148,8 @@ pub enum DataKey { ProposalVoteSnapshot(u64), /// Receipt recording the voting weight cast by `(proposal_id, voter)` to prevent double-voting. VoterReceipt(u64, Address), + /// Minimum voting period in ledger sequences (~1 day = 17280 ledgers) (Issue #772). + VotingPeriodLedgers, /// Bytecode hash for proposal `u64` execution verification (Issue #774). /// Stored to enable on-chain verification that executed bytecode matches the proposal. ProposalExecutionBytecodeHash(u64),