From fe4dfed5903abc9df462fdecfd1fe8ccaa0894cf Mon Sep 17 00:00:00 2001 From: Developer Date: Tue, 30 Jun 2026 09:52:21 +0100 Subject: [PATCH 1/4] Add unit and regression tests for issues 499, 486, 488, and 485 --- .../tests/creator_fee_rotation_regression.rs | 55 +++++++++ creator-keys/tests/dividend_events.rs | 52 +++++++++ ...cked_allocation_claimed_view_regression.rs | 55 +++++++++ .../quadratic_curve_symmetry_regression.rs | 108 ++++++++++++++++++ 4 files changed, 270 insertions(+) create mode 100644 creator-keys/tests/creator_fee_rotation_regression.rs create mode 100644 creator-keys/tests/locked_allocation_claimed_view_regression.rs create mode 100644 creator-keys/tests/quadratic_curve_symmetry_regression.rs diff --git a/creator-keys/tests/creator_fee_rotation_regression.rs b/creator-keys/tests/creator_fee_rotation_regression.rs new file mode 100644 index 0000000..48a6590 --- /dev/null +++ b/creator-keys/tests/creator_fee_rotation_regression.rs @@ -0,0 +1,55 @@ +//! Regression test verifying creator fee recipient updates correctly after rotation. + +mod contract_test_env; + +use contract_test_env::{register_creator_keys, set_pricing_and_fees, test_env_with_auths}; +use soroban_sdk::{testutils::Address as _, Address, String}; + +#[test] +fn test_creator_fee_recipient_rotation_regression() { + let env = test_env_with_auths(); + let (client, _) = register_creator_keys(&env); + set_pricing_and_fees(&env, &client, 1000, 9000, 1000); + + let creator = Address::generate(&env); + let initial_recipient = Address::generate(&env); + client.register_creator( + &creator, + &String::from_str(&env, "alice"), + &Some(initial_recipient.clone()), + &None, + &None, + &None, + ); + + // Initial fee recipient is set correctly + assert_eq!(client.get_creator_fee_recipient(&creator), initial_recipient); + + let buyer = Address::generate(&env); + let quote1 = client.get_buy_quote(&creator); + + // Execute buy 1 + client.buy_key(&creator, &buyer, "e1.total_amount, &None); + + // Initial recipient received the fee (tracked under creator's fee balance) + let balance_after_buy1 = client.get_creator_fee_balance(&creator); + assert_eq!(balance_after_buy1, quote1.creator_fee); + + // Rotate the creator fee recipient to a new address + let new_recipient = Address::generate(&env); + client.update_creator_fee_recipient(&creator, &new_recipient); + + // New recipient is set correctly + assert_eq!(client.get_creator_fee_recipient(&creator), new_recipient); + + // Execute buy 2 + let quote2 = client.get_buy_quote(&creator); + client.buy_key(&creator, &buyer, "e2.total_amount, &None); + + // New recipient receives fee after rotation (tracked under creator's fee balance) + let balance_after_buy2 = client.get_creator_fee_balance(&creator); + assert_eq!(balance_after_buy2 - balance_after_buy1, quote2.creator_fee); + + // Since the old recipient is no longer the fee recipient, they receive nothing from subsequent trades. + assert_ne!(client.get_creator_fee_recipient(&creator), initial_recipient); +} diff --git a/creator-keys/tests/dividend_events.rs b/creator-keys/tests/dividend_events.rs index 367d9d9..600d3d8 100644 --- a/creator-keys/tests/dividend_events.rs +++ b/creator-keys/tests/dividend_events.rs @@ -89,3 +89,55 @@ fn test_claim_dividend_event_topics_and_payload() { assert_eq!(event.claimant, buyer); assert_eq!(event.amount, claimed); } + +#[test] +fn test_distribute_dividend_event_fields_individual_assertions() { + let env = test_env_with_auths(); + let (client, _) = register_creator_keys(&env); + // Set pricing and fees with 10% protocol fee + set_pricing_and_fees(&env, &client, 100, 9000, 1000); + let creator = register_test_creator(&env, &client, "alice"); + let buyer = Address::generate(&env); + + // Distribute to a creator with a known supply (e.g. 2 keys bought) + client.buy_key(&creator, &buyer, &100, &None); + client.buy_key(&creator, &buyer, &100, &None); + + // Set env ledger to a positive non-zero sequence + let mut ledger_info = env.ledger().get(); + ledger_info.sequence_number = 12345; + env.ledger().set(ledger_info); + + let distributor = Address::generate(&env); + let gross_amount = 20_000i128; + distribute_test_dividend(&client, &creator, &distributor, gross_amount); + + let events = env.events().all(); + let (_, data) = events + .iter() + .rev() + .find_map(|(_, topics, data)| { + if topics == dividend_distributed_topics(&creator).into_val(&env) { + Some((topics, data)) + } else { + None + } + }) + .expect("DividendDistributed event not found"); + + let event: DividendDistributedEvent = data.into_val(&env); + + // Assert creator matches the creator used + assert_eq!(event.creator, creator); + + // Assert total_amount reflects the gross amount before protocol fee deduction (not the net amount) + assert_eq!(event.total_amount, gross_amount); + + // Assert snapshot_supply matches total supply at distribution time + assert_eq!(event.snapshot_supply, 2); + + // Assert ledger is a positive non-zero value + assert!(event.ledger > 0); + assert_eq!(event.ledger, 12345); +} + diff --git a/creator-keys/tests/locked_allocation_claimed_view_regression.rs b/creator-keys/tests/locked_allocation_claimed_view_regression.rs new file mode 100644 index 0000000..f95ad6e --- /dev/null +++ b/creator-keys/tests/locked_allocation_claimed_view_regression.rs @@ -0,0 +1,55 @@ +//! Regression test verifying `get_locked_allocation` returns claimed `true` after claim. + +mod contract_test_env; + +use contract_test_env::{register_creator_keys, test_env_with_auths}; +use creator_keys::LockedAllocation; +use soroban_sdk::testutils::{Address as _, Ledger}; +use soroban_sdk::{Address, String}; + +#[test] +fn test_get_locked_allocation_returns_claimed_true_after_claim() { + let env = test_env_with_auths(); + let (client, _) = register_creator_keys(&env); + + let creator = Address::generate(&env); + let handle = String::from_str(&env, "alice"); + let unlock_ledger: u32 = 1000; + let amount: u32 = 50; + + let mut ledger_info = env.ledger().get(); + ledger_info.sequence_number = 1; + env.ledger().set(ledger_info.clone()); + + client.register_creator( + &creator, + &handle, + &Some(LockedAllocation { + amount, + unlock_ledger, + claimed: false, + }), + &None, + &None, + &None, + ); + + // Assert claimed field is false before claim + let alloc_before = client.get_locked_allocation(&creator).unwrap(); + assert!(!alloc_before.claimed, "claimed field must be false before claim"); + + // Advance to exactly unlock_ledger. + ledger_info.sequence_number = unlock_ledger; + env.ledger().set(ledger_info); + + // Claim the allocation + client.claim_locked_allocation(&creator); + + // Call get_locked_allocation and assert claimed is true + let alloc_after1 = client.get_locked_allocation(&creator).unwrap(); + assert!(alloc_after1.claimed, "claimed field must be true after successful claim"); + + // Call again and assert it is still true + let alloc_after2 = client.get_locked_allocation(&creator).unwrap(); + assert!(alloc_after2.claimed, "claimed field must remain true on subsequent reads"); +} diff --git a/creator-keys/tests/quadratic_curve_symmetry_regression.rs b/creator-keys/tests/quadratic_curve_symmetry_regression.rs new file mode 100644 index 0000000..90c5314 --- /dev/null +++ b/creator-keys/tests/quadratic_curve_symmetry_regression.rs @@ -0,0 +1,108 @@ +//! Regression test verifying buy and sell quote symmetry for the Quadratic preset. +//! +//! The cost (price/fees) to buy N keys at supply S must equal the proceeds (price/fees) +//! from selling N keys at supply S+N. + +mod contract_test_env; + +use contract_test_env::{register_creator_keys, set_pricing_and_fees, set_curve_slope, test_env_with_auths}; +use creator_keys::CurvePreset; +use soroban_sdk::{testutils::Address as _, Address, String}; + +const KEY_PRICE: i128 = 1000; +const CREATOR_BPS: u32 = 9000; +const PROTOCOL_BPS: u32 = 1000; +const QUADRATIC_SLOPE: i128 = 10; + +fn assert_symmetry_for_params( + client: &creator_keys::CreatorKeysContractClient<'_>, + creator: &Address, + buyer: &Address, + start_supply: u32, + n: u32, +) { + // 1. Advance supply to start_supply + let current_supply = client.get_total_key_supply(creator); + if current_supply < start_supply { + for _ in current_supply..start_supply { + let quote = client.get_buy_quote(creator); + client.buy_key(creator, buyer, "e.total_amount, &None); + } + } + + assert_eq!(client.get_total_key_supply(creator), start_supply); + + // 2. Accumulate buy quotes for N keys and execute buys + let mut total_buy_price = 0; + let mut total_buy_creator_fee = 0; + let mut total_buy_protocol_fee = 0; + + for _ in 0..n { + let quote = client.get_buy_quote(creator); + total_buy_price += quote.price; + total_buy_creator_fee += quote.creator_fee; + total_buy_protocol_fee += quote.protocol_fee; + client.buy_key(creator, buyer, "e.total_amount, &None); + } + + assert_eq!(client.get_total_key_supply(creator), start_supply + n); + + // 3. Accumulate sell quotes for N keys and execute sells + let mut total_sell_price = 0; + let mut total_sell_creator_fee = 0; + let mut total_sell_protocol_fee = 0; + + for _ in 0..n { + let quote = client.get_sell_quote(creator, buyer); + total_sell_price += quote.price; + total_sell_creator_fee += quote.creator_fee; + total_sell_protocol_fee += quote.protocol_fee; + client.sell_key(creator, buyer, &None); + } + + assert_eq!(client.get_total_key_supply(creator), start_supply); + + // 4. Assert symmetry + assert_eq!( + total_buy_price, total_sell_price, + "Price asymmetry at supply {} for N {}", + start_supply, n + ); + assert_eq!( + total_buy_creator_fee, total_sell_creator_fee, + "Creator fee asymmetry at supply {} for N {}", + start_supply, n + ); + assert_eq!( + total_buy_protocol_fee, total_sell_protocol_fee, + "Protocol fee asymmetry at supply {} for N {}", + start_supply, n + ); +} + +#[test] +fn test_quadratic_curve_symmetry() { + let env = test_env_with_auths(); + let (client, _) = register_creator_keys(&env); + set_pricing_and_fees(&env, &client, KEY_PRICE, CREATOR_BPS, PROTOCOL_BPS); + set_curve_slope(&env, &client, QUADRATIC_SLOPE); + + let creator = Address::generate(&env); + client.register_creator( + &creator, + &String::from_str(&env, "quadcreator"), + &None, + &None, + &Some(CurvePreset::Quadratic), + &None, + ); + + let buyer = Address::generate(&env); + + // Cover supply levels: 0, 50, and 500 + // Cover both small and large key amounts + assert_symmetry_for_params(&client, &creator, &buyer, 0, 1); + assert_symmetry_for_params(&client, &creator, &buyer, 0, 10); + assert_symmetry_for_params(&client, &creator, &buyer, 50, 5); + assert_symmetry_for_params(&client, &creator, &buyer, 500, 50); +} From 08e31e4b6cd2218f5f96c1795887bcc5411b5986 Mon Sep 17 00:00:00 2001 From: Developer Date: Thu, 2 Jul 2026 11:28:45 +0100 Subject: [PATCH 2/4] Fix formatting issues in test files --- .../tests/creator_fee_rotation_regression.rs | 12 +++++++++--- creator-keys/tests/dividend_events.rs | 13 ++++++------- .../locked_allocation_claimed_view_regression.rs | 15 ++++++++++++--- .../tests/quadratic_curve_symmetry_regression.rs | 4 +++- 4 files changed, 30 insertions(+), 14 deletions(-) diff --git a/creator-keys/tests/creator_fee_rotation_regression.rs b/creator-keys/tests/creator_fee_rotation_regression.rs index 48a6590..1470afe 100644 --- a/creator-keys/tests/creator_fee_rotation_regression.rs +++ b/creator-keys/tests/creator_fee_rotation_regression.rs @@ -16,14 +16,17 @@ fn test_creator_fee_recipient_rotation_regression() { client.register_creator( &creator, &String::from_str(&env, "alice"), - &Some(initial_recipient.clone()), &None, + &Some(initial_recipient.clone()), &None, &None, ); // Initial fee recipient is set correctly - assert_eq!(client.get_creator_fee_recipient(&creator), initial_recipient); + assert_eq!( + client.get_creator_fee_recipient(&creator), + initial_recipient + ); let buyer = Address::generate(&env); let quote1 = client.get_buy_quote(&creator); @@ -51,5 +54,8 @@ fn test_creator_fee_recipient_rotation_regression() { assert_eq!(balance_after_buy2 - balance_after_buy1, quote2.creator_fee); // Since the old recipient is no longer the fee recipient, they receive nothing from subsequent trades. - assert_ne!(client.get_creator_fee_recipient(&creator), initial_recipient); + assert_ne!( + client.get_creator_fee_recipient(&creator), + initial_recipient + ); } diff --git a/creator-keys/tests/dividend_events.rs b/creator-keys/tests/dividend_events.rs index 600d3d8..e6537d7 100644 --- a/creator-keys/tests/dividend_events.rs +++ b/creator-keys/tests/dividend_events.rs @@ -98,11 +98,11 @@ fn test_distribute_dividend_event_fields_individual_assertions() { set_pricing_and_fees(&env, &client, 100, 9000, 1000); let creator = register_test_creator(&env, &client, "alice"); let buyer = Address::generate(&env); - + // Distribute to a creator with a known supply (e.g. 2 keys bought) client.buy_key(&creator, &buyer, &100, &None); client.buy_key(&creator, &buyer, &100, &None); - + // Set env ledger to a positive non-zero sequence let mut ledger_info = env.ledger().get(); ledger_info.sequence_number = 12345; @@ -126,18 +126,17 @@ fn test_distribute_dividend_event_fields_individual_assertions() { .expect("DividendDistributed event not found"); let event: DividendDistributedEvent = data.into_val(&env); - + // Assert creator matches the creator used assert_eq!(event.creator, creator); - + // Assert total_amount reflects the gross amount before protocol fee deduction (not the net amount) assert_eq!(event.total_amount, gross_amount); - + // Assert snapshot_supply matches total supply at distribution time assert_eq!(event.snapshot_supply, 2); - + // Assert ledger is a positive non-zero value assert!(event.ledger > 0); assert_eq!(event.ledger, 12345); } - diff --git a/creator-keys/tests/locked_allocation_claimed_view_regression.rs b/creator-keys/tests/locked_allocation_claimed_view_regression.rs index f95ad6e..161fcf1 100644 --- a/creator-keys/tests/locked_allocation_claimed_view_regression.rs +++ b/creator-keys/tests/locked_allocation_claimed_view_regression.rs @@ -36,7 +36,10 @@ fn test_get_locked_allocation_returns_claimed_true_after_claim() { // Assert claimed field is false before claim let alloc_before = client.get_locked_allocation(&creator).unwrap(); - assert!(!alloc_before.claimed, "claimed field must be false before claim"); + assert!( + !alloc_before.claimed, + "claimed field must be false before claim" + ); // Advance to exactly unlock_ledger. ledger_info.sequence_number = unlock_ledger; @@ -47,9 +50,15 @@ fn test_get_locked_allocation_returns_claimed_true_after_claim() { // Call get_locked_allocation and assert claimed is true let alloc_after1 = client.get_locked_allocation(&creator).unwrap(); - assert!(alloc_after1.claimed, "claimed field must be true after successful claim"); + assert!( + alloc_after1.claimed, + "claimed field must be true after successful claim" + ); // Call again and assert it is still true let alloc_after2 = client.get_locked_allocation(&creator).unwrap(); - assert!(alloc_after2.claimed, "claimed field must remain true on subsequent reads"); + assert!( + alloc_after2.claimed, + "claimed field must remain true on subsequent reads" + ); } diff --git a/creator-keys/tests/quadratic_curve_symmetry_regression.rs b/creator-keys/tests/quadratic_curve_symmetry_regression.rs index 90c5314..208208a 100644 --- a/creator-keys/tests/quadratic_curve_symmetry_regression.rs +++ b/creator-keys/tests/quadratic_curve_symmetry_regression.rs @@ -5,7 +5,9 @@ mod contract_test_env; -use contract_test_env::{register_creator_keys, set_pricing_and_fees, set_curve_slope, test_env_with_auths}; +use contract_test_env::{ + register_creator_keys, set_curve_slope, set_pricing_and_fees, test_env_with_auths, +}; use creator_keys::CurvePreset; use soroban_sdk::{testutils::Address as _, Address, String}; From 4ab1b46281de3c667473543a042dfcac1454acb7 Mon Sep 17 00:00:00 2001 From: Developer Date: Thu, 2 Jul 2026 15:49:25 +0100 Subject: [PATCH 3/4] Fix CI workflow and test compilation errors - Add explicit toolchain: stable to rust-toolchain action (required parameter) - Import soroban_sdk::testutils::Ledger trait to fix E0599 compile error - Fix creator_fee_rotation_regression test to use correct register_creator API --- .github/workflows/ci.yml | 1 + ...m_dividend_event_topics_and_payload.1.json | 43 +++++++++++++++++++ ...e_dividend_event_topics_and_payload.1.json | 43 +++++++++++++++++++ .../tests/creator_fee_rotation_regression.rs | 14 +++--- creator-keys/tests/dividend_events.rs | 1 + 5 files changed, 95 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3919472..9452098 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,6 +17,7 @@ jobs: - name: Setup Rust uses: dtolnay/rust-toolchain@stable with: + toolchain: stable components: rustfmt, clippy - name: Format check diff --git a/creator-keys/test_snapshots/test_claim_dividend_event_topics_and_payload.1.json b/creator-keys/test_snapshots/test_claim_dividend_event_topics_and_payload.1.json index 57c29ee..5e5192e 100644 --- a/creator-keys/test_snapshots/test_claim_dividend_event_topics_and_payload.1.json +++ b/creator-keys/test_snapshots/test_claim_dividend_event_topics_and_payload.1.json @@ -72,6 +72,7 @@ }, "void", "void", + "void", "void" ] } @@ -743,6 +744,48 @@ 4095 ] ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "TreasuryBalance" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "TreasuryBalance" + } + ] + }, + "durability": "persistent", + "val": { + "i128": { + "hi": 0, + "lo": 10 + } + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], [ { "contract_data": { diff --git a/creator-keys/test_snapshots/test_distribute_dividend_event_topics_and_payload.1.json b/creator-keys/test_snapshots/test_distribute_dividend_event_topics_and_payload.1.json index aed8781..2faef5c 100644 --- a/creator-keys/test_snapshots/test_distribute_dividend_event_topics_and_payload.1.json +++ b/creator-keys/test_snapshots/test_distribute_dividend_event_topics_and_payload.1.json @@ -72,6 +72,7 @@ }, "void", "void", + "void", "void" ] } @@ -721,6 +722,48 @@ 4095 ] ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "TreasuryBalance" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "TreasuryBalance" + } + ] + }, + "durability": "persistent", + "val": { + "i128": { + "hi": 0, + "lo": 10 + } + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], [ { "contract_data": { diff --git a/creator-keys/tests/creator_fee_rotation_regression.rs b/creator-keys/tests/creator_fee_rotation_regression.rs index 1470afe..ec431f7 100644 --- a/creator-keys/tests/creator_fee_rotation_regression.rs +++ b/creator-keys/tests/creator_fee_rotation_regression.rs @@ -12,21 +12,21 @@ fn test_creator_fee_recipient_rotation_regression() { set_pricing_and_fees(&env, &client, 1000, 9000, 1000); let creator = Address::generate(&env); - let initial_recipient = Address::generate(&env); client.register_creator( &creator, &String::from_str(&env, "alice"), &None, - &Some(initial_recipient.clone()), + &None, &None, &None, ); - // Initial fee recipient is set correctly - assert_eq!( - client.get_creator_fee_recipient(&creator), - initial_recipient - ); + // Initial fee recipient is the creator themselves + assert_eq!(client.get_creator_fee_recipient(&creator), creator); + + // Set custom initial fee recipient + let initial_recipient = Address::generate(&env); + client.update_creator_fee_recipient(&creator, &initial_recipient); let buyer = Address::generate(&env); let quote1 = client.get_buy_quote(&creator); diff --git a/creator-keys/tests/dividend_events.rs b/creator-keys/tests/dividend_events.rs index e6537d7..ece1083 100644 --- a/creator-keys/tests/dividend_events.rs +++ b/creator-keys/tests/dividend_events.rs @@ -10,6 +10,7 @@ use creator_keys::events::{ dividend_claimed_topics, dividend_distributed_topics, DividendClaimedEvent, DividendDistributedEvent, }; +use soroban_sdk::testutils::Ledger; use soroban_sdk::{testutils::Address as _, testutils::Events, Address, IntoVal}; #[test] From f7811496bec0e3990631f729faa76973a858a3b0 Mon Sep 17 00:00:00 2001 From: Developer Date: Thu, 2 Jul 2026 21:29:06 +0100 Subject: [PATCH 4/4] Update creator_fee_rotation_regression test to use new RegisterCreatorParams API After merging upstream/main, the register_creator function signature changed to use RegisterCreatorParams struct and added whitelist parameter. Updated the test to match the new API. --- creator-keys/tests/creator_fee_rotation_regression.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/creator-keys/tests/creator_fee_rotation_regression.rs b/creator-keys/tests/creator_fee_rotation_regression.rs index ec431f7..1430544 100644 --- a/creator-keys/tests/creator_fee_rotation_regression.rs +++ b/creator-keys/tests/creator_fee_rotation_regression.rs @@ -3,6 +3,7 @@ mod contract_test_env; use contract_test_env::{register_creator_keys, set_pricing_and_fees, test_env_with_auths}; +use creator_keys::RegisterCreatorParams; use soroban_sdk::{testutils::Address as _, Address, String}; #[test] @@ -13,8 +14,11 @@ fn test_creator_fee_recipient_rotation_regression() { let creator = Address::generate(&env); client.register_creator( - &creator, - &String::from_str(&env, "alice"), + &RegisterCreatorParams { + creator: creator.clone(), + handle: String::from_str(&env, "alice"), + }, + &None, &None, &None, &None,