From 3dbeefd8f2251a285504013fb04e4ed3a177690a Mon Sep 17 00:00:00 2001 From: Sakariyah Abdulhazeem <150973162+zeemscript@users.noreply.github.com> Date: Mon, 24 Aug 2026 20:37:37 +0100 Subject: [PATCH 1/8] test: verify protocol initialisation sets and preserves all state fields - full init sequence makes admin, fee config, protocol fee bps, treasury, key price readable; is_protocol_config_initialized true - pre-init views return None/false defaults with FeeConfigNotSet / KeyPriceNotSet errors - repeated initialisation is idempotent: all setters Ok, every field unchanged, exactly one ContractInitialized event - is_protocol_config_initialized tracks fee-config presence only Note: contract has no already_initialised guard by design (idempotent setters), so second-init assertions cover state invariance instead. Closes #723 --- .../tests/protocol_initialisation_state.rs | 216 ++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100644 creator-keys/tests/protocol_initialisation_state.rs diff --git a/creator-keys/tests/protocol_initialisation_state.rs b/creator-keys/tests/protocol_initialisation_state.rs new file mode 100644 index 0000000..e57208b --- /dev/null +++ b/creator-keys/tests/protocol_initialisation_state.rs @@ -0,0 +1,216 @@ +//! Tests for protocol initialisation state (accesslayerorg/accesslayer-contracts#723). +//! +//! There is no single `initialise` entrypoint: protocol initialisation is the +//! setter sequence `set_protocol_admin`, `set_key_price`, `set_fee_config`, +//! `set_treasury_address`. Verifies that: +//! - After a full initialisation sequence every state field reads back exactly +//! what was set (protocol admin, fee config bps split, protocol fee bps, +//! treasury address, key price) and `is_protocol_config_initialized` is true. +//! - Before any initialisation all views report unset defaults (`None` / +//! `false`) and the key-price storage key is absent. +//! - Repeating the full setter sequence with identical values succeeds without +//! error, leaves every observable field unchanged, keeps +//! `is_protocol_config_initialized` true, and emits the `CONTRACT_INITIALIZED` +//! event exactly once across both passes (idempotent re-initialisation). +//! - `is_protocol_config_initialized` reflects fee-config presence only: it +//! stays false after `set_protocol_admin` alone, and becomes true after +//! `set_fee_config` even while treasury and key price remain unset. + +mod contract_test_env; + +use contract_test_env::{assert_storage_absent, register_creator_keys, test_env_with_auths}; +use creator_keys::{constants, events, fee, ContractError, CreatorKeysContractClient}; +use soroban_sdk::{ + testutils::{Address as _, Events}, + Address, Env, IntoVal, Symbol, +}; + +/// Creator share of the fee split used by these fixtures. +const CREATOR_BPS: u32 = 8_500; + +/// Protocol share of the fee split used by these fixtures. +const PROTOCOL_BPS: u32 = 1_500; + +/// Key price written during initialisation. +const KEY_PRICE: i128 = 123_456; + +/// Runs the four-setter protocol initialisation sequence with the given admin. +fn run_initialisation( + client: &CreatorKeysContractClient<'_>, + admin: &Address, + treasury: &Address, +) { + client.set_protocol_admin(admin, admin); + client.set_key_price(admin, &KEY_PRICE); + client.set_fee_config(admin, &CREATOR_BPS, &PROTOCOL_BPS); + client.set_treasury_address(admin, treasury); +} + +/// Counts `ContractInitializedEvent` records currently captured in the environment. +fn count_initialization_events(env: &Env) -> usize { + env.events() + .all() + .iter() + .filter(|(_, topics, _)| { + topics + .get(events::TOPIC_EVENT_NAME_INDEX) + .map(|v| { + let name: Symbol = v.into_val(env); + name == events::CONTRACT_INITIALIZED_EVENT_NAME + }) + .unwrap_or(false) + }) + .count() +} + +/// After the full init sequence every state field equals the value that was set. +#[test] +fn test_full_initialization_sets_all_state_fields() { + let env = test_env_with_auths(); + let (client, contract_id) = register_creator_keys(&env); + + let admin = Address::generate(&env); + let treasury = Address::generate(&env); + + run_initialisation(&client, &admin, &treasury); + + assert_eq!(client.get_protocol_admin(), Some(admin.clone())); + assert_eq!( + client.get_fee_config(), + Some(fee::FeeConfig { + creator_bps: CREATOR_BPS, + protocol_bps: PROTOCOL_BPS, + }) + ); + assert_eq!( + client.get_protocol_fee_bps(), + PROTOCOL_BPS, + "protocol fee bps must match the configured split" + ); + assert_eq!(client.get_treasury_address(), Some(treasury)); + assert!(client.is_protocol_config_initialized()); + + let price_reader = Address::generate(&env); + assert_eq!( + client.try_query_price(&price_reader, &0), + Ok(Ok(KEY_PRICE)), + "query_price must surface the initialised base key price" + ); + + let stored_price: Option = env + .as_contract(&contract_id, || { + env.storage().persistent().get(&constants::storage::KEY_PRICE) + }); + assert_eq!( + stored_price, + Some(KEY_PRICE), + "KEY_PRICE storage must hold the initialised price" + ); +} + +/// Before initialisation all views report their unset defaults and key price is absent. +#[test] +fn test_views_report_unset_defaults_before_initialization() { + let env = test_env_with_auths(); + let (client, contract_id) = register_creator_keys(&env); + + assert_eq!(client.get_protocol_admin(), None); + assert_eq!(client.get_fee_config(), None); + assert_eq!(client.get_treasury_address(), None); + assert!(!client.is_protocol_config_initialized()); + + assert_eq!( + client.try_get_protocol_fee_bps(), + Err(Ok(ContractError::FeeConfigNotSet)), + "fee bps view must fail with FeeConfigNotSet before initialisation" + ); + + let reader = Address::generate(&env); + assert_eq!( + client.try_query_price(&reader, &0), + Err(Ok(ContractError::KeyPriceNotSet)), + "key price view must fail with KeyPriceNotSet before initialisation" + ); + + env.as_contract(&contract_id, || { + assert_storage_absent(&env, &constants::storage::KEY_PRICE); + }); +} + +/// Repeating the full init sequence with identical values succeeds, mutates nothing, +/// and emits CONTRACT_INITIALIZED exactly once across both passes. +#[test] +fn test_repeated_initialization_is_idempotent_and_state_invariant() { + let env = test_env_with_auths(); + let (client, _) = register_creator_keys(&env); + + let admin = Address::generate(&env); + let treasury = Address::generate(&env); + + run_initialisation(&client, &admin, &treasury); + + let admin_before = client.get_protocol_admin(); + let config_before = client.get_fee_config(); + let bps_before = client.get_protocol_fee_bps(); + let treasury_before = client.get_treasury_address(); + assert_eq!(count_initialization_events(&env), 1); + + assert_eq!( + client.try_set_protocol_admin(&admin, &admin), + Ok(Ok(())), + "re-setting the same admin must succeed" + ); + assert_eq!( + client.try_set_key_price(&admin, &KEY_PRICE), + Ok(Ok(())), + "re-setting the same key price must succeed" + ); + assert_eq!( + client.try_set_fee_config(&admin, &CREATOR_BPS, &PROTOCOL_BPS), + Ok(Ok(())), + "re-setting the same fee config must succeed" + ); + client.set_treasury_address(&admin, &treasury); + + assert_eq!(client.get_protocol_admin(), admin_before); + assert_eq!(client.get_fee_config(), config_before); + assert_eq!(client.get_protocol_fee_bps(), bps_before); + assert_eq!(client.get_treasury_address(), treasury_before); + assert!(client.is_protocol_config_initialized()); + assert_eq!( + count_initialization_events(&env), + 1, + "the idempotent second pass must not emit another init event" + ); +} + +/// is_protocol_config_initialized tracks fee-config presence, not the other setters. +#[test] +fn test_is_protocol_config_initialized_reflects_fee_config_presence_only() { + let env = test_env_with_auths(); + let (client, _) = register_creator_keys(&env); + + let admin = Address::generate(&env); + assert!(!client.is_protocol_config_initialized()); + + client.set_protocol_admin(&admin, &admin); + assert!( + !client.is_protocol_config_initialized(), + "admin setup alone must not mark the protocol configuration initialized" + ); + + client.set_fee_config(&admin, &CREATOR_BPS, &PROTOCOL_BPS); + assert!(client.is_protocol_config_initialized()); + + assert_eq!( + client.get_treasury_address(), + None, + "treasury may remain unset once the fee config exists" + ); + let reader = Address::generate(&env); + assert_eq!( + client.try_query_price(&reader, &0), + Err(Ok(ContractError::KeyPriceNotSet)), + "key price may remain unset once the fee config exists" + ); +} From eb6ade69cea43200f45e655bdc158304c340296c Mon Sep 17 00:00:00 2001 From: Sakariyah Abdulhazeem <150973162+zeemscript@users.noreply.github.com> Date: Mon, 24 Aug 2026 20:41:37 +0100 Subject: [PATCH 2/8] style: apply rustfmt to protocol initialisation tests --- .../tests/protocol_initialisation_state.rs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/creator-keys/tests/protocol_initialisation_state.rs b/creator-keys/tests/protocol_initialisation_state.rs index e57208b..314b784 100644 --- a/creator-keys/tests/protocol_initialisation_state.rs +++ b/creator-keys/tests/protocol_initialisation_state.rs @@ -35,11 +35,7 @@ const PROTOCOL_BPS: u32 = 1_500; const KEY_PRICE: i128 = 123_456; /// Runs the four-setter protocol initialisation sequence with the given admin. -fn run_initialisation( - client: &CreatorKeysContractClient<'_>, - admin: &Address, - treasury: &Address, -) { +fn run_initialisation(client: &CreatorKeysContractClient<'_>, admin: &Address, treasury: &Address) { client.set_protocol_admin(admin, admin); client.set_key_price(admin, &KEY_PRICE); client.set_fee_config(admin, &CREATOR_BPS, &PROTOCOL_BPS); @@ -97,10 +93,11 @@ fn test_full_initialization_sets_all_state_fields() { "query_price must surface the initialised base key price" ); - let stored_price: Option = env - .as_contract(&contract_id, || { - env.storage().persistent().get(&constants::storage::KEY_PRICE) - }); + let stored_price: Option = env.as_contract(&contract_id, || { + env.storage() + .persistent() + .get(&constants::storage::KEY_PRICE) + }); assert_eq!( stored_price, Some(KEY_PRICE), From 3e4b60d354faaf17dcdc654fd644cd70655c6ecf Mon Sep 17 00:00:00 2001 From: Sakariyah Abdulhazeem <150973162+zeemscript@users.noreply.github.com> Date: Mon, 24 Aug 2026 20:46:20 +0100 Subject: [PATCH 3/8] fix: compare fee config by bps fields (FeeConfig lacks Debug) --- .../tests/protocol_initialisation_state.rs | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/creator-keys/tests/protocol_initialisation_state.rs b/creator-keys/tests/protocol_initialisation_state.rs index 314b784..785d180 100644 --- a/creator-keys/tests/protocol_initialisation_state.rs +++ b/creator-keys/tests/protocol_initialisation_state.rs @@ -19,7 +19,7 @@ mod contract_test_env; use contract_test_env::{assert_storage_absent, register_creator_keys, test_env_with_auths}; -use creator_keys::{constants, events, fee, ContractError, CreatorKeysContractClient}; +use creator_keys::{constants, events, ContractError, CreatorKeysContractClient}; use soroban_sdk::{ testutils::{Address as _, Events}, Address, Env, IntoVal, Symbol, @@ -42,6 +42,14 @@ fn run_initialisation(client: &CreatorKeysContractClient<'_>, admin: &Address, t client.set_treasury_address(admin, treasury); } +/// Field-level view of the stored fee config. `FeeConfig` does not implement +/// `Debug`, so assertions compare its bps fields as a tuple instead. +fn fee_config_bps(client: &CreatorKeysContractClient<'_>) -> Option<(u32, u32)> { + client + .get_fee_config() + .map(|config| (config.creator_bps, config.protocol_bps)) +} + /// Counts `ContractInitializedEvent` records currently captured in the environment. fn count_initialization_events(env: &Env) -> usize { env.events() @@ -72,11 +80,9 @@ fn test_full_initialization_sets_all_state_fields() { assert_eq!(client.get_protocol_admin(), Some(admin.clone())); assert_eq!( - client.get_fee_config(), - Some(fee::FeeConfig { - creator_bps: CREATOR_BPS, - protocol_bps: PROTOCOL_BPS, - }) + fee_config_bps(&client), + Some((CREATOR_BPS, PROTOCOL_BPS)), + "fee config must store the initialised bps split" ); assert_eq!( client.get_protocol_fee_bps(), @@ -112,7 +118,7 @@ fn test_views_report_unset_defaults_before_initialization() { let (client, contract_id) = register_creator_keys(&env); assert_eq!(client.get_protocol_admin(), None); - assert_eq!(client.get_fee_config(), None); + assert_eq!(fee_config_bps(&client), None); assert_eq!(client.get_treasury_address(), None); assert!(!client.is_protocol_config_initialized()); @@ -147,7 +153,7 @@ fn test_repeated_initialization_is_idempotent_and_state_invariant() { run_initialisation(&client, &admin, &treasury); let admin_before = client.get_protocol_admin(); - let config_before = client.get_fee_config(); + let config_before = fee_config_bps(&client); let bps_before = client.get_protocol_fee_bps(); let treasury_before = client.get_treasury_address(); assert_eq!(count_initialization_events(&env), 1); @@ -170,7 +176,7 @@ fn test_repeated_initialization_is_idempotent_and_state_invariant() { client.set_treasury_address(&admin, &treasury); assert_eq!(client.get_protocol_admin(), admin_before); - assert_eq!(client.get_fee_config(), config_before); + assert_eq!(fee_config_bps(&client), config_before); assert_eq!(client.get_protocol_fee_bps(), bps_before); assert_eq!(client.get_treasury_address(), treasury_before); assert!(client.is_protocol_config_initialized()); From af6021fa4030b646be6a9eca6f6f32c80abdd9d4 Mon Sep 17 00:00:00 2001 From: Sakariyah Abdulhazeem <150973162+zeemscript@users.noreply.github.com> Date: Mon, 24 Aug 2026 20:52:31 +0100 Subject: [PATCH 4/8] fix: assert per-pass init event delta (events().all() drains log) --- creator-keys/tests/protocol_initialisation_state.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/creator-keys/tests/protocol_initialisation_state.rs b/creator-keys/tests/protocol_initialisation_state.rs index 785d180..e11b09d 100644 --- a/creator-keys/tests/protocol_initialisation_state.rs +++ b/creator-keys/tests/protocol_initialisation_state.rs @@ -141,7 +141,8 @@ fn test_views_report_unset_defaults_before_initialization() { } /// Repeating the full init sequence with identical values succeeds, mutates nothing, -/// and emits CONTRACT_INITIALIZED exactly once across both passes. +/// and emits CONTRACT_INITIALIZED exactly once across both passes. Note: +/// `env.events().all()` drains the log, so each pass asserts its own event delta. #[test] fn test_repeated_initialization_is_idempotent_and_state_invariant() { let env = test_env_with_auths(); @@ -182,7 +183,7 @@ fn test_repeated_initialization_is_idempotent_and_state_invariant() { assert!(client.is_protocol_config_initialized()); assert_eq!( count_initialization_events(&env), - 1, + 0, "the idempotent second pass must not emit another init event" ); } From e93a52c67ddb9a76f332a63715a8af3cac5de5f3 Mon Sep 17 00:00:00 2001 From: Sakariyah Abdulhazeem <150973162+zeemscript@users.noreply.github.com> Date: Mon, 24 Aug 2026 21:00:30 +0100 Subject: [PATCH 5/8] debug: dump event log in idempotence test --- creator-keys/tests/protocol_initialisation_state.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/creator-keys/tests/protocol_initialisation_state.rs b/creator-keys/tests/protocol_initialisation_state.rs index e11b09d..2f6539f 100644 --- a/creator-keys/tests/protocol_initialisation_state.rs +++ b/creator-keys/tests/protocol_initialisation_state.rs @@ -153,6 +153,16 @@ fn test_repeated_initialization_is_idempotent_and_state_invariant() { run_initialisation(&client, &admin, &treasury); + let log = env.events().all(); + eprintln!("DEBUG total_events={}", log.len()); + for (i, (contract, topics, _)) in log.iter().enumerate() { + let name: Symbol = topics + .get(events::TOPIC_EVENT_NAME_INDEX) + .map(|v| v.into_val(&env)) + .unwrap_or_else(|| Symbol::new(&env, "none")); + eprintln!("DEBUG event[{i}] contract={contract} topic0={name:?} ntopics={}", topics.len()); + } + let admin_before = client.get_protocol_admin(); let config_before = fee_config_bps(&client); let bps_before = client.get_protocol_fee_bps(); From 0f32104eb608cf842bf793e45e6f62c31f7fbe6d Mon Sep 17 00:00:00 2001 From: Sakariyah Abdulhazeem <150973162+zeemscript@users.noreply.github.com> Date: Mon, 24 Aug 2026 21:00:42 +0100 Subject: [PATCH 6/8] style: rustfmt --- creator-keys/tests/protocol_initialisation_state.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/creator-keys/tests/protocol_initialisation_state.rs b/creator-keys/tests/protocol_initialisation_state.rs index 2f6539f..b56a6a6 100644 --- a/creator-keys/tests/protocol_initialisation_state.rs +++ b/creator-keys/tests/protocol_initialisation_state.rs @@ -160,7 +160,10 @@ fn test_repeated_initialization_is_idempotent_and_state_invariant() { .get(events::TOPIC_EVENT_NAME_INDEX) .map(|v| v.into_val(&env)) .unwrap_or_else(|| Symbol::new(&env, "none")); - eprintln!("DEBUG event[{i}] contract={contract} topic0={name:?} ntopics={}", topics.len()); + eprintln!( + "DEBUG event[{i}] contract={contract} topic0={name:?} ntopics={}", + topics.len() + ); } let admin_before = client.get_protocol_admin(); From 9edbf46ba75ed50cda8dd83d9c6efcb8e3939e52 Mon Sep 17 00:00:00 2001 From: Sakariyah Abdulhazeem <150973162+zeemscript@users.noreply.github.com> Date: Mon, 24 Aug 2026 21:03:16 +0100 Subject: [PATCH 7/8] debug: use Debug for Address in dump --- creator-keys/tests/protocol_initialisation_state.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/creator-keys/tests/protocol_initialisation_state.rs b/creator-keys/tests/protocol_initialisation_state.rs index b56a6a6..17747f9 100644 --- a/creator-keys/tests/protocol_initialisation_state.rs +++ b/creator-keys/tests/protocol_initialisation_state.rs @@ -161,7 +161,7 @@ fn test_repeated_initialization_is_idempotent_and_state_invariant() { .map(|v| v.into_val(&env)) .unwrap_or_else(|| Symbol::new(&env, "none")); eprintln!( - "DEBUG event[{i}] contract={contract} topic0={name:?} ntopics={}", + "DEBUG event[{i}] contract={contract:?} topic0={name:?} ntopics={}", topics.len() ); } From 3b1b3349e8d27c2ae8e194b7e85c09dc7d351f0c Mon Sep 17 00:00:00 2001 From: Sakariyah Abdulhazeem <150973162+zeemscript@users.noreply.github.com> Date: Mon, 24 Aug 2026 21:09:02 +0100 Subject: [PATCH 8/8] fix: count init events per-invocation (Events::all is last-call only) --- .../tests/protocol_initialisation_state.rs | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/creator-keys/tests/protocol_initialisation_state.rs b/creator-keys/tests/protocol_initialisation_state.rs index 17747f9..382c642 100644 --- a/creator-keys/tests/protocol_initialisation_state.rs +++ b/creator-keys/tests/protocol_initialisation_state.rs @@ -50,7 +50,10 @@ fn fee_config_bps(client: &CreatorKeysContractClient<'_>) -> Option<(u32, u32)> .map(|config| (config.creator_bps, config.protocol_bps)) } -/// Counts `ContractInitializedEvent` records currently captured in the environment. +/// Counts `ContractInitializedEvent` records emitted by the most recent contract +/// invocation. `env.events().all()` exposes only the last invocation's events, +/// not a cumulative log, so callers must invoke the contract call under test +/// immediately before counting. fn count_initialization_events(env: &Env) -> usize { env.events() .all() @@ -142,7 +145,8 @@ fn test_views_report_unset_defaults_before_initialization() { /// Repeating the full init sequence with identical values succeeds, mutates nothing, /// and emits CONTRACT_INITIALIZED exactly once across both passes. Note: -/// `env.events().all()` drains the log, so each pass asserts its own event delta. +/// `env.events().all()` only exposes the events of the most recent contract +/// invocation, so each pass ends on its `set_fee_config` call before counting. #[test] fn test_repeated_initialization_is_idempotent_and_state_invariant() { let env = test_env_with_auths(); @@ -151,27 +155,24 @@ fn test_repeated_initialization_is_idempotent_and_state_invariant() { let admin = Address::generate(&env); let treasury = Address::generate(&env); - run_initialisation(&client, &admin, &treasury); + // Pass 1: fee config last so its initialization event is visible to all(). + client.set_protocol_admin(&admin, &admin); + client.set_key_price(&admin, &KEY_PRICE); + client.set_treasury_address(&admin, &treasury); + client.set_fee_config(&admin, &CREATOR_BPS, &PROTOCOL_BPS); - let log = env.events().all(); - eprintln!("DEBUG total_events={}", log.len()); - for (i, (contract, topics, _)) in log.iter().enumerate() { - let name: Symbol = topics - .get(events::TOPIC_EVENT_NAME_INDEX) - .map(|v| v.into_val(&env)) - .unwrap_or_else(|| Symbol::new(&env, "none")); - eprintln!( - "DEBUG event[{i}] contract={contract:?} topic0={name:?} ntopics={}", - topics.len() - ); - } + assert_eq!( + count_initialization_events(&env), + 1, + "first initialisation must emit exactly one ContractInitialized event" + ); let admin_before = client.get_protocol_admin(); let config_before = fee_config_bps(&client); let bps_before = client.get_protocol_fee_bps(); let treasury_before = client.get_treasury_address(); - assert_eq!(count_initialization_events(&env), 1); + // Pass 2: repeat every setter with identical values; each must succeed. assert_eq!( client.try_set_protocol_admin(&admin, &admin), Ok(Ok(())), @@ -182,12 +183,12 @@ fn test_repeated_initialization_is_idempotent_and_state_invariant() { Ok(Ok(())), "re-setting the same key price must succeed" ); + client.set_treasury_address(&admin, &treasury); assert_eq!( client.try_set_fee_config(&admin, &CREATOR_BPS, &PROTOCOL_BPS), Ok(Ok(())), "re-setting the same fee config must succeed" ); - client.set_treasury_address(&admin, &treasury); assert_eq!(client.get_protocol_admin(), admin_before); assert_eq!(fee_config_bps(&client), config_before);