Skip to content

Commit 1f2c9e1

Browse files
authored
fix: wire config freeze gating into set_config (#63)
* fix: wire config freeze gating into set_config - Add ConfigFrozen=16 error variant (InvalidInput renumbered to 17) - Add require_not_frozen() check in set_config() - Expose freeze_config/unfreeze_config as admin-gated methods - Expose is_config_frozen() public query - Add cfg_frz/cfg_unfrz events with event_schema docs - Update get_failure_schema with ConfigFrozen and InvalidInput - Add freeze feature to contract metadata - Rewrite config_freeze.rs tests (was dead code) - Add freeze integration tests (lifecycle, auth, events) - Fix test_pause_rejects_long_reason expected error code * chore: apply cargo fmt * fix: remove unused import in config_freeze tests
1 parent e908600 commit 1f2c9e1

4 files changed

Lines changed: 212 additions & 27 deletions

File tree

apexchainx_calculator/src/config_freeze.rs

Lines changed: 54 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -53,56 +53,87 @@ pub fn is_config_frozen(env: &Env) -> bool {
5353

5454
#[cfg(test)]
5555
mod tests {
56-
use super::*;
57-
use soroban_sdk::{testutils::Address as _, Address, Env};
5856
use crate::{SLACalculatorContract, SLACalculatorContractClient};
57+
use soroban_sdk::{testutils::Address as _, Address, Env};
58+
59+
fn setup() -> (Env, SLACalculatorContractClient<'static>, Address, Address) {
60+
let env = Env::default();
61+
let contract_id = env.register_contract(None, SLACalculatorContract);
62+
let client = SLACalculatorContractClient::new(&env, &contract_id);
63+
let admin = Address::generate(&env);
64+
let operator = Address::generate(&env);
65+
client.initialize(&admin, &operator);
66+
(env, client, admin, operator)
67+
}
5968

6069
#[test]
6170
fn test_config_unfrozen_by_default() {
62-
let env = Env::default();
63-
assert!(!is_config_frozen(&env));
71+
let (_env, client, _admin, _operator) = setup();
72+
assert!(!client.is_config_frozen());
6473
}
6574

6675
#[test]
6776
fn test_freeze_and_query() {
68-
let env = Env::default();
69-
freeze_config(&env);
70-
assert!(is_config_frozen(&env));
77+
let (_env, client, admin, _operator) = setup();
78+
client.freeze_config(&admin);
79+
assert!(client.is_config_frozen());
7180
}
7281

7382
#[test]
7483
fn test_unfreeze_restores_mutable_state() {
75-
let env = Env::default();
76-
freeze_config(&env);
77-
unfreeze_config(&env);
78-
assert!(!is_config_frozen(&env));
84+
let (_env, client, admin, _operator) = setup();
85+
client.freeze_config(&admin);
86+
client.unfreeze_config(&admin);
87+
assert!(!client.is_config_frozen());
7988
}
8089

8190
#[test]
82-
fn test_frozen_config_blocks_set_config() {
83-
let env = Env::default();
84-
let contract_id = env.register_contract(None, SLACalculatorContract);
85-
let client = SLACalculatorContractClient::new(&env, &contract_id);
86-
let admin = Address::generate(&env);
87-
let operator = Address::generate(&env);
88-
client.initialize(&admin, &operator);
89-
freeze_config(&env);
90-
assert!(is_config_frozen(&env));
91+
fn test_frozen_config_flag() {
92+
let (_env, client, admin, _operator) = setup();
93+
client.freeze_config(&admin);
94+
assert!(client.is_config_frozen());
95+
}
96+
97+
#[test]
98+
#[should_panic(expected = "#16")]
99+
fn test_set_config_fails_when_frozen() {
100+
let (_env, client, admin, _operator) = setup();
101+
client.freeze_config(&admin);
102+
client.set_config(
103+
&admin,
104+
&soroban_sdk::symbol_short!("critical"),
105+
&15,
106+
&100,
107+
&750,
108+
);
109+
}
110+
111+
#[test]
112+
fn test_unfreeze_allows_set_config() {
113+
let (_env, client, admin, _operator) = setup();
114+
client.freeze_config(&admin);
115+
assert!(client.is_config_frozen());
116+
client.unfreeze_config(&admin);
117+
assert!(!client.is_config_frozen());
118+
client.set_config(
119+
&admin,
120+
&soroban_sdk::symbol_short!("critical"),
121+
&15,
122+
&100,
123+
&750,
124+
);
91125
}
92126

93127
#[test]
94128
#[should_panic]
95129
fn test_stranger_cannot_set_config_when_unfrozen() {
96-
// Verify that even without a freeze, an unauthorized caller cannot
97-
// mutate config (admin-gated role check, not freeze-gated logic).
98130
let env = Env::default();
99131
let contract_id = env.register_contract(None, SLACalculatorContract);
100132
let client = SLACalculatorContractClient::new(&env, &contract_id);
101133
let admin = Address::generate(&env);
102134
let operator = Address::generate(&env);
103135
client.initialize(&admin, &operator);
104136
let stranger = Address::generate(&env);
105-
// stranger does not hold the admin role – require_admin must reject
106137
client.set_config(
107138
&stranger,
108139
&soroban_sdk::symbol_short!("critical"),

apexchainx_calculator/src/event_schema.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,16 @@
9292
//! - topic[2]: caller Address
9393
//! - payload: ()
9494
//!
95+
//! ## cfg_frz (`cfg_frz`)
96+
//! Emitted when the configuration is frozen by admin.
97+
//! - topic[2]: caller Address
98+
//! - payload: ()
99+
//!
100+
//! ## cfg_unfrz (`cfg_unfrz`)
101+
//! Emitted when the configuration is unfrozen by admin.
102+
//! - topic[2]: caller Address
103+
//! - payload: ()
104+
//!
95105
//! # Schema Versioning
96106
//!
97107
//! Breaking changes (field removal, type changes, reordering) MUST increment
@@ -122,6 +132,8 @@ pub const EVENT_ADMIN_REN: Symbol = symbol_short!("adm_ren");
122132
pub const EVENT_OP_PROP: Symbol = symbol_short!("op_prop");
123133
pub const EVENT_OP_ACC: Symbol = symbol_short!("op_acc");
124134
pub const EVENT_OP_CAN: Symbol = symbol_short!("op_can");
135+
pub const EVENT_CONFIG_FREEZE: Symbol = symbol_short!("cfg_frz");
136+
pub const EVENT_CONFIG_UNFREEZE: Symbol = symbol_short!("cfg_unfrz");
125137

126138
/// Returns the canonical event version string for consumer documentation.
127139
pub fn current_event_version() -> Symbol {
@@ -156,6 +168,8 @@ mod tests {
156168
EVENT_OP_PROP,
157169
EVENT_OP_ACC,
158170
EVENT_OP_CAN,
171+
EVENT_CONFIG_FREEZE,
172+
EVENT_CONFIG_UNFREEZE,
159173
];
160174

161175
for i in 0..names.len() {

apexchainx_calculator/src/lib.rs

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ pub struct SLACalculatorContract;
1313
mod tests;
1414

1515
pub mod config_bundle;
16+
pub mod config_freeze;
1617
pub mod config_metadata;
1718
pub mod coordination_harness;
1819
pub mod cross_contract_safety;
@@ -200,6 +201,12 @@ const EVENT_OP_ACC: Symbol = symbol_short!("op_acc");
200201
/// Emitted when a pending operator proposal is cancelled. (SC-024)
201202
const EVENT_OP_CAN: Symbol = symbol_short!("op_can");
202203

204+
/// Emitted when the configuration is frozen by admin.
205+
const EVENT_CONFIG_FREEZE: Symbol = symbol_short!("cfg_frz");
206+
207+
/// Emitted when the configuration is unfrozen by admin.
208+
const EVENT_CONFIG_UNFREEZE: Symbol = symbol_short!("cfg_unfrz");
209+
203210
/// Canonical event version symbol used by all events.
204211
const EVENT_VERSION: Symbol = symbol_short!("v1");
205212

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

258267
// -----------------------------------------------------------------------
@@ -832,6 +841,38 @@ impl SLACalculatorContract {
832841
Ok(env.storage().instance().get(&PAUSE_INFO_KEY))
833842
}
834843

844+
// -------------------------------------------------------------------
845+
// Config freeze / unfreeze (admin only)
846+
// -------------------------------------------------------------------
847+
848+
/// Freezes the configuration, blocking further config updates.
849+
/// Admin only. Emits a `cfg_frz` event.
850+
pub fn freeze_config(env: Env, caller: Address) -> Result<(), SLAError> {
851+
Self::check_version(&env)?;
852+
Self::require_admin(&env, &caller)?;
853+
config_freeze::freeze_config(&env);
854+
env.events()
855+
.publish((EVENT_CONFIG_FREEZE, EVENT_VERSION, caller), ());
856+
Ok(())
857+
}
858+
859+
/// Unfreezes the configuration, re-allowing config updates.
860+
/// Admin only. Emits a `cfg_unfrz` event.
861+
pub fn unfreeze_config(env: Env, caller: Address) -> Result<(), SLAError> {
862+
Self::check_version(&env)?;
863+
Self::require_admin(&env, &caller)?;
864+
config_freeze::unfreeze_config(&env);
865+
env.events()
866+
.publish((EVENT_CONFIG_UNFREEZE, EVENT_VERSION, caller), ());
867+
Ok(())
868+
}
869+
870+
/// Returns `true` when the configuration is currently frozen.
871+
pub fn is_config_frozen(env: Env) -> Result<bool, SLAError> {
872+
Self::check_version(&env)?;
873+
Ok(config_freeze::is_config_frozen(&env))
874+
}
875+
835876
// -------------------------------------------------------------------
836877
// Config management (admin only) #28
837878
// -------------------------------------------------------------------
@@ -846,6 +887,7 @@ impl SLACalculatorContract {
846887
) -> Result<(), SLAError> {
847888
Self::check_version(&env)?;
848889
Self::require_admin(&env, &caller)?; // #28 – admin role enforced
890+
Self::require_not_frozen(&env)?;
849891

850892
// #70 – Validate configuration parameters
851893
Self::validate_config(
@@ -962,7 +1004,7 @@ impl SLACalculatorContract {
9621004

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

9871031
for (code, label, description) in entries {
@@ -1049,6 +1093,7 @@ impl SLACalculatorContract {
10491093
features.push_back(symbol_short!("safe_call"));
10501094
features.push_back(symbol_short!("ver_nego"));
10511095
features.push_back(symbol_short!("corr_id"));
1096+
features.push_back(symbol_short!("freeze"));
10521097

10531098
Ok(ContractMetadata {
10541099
contract_name: symbol_short!("sla_calc"),
@@ -1311,6 +1356,13 @@ impl SLACalculatorContract {
13111356
Ok(())
13121357
}
13131358

1359+
fn require_not_frozen(env: &Env) -> Result<(), SLAError> {
1360+
if config_freeze::is_config_frozen(env) {
1361+
return Err(SLAError::ConfigFrozen);
1362+
}
1363+
Ok(())
1364+
}
1365+
13141366
/// #70 – Validates configuration parameters to ensure safe and meaningful values.
13151367
fn validate_config(
13161368
severity: &Symbol,

apexchainx_calculator/src/tests.rs

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,94 @@ fn test_calculate_sla_works_after_unpause() {
444444
assert_eq!(result.status, symbol_short!("met"));
445445
}
446446

447+
// ============================================================
448+
// Config freeze / unfreeze
449+
// ============================================================
450+
451+
#[test]
452+
fn test_config_starts_unfrozen() {
453+
let (_env, client, _actors) = setup();
454+
assert!(!client.is_config_frozen());
455+
}
456+
457+
#[test]
458+
fn test_admin_can_freeze_and_unfreeze() {
459+
let (_env, client, actors) = setup();
460+
assert!(!client.is_config_frozen());
461+
462+
client.freeze_config(&actors.admin);
463+
assert!(client.is_config_frozen());
464+
465+
client.unfreeze_config(&actors.admin);
466+
assert!(!client.is_config_frozen());
467+
}
468+
469+
#[test]
470+
#[should_panic]
471+
fn test_operator_cannot_freeze() {
472+
let (_env, client, actors) = setup();
473+
client.freeze_config(&actors.operator);
474+
}
475+
476+
#[test]
477+
#[should_panic]
478+
fn test_stranger_cannot_freeze() {
479+
let (_env, client, actors) = setup();
480+
client.freeze_config(&actors.stranger);
481+
}
482+
483+
#[test]
484+
#[should_panic]
485+
fn test_operator_cannot_unfreeze() {
486+
let (_env, client, actors) = setup();
487+
client.freeze_config(&actors.admin);
488+
client.unfreeze_config(&actors.operator);
489+
}
490+
491+
#[test]
492+
#[should_panic]
493+
fn test_set_config_blocked_when_frozen() {
494+
let (_env, client, actors) = setup();
495+
client.freeze_config(&actors.admin);
496+
client.set_config(&actors.admin, &symbol_short!("critical"), &15, &100, &750);
497+
}
498+
499+
#[test]
500+
fn test_set_config_works_after_unfreeze() {
501+
let (_env, client, actors) = setup();
502+
client.freeze_config(&actors.admin);
503+
client.unfreeze_config(&actors.admin);
504+
// Should not panic after unfreeze
505+
client.set_config(&actors.admin, &symbol_short!("critical"), &15, &100, &750);
506+
}
507+
508+
#[test]
509+
fn test_freeze_emits_event() {
510+
let (env, client, actors) = setup();
511+
client.freeze_config(&actors.admin);
512+
let events = env.events().all();
513+
let (_, topics, _) = events.last().unwrap();
514+
assert_eq!(topics.len(), 3);
515+
let name: Symbol = topics.get(0).unwrap().try_into_val(&env).unwrap();
516+
let version: Symbol = topics.get(1).unwrap().try_into_val(&env).unwrap();
517+
assert_eq!(name, symbol_short!("cfg_frz"));
518+
assert_eq!(version, symbol_short!("v1"));
519+
}
520+
521+
#[test]
522+
fn test_unfreeze_emits_event() {
523+
let (env, client, actors) = setup();
524+
client.freeze_config(&actors.admin);
525+
client.unfreeze_config(&actors.admin);
526+
let events = env.events().all();
527+
let (_, topics, _) = events.last().unwrap();
528+
assert_eq!(topics.len(), 3);
529+
let name: Symbol = topics.get(0).unwrap().try_into_val(&env).unwrap();
530+
let version: Symbol = topics.get(1).unwrap().try_into_val(&env).unwrap();
531+
assert_eq!(name, symbol_short!("cfg_unfrz"));
532+
assert_eq!(version, symbol_short!("v1"));
533+
}
534+
447535
// ============================================================
448536
// SLA business logic correctness
449537
// ============================================================
@@ -1574,7 +1662,7 @@ fn test_get_contract_metadata_returns_expected_fields() {
15741662
assert_eq!(meta.storage_version, 1);
15751663
assert_eq!(meta.result_schema_version, 1);
15761664
assert_eq!(meta.supported_severities.len(), 4);
1577-
assert_eq!(meta.features.len(), 9);
1665+
assert_eq!(meta.features.len(), 10);
15781666
}
15791667

15801668
#[test]
@@ -1837,7 +1925,7 @@ fn test_pause_stores_reason_and_timestamp() {
18371925
}
18381926

18391927
#[test]
1840-
#[should_panic(expected = "#16")]
1928+
#[should_panic(expected = "#17")]
18411929
fn test_pause_rejects_long_reason() {
18421930
let (env, client, actors) = setup();
18431931
// 257-byte reason exceeds MAX_REASON_LEN (256)

0 commit comments

Comments
 (0)