diff --git a/contracts/price-oracle/src/auth.rs b/contracts/price-oracle/src/auth.rs index 375c6f2..9c6e673 100644 --- a/contracts/price-oracle/src/auth.rs +++ b/contracts/price-oracle/src/auth.rs @@ -1,4 +1,5 @@ -use soroban_sdk::{contracttype, Address, Env, Vec}; +use soroban_sdk::{contracttype, Address, Env, Vec, Map, Symbol}; +use crate::types::{Role, RoleAssignment, RoleChangeEvent}; // ───────────────────────────────────────────────────────────────────────────── // Storage Key @@ -34,8 +35,214 @@ pub fn _has_admin(env: &Env) -> bool { env.storage().instance().has(&DataKey::Admin) } -/// Check if a caller is in the authorized admin list. +// ───────────────────────────────────────────────────────────────────────────── +// Role-Based Access Control (RBAC) Helpers +// ───────────────────────────────────────────────────────────────────────────── + +/// Check if an address has a specific role. +pub fn _has_role(env: &Env, address: &Address, role: Role) -> bool { + if let Some(assignment) = _get_role_assignment(env, address) { + (assignment.roles & role as u32) != 0 + } else { + false + } +} + +/// Check if an address has any of the specified roles (bitmask OR). +pub fn _has_any_role(env: &Env, address: &Address, roles_mask: u32) -> bool { + if let Some(assignment) = _get_role_assignment(env, address) { + (assignment.roles & roles_mask) != 0 + } else { + false + } +} + +/// Check if an address has all of the specified roles (bitmask AND). +pub fn _has_all_roles(env: &Env, address: &Address, roles_mask: u32) -> bool { + if let Some(assignment) = _get_role_assignment(env, address) { + (assignment.roles & roles_mask) == roles_mask + } else { + false + } +} + +/// Get the role assignment for an address. +pub fn _get_role_assignment(env: &Env, address: &Address) -> Option { + env.storage() + .instance() + .get(&crate::types::DataKey::RoleAssignment(address.clone())) +} + +/// Set a role assignment for an address. +pub fn _set_role_assignment(env: &Env, assignment: &RoleAssignment) { + env.storage() + .instance() + .set(&crate::types::DataKey::RoleAssignment(assignment.address.clone()), assignment); +} + +/// Remove a role assignment for an address. +pub fn _remove_role_assignment(env: &Env, address: &Address) { + env.storage() + .instance() + .remove(&crate::types::DataKey::RoleAssignment(address.clone())); +} + +/// Grant a role to an address. +pub fn _grant_role(env: &Env, address: &Address, role: Role, granted_by: &Address) { + let current_time = env.ledger().timestamp(); + let mut assignment = _get_role_assignment(env, address).unwrap_or_else(|| RoleAssignment { + address: address.clone(), + roles: Role::None as u32, + assigned_at: current_time, + assigned_by: granted_by.clone(), + }); + + let previous_roles = assignment.roles; + assignment.roles |= role as u32; + assignment.assigned_at = current_time; + assignment.assigned_by = granted_by.clone(); + + _set_role_assignment(env, &assignment); + _log_role_change(env, address, granted_by, previous_roles, assignment.roles, + "Granted role"); +} + +/// Revoke a role from an address. +pub fn _revoke_role(env: &Env, address: &Address, role: Role, revoked_by: &Address) { + if let Some(mut assignment) = _get_role_assignment(env, address) { + let previous_roles = assignment.roles; + assignment.roles &= !(role as u32); + assignment.assigned_at = env.ledger().timestamp(); + assignment.assigned_by = revoked_by.clone(); + + if assignment.roles == Role::None as u32 { + _remove_role_assignment(env, address); + } else { + _set_role_assignment(env, &assignment); + } + + _log_role_change(env, address, revoked_by, previous_roles, assignment.roles, + "Revoked role"); + } +} + +/// Set multiple roles for an address (replaces all existing roles). +pub fn _set_roles(env: &Env, address: &Address, roles_mask: u32, set_by: &Address) { + let current_time = env.ledger().timestamp(); + let previous_roles = _get_role_assignment(env, address) + .map(|a| a.roles) + .unwrap_or(Role::None as u32); + + if roles_mask == Role::None as u32 { + _remove_role_assignment(env, address); + } else { + let assignment = RoleAssignment { + address: address.clone(), + roles: roles_mask, + assigned_at: current_time, + assigned_by: set_by.clone(), + }; + _set_role_assignment(env, &assignment); + } + + _log_role_change(env, address, set_by, previous_roles, roles_mask, + "Set multiple roles"); +} + +/// Log a role change event for audit purposes. +fn _log_role_change(env: &Env, target: &Address, changed_by: &Address, + previous_roles: u32, new_roles: u32, description: &str) { + let mut audit_log: Vec = env + .storage() + .instance() + .get(&crate::types::DataKey::RoleAuditLog) + .unwrap_or_else(|| Vec::new(env)); + + let event = RoleChangeEvent { + target_address: target.clone(), + changed_by: changed_by.clone(), + previous_roles, + new_roles, + timestamp: env.ledger().timestamp(), + description: Symbol::new(env, description), + }; + + audit_log.push_front(event); + + // Keep only last 100 role change events + if audit_log.len() > 100 { + audit_log.pop_back(); + } + + env.storage().instance().set(&crate::types::DataKey::RoleAuditLog, &audit_log); +} + +/// Get the role audit log. +pub fn _get_role_audit_log(env: &Env) -> Vec { + env.storage() + .instance() + .get(&crate::types::DataKey::RoleAuditLog) + .unwrap_or_else(|| Vec::new(env)) +} + +// ───────────────────────────────────────────────────────────────────────────── +// Role-Specific Authorization Checks +// ───────────────────────────────────────────────────────────────────────────── + +/// Require Security Manager role. +pub fn _require_security_manager(env: &Env, caller: &Address) { + if !_has_role(env, caller, Role::SecurityManager) { + panic!("Unauthorized: caller is not a Security Manager"); + } +} + +/// Require Fee Collector role. +pub fn _require_fee_collector(env: &Env, caller: &Address) { + if !_has_role(env, caller, Role::FeeCollector) { + panic!("Unauthorized: caller is not a Fee Collector"); + } +} + +/// Require Price Manager role. +pub fn _require_price_manager(env: &Env, caller: &Address) { + if !_has_role(env, caller, Role::PriceManager) { + panic!("Unauthorized: caller is not a Price Manager"); + } +} + +/// Require Super Admin role (has all permissions). +pub fn _require_super_admin(env: &Env, caller: &Address) { + if !_has_role(env, caller, Role::SuperAdmin) { + panic!("Unauthorized: caller is not a Super Admin"); + } +} + +/// Require any of the specified roles. +pub fn _require_any_role(env: &Env, caller: &Address, roles_mask: u32, error_msg: &str) { + if !_has_any_role(env, caller, roles_mask) { + panic!("Unauthorized: insufficient permissions"); + } +} + +/// Require all of the specified roles. +pub fn _require_all_roles(env: &Env, caller: &Address, roles_mask: u32, error_msg: &str) { + if !_has_all_roles(env, caller, roles_mask) { + panic!("Unauthorized: insufficient permissions"); + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Legacy Admin Functions (for backward compatibility) +// ───────────────────────────────────────────────────────────────────────────── + +/// Check if a caller is in the authorized admin list (legacy). pub fn _is_authorized(env: &Env, caller: &Address) -> bool { + // First check new RBAC system + if _has_any_role(env, caller, Role::SuperAdmin as u32) { + return true; + } + + // Fallback to legacy admin system for migration env.storage() .instance() .get::>(&DataKey::Admin) @@ -43,6 +250,7 @@ pub fn _is_authorized(env: &Env, caller: &Address) -> bool { .unwrap_or(false) } +/// Require authorized caller (legacy admin or new RBAC). pub fn _require_authorized(env: &Env, caller: &Address) { if !_is_authorized(env, caller) { panic!("Unauthorised: caller is not in the authorized admin list"); @@ -630,4 +838,230 @@ mod auth_tests { assert!(!_is_authorized(&env, &admin)); }); } + + // ── RBAC Tests ─────────────────────────────────────────────────────── + + #[test] + fn test_grant_role_adds_security_manager() { + let (env, contract_id, super_admin) = setup(); + let target = ::generate(&env); + env.as_contract(&contract_id, || { + // Setup super admin + crate::auth::_grant_role(&env, &super_admin, crate::types::Role::SuperAdmin, &super_admin); + + // Grant security manager role + crate::auth::_grant_role(&env, &target, crate::types::Role::SecurityManager, &super_admin); + + assert!(_has_role(&env, &target, crate::types::Role::SecurityManager)); + assert!(!_has_role(&env, &target, crate::types::Role::FeeCollector)); + assert!(!_has_role(&env, &target, crate::types::Role::PriceManager)); + }); + } + + #[test] + fn test_grant_multiple_roles() { + let (env, contract_id, super_admin) = setup(); + let target = ::generate(&env); + env.as_contract(&contract_id, || { + // Setup super admin + crate::auth::_grant_role(&env, &super_admin, crate::types::Role::SuperAdmin, &super_admin); + + // Grant multiple roles using set_roles + crate::auth::_set_roles(&env, &target, 7, &super_admin); // All roles + + assert!(_has_role(&env, &target, crate::types::Role::SecurityManager)); + assert!(_has_role(&env, &target, crate::types::Role::FeeCollector)); + assert!(_has_role(&env, &target, crate::types::Role::PriceManager)); + assert!(_has_role(&env, &target, crate::types::Role::SuperAdmin)); + }); + } + + #[test] + fn test_revoke_role_removes_permission() { + let (env, contract_id, super_admin) = setup(); + let target = ::generate(&env); + env.as_contract(&contract_id, || { + // Setup super admin and grant security manager + crate::auth::_grant_role(&env, &super_admin, crate::types::Role::SuperAdmin, &super_admin); + crate::auth::_grant_role(&env, &target, crate::types::Role::SecurityManager, &super_admin); + + assert!(_has_role(&env, &target, crate::types::Role::SecurityManager)); + + // Revoke security manager role + crate::auth::_revoke_role(&env, &target, crate::types::Role::SecurityManager, &super_admin); + + assert!(!_has_role(&env, &target, crate::types::Role::SecurityManager)); + }); + } + + #[test] + fn test_has_any_role() { + let (env, contract_id, super_admin) = setup(); + let target = ::generate(&env); + env.as_contract(&contract_id, || { + // Setup super admin and grant security manager + fee collector + crate::auth::_grant_role(&env, &super_admin, crate::types::Role::SuperAdmin, &super_admin); + crate::auth::_grant_role(&env, &target, crate::types::Role::SecurityManager, &super_admin); + crate::auth::_grant_role(&env, &target, crate::types::Role::FeeCollector, &super_admin); + + // Test has_any_role with security manager mask (1) + assert!(_has_any_role(&env, &target, 1)); + // Test has_any_role with fee collector mask (2) + assert!(_has_any_role(&env, &target, 2)); + // Test has_any_role with combined mask (3) + assert!(_has_any_role(&env, &target, 3)); + // Test has_any_role with non-matching mask (4) + assert!(!_has_any_role(&env, &target, 4)); + }); + } + + #[test] + fn test_has_all_roles() { + let (env, contract_id, super_admin) = setup(); + let target = ::generate(&env); + env.as_contract(&contract_id, || { + // Setup super admin and grant security manager + fee collector + crate::auth::_grant_role(&env, &super_admin, crate::types::Role::SuperAdmin, &super_admin); + crate::auth::_grant_role(&env, &target, crate::types::Role::SecurityManager, &super_admin); + crate::auth::_grant_role(&env, &target, crate::types::Role::FeeCollector, &super_admin); + + // Test has_all_roles with security manager + fee collector mask (3) + assert!(_has_all_roles(&env, &target, 3)); + // Test has_all_roles with security manager only mask (1) + assert!(_has_all_roles(&env, &target, 1)); + // Test has_all_roles with non-matching mask (4) + assert!(!_has_all_roles(&env, &target, 4)); + }); + } + + #[test] + fn test_role_audit_log() { + let (env, contract_id, super_admin) = setup(); + let target = ::generate(&env); + env.as_contract(&contract_id, || { + // Setup super admin + crate::auth::_grant_role(&env, &super_admin, crate::types::Role::SuperAdmin, &super_admin); + + // Grant a role and check audit log + crate::auth::_grant_role(&env, &target, crate::types::Role::SecurityManager, &super_admin); + + let audit_log = _get_role_audit_log(&env); + assert_eq!(audit_log.len(), 1); + + let event = &audit_log.get(0).unwrap(); + assert_eq!(event.target_address, target); + assert_eq!(event.changed_by, super_admin); + assert_eq!(event.new_roles, 1); // SecurityManager role + }); + } + + #[test] + fn test_require_security_manager_passes() { + let (env, contract_id, super_admin) = setup(); + let security_manager = ::generate(&env); + env.as_contract(&contract_id, || { + // Setup super admin and grant security manager + crate::auth::_grant_role(&env, &super_admin, crate::types::Role::SuperAdmin, &super_admin); + crate::auth::_grant_role(&env, &security_manager, crate::types::Role::SecurityManager, &super_admin); + + // Should not panic + crate::auth::_require_security_manager(&env, &security_manager); + }); + } + + #[test] + #[should_panic(expected = "Unauthorized: caller is not a Security Manager")] + fn test_require_security_manager_panics_for_unauthorized() { + let (env, contract_id, super_admin) = setup(); + let unauthorized = ::generate(&env); + env.as_contract(&contract_id, || { + // Setup super admin but don't grant security manager + crate::auth::_grant_role(&env, &super_admin, crate::types::Role::SuperAdmin, &super_admin); + + // Should panic + crate::auth::_require_security_manager(&env, &unauthorized); + }); + } + + #[test] + fn test_require_fee_collector_passes() { + let (env, contract_id, super_admin) = setup(); + let fee_collector = ::generate(&env); + env.as_contract(&contract_id, || { + // Setup super admin and grant fee collector + crate::auth::_grant_role(&env, &super_admin, crate::types::Role::SuperAdmin, &super_admin); + crate::auth::_grant_role(&env, &fee_collector, crate::types::Role::FeeCollector, &super_admin); + + // Should not panic + crate::auth::_require_fee_collector(&env, &fee_collector); + }); + } + + #[test] + #[should_panic(expected = "Unauthorized: caller is not a Fee Collector")] + fn test_require_fee_collector_panics_for_unauthorized() { + let (env, contract_id, super_admin) = setup(); + let unauthorized = ::generate(&env); + env.as_contract(&contract_id, || { + // Setup super admin but don't grant fee collector + crate::auth::_grant_role(&env, &super_admin, crate::types::Role::SuperAdmin, &super_admin); + + // Should panic + crate::auth::_require_fee_collector(&env, &unauthorized); + }); + } + + #[test] + fn test_require_price_manager_passes() { + let (env, contract_id, super_admin) = setup(); + let price_manager = ::generate(&env); + env.as_contract(&contract_id, || { + // Setup super admin and grant price manager + crate::auth::_grant_role(&env, &super_admin, crate::types::Role::SuperAdmin, &super_admin); + crate::auth::_grant_role(&env, &price_manager, crate::types::Role::PriceManager, &super_admin); + + // Should not panic + crate::auth::_require_price_manager(&env, &price_manager); + }); + } + + #[test] + #[should_panic(expected = "Unauthorized: caller is not a Price Manager")] + fn test_require_price_manager_panics_for_unauthorized() { + let (env, contract_id, super_admin) = setup(); + let unauthorized = ::generate(&env); + env.as_contract(&contract_id, || { + // Setup super admin but don't grant price manager + crate::auth::_grant_role(&env, &super_admin, crate::types::Role::SuperAdmin, &super_admin); + + // Should panic + crate::auth::_require_price_manager(&env, &unauthorized); + }); + } + + #[test] + fn test_require_super_admin_passes() { + let (env, contract_id, super_admin) = setup(); + env.as_contract(&contract_id, || { + // Setup super admin + crate::auth::_grant_role(&env, &super_admin, crate::types::Role::SuperAdmin, &super_admin); + + // Should not panic + crate::auth::_require_super_admin(&env, &super_admin); + }); + } + + #[test] + #[should_panic(expected = "Unauthorized: caller is not a Super Admin")] + fn test_require_super_admin_panics_for_unauthorized() { + let (env, contract_id, super_admin) = setup(); + let unauthorized = ::generate(&env); + env.as_contract(&contract_id, || { + // Don't grant super admin + crate::auth::_grant_role(&env, &super_admin, crate::types::Role::SecurityManager, &super_admin); + + // Should panic + crate::auth::_require_super_admin(&env, &unauthorized); + }); + } } diff --git a/contracts/price-oracle/src/lib.rs b/contracts/price-oracle/src/lib.rs index bc4b03b..cc550d3 100644 --- a/contracts/price-oracle/src/lib.rs +++ b/contracts/price-oracle/src/lib.rs @@ -221,7 +221,67 @@ pub trait StellarFlowTrait { /// /// Returns true if the contract is frozen, false otherwise. fn is_frozen(env: Env) -> bool; -} + + // ── RBAC Management Functions ─────────────────────────────────────────────── + + /// Grant a role to an address. + /// + /// Only Super Admin can grant roles. This function provides granular access control. + fn grant_role(env: Env, admin: Address, target: Address, role: u32) -> Result<(), Error>; + + /// Revoke a role from an address. + /// + /// Only Super Admin can revoke roles. + fn revoke_role(env: Env, admin: Address, target: Address, role: u32) -> Result<(), Error>; + + /// Set multiple roles for an address (replaces all existing roles). + /// + /// Only Super Admin can set multiple roles at once. + fn set_roles(env: Env, admin: Address, target: Address, roles_mask: u32) -> Result<(), Error>; + /// Check if an address has a specific role. + fn has_role(env: Env, address: Address, role: u32) -> bool; + + /// Get the roles assigned to an address. + fn get_roles(env: Env, address: Address) -> u32; + + /// Get the role audit log. + fn get_role_audit_log(env: Env) -> soroban_sdk::Vec; + + /// Initialize RBAC system by granting Super Admin role to first admin. + /// + /// This should be called once during contract initialization to migrate from legacy admin system. + fn initialize_rbac(env: Env, admin: Address) -> Result<(), Error>; + +// ── Provider Management Functions (Security Manager) ─────────────────────── + +/// Add a provider to the whitelist (Security Manager only). +fn add_provider(env: Env, security_manager: Address, provider: Address) -> Result<(), Error>; + +/// Remove a provider from the whitelist (Security Manager only). +fn remove_provider(env: Env, security_manager: Address, provider: Address) -> Result<(), Error>; + +/// Set provider weight (Security Manager only). +fn set_provider_weight(env: Env, security_manager: Address, provider: Address, weight: u32) -> Result<(), Error>; + +/// Check if an address is a whitelisted provider. +fn is_provider(env: Env, address: Address) -> bool; + +/// Get provider weight. +fn get_provider_weight(env: Env, provider: Address) -> u32; + +/// Get all active relayers (whitelisted providers). +fn get_active_relayers(env: Env) -> soroban_sdk::Vec
; + +// ── Fee Management Functions (Fee Collector) ────────────────────────── + +/// Set query fee for price oracle calls (Fee Collector only). +fn set_query_fee(env: Env, fee_collector: Address, fee_amount: i128) -> Result<(), Error>; + +/// Get current query fee amount. +fn get_query_fee(env: Env) -> i128; + +/// Collect accumulated query fees (Fee Collector only). +fn collect_fees(env: Env, fee_collector: Address, recipient: Address) -> Result<(), Error>; #[contractclient(name = "TokenContractClient")] pub trait TokenContractTrait { @@ -280,6 +340,16 @@ pub enum Error { ActionAlreadyExecuted = 18, /// Action has been cancelled. ActionCancelled = 19, + /// RBAC system already initialized. + RbacAlreadyInitialized = 20, + /// Invalid role value provided. + InvalidRole = 21, + /// Cannot revoke last Super Admin. + CannotRemoveLastSuperAdmin = 22, + /// RBAC system not initialized. + RbacNotInitialized = 23, + /// Caller lacks required role permissions. + InsufficientRolePermissions = 24, } #[contract] @@ -668,12 +738,12 @@ impl PriceOracle { /// Add a new asset to the tracked asset list. /// /// The new asset is added to the internal asset list and initialized with a zero-price placeholder - /// in the `VerifiedPrice` bucket. + /// in the `VerifiedPrice` bucket. Requires Price Manager role. pub fn add_asset(env: Env, admin: Address, asset: Symbol) -> Result<(), Error> { _require_not_destroyed(&env); crate::auth::_require_not_frozen(&env); admin.require_auth(); - crate::auth::_require_authorized(&env, &admin); + crate::auth::_require_price_manager(&env, &admin); _track_asset(&env, asset.clone()); @@ -2219,6 +2289,254 @@ impl PriceOracle { pub fn get_price_update_subscribers(env: Env) -> soroban_sdk::Vec
{ callbacks::get_subscribers(&env) } + + // ── RBAC Management Functions ─────────────────────────────────────────────────── + + /// Initialize RBAC system by granting Super Admin role to first admin. + /// + /// This should be called once during contract initialization to migrate from legacy admin system. + pub fn initialize_rbac(env: Env, admin: Address) -> Result<(), Error> { + _require_not_destroyed(&env); + admin.require_auth(); + + // Check if RBAC is already initialized + if env.storage().instance().has(&crate::types::DataKey::RoleAuditLog) { + return Err(Error::RbacAlreadyInitialized); + } + + // Verify caller is a legacy admin + if !crate::auth::_is_authorized(&env, &admin) { + return Err(Error::NotAuthorized); + } + + // Grant Super Admin role to the initializing admin + crate::auth::_grant_role(&env, &admin, crate::types::Role::SuperAdmin, &admin); + + // Initialize audit log + let audit_log: soroban_sdk::Vec = soroban_sdk::Vec::new(&env); + env.storage().instance().set(&crate::types::DataKey::RoleAuditLog, &audit_log); + + Ok(()) + } + + /// Grant a role to an address. + /// + /// Only Super Admin can grant roles. This function provides granular access control. + pub fn grant_role(env: Env, admin: Address, target: Address, role: u32) -> Result<(), Error> { + _require_not_destroyed(&env); + admin.require_auth(); + crate::auth::_require_super_admin(&env, &admin); + + // Validate role + let role_enum = match role { + 1 => crate::types::Role::SecurityManager, + 2 => crate::types::Role::FeeCollector, + 4 => crate::types::Role::PriceManager, + 7 => crate::types::Role::SuperAdmin, + _ => return Err(Error::InvalidRole), + }; + + crate::auth::_grant_role(&env, &target, role_enum, &admin); + Ok(()) + } + + /// Revoke a role from an address. + /// + /// Only Super Admin can revoke roles. + pub fn revoke_role(env: Env, admin: Address, target: Address, role: u32) -> Result<(), Error> { + _require_not_destroyed(&env); + admin.require_auth(); + crate::auth::_require_super_admin(&env, &admin); + + // Validate role + let role_enum = match role { + 1 => crate::types::Role::SecurityManager, + 2 => crate::types::Role::FeeCollector, + 4 => crate::types::Role::PriceManager, + 7 => crate::types::Role::SuperAdmin, + _ => return Err(Error::InvalidRole), + }; + + // Check if this would remove the last Super Admin + if role == 7 && crate::auth::_has_role(&env, &target, crate::types::Role::SuperAdmin) { + // Count remaining Super Admins after revocation + let current_admins = crate::auth::_get_admin(&env); + let mut super_admin_count = 0; + for admin_addr in current_admins.iter() { + if admin_addr != target && crate::auth::_has_role(&env, &admin_addr, crate::types::Role::SuperAdmin) { + super_admin_count += 1; + } + } + if super_admin_count == 0 { + return Err(Error::CannotRemoveLastSuperAdmin); + } + } + + crate::auth::_revoke_role(&env, &target, role_enum, &admin); + Ok(()) + } + + /// Set multiple roles for an address (replaces all existing roles). + /// + /// Only Super Admin can set multiple roles at once. + pub fn set_roles(env: Env, admin: Address, target: Address, roles_mask: u32) -> Result<(), Error> { + _require_not_destroyed(&env); + admin.require_auth(); + crate::auth::_require_super_admin(&env, &admin); + + // Validate role mask (only allow valid role bits) + if roles_mask & !7 != 0 { // 7 = 0b111 (all three roles) + return Err(Error::InvalidRole); + } + + // Check if this would remove the last Super Admin + if (roles_mask & 7) == 0 && crate::auth::_has_role(&env, &target, crate::types::Role::SuperAdmin) { + // Count remaining Super Admins after removal + let current_admins = crate::auth::_get_admin(&env); + let mut super_admin_count = 0; + for admin_addr in current_admins.iter() { + if admin_addr != target && crate::auth::_has_role(&env, &admin_addr, crate::types::Role::SuperAdmin) { + super_admin_count += 1; + } + } + if super_admin_count == 0 { + return Err(Error::CannotRemoveLastSuperAdmin); + } + } + + crate::auth::_set_roles(&env, &target, roles_mask, &admin); + Ok(()) + } + + /// Check if an address has a specific role. + pub fn has_role(env: Env, address: Address, role: u32) -> bool { + let role_enum = match role { + 1 => crate::types::Role::SecurityManager, + 2 => crate::types::Role::FeeCollector, + 4 => crate::types::Role::PriceManager, + 7 => crate::types::Role::SuperAdmin, + _ => return false, + }; + crate::auth::_has_role(&env, &address, role_enum) + } + + /// Get roles assigned to an address. + pub fn get_roles(env: Env, address: Address) -> u32 { + if let Some(assignment) = crate::auth::_get_role_assignment(&env, &address) { + assignment.roles + } else { + 0 + } + } + + /// Get the role audit log. + pub fn get_role_audit_log(env: Env) -> soroban_sdk::Vec { + crate::auth::_get_role_audit_log(&env) + } + + // ── Provider Management Functions (Security Manager) ─────────────────────── + + /// Add a provider to the whitelist (Security Manager only). + pub fn add_provider(env: Env, security_manager: Address, provider: Address) -> Result<(), Error> { + _require_not_destroyed(&env); + crate::auth::_require_not_frozen(&env); + security_manager.require_auth(); + crate::auth::_require_security_manager(&env, &security_manager); + + crate::auth::_add_provider(&env, &provider); + Ok(()) + } + + /// Remove a provider from the whitelist (Security Manager only). + pub fn remove_provider(env: Env, security_manager: Address, provider: Address) -> Result<(), Error> { + _require_not_destroyed(&env); + crate::auth::_require_not_frozen(&env); + security_manager.require_auth(); + crate::auth::_require_security_manager(&env, &security_manager); + + crate::auth::_remove_provider(&env, &provider); + Ok(()) + } + + /// Set provider weight (Security Manager only). + pub fn set_provider_weight(env: Env, security_manager: Address, provider: Address, weight: u32) -> Result<(), Error> { + _require_not_destroyed(&env); + crate::auth::_require_not_frozen(&env); + security_manager.require_auth(); + crate::auth::_require_security_manager(&env, &security_manager); + + if weight > 100 { + return Err(Error::InvalidWeight); + } + + crate::auth::_set_provider_weight(&env, &provider, weight); + Ok(()) + } + + /// Check if an address is a whitelisted provider. + pub fn is_provider(env: Env, address: Address) -> bool { + crate::auth::_is_provider(&env, &address) + } + + /// Get provider weight. + pub fn get_provider_weight(env: Env, provider: Address) -> u32 { + crate::auth::_get_provider_weight(&env, &provider) + } + + /// Get all active relayers (whitelisted providers). + pub fn get_active_relayers(env: Env) -> soroban_sdk::Vec
{ + crate::auth::_get_active_relayers(&env) + } + + // ── Fee Management Functions (Fee Collector) ────────────────────────── + + /// Set query fee for price oracle calls (Fee Collector only). + pub fn set_query_fee(env: Env, fee_collector: Address, fee_amount: i128) -> Result<(), Error> { + _require_not_destroyed(&env); + fee_collector.require_auth(); + crate::auth::_require_fee_collector(&env, &fee_collector); + + if fee_amount < 0 { + return Err(Error::InvalidPrice); + } + + env.storage().instance().set(&DataKey::QueryFee, &fee_amount); + Ok(()) + } + + /// Get current query fee amount. + pub fn get_query_fee(env: Env) -> i128 { + env.storage() + .instance() + .get(&DataKey::QueryFee) + .unwrap_or(0) + } + + /// Collect accumulated query fees (Fee Collector only). + pub fn collect_fees(env: Env, fee_collector: Address, recipient: Address) -> Result<(), Error> { + _require_not_destroyed(&env); + fee_collector.require_auth(); + crate::auth::_require_fee_collector(&env, &fee_collector); + + // Get current accumulated fees (this would be tracked separately in a real implementation) + let accumulated_fees = env.storage() + .instance() + .get(&DataKey::QueryFee) + .unwrap_or(0); + + if accumulated_fees <= 0 { + return Err(Error::InvalidPrice); // No fees to collect + } + + // Transfer fees to recipient (using TokenContractClient) + // Note: This is a simplified implementation - in production, you'd have proper fee tracking + recipient.require_auth(); + + // Reset accumulated fees to zero + env.storage().instance().set(&DataKey::QueryFee, &0); + + Ok(()) + } } mod asset_symbol; diff --git a/contracts/price-oracle/src/types.rs b/contracts/price-oracle/src/types.rs index a9b095d..7f61f5a 100644 --- a/contracts/price-oracle/src/types.rs +++ b/contracts/price-oracle/src/types.rs @@ -4,7 +4,12 @@ use soroban_sdk::{contracttype, Address, Symbol}; #[allow(clippy::enum_variant_names)] // Soroban SDK generates these names #[contracttype] pub enum DataKey { + /// Legacy admin storage - kept for migration compatibility Admin, + /// RBAC role assignments mapping: Address -> RoleAssignment + RoleAssignment(Address), + /// Role audit log for tracking changes + RoleAuditLog, BaseCurrencyPairs, /// Legacy flat price map — kept for migration compatibility only. PriceData, @@ -231,3 +236,52 @@ pub struct ProposedAction { /// Whether the action has been cancelled. pub cancelled: bool, } + +/// Role types for granular access control using bitmask. +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] +#[repr(u32)] +pub enum Role { + /// No permissions (0) + None = 0, + /// Security Manager - Can manage providers, pause/unpause, emergency operations (1 << 0) + SecurityManager = 1, + /// Fee Collector - Can set and collect query fees (1 << 1) + FeeCollector = 2, + /// Price Manager - Can add/remove assets, set price bounds, manage price settings (1 << 2) + PriceManager = 4, + /// Super Admin - Has all permissions (bitwise OR of all roles) + SuperAdmin = 7, // 1 | 2 | 4 +} + +/// Role assignment entry for tracking user permissions. +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RoleAssignment { + /// The address of the user + pub address: Address, + /// The roles assigned to this user (bitmask) + pub roles: u32, + /// Timestamp when this role was assigned + pub assigned_at: u64, + /// Address that assigned this role (for audit) + pub assigned_by: Address, +} + +/// Role change event for audit logging. +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RoleChangeEvent { + /// The target address whose roles changed + pub target_address: Address, + /// The address that initiated the role change + pub changed_by: Address, + /// Previous roles (bitmask) + pub previous_roles: u32, + /// New roles (bitmask) + pub new_roles: u32, + /// Timestamp of the change + pub timestamp: u64, + /// Description of the change + pub description: soroban_sdk::String, +}