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
4 changes: 2 additions & 2 deletions contracts/events/src/admin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,12 @@ pub fn set_admin(env: &Env, new_admin: Address) -> Result<(), Error> {
}

pub fn accept_admin(env: &Env) -> Result<(), Error> {
let pending = storage::get_pending_admin(env).ok_or(Error::PendingAdminMismatch)?;
let pending = storage::get_pending_admin(env).ok_or(Error::PendingRotationMismatch)?;

if env.ledger().sequence() > pending.expires_at_ledger {
storage::clear_pending_admin(env);
storage::touch_instance(env);
return Err(Error::PendingAdminExpired);
return Err(Error::PendingRotationExpired);
}

pending.target.require_auth();
Expand Down
8 changes: 6 additions & 2 deletions contracts/events/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,12 @@ pub enum Error {

Unauthorized = 10,
NotAdmin = 11,
PendingAdminMismatch = 12,
PendingAdminExpired = 13,
// Shared by both two-step rotations (admin and event manager): no pending
// proposal / target mismatch (12) and pending proposal expired (13). The
// enum is at the 50-case XDR cap, so the manager flow reuses these rather
// than adding variants.
PendingRotationMismatch = 12,
PendingRotationExpired = 13,

TokenNotSupported = 20,
FeeAccountMissingTrustline = 21,
Expand Down
95 changes: 87 additions & 8 deletions contracts/events/src/event_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,16 @@ use crate::profile_client;
use crate::storage;
use crate::token_whitelist;
use crate::types::{
CancellationBranch, CancellationState, CreateEventParams, EventRecord, EventStatus, Pillar,
PrizeAward, ReleaseKind, Submission, Winner, WinnerSpec,
CancellationBranch, CancellationState, CreateEventParams, EventRecord, EventStatus,
PendingManager, Pillar, PrizeAward, ReleaseKind, Submission, Winner, WinnerSpec,
};

const MAX_TITLE_LEN: u32 = 120;

const MAX_WINNERS_PER_SELECT: u32 = 50;

const PENDING_MANAGER_TTL_LEDGERS: u32 = 17_280;

// Anchored at selection time, not the event deadline (which usually passes
// before winners are selected). A per-event override needs a migration.
pub const PRIZE_CLAIM_WINDOW_SECS: u64 = 90 * 24 * 60 * 60;
Expand Down Expand Up @@ -135,10 +137,6 @@ pub fn create_event(env: &Env, params: CreateEventParams, op_id: BytesN<32>) ->
storage::set_event(env, id, &record);
storage::set_non_owner_contribution_total(env, id, 0);

if let Some(manager) = &params.manager {
storage::set_event_manager(env, id, manager);
}

if is_crowdfunding {
storage::append_winner(
env,
Expand All @@ -164,15 +162,91 @@ pub fn create_event(env: &Env, params: CreateEventParams, op_id: BytesN<32>) ->
}
.publish(env);

if let Some(manager) = &params.manager {
let expires_at = env
.ledger()
.sequence()
.saturating_add(PENDING_MANAGER_TTL_LEDGERS);
let pending = PendingManager {
target: manager.clone(),
expires_at_ledger: expires_at,
};
storage::set_pending_manager(env, id, &pending);
evt::ManagerProposed {
event_id: id,
target: manager.clone(),
expires_at_ledger: expires_at,
}
.publish(env);
}

idempotency::mark_seen(env, &op_id);
Ok(id)
}

pub fn set_manager(env: &Env, event_id: u64, new_manager: Address) -> Result<(), Error> {
// ============================================================
// MANAGER ROTATION (two-step propose / accept)
// ============================================================
pub fn propose_manager(env: &Env, event_id: u64, new_manager: Address) -> Result<(), Error> {
admin::require_not_paused(env)?;
let event = storage::get_event(env, event_id).ok_or(Error::EventNotFound)?;
resolve_manager(env, event_id, &event.owner).require_auth();
storage::set_event_manager(env, event_id, &new_manager);

let expires_at = env
.ledger()
.sequence()
.saturating_add(PENDING_MANAGER_TTL_LEDGERS);
let pending = PendingManager {
target: new_manager.clone(),
expires_at_ledger: expires_at,
};
storage::set_pending_manager(env, event_id, &pending);

evt::ManagerProposed {
event_id,
target: new_manager,
expires_at_ledger: expires_at,
}
.publish(env);
Ok(())
}

pub fn accept_manager(env: &Env, event_id: u64) -> Result<(), Error> {
admin::require_not_paused(env)?;
storage::get_event(env, event_id).ok_or(Error::EventNotFound)?;

let pending =
storage::get_pending_manager(env, event_id).ok_or(Error::PendingRotationMismatch)?;

if env.ledger().sequence() > pending.expires_at_ledger {
storage::clear_pending_manager(env, event_id);
return Err(Error::PendingRotationExpired);
}
Comment thread
0xdevcollins marked this conversation as resolved.

pending.target.require_auth();

storage::set_event_manager(env, event_id, &pending.target);
storage::clear_pending_manager(env, event_id);

evt::ManagerChanged {
event_id,
new_manager: pending.target,
}
.publish(env);
Ok(())
}

pub fn cancel_pending_manager(env: &Env, event_id: u64) -> Result<(), Error> {
admin::require_not_paused(env)?;
let event = storage::get_event(env, event_id).ok_or(Error::EventNotFound)?;
resolve_manager(env, event_id, &event.owner).require_auth();

if storage::get_pending_manager(env, event_id).is_none() {
return Err(Error::PendingRotationMismatch);
}
storage::clear_pending_manager(env, event_id);

evt::PendingManagerCancelled { event_id }.publish(env);
Ok(())
}

Expand All @@ -181,6 +255,11 @@ pub fn get_manager(env: &Env, event_id: u64) -> Result<Address, Error> {
Ok(resolve_manager(env, event_id, &event.owner))
}

pub fn get_pending_manager(env: &Env, event_id: u64) -> Result<Option<PendingManager>, Error> {
storage::get_event(env, event_id).ok_or(Error::EventNotFound)?;
Ok(storage::get_pending_manager(env, event_id))
}

// ============================================================
// ADD FUNDS (partner / community contribution)
// ============================================================
Expand Down
18 changes: 18 additions & 0 deletions contracts/events/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,24 @@ pub struct EventCancelled {
pub id: u64,
}

#[contractevent]
pub struct ManagerProposed {
pub event_id: u64,
pub target: Address,
pub expires_at_ledger: u32,
}

#[contractevent]
pub struct ManagerChanged {
pub event_id: u64,
pub new_manager: Address,
}

#[contractevent]
pub struct PendingManagerCancelled {
pub event_id: u64,
}

#[contractevent]
pub struct FundsAdded {
pub event_id: u64,
Expand Down
16 changes: 14 additions & 2 deletions contracts/events/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,14 +228,26 @@ impl EventsContract {
// ============================================================
// MANAGEMENT AUTHORITY (manager != funder/owner)
// ============================================================
pub fn set_manager(env: Env, event_id: u64, new_manager: Address) -> Result<(), Error> {
event_ops::set_manager(&env, event_id, new_manager)
pub fn propose_manager(env: Env, event_id: u64, new_manager: Address) -> Result<(), Error> {
event_ops::propose_manager(&env, event_id, new_manager)
}

pub fn accept_manager(env: Env, event_id: u64) -> Result<(), Error> {
event_ops::accept_manager(&env, event_id)
}

pub fn cancel_pending_manager(env: Env, event_id: u64) -> Result<(), Error> {
event_ops::cancel_pending_manager(&env, event_id)
}

pub fn get_manager(env: Env, event_id: u64) -> Result<Address, Error> {
event_ops::get_manager(&env, event_id)
}

pub fn get_pending_manager(env: Env, event_id: u64) -> Result<Option<PendingManager>, Error> {
event_ops::get_pending_manager(&env, event_id)
}

pub fn claim_milestone(
env: Env,
event_id: u64,
Expand Down
25 changes: 23 additions & 2 deletions contracts/events/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ use soroban_sdk::String;

use crate::errors::Error;
use crate::types::{
CancellationState, DataKey, EventRecord, PendingAdmin, PendingUpgrade, PrizeAward, Submission,
Winner,
CancellationState, DataKey, EventRecord, PendingAdmin, PendingManager, PendingUpgrade,
PrizeAward, Submission, Winner,
};

// ============================================================
Expand Down Expand Up @@ -287,6 +287,27 @@ pub fn set_event_manager(env: &Env, id: u64, manager: &Address) {
touch_event_persistent(env, &key);
}

pub fn get_pending_manager(env: &Env, id: u64) -> Option<PendingManager> {
let key = DataKey::PendingManager(id);
let p: Option<PendingManager> = env.storage().persistent().get(&key);
if p.is_some() {
touch_event_persistent(env, &key);
}
p
}

pub fn set_pending_manager(env: &Env, id: u64, pending: &PendingManager) {
let key = DataKey::PendingManager(id);
env.storage().persistent().set(&key, pending);
touch_event_persistent(env, &key);
}

pub fn clear_pending_manager(env: &Env, id: u64) {
env.storage()
.persistent()
.remove(&DataKey::PendingManager(id));
}

// ============================================================
// APPLICANTS (paged, persistent)
// ============================================================
Expand Down
3 changes: 2 additions & 1 deletion contracts/events/src/tests/cancel_refund.rs
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,8 @@ fn non_manager_cranks_and_finalizes_with_exact_payout_deltas() {
let ctx = setup();
let id = create_hackathon(&ctx);
let manager = Address::generate(&ctx.env);
ctx.events.set_manager(&id, &manager);
ctx.events.propose_manager(&id, &manager);
ctx.events.accept_manager(&id);
let p1 = Address::generate(&ctx.env);
let p2 = Address::generate(&ctx.env);
let p1_amount = 200_0000000_i128;
Expand Down
Loading
Loading