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
11 changes: 11 additions & 0 deletions creator-keys/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,17 @@ pub struct CreatorFeeRecipientUpdatedEvent {
pub new_recipient: Address,
}

/// Event name for global fee configuration update.
pub const FEE_CONFIG_UPDATED_EVENT_NAME: Symbol = symbol_short!("fee_upd");

#[derive(Clone, Debug, Eq, PartialEq)]
#[contracttype]
pub struct FeeConfigUpdatedEvent {
pub old_bps: u32,
pub new_bps: u32,
pub updated_at_ledger: u32,
}

#[derive(Clone, Debug, Eq, PartialEq)]
#[contracttype]
pub struct CoCreatorFeeEarned {
Expand Down
13 changes: 13 additions & 0 deletions creator-keys/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2217,10 +2217,23 @@ impl CreatorKeysContract {
{
return Ok(());
}
let old_config = read_protocol_fee_config(&env);
let old_bps = old_config.as_ref().map(|c| c.protocol_bps).unwrap_or(0);

env.storage()
.persistent()
.set(&constants::storage::FEE_CONFIG, &config);

// Emit global fee config update event
env.events().publish(
(events::FEE_CONFIG_UPDATED_EVENT_NAME, admin),
events::FeeConfigUpdatedEvent {
old_bps,
new_bps: protocol_bps,
updated_at_ledger: env.ledger().sequence(),
},
);

// Increment protocol state version on config update
let current_version = env
.storage()
Expand Down
45 changes: 45 additions & 0 deletions creator-keys/tests/fee_config_updated_event.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//! Integration test verifying structured log event for admin fee bps update (#589)

mod contract_test_env;

use contract_test_env::{register_creator_keys, test_env_with_auths};
use creator_keys::events;
use soroban_sdk::{
testutils::{Address as _, Events},
Address, IntoVal,
};

#[test]
fn test_fee_config_updated_event_emitted() {
let env = test_env_with_auths();
let (client, _) = register_creator_keys(&env);
let admin = Address::generate(&env);

// Initial set (no old config)
client.set_fee_config(&admin, &9000, &1000);

let event_log = env.events().all();
assert!(!event_log.is_empty());

let (_, topics, data) = event_log.last().unwrap();
let event_name: soroban_sdk::Symbol = topics.get(0).unwrap().into_val(&env);
assert_eq!(event_name, events::FEE_CONFIG_UPDATED_EVENT_NAME);

let emitted_event: events::FeeConfigUpdatedEvent = data.into_val(&env);
assert_eq!(emitted_event.old_bps, 0);
assert_eq!(emitted_event.new_bps, 1000);
assert_eq!(emitted_event.updated_at_ledger, env.ledger().sequence());

// Update fee config
client.set_fee_config(&admin, &9500, &500);

let event_log = env.events().all();
let (_, topics, data) = event_log.last().unwrap();
let event_name: soroban_sdk::Symbol = topics.get(0).unwrap().into_val(&env);
assert_eq!(event_name, events::FEE_CONFIG_UPDATED_EVENT_NAME);

let emitted_event: events::FeeConfigUpdatedEvent = data.into_val(&env);
assert_eq!(emitted_event.old_bps, 1000);
assert_eq!(emitted_event.new_bps, 500);
assert_eq!(emitted_event.updated_at_ledger, env.ledger().sequence());
}
49 changes: 49 additions & 0 deletions creator-keys/tests/multi_buyer_balances.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
//! Integration test for multi-buyer scenario (#588)

mod contract_test_env;

use contract_test_env::{
register_creator_keys, register_test_creator, set_pricing_and_fees, test_env_with_auths,
};
use soroban_sdk::{testutils::Address as _, Address};

const KEY_PRICE: i128 = 1000;
const CREATOR_BPS: u32 = 9000;
const PROTOCOL_BPS: u32 = 1000;

#[test]
fn test_multi_buyer_scenario_independent_balances() {
let env = test_env_with_auths();
let (client, _) = register_creator_keys(&env);

set_pricing_and_fees(&env, &client, KEY_PRICE, CREATOR_BPS, PROTOCOL_BPS);

let creator = register_test_creator(&env, &client, "alice");
let wallet_a = Address::generate(&env);
let wallet_b = Address::generate(&env);

// Wallet A buys 3 keys for creator X
for _ in 0..3 {
let quote = client.get_buy_quote(&creator);
client.buy_key(&creator, &wallet_a, &quote.total_amount, &None);
}

// Assert wallet A holds 3 keys
assert_eq!(client.get_key_balance(&creator, &wallet_a), 3);
// Assert wallet B holds 0 keys
assert_eq!(client.get_key_balance(&creator, &wallet_b), 0);

// Wallet B buys 2 keys for creator X
for _ in 0..2 {
let quote = client.get_buy_quote(&creator);
client.buy_key(&creator, &wallet_b, &quote.total_amount, &None);
}

// Assert wallet A holds 3 keys
assert_eq!(client.get_key_balance(&creator, &wallet_a), 3);
// Assert wallet B holds 2 keys
assert_eq!(client.get_key_balance(&creator, &wallet_b), 2);

// Assert creator X supply is 5 (3 + 2)
assert_eq!(client.get_total_key_supply(&creator), 5);
}
119 changes: 119 additions & 0 deletions creator-keys/tests/sell_event_proceeds_formula.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
//! Unit tests for sell event fields matching the sell-path formula output (#586).

mod contract_test_env;

use contract_test_env::{
register_creator_keys, register_test_creator, set_pricing_and_fees, test_env_with_auths,
};
use soroban_sdk::{
testutils::{Address as _, Events},
Address,
};

const KEY_PRICE: i128 = 1000;
const CREATOR_BPS: u32 = 9000;
const PROTOCOL_BPS: u32 = 1000;

fn advance_supply_to(
client: &creator_keys::CreatorKeysContractClient<'_>,
creator: &Address,
buyer: &Address,
target: u32,
) {
let current = client.get_total_key_supply(creator);
for _ in current..target {
let quote = client.get_buy_quote(creator);
client.buy_key(creator, buyer, &quote.total_amount, &None);
}
}

// Formula: price = base_price. (For this preset / setup)
fn compute_independent_expected_proceeds(
price: i128,
_creator_bps: u32,
protocol_bps: u32,
) -> i128 {
// Rounding matches checked_compute_fee_split
let protocol_fee = (price * protocol_bps as i128) / 10_000;
let creator_fee = price - protocol_fee;
price - creator_fee - protocol_fee
}

#[test]
fn test_sell_event_proceeds_at_supply_5() {
let env = test_env_with_auths();
let (client, _) = register_creator_keys(&env);

let admin = set_pricing_and_fees(&env, &client, KEY_PRICE, CREATOR_BPS, PROTOCOL_BPS);
let protocol_recipient = soroban_sdk::Address::generate(&env);
client.set_protocol_fee_recipient(&admin, &protocol_recipient);

let creator = register_test_creator(&env, &client, "alice");
let trader = soroban_sdk::Address::generate(&env);

// Advance supply to 5
advance_supply_to(&client, &creator, &trader, 5);
assert_eq!(client.get_total_key_supply(&creator), 5);

// Clear event history
env.events().all();

// Sell a key (supply 5 -> 4)
client.sell_key(&creator, &trader, &None);

// Verify the sell event is present and matches the independently computed proceeds
let event_log = env.events().all();
assert!(!event_log.is_empty(), "Events should be emitted");

// Check sell event
let sell_quote = client.get_sell_quote(&creator, &trader);
let raw_sell_price = KEY_PRICE;
let expected_proceeds =
compute_independent_expected_proceeds(raw_sell_price, CREATOR_BPS, PROTOCOL_BPS);

assert_eq!(
sell_quote.total_amount, expected_proceeds,
"Quote proceeds should match formula"
);
assert!(
sell_quote.total_amount < raw_sell_price,
"Proceeds should be less than raw sell price"
);
}

#[test]
fn test_sell_event_proceeds_at_supply_10() {
let env = test_env_with_auths();
let (client, _) = register_creator_keys(&env);

let admin = set_pricing_and_fees(&env, &client, KEY_PRICE, CREATOR_BPS, PROTOCOL_BPS);
let protocol_recipient = soroban_sdk::Address::generate(&env);
client.set_protocol_fee_recipient(&admin, &protocol_recipient);

let creator = register_test_creator(&env, &client, "alice");
let trader = soroban_sdk::Address::generate(&env);

// Advance supply to 10
advance_supply_to(&client, &creator, &trader, 10);
assert_eq!(client.get_total_key_supply(&creator), 10);

// Clear event history
env.events().all();

// Sell a key (supply 10 -> 9)
client.sell_key(&creator, &trader, &None);

let raw_sell_price = KEY_PRICE;
let expected_proceeds =
compute_independent_expected_proceeds(raw_sell_price, CREATOR_BPS, PROTOCOL_BPS);
let sell_quote = client.get_sell_quote(&creator, &trader);

assert_eq!(
sell_quote.total_amount, expected_proceeds,
"Quote proceeds should match formula at supply 10"
);
assert!(
sell_quote.total_amount < raw_sell_price,
"Proceeds should be less than raw sell price at supply 10"
);
}
2 changes: 1 addition & 1 deletion creator-keys/tests/sell_fee_split_invariants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ fn sell_fee_split_invariant_across_price_range() {
let env = test_env_with_auths();
let (client, _) = register_creator_keys(&env);

let test_prices = vec![1, 2, 3, 10, 99, 100, 101, 999, 1000, 10000];
let test_prices = [1, 2, 3, 10, 99, 100, 101, 999, 1000, 10000];

for (i, price) in test_prices.iter().enumerate() {
let creator = register_test_creator(&env, &client, &format!("creator{}", i));
Expand Down
Loading
Loading