Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion contracts/governor/src/events.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use soroban_sdk::{symbol_short, Address, Env};
use soroban_sdk::{symbol_short, Address, Env, Symbol};

use crate::Role;

Expand Down Expand Up @@ -72,6 +72,14 @@ pub fn propose_authority(env: &Env, caller: &Address, new_authority: &Address) {
);
}

/// Emitted when an `Admin` cancels a pending authority proposal.
pub fn revoke_propose_authority(env: &Env, caller: &Address, pending_authority: &Address) {
env.events().publish(
(Symbol::new(env, "authority_proposal_revoked"), caller.clone()),
pending_authority.clone(),
);
}

/// Emitted when the pending authority accepts the transfer.
///
/// # Indexer & Context Note
Expand Down
80 changes: 73 additions & 7 deletions contracts/governor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ use drip_common::is_zero_address;

pub use config::GovernorConfig;
pub use errors::Error;
pub use role::Role;
pub use role::{Role, RoleMembersPage};
use storage::DataKey;
pub use storage::{Proposal, ProposalStatus};

Expand Down Expand Up @@ -112,6 +112,16 @@ impl DripGovernor {
config::load(&env)
}

/// Current protocol fee recipient, without fetching the full config.
pub fn fee_recipient(env: Env) -> Result<Address, Error> {
Ok(config::load(&env)?.fee_recipient)
}

/// Current protocol fee in basis points, without fetching the full config.
pub fn fee_bps(env: Env) -> Result<u32, Error> {
Ok(config::load(&env)?.fee_bps)
}

/// Read-only: current minimum stream duration in seconds.
///
/// Focused accessor for callers that only need this one field, avoiding
Expand All @@ -131,13 +141,17 @@ impl DripGovernor {
role::has_role(&env, role, &account)
}

/// Returns every account currently holding `role`.
/// Returns a page of accounts holding `role`, plus the total member count.
///
/// Enables on-chain role-membership auditing without replaying every
/// `grant_role`/`revoke_role` event from genesis. The index is maintained
/// automatically by `grant_role` and `revoke_role`.
pub fn role_members(env: Env, role: Role) -> Vec<Address> {
role::role_members(&env, role)
/// The page is capped at 100 accounts. The index is maintained by the
/// single-account and batch role mutation methods.
pub fn role_members(
env: Env,
role: Role,
offset: u32,
limit: u32,
) -> RoleMembersPage {
role::role_members(&env, role, offset, limit)
}

/// Current maximum stream duration in seconds, without fetching the full
Expand Down Expand Up @@ -313,6 +327,45 @@ impl DripGovernor {
Ok(())
}

/// Grants `role` to each account. Only an `Admin` may call this.
///
/// Emits the same per-account event as `grant_role`, only for new grants.
pub fn grant_role_batch(
env: Env,
caller: Address,
role: Role,
accounts: Vec<Address>,
) -> Result<(), Error> {
role::require_role(&env, &caller, Role::Admin)?;
for index in 0..accounts.len() {
let account = accounts.get(index).unwrap();
if role::grant(&env, role, &account) {
events::grant_role(&env, &caller, role, &account);
}
}
Ok(())
}

/// Revokes `role` from each account. Only an `Admin` may call this.
///
/// Preserves `revoke_role` idempotency and last-admin protection, and
/// emits one event for each account whose role was actually removed.
pub fn revoke_role_batch(
env: Env,
caller: Address,
role: Role,
accounts: Vec<Address>,
) -> Result<(), Error> {
role::require_role(&env, &caller, Role::Admin)?;
for index in 0..accounts.len() {
let account = accounts.get(index).unwrap();
if role::revoke(&env, role, &account)? {
events::revoke_role(&env, &caller, role, &account);
}
}
Ok(())
}

/// Hands the full `Admin` role from `caller` to `new_authority`.
///
/// Grants first so the subsequent revoke can never trip the `LastAdmin`
Expand Down Expand Up @@ -369,6 +422,19 @@ impl DripGovernor {
Ok(())
}

/// Cancels the pending authority transfer. Only an `Admin` may call this.
pub fn revoke_propose_authority(env: Env, caller: Address) -> Result<(), Error> {
role::require_role(&env, &caller, Role::Admin)?;
let storage = env.storage().instance();
let pending: Address = storage
.get(&DataKey::PendingAuthority)
.ok_or(Error::NoPendingAuthority)?;
storage.remove(&DataKey::PendingAuthority);
storage.remove(&DataKey::PendingAuthorityProposer);
events::revoke_propose_authority(&env, &caller, &pending);
Ok(())
}

/// Accept the pending authority transfer (step 2 of 2).
///
/// Must be called by the pending authority address itself. This completes
Expand Down
30 changes: 25 additions & 5 deletions contracts/governor/src/role.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use soroban_sdk::{Address, Env, Vec as SorobanVec};
use soroban_sdk::{contracttype, Address, Env, Vec};

use drip_common::rbac;

Expand All @@ -9,6 +9,16 @@ use crate::Error;
/// Re-export so callers using `role::Role` continue to work unchanged.
pub use crate::storage::Role;

/// A page of role members, with the total count for pagination.
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RoleMembersPage {
pub members: Vec<Address>,
pub total: u32,
}

const MAX_ROLE_MEMBERS_PAGE_SIZE: u32 = 100;

// ── Key helpers ────────────────────────────────────────────────────────────

fn role_key(role: Role, account: &Address) -> DataKey {
Expand Down Expand Up @@ -105,12 +115,22 @@ pub fn require_role(env: &Env, caller: &Address, role: Role) -> Result<(), Error
.map_err(|_| Error::NotAuthorized)
}

/// Returns every account currently holding `role`.
/// Returns a page of accounts currently holding `role`.
///
/// Reads from the `RoleMembers` index maintained by `grant`/`revoke`.
/// Returns an empty vector if no accounts hold the role.
pub fn role_members(env: &Env, role: Role) -> SorobanVec<Address> {
rbac::role_members(env, &members_key(role))
/// The result is capped at 100 members and includes the total member count.
pub fn role_members(env: &Env, role: Role, offset: u32, limit: u32) -> RoleMembersPage {
let all_members = rbac::role_members(env, &members_key(role));
let total = all_members.len();
let start = offset.min(total);
let end = start
.saturating_add(limit.min(MAX_ROLE_MEMBERS_PAGE_SIZE))
.min(total);
let mut members = Vec::new(env);
for index in start..end {
members.push_back(all_members.get(index).unwrap());
}
RoleMembersPage { members, total }
}

/// Requires that `caller` authorized the transaction and holds `role` **or**
Expand Down
30 changes: 30 additions & 0 deletions contracts/stream/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1230,6 +1230,36 @@ impl DripStream {
state::load(&env)
}

/// Read-only: the stream sender.
pub fn sender(env: Env) -> Address {
state::load(&env).sender
}

/// Read-only: the stream recipient.
pub fn recipient(env: Env) -> Address {
state::load(&env).recipient
}

/// Read-only: the token escrowed by this stream.
pub fn token(env: Env) -> Address {
state::load(&env).token
}

/// Read-only: the stream's current rate per second.
pub fn rate_per_second(env: Env) -> i128 {
state::load(&env).rate_per_second
}

/// Read-only: whether this stream has no scheduled end time.
pub fn is_open_ended(env: Env) -> bool {
state::load(&env).end_time == 0
}

/// Read-only: maximum safe pause duration, in seconds.
pub fn max_pause_secs(_env: Env) -> u64 {
ttl::MAX_PAUSE_SECS
}

/// Latest committed event sequence.
///
/// Event consumers can compare this value with the last sequence they
Expand Down