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/tests/creator_fee_rotation_regression.rs b/creator-keys/tests/creator_fee_rotation_regression.rs new file mode 100644 index 0000000..1430544 --- /dev/null +++ b/creator-keys/tests/creator_fee_rotation_regression.rs @@ -0,0 +1,65 @@ +//! 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 creator_keys::RegisterCreatorParams; +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); + client.register_creator( + &RegisterCreatorParams { + creator: creator.clone(), + handle: String::from_str(&env, "alice"), + }, + &None, + &None, + &None, + &None, + &None, + ); + + // 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); + + // 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..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] @@ -89,3 +90,54 @@ 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..161fcf1 --- /dev/null +++ b/creator-keys/tests/locked_allocation_claimed_view_regression.rs @@ -0,0 +1,64 @@ +//! 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..208208a --- /dev/null +++ b/creator-keys/tests/quadratic_curve_symmetry_regression.rs @@ -0,0 +1,110 @@ +//! 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_curve_slope, set_pricing_and_fees, 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); +}