diff --git a/contract/contracts/access-control/src/lib.rs b/contract/contracts/access-control/src/lib.rs index ab68cce..7e42530 100644 --- a/contract/contracts/access-control/src/lib.rs +++ b/contract/contracts/access-control/src/lib.rs @@ -1,6 +1,6 @@ #![no_std] use predifi_errors::PrediFiError; -use soroban_sdk::{contract, contractimpl, contracttype, Address, Env}; +use soroban_sdk::{contract, contractevent, contractimpl, contracttype, Address, Env}; #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] @@ -12,31 +12,67 @@ pub enum Role { User = 4, } +#[contractevent(topics = ["admin_init"])] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct AdminInitEvent { + pub admin: Address, +} + +#[contractevent(topics = ["role_assigned"])] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RoleAssignedEvent { + pub admin: Address, + pub user: Address, + pub role: Role, +} + +#[contractevent(topics = ["role_revoked"])] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RoleRevokedEvent { + pub admin: Address, + pub user: Address, + pub role: Role, +} + +#[contractevent(topics = ["role_transferred"])] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RoleTransferredEvent { + pub admin: Address, + pub from: Address, + pub to: Address, + pub role: Role, +} + +#[contractevent(topics = ["admin_transferred"])] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct AdminTransferredEvent { + pub admin: Address, + pub new_admin: Address, +} + +#[contractevent(topics = ["all_roles_revoked"])] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct AllRolesRevokedEvent { + pub admin: Address, + pub user: Address, +} + #[contracttype] #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum PoolStatus { - /// The pool is open for predictions. Active, - /// The event has occurred and the outcome is determined. Resolved, - /// The pool is closed for new predictions but not yet resolved. Closed, - /// The outcome is being disputed. Disputed, } #[contracttype] #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum PoolCategory { - /// Sports-related predictions. Sports, - /// Political predictions. Politics, - /// Financial predictions. Finance, - /// Entertainment predictions. Entertainment, - /// Other categories. Other, } @@ -53,31 +89,18 @@ pub struct AccessControl; #[contractimpl] impl AccessControl { - /// Initialize the contract with an initial admin address. - /// - /// # Arguments - /// * `admin` - The address to be appointed as the initial super admin. - /// - /// # Errors - /// * Panics with `"AlreadyInitializedOrConfigNotSet"` if the contract has already been initialized. pub fn init(env: Env, admin: Address) { if env.storage().instance().has(&DataKey::Admin) { soroban_sdk::panic_with_error!(&env, PrediFiError::AlreadyInitializedOrConfigNotSet); } env.storage().instance().set(&DataKey::Admin, &admin); - // Also grant the Admin role to the admin address. env.storage() .persistent() - .set(&DataKey::Role(admin, Role::Admin), &()); + .set(&DataKey::Role(admin.clone(), Role::Admin), &()); + + AdminInitEvent { admin }.publish(&env); } - /// Returns the current super admin address. - /// - /// # Returns - /// The address of the current super admin. - /// - /// # Errors - /// * Panics with `"NotInitialized"` if the contract hasn't been initialized yet. pub fn get_admin(env: Env) -> Address { env.storage() .instance() @@ -85,17 +108,6 @@ impl AccessControl { .expect("NotInitialized") } - /// Assigns a specific role to a user. - /// - /// Only the current super admin can call this function. - /// - /// # Arguments - /// * `admin_caller` - The address of the admin calling the function. - /// * `user` - The address to receive the role. - /// * `role` - The role to be assigned. - /// - /// # Errors - /// * `Unauthorized` - If the caller is not the super admin. pub fn assign_role( env: Env, admin_caller: Address, @@ -111,22 +123,17 @@ impl AccessControl { env.storage() .persistent() - .set(&DataKey::Role(user, role), &()); + .set(&DataKey::Role(user.clone(), role.clone()), &()); + + RoleAssignedEvent { + admin: admin_caller, + user, + role, + } + .publish(&env); Ok(()) } - /// Revokes a specific role from a user. - /// - /// Only the current super admin can call this function. - /// - /// # Arguments - /// * `admin_caller` - The address of the admin calling the function. - /// * `user` - The address from which the role will be revoked. - /// * `role` - The role to be revoked. - /// - /// # Errors - /// * `Unauthorized` - If the caller is not the super admin. - /// * `InsufficientPermissions` - If the user doesn't have the specified role. pub fn revoke_role( env: Env, admin_caller: Address, @@ -150,35 +157,21 @@ impl AccessControl { env.storage() .persistent() - .remove(&DataKey::Role(user, role)); + .remove(&DataKey::Role(user.clone(), role.clone())); + + RoleRevokedEvent { + admin: admin_caller, + user, + role, + } + .publish(&env); Ok(()) } - /// Checks if a user has a specific role. - /// - /// # Arguments - /// * `user` - The address to check. - /// * `role` - The role to check for. - /// - /// # Returns - /// `true` if the user has the role, `false` otherwise. pub fn has_role(env: Env, user: Address, role: Role) -> bool { env.storage().persistent().has(&DataKey::Role(user, role)) } - /// Transfers a role from one address to another. - /// - /// Only the current super admin can call this function. - /// - /// # Arguments - /// * `admin_caller` - The address of the admin calling the function. - /// * `from` - The address currently holding the role. - /// * `to` - The address to receive the role. - /// * `role` - The role to be transferred. - /// - /// # Errors - /// * `Unauthorized` - If the caller is not the super admin. - /// * `InsufficientPermissions` - If the `from` address doesn't have the specified role. pub fn transfer_role( env: Env, admin_caller: Address, @@ -203,23 +196,21 @@ impl AccessControl { env.storage() .persistent() - .remove(&DataKey::Role(from, role.clone())); + .remove(&DataKey::Role(from.clone(), role.clone())); env.storage() .persistent() - .set(&DataKey::Role(to, role), &()); + .set(&DataKey::Role(to.clone(), role.clone()), &()); + + RoleTransferredEvent { + admin: admin_caller, + from, + to, + role, + } + .publish(&env); Ok(()) } - /// Transfers the super admin status to a new address. - /// - /// Only the current super admin can call this function. - /// - /// # Arguments - /// * `admin_caller` - The address of the current admin. - /// * `new_admin` - The address to become the new super admin. - /// - /// # Errors - /// * `Unauthorized` - If the caller is not the current super admin. pub fn transfer_admin( env: Env, admin_caller: Address, @@ -232,27 +223,24 @@ impl AccessControl { return Err(PrediFiError::Unauthorized); } - // Update the admin address. env.storage().instance().set(&DataKey::Admin, &new_admin); - // Transfer the Admin role record. env.storage() .persistent() .remove(&DataKey::Role(current_admin, Role::Admin)); env.storage() .persistent() - .set(&DataKey::Role(new_admin, Role::Admin), &()); + .set(&DataKey::Role(new_admin.clone(), Role::Admin), &()); + + AdminTransferredEvent { + admin: admin_caller, + new_admin, + } + .publish(&env); Ok(()) } - /// Checks if a user is the current super admin. - /// - /// # Arguments - /// * `user` - The address to check. - /// - /// # Returns - /// `true` if the user is the current super admin, `false` otherwise. pub fn is_admin(env: Env, user: Address) -> bool { let stored: Option
= env.storage().instance().get(&DataKey::Admin); match stored { @@ -261,16 +249,6 @@ impl AccessControl { } } - /// Revokes all roles from a user. - /// - /// Only the current super admin can call this function. - /// - /// # Arguments - /// * `admin_caller` - The address of the admin calling the function. - /// * `user` - The address from which all roles will be revoked. - /// - /// # Errors - /// * `Unauthorized` - If the caller is not the super admin. pub fn revoke_all_roles( env: Env, admin_caller: Address, @@ -283,7 +261,6 @@ impl AccessControl { return Err(PrediFiError::Unauthorized); } - // Revoke all possible roles. for role in [ Role::Admin, Role::Operator, @@ -299,17 +276,15 @@ impl AccessControl { } } + AllRolesRevokedEvent { + admin: admin_caller, + user, + } + .publish(&env); + Ok(()) } - /// Checks if a user has any of the specified roles. - /// - /// # Arguments - /// * `user` - The address to check. - /// * `roles` - A vector of roles to check. - /// - /// # Returns - /// `true` if the user has at least one of the specified roles, `false` otherwise. pub fn has_any_role(env: Env, user: Address, roles: soroban_sdk::Vec) -> bool { for role in roles.iter() { if env diff --git a/contract/contracts/predifi-contract/src/lib.rs b/contract/contracts/predifi-contract/src/lib.rs index 09826a3..ed1b5dc 100644 --- a/contract/contracts/predifi-contract/src/lib.rs +++ b/contract/contracts/predifi-contract/src/lib.rs @@ -1,7 +1,8 @@ #![no_std] use soroban_sdk::{ - contract, contracterror, contractimpl, contracttype, token, Address, Env, IntoVal, Symbol, Vec, + contract, contracterror, contractevent, contractimpl, contracttype, token, Address, Env, + IntoVal, Symbol, Vec, }; const DAY_IN_LEDGERS: u32 = 17280; @@ -66,6 +67,77 @@ pub struct Prediction { pub outcome: u32, } +// ── Events ─────────────────────────────────────────────────────────────────── + +#[contractevent(topics = ["init"])] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct InitEvent { + pub access_control: Address, + pub treasury: Address, + pub fee_bps: u32, +} + +#[contractevent(topics = ["pause"])] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PauseEvent { + pub admin: Address, +} + +#[contractevent(topics = ["unpause"])] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct UnpauseEvent { + pub admin: Address, +} + +#[contractevent(topics = ["fee_update"])] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct FeeUpdateEvent { + pub admin: Address, + pub fee_bps: u32, +} + +#[contractevent(topics = ["treasury_update"])] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct TreasuryUpdateEvent { + pub admin: Address, + pub treasury: Address, +} + +#[contractevent(topics = ["pool_created"])] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PoolCreatedEvent { + pub pool_id: u64, + pub end_time: u64, + pub token: Address, +} + +#[contractevent(topics = ["pool_resolved"])] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PoolResolvedEvent { + pub pool_id: u64, + pub operator: Address, + pub outcome: u32, +} + +#[contractevent(topics = ["prediction_placed"])] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PredictionPlacedEvent { + pub pool_id: u64, + pub user: Address, + pub amount: i128, + pub outcome: u32, +} + +#[contractevent(topics = ["winnings_claimed"])] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct WinningsClaimedEvent { + pub pool_id: u64, + pub user: Address, + pub amount: i128, +} + +// ───────────────────────────────────────────────────────────────────────────── + #[contract] pub struct PredifiContract; @@ -134,12 +206,19 @@ impl PredifiContract { if !env.storage().instance().has(&DataKey::Config) { let config = Config { fee_bps, - treasury, - access_control, + treasury: treasury.clone(), + access_control: access_control.clone(), }; env.storage().instance().set(&DataKey::Config, &config); env.storage().instance().set(&DataKey::PoolIdCounter, &0u64); Self::extend_instance(&env); + + InitEvent { + access_control, + treasury, + fee_bps, + } + .publish(&env); } } @@ -150,6 +229,8 @@ impl PredifiContract { .unwrap_or_else(|_| panic!("Unauthorized: missing required role")); env.storage().instance().set(&DataKey::Paused, &true); Self::extend_instance(&env); + + PauseEvent { admin }.publish(&env); } /// Unpause the contract. Only callable by Admin (role 0). @@ -159,6 +240,8 @@ impl PredifiContract { .unwrap_or_else(|_| panic!("Unauthorized: missing required role")); env.storage().instance().set(&DataKey::Paused, &false); Self::extend_instance(&env); + + UnpauseEvent { admin }.publish(&env); } /// Set fee in basis points. Caller must have Admin role (0). @@ -171,6 +254,8 @@ impl PredifiContract { config.fee_bps = fee_bps; env.storage().instance().set(&DataKey::Config, &config); Self::extend_instance(&env); + + FeeUpdateEvent { admin, fee_bps }.publish(&env); Ok(()) } @@ -180,9 +265,11 @@ impl PredifiContract { admin.require_auth(); Self::require_role(&env, &admin, 0)?; let mut config = Self::get_config(&env); - config.treasury = treasury; + config.treasury = treasury.clone(); env.storage().instance().set(&DataKey::Config, &config); Self::extend_instance(&env); + + TreasuryUpdateEvent { admin, treasury }.publish(&env); Ok(()) } @@ -205,7 +292,7 @@ impl PredifiContract { end_time, resolved: false, outcome: 0, - token, + token: token.clone(), total_stake: 0, }; @@ -218,6 +305,13 @@ impl PredifiContract { .set(&DataKey::PoolIdCounter, &(pool_id + 1)); Self::extend_instance(&env); + PoolCreatedEvent { + pool_id, + end_time, + token, + } + .publish(&env); + pool_id } @@ -246,6 +340,13 @@ impl PredifiContract { env.storage().persistent().set(&pool_key, &pool); Self::extend_persistent(&env, &pool_key); + + PoolResolvedEvent { + pool_id, + operator, + outcome, + } + .publish(&env); Ok(()) } @@ -295,6 +396,14 @@ impl PredifiContract { env.storage().persistent().set(&count_key, &(count + 1)); Self::extend_persistent(&env, &count_key); + + PredictionPlacedEvent { + pool_id, + user, + amount, + outcome, + } + .publish(&env); } /// Claim winnings from a resolved pool. Returns the amount paid out (0 for losers). @@ -360,6 +469,13 @@ impl PredifiContract { let token_client = token::Client::new(&env, &pool.token); token_client.transfer(&env.current_contract_address(), &user, &winnings); + WinningsClaimedEvent { + pool_id, + user, + amount: winnings, + } + .publish(&env); + Ok(winnings) }