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
18 changes: 18 additions & 0 deletions creator-keys/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2710,6 +2710,24 @@ impl CreatorKeysContract {
checked_format_quote_response(price, creator_fee, protocol_fee, true)
}

/// Read-only price query helper for a given creator and supply step.
///
/// Computes the bonding curve price for `supply` without requiring authorization
/// or mutating contract state.
///
/// Returns `Err(ContractError::KeyPriceNotSet)` if base key price is not set,
/// or `Err(ContractError::Overflow)` if arithmetic overflows or supply exceeds `u32::MAX`.
pub fn query_price(env: Env, creator: Address, supply: u64) -> Result<i128, ContractError> {
let supply_u32 = u32::try_from(supply).map_err(|_| ContractError::Overflow)?;
let base_price: i128 = env
.storage()
.persistent()
.get(&constants::storage::KEY_PRICE)
.ok_or(ContractError::KeyPriceNotSet)?;

compute_bonding_curve_price(&env, &creator, base_price, supply_u32)
}

/// Read-only view: returns the total creator buyback cost for a given amount.
///
/// The returned value is `base_price(amount) + protocol_fee(amount)` because the
Expand Down
121 changes: 121 additions & 0 deletions creator-keys/src/test_issues.rs
Original file line number Diff line number Diff line change
Expand Up @@ -701,4 +701,125 @@ mod issue_tests {
"different creators must produce different locked allocation keys"
);
}

// =============================================================================
// Tests for Issue #572
// =============================================================================

#[test]
fn test_query_price_at_boundary_supplies() {
let env = Env::default();
env.mock_all_auths();
let contract_id = env.register(CreatorKeysContract, ());
let client = CreatorKeysContractClient::new(&env, &contract_id);
let admin = Address::generate(&env);

let base_price = 100i128;
client.set_key_price(&admin, &base_price);

// Test for Linear curve
let creator = register_creator(&env, &client, None);
for supply in [0u64, 1u64, 50u64] {
let queried_price = client.query_price(&creator, &supply);
let expected_price = env.as_contract(&contract_id, || {
compute_bonding_curve_price(&env, &creator, base_price, supply as u32).unwrap()
});
assert_eq!(
queried_price, expected_price,
"query_price output must match buy-path bonding curve formula for supply {}",
supply
);
}

// Test for Flat curve
let flat_creator = Address::generate(&env);
let handle = String::from_str(&env, "bob");
client.register_creator(
&crate::RegisterCreatorParams {
creator: flat_creator.clone(),
handle,
},
&None,
&None,
&None,
&Some(CurvePreset::Flat),
&None,
&None,
);
for supply in [0u64, 1u64, 50u64] {
let price = client.query_price(&flat_creator, &supply);
assert_eq!(price, base_price);
}

// Test for Quadratic curve
let quad_creator = Address::generate(&env);
let handle_q = String::from_str(&env, "charlie");
client.register_creator(
&crate::RegisterCreatorParams {
creator: quad_creator.clone(),
handle: handle_q,
},
&None,
&None,
&None,
&Some(CurvePreset::Quadratic),
&None,
&None,
);
for supply in [0u64, 1u64, 50u64] {
let queried_price = client.query_price(&quad_creator, &supply);
let expected_price = env.as_contract(&contract_id, || {
compute_bonding_curve_price(&env, &quad_creator, base_price, supply as u32).unwrap()
});
assert_eq!(queried_price, expected_price);
}
}

#[test]
fn test_protocol_fee_calculation_at_boundary_supplies() {
let env = Env::default();
env.mock_all_auths();
let contract_id = env.register(CreatorKeysContract, ());
let client = CreatorKeysContractClient::new(&env, &contract_id);
let admin = Address::generate(&env);

let base_price = 100i128;
client.set_key_price(&admin, &base_price);

let protocol_bps = 250u32; // 2.5%
let creator_bps = 500u32; // 5.0%
client.set_fee_config(&admin, &creator_bps, &protocol_bps);

let creator = register_creator(&env, &client, None);

// Boundary supply values: 0, 1, 1000
for supply in [0u64, 1u64, 1000u64] {
let price = client.query_price(&creator, &supply);
let computed_fee = crate::fee::apply_percentage_fee(price, protocol_bps).unwrap();
let expected_fee = (price * protocol_bps as i128) / crate::fee::BPS_MAX as i128;

assert_eq!(
computed_fee, expected_fee,
"Protocol fee for supply {} must match expected BPS calculation",
supply
);
}
}

#[test]
fn test_protocol_fee_floors_on_non_whole_stroops() {
// Price = 1050 stroops, protocol_bps = 50 bps (0.5%)
// Exact: 1050 * 50 / 10000 = 5.25 stroops -> Floors to 5 stroops
let price_a = 1050i128;
let bps_a = 50u32;
let fee_a = crate::fee::apply_percentage_fee(price_a, bps_a).unwrap();
assert_eq!(fee_a, 5, "Fee 5.25 stroops must floor to 5 stroops");

// Price = 105 stroops, protocol_bps = 50 bps (0.5%)
// Exact: 105 * 50 / 10000 = 0.525 stroops -> Floors to 0 stroops
let price_b = 105i128;
let bps_b = 50u32;
let fee_b = crate::fee::apply_percentage_fee(price_b, bps_b).unwrap();
assert_eq!(fee_b, 0, "Fee 0.525 stroops must floor to 0 stroops");
}
}
Loading