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
77 changes: 54 additions & 23 deletions apexchainx_calculator/src/config_freeze.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,56 +53,87 @@ pub fn is_config_frozen(env: &Env) -> bool {

#[cfg(test)]
mod tests {
use super::*;
use soroban_sdk::{testutils::Address as _, Address, Env};
use crate::{SLACalculatorContract, SLACalculatorContractClient};
use soroban_sdk::{testutils::Address as _, Address, Env};

fn setup() -> (Env, SLACalculatorContractClient<'static>, Address, Address) {
let env = Env::default();
let contract_id = env.register_contract(None, SLACalculatorContract);
let client = SLACalculatorContractClient::new(&env, &contract_id);
let admin = Address::generate(&env);
let operator = Address::generate(&env);
client.initialize(&admin, &operator);
(env, client, admin, operator)
}

#[test]
fn test_config_unfrozen_by_default() {
let env = Env::default();
assert!(!is_config_frozen(&env));
let (_env, client, _admin, _operator) = setup();
assert!(!client.is_config_frozen());
}

#[test]
fn test_freeze_and_query() {
let env = Env::default();
freeze_config(&env);
assert!(is_config_frozen(&env));
let (_env, client, admin, _operator) = setup();
client.freeze_config(&admin);
assert!(client.is_config_frozen());
}

#[test]
fn test_unfreeze_restores_mutable_state() {
let env = Env::default();
freeze_config(&env);
unfreeze_config(&env);
assert!(!is_config_frozen(&env));
let (_env, client, admin, _operator) = setup();
client.freeze_config(&admin);
client.unfreeze_config(&admin);
assert!(!client.is_config_frozen());
}

#[test]
fn test_frozen_config_blocks_set_config() {
let env = Env::default();
let contract_id = env.register_contract(None, SLACalculatorContract);
let client = SLACalculatorContractClient::new(&env, &contract_id);
let admin = Address::generate(&env);
let operator = Address::generate(&env);
client.initialize(&admin, &operator);
freeze_config(&env);
assert!(is_config_frozen(&env));
fn test_frozen_config_flag() {
let (_env, client, admin, _operator) = setup();
client.freeze_config(&admin);
assert!(client.is_config_frozen());
}

#[test]
#[should_panic(expected = "#16")]
fn test_set_config_fails_when_frozen() {
let (_env, client, admin, _operator) = setup();
client.freeze_config(&admin);
client.set_config(
&admin,
&soroban_sdk::symbol_short!("critical"),
&15,
&100,
&750,
);
}

#[test]
fn test_unfreeze_allows_set_config() {
let (_env, client, admin, _operator) = setup();
client.freeze_config(&admin);
assert!(client.is_config_frozen());
client.unfreeze_config(&admin);
assert!(!client.is_config_frozen());
client.set_config(
&admin,
&soroban_sdk::symbol_short!("critical"),
&15,
&100,
&750,
);
}

#[test]
#[should_panic]
fn test_stranger_cannot_set_config_when_unfrozen() {
// Verify that even without a freeze, an unauthorized caller cannot
// mutate config (admin-gated role check, not freeze-gated logic).
let env = Env::default();
let contract_id = env.register_contract(None, SLACalculatorContract);
let client = SLACalculatorContractClient::new(&env, &contract_id);
let admin = Address::generate(&env);
let operator = Address::generate(&env);
client.initialize(&admin, &operator);
let stranger = Address::generate(&env);
// stranger does not hold the admin role – require_admin must reject
client.set_config(
&stranger,
&soroban_sdk::symbol_short!("critical"),
Expand Down
14 changes: 14 additions & 0 deletions apexchainx_calculator/src/event_schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,16 @@
//! - topic[2]: caller Address
//! - payload: ()
//!
//! ## cfg_frz (`cfg_frz`)
//! Emitted when the configuration is frozen by admin.
//! - topic[2]: caller Address
//! - payload: ()
//!
//! ## cfg_unfrz (`cfg_unfrz`)
//! Emitted when the configuration is unfrozen by admin.
//! - topic[2]: caller Address
//! - payload: ()
//!
//! # Schema Versioning
//!
//! Breaking changes (field removal, type changes, reordering) MUST increment
Expand Down Expand Up @@ -122,6 +132,8 @@ pub const EVENT_ADMIN_REN: Symbol = symbol_short!("adm_ren");
pub const EVENT_OP_PROP: Symbol = symbol_short!("op_prop");
pub const EVENT_OP_ACC: Symbol = symbol_short!("op_acc");
pub const EVENT_OP_CAN: Symbol = symbol_short!("op_can");
pub const EVENT_CONFIG_FREEZE: Symbol = symbol_short!("cfg_frz");
pub const EVENT_CONFIG_UNFREEZE: Symbol = symbol_short!("cfg_unfrz");

/// Returns the canonical event version string for consumer documentation.
pub fn current_event_version() -> Symbol {
Expand Down Expand Up @@ -156,6 +168,8 @@ mod tests {
EVENT_OP_PROP,
EVENT_OP_ACC,
EVENT_OP_CAN,
EVENT_CONFIG_FREEZE,
EVENT_CONFIG_UNFREEZE,
];

for i in 0..names.len() {
Expand Down
56 changes: 54 additions & 2 deletions apexchainx_calculator/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pub struct SLACalculatorContract;
mod tests;

pub mod config_bundle;
pub mod config_freeze;
pub mod config_metadata;
pub mod coordination_harness;
pub mod cross_contract_safety;
Expand Down Expand Up @@ -200,6 +201,12 @@ const EVENT_OP_ACC: Symbol = symbol_short!("op_acc");
/// Emitted when a pending operator proposal is cancelled. (SC-024)
const EVENT_OP_CAN: Symbol = symbol_short!("op_can");

/// Emitted when the configuration is frozen by admin.
const EVENT_CONFIG_FREEZE: Symbol = symbol_short!("cfg_frz");

/// Emitted when the configuration is unfrozen by admin.
const EVENT_CONFIG_UNFREEZE: Symbol = symbol_short!("cfg_unfrz");

/// Canonical event version symbol used by all events.
const EVENT_VERSION: Symbol = symbol_short!("v1");

Expand Down Expand Up @@ -251,8 +258,10 @@ pub enum SLAError {
InvalidPenaltyAmount = 14,
/// Computed reward amount is invalid (e.g., zero or negative). (SC-W5-046)
InvalidRewardAmount = 15,
/// Configuration is frozen — config changes are blocked.
ConfigFrozen = 16,
/// Input parameter violates documented constraints (e.g., reason too long). (#68)
InvalidInput = 16,
InvalidInput = 17,
}

// -----------------------------------------------------------------------
Expand Down Expand Up @@ -832,6 +841,38 @@ impl SLACalculatorContract {
Ok(env.storage().instance().get(&PAUSE_INFO_KEY))
}

// -------------------------------------------------------------------
// Config freeze / unfreeze (admin only)
// -------------------------------------------------------------------

/// Freezes the configuration, blocking further config updates.
/// Admin only. Emits a `cfg_frz` event.
pub fn freeze_config(env: Env, caller: Address) -> Result<(), SLAError> {
Self::check_version(&env)?;
Self::require_admin(&env, &caller)?;
config_freeze::freeze_config(&env);
env.events()
.publish((EVENT_CONFIG_FREEZE, EVENT_VERSION, caller), ());
Ok(())
}

/// Unfreezes the configuration, re-allowing config updates.
/// Admin only. Emits a `cfg_unfrz` event.
pub fn unfreeze_config(env: Env, caller: Address) -> Result<(), SLAError> {
Self::check_version(&env)?;
Self::require_admin(&env, &caller)?;
config_freeze::unfreeze_config(&env);
env.events()
.publish((EVENT_CONFIG_UNFREEZE, EVENT_VERSION, caller), ());
Ok(())
}

/// Returns `true` when the configuration is currently frozen.
pub fn is_config_frozen(env: Env) -> Result<bool, SLAError> {
Self::check_version(&env)?;
Ok(config_freeze::is_config_frozen(&env))
}

// -------------------------------------------------------------------
// Config management (admin only) #28
// -------------------------------------------------------------------
Expand All @@ -846,6 +887,7 @@ impl SLACalculatorContract {
) -> Result<(), SLAError> {
Self::check_version(&env)?;
Self::require_admin(&env, &caller)?; // #28 – admin role enforced
Self::require_not_frozen(&env)?;

// #70 – Validate configuration parameters
Self::validate_config(
Expand Down Expand Up @@ -962,7 +1004,7 @@ impl SLACalculatorContract {

// Emit in numeric order for deterministic consumption
// All descriptions must be <= 32 bytes (Soroban Symbol constraint)
let entries: [(u32, &str, &str); 15] = [
let entries: [(u32, &str, &str); 17] = [
(1, "AlreadyInitialized", "Contract already initialized"),
(2, "NotInitialized", "Contract not yet initialized"),
(3, "Unauthorized", "Caller lacks required role"),
Expand All @@ -982,6 +1024,8 @@ impl SLACalculatorContract {
(13, "DuplicateOutageInput", "Duplicate outage input"),
(14, "InvalidPenaltyAmount", "Invalid penalty amount"),
(15, "InvalidRewardAmount", "Invalid reward amount"),
(16, "ConfigFrozen", "Configuration is frozen"),
(17, "InvalidInput", "Invalid input parameter"),
];

for (code, label, description) in entries {
Expand Down Expand Up @@ -1049,6 +1093,7 @@ impl SLACalculatorContract {
features.push_back(symbol_short!("safe_call"));
features.push_back(symbol_short!("ver_nego"));
features.push_back(symbol_short!("corr_id"));
features.push_back(symbol_short!("freeze"));

Ok(ContractMetadata {
contract_name: symbol_short!("sla_calc"),
Expand Down Expand Up @@ -1311,6 +1356,13 @@ impl SLACalculatorContract {
Ok(())
}

fn require_not_frozen(env: &Env) -> Result<(), SLAError> {
if config_freeze::is_config_frozen(env) {
return Err(SLAError::ConfigFrozen);
}
Ok(())
}

/// #70 – Validates configuration parameters to ensure safe and meaningful values.
fn validate_config(
severity: &Symbol,
Expand Down
92 changes: 90 additions & 2 deletions apexchainx_calculator/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,94 @@ fn test_calculate_sla_works_after_unpause() {
assert_eq!(result.status, symbol_short!("met"));
}

// ============================================================
// Config freeze / unfreeze
// ============================================================

#[test]
fn test_config_starts_unfrozen() {
let (_env, client, _actors) = setup();
assert!(!client.is_config_frozen());
}

#[test]
fn test_admin_can_freeze_and_unfreeze() {
let (_env, client, actors) = setup();
assert!(!client.is_config_frozen());

client.freeze_config(&actors.admin);
assert!(client.is_config_frozen());

client.unfreeze_config(&actors.admin);
assert!(!client.is_config_frozen());
}

#[test]
#[should_panic]
fn test_operator_cannot_freeze() {
let (_env, client, actors) = setup();
client.freeze_config(&actors.operator);
}

#[test]
#[should_panic]
fn test_stranger_cannot_freeze() {
let (_env, client, actors) = setup();
client.freeze_config(&actors.stranger);
}

#[test]
#[should_panic]
fn test_operator_cannot_unfreeze() {
let (_env, client, actors) = setup();
client.freeze_config(&actors.admin);
client.unfreeze_config(&actors.operator);
}

#[test]
#[should_panic]
fn test_set_config_blocked_when_frozen() {
let (_env, client, actors) = setup();
client.freeze_config(&actors.admin);
client.set_config(&actors.admin, &symbol_short!("critical"), &15, &100, &750);
}

#[test]
fn test_set_config_works_after_unfreeze() {
let (_env, client, actors) = setup();
client.freeze_config(&actors.admin);
client.unfreeze_config(&actors.admin);
// Should not panic after unfreeze
client.set_config(&actors.admin, &symbol_short!("critical"), &15, &100, &750);
}

#[test]
fn test_freeze_emits_event() {
let (env, client, actors) = setup();
client.freeze_config(&actors.admin);
let events = env.events().all();
let (_, topics, _) = events.last().unwrap();
assert_eq!(topics.len(), 3);
let name: Symbol = topics.get(0).unwrap().try_into_val(&env).unwrap();
let version: Symbol = topics.get(1).unwrap().try_into_val(&env).unwrap();
assert_eq!(name, symbol_short!("cfg_frz"));
assert_eq!(version, symbol_short!("v1"));
}

#[test]
fn test_unfreeze_emits_event() {
let (env, client, actors) = setup();
client.freeze_config(&actors.admin);
client.unfreeze_config(&actors.admin);
let events = env.events().all();
let (_, topics, _) = events.last().unwrap();
assert_eq!(topics.len(), 3);
let name: Symbol = topics.get(0).unwrap().try_into_val(&env).unwrap();
let version: Symbol = topics.get(1).unwrap().try_into_val(&env).unwrap();
assert_eq!(name, symbol_short!("cfg_unfrz"));
assert_eq!(version, symbol_short!("v1"));
}

// ============================================================
// SLA business logic correctness
// ============================================================
Expand Down Expand Up @@ -1574,7 +1662,7 @@ fn test_get_contract_metadata_returns_expected_fields() {
assert_eq!(meta.storage_version, 1);
assert_eq!(meta.result_schema_version, 1);
assert_eq!(meta.supported_severities.len(), 4);
assert_eq!(meta.features.len(), 9);
assert_eq!(meta.features.len(), 10);
}

#[test]
Expand Down Expand Up @@ -1837,7 +1925,7 @@ fn test_pause_stores_reason_and_timestamp() {
}

#[test]
#[should_panic(expected = "#16")]
#[should_panic(expected = "#17")]
fn test_pause_rejects_long_reason() {
let (env, client, actors) = setup();
// 257-byte reason exceeds MAX_REASON_LEN (256)
Expand Down
Loading