Skip to content
Open
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
37 changes: 19 additions & 18 deletions creator-keys/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2594,25 +2594,26 @@ impl CreatorKeysContract {
.ok_or(ContractError::Overflow)?;
let post_price = compute_bonding_curve_price(&env, &creator, base_price, post_supply)?;

if pre_price > 0 && post_price > pre_price {
let price_change = (post_price - pre_price) as u128;
let pre_price_u128 = pre_price as u128;
let threshold_pct_u128 = threshold_pct as u128;
if price_change
.checked_mul(100)
.ok_or(ContractError::Overflow)?
>= pre_price_u128
.checked_mul(threshold_pct_u128)
if pre_price > 0 && post_price > pre_price {
let price_change = (post_price - pre_price) as u128;
let pre_price_u128 = pre_price as u128;
let threshold_pct_u128 = threshold_pct as u128;
if price_change
.checked_mul(100)
.ok_or(ContractError::Overflow)?
{
env.events().publish(
(events::circuit_breaker_triggered_topics(),),
events::CircuitBreakerTriggeredEvent {
pre_price,
post_price,
},
);
return Err(ContractError::CircuitBreakerTriggered);
>= pre_price_u128
.checked_mul(threshold_pct_u128)
.ok_or(ContractError::Overflow)?
{
env.events().publish(
(events::circuit_breaker_triggered_topics(),),
events::CircuitBreakerTriggeredEvent {
pre_price,
post_price,
},
);
return Err(ContractError::CircuitBreakerTriggered);
}
}

pre_price
Expand Down
192 changes: 192 additions & 0 deletions creator-keys/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2451,3 +2451,195 @@ fn test_buy_and_sell_events_contain_matching_creator_id() {
"sell event creator_id field must match the creator address"
);
}

// ── transfer_keys holder-count accounting ──────────────────────────────────
//
// `holder_count` is the number of wallets with a non-zero balance, and
// `transfer_keys` is the one path that can both remove and add a holder in a
// single call. The existing transfer tests assert balances and supply but
// never the holder count, so the three transitions below were untested:
// a sender emptied out, a first-time recipient, and a partial transfer that
// changes neither.

/// Registers a creator and gives `holder` `count` keys, returning nothing —
/// the tests below read holder_count through the public view.
fn setup_creator_with_keys(
env: &Env,
client: &CreatorKeysContractClient,
admin: &Address,
creator: &Address,
holder: &Address,
count: u32,
) {
client.set_key_price(admin, &100i128);
client.register_creator(
&crate::RegisterCreatorParams {
creator: creator.clone(),
handle: String::from_str(env, "alice"),
},
&None,
&None,
&None,
&None,
&None,
);
for _ in 0..count {
client.buy_key(creator, holder, &100i128, &None);
}
}

#[test]
fn test_transfer_keys_full_balance_decrements_holder_count() {
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 creator = Address::generate(&env);
let sender = Address::generate(&env);
let recipient = Address::generate(&env);

setup_creator_with_keys(&env, &client, &admin, &creator, &sender, 2);

// One holder: the sender. The recipient holds nothing yet.
assert_eq!(client.get_creator_holder_count(&creator), 1);

// Sender transfers their entire balance out.
client.transfer_keys(&creator, &sender, &recipient, &2);

assert_eq!(client.get_key_balance(&creator, &sender), 0);

// Net count is still 1 — the sender left the holder set as the recipient
// joined it — so assert the membership directly rather than trusting the
// total, which a decrement-only or increment-only bug would also satisfy.
assert_eq!(client.get_creator_holder_count(&creator), 1);
assert_eq!(client.get_key_balance(&creator, &recipient), 2);
}

#[test]
fn test_transfer_keys_full_balance_to_existing_holder_decrements_count() {
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 creator = Address::generate(&env);
let sender = Address::generate(&env);
let recipient = Address::generate(&env);

setup_creator_with_keys(&env, &client, &admin, &creator, &sender, 2);
// Give the recipient a key of their own so they are already a holder.
client.buy_key(&creator, &recipient, &100i128, &None);
assert_eq!(client.get_creator_holder_count(&creator), 2);

// Sender empties out to a wallet that already holds keys. Only the
// decrement should fire, which isolates it from the increment.
client.transfer_keys(&creator, &sender, &recipient, &2);

assert_eq!(client.get_key_balance(&creator, &sender), 0);
assert_eq!(client.get_creator_holder_count(&creator), 1);
}

#[test]
fn test_transfer_keys_first_time_recipient_increments_holder_count() {
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 creator = Address::generate(&env);
let sender = Address::generate(&env);
let recipient = Address::generate(&env);

setup_creator_with_keys(&env, &client, &admin, &creator, &sender, 3);
assert_eq!(client.get_creator_holder_count(&creator), 1);

// Partial transfer to a wallet holding nothing: the sender stays a holder
// and the recipient becomes one, so the count must rise to 2. This
// isolates the increment from the decrement.
client.transfer_keys(&creator, &sender, &recipient, &1);

assert_eq!(client.get_key_balance(&creator, &sender), 2);
assert_eq!(client.get_key_balance(&creator, &recipient), 1);
assert_eq!(client.get_creator_holder_count(&creator), 2);
}

#[test]
fn test_transfer_keys_partial_to_existing_holder_leaves_count_unchanged() {
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 creator = Address::generate(&env);
let sender = Address::generate(&env);
let recipient = Address::generate(&env);

setup_creator_with_keys(&env, &client, &admin, &creator, &sender, 3);
client.buy_key(&creator, &recipient, &100i128, &None);

let before = client.get_creator_holder_count(&creator);
assert_eq!(before, 2);

// Neither side crosses zero, so neither branch should fire.
client.transfer_keys(&creator, &sender, &recipient, &1);

assert_eq!(client.get_key_balance(&creator, &sender), 2);
assert_eq!(client.get_key_balance(&creator, &recipient), 2);
assert_eq!(client.get_creator_holder_count(&creator), before);
}

#[test]
fn test_transfer_keys_holder_count_survives_a_round_trip() {
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 creator = Address::generate(&env);
let sender = Address::generate(&env);
let recipient = Address::generate(&env);

setup_creator_with_keys(&env, &client, &admin, &creator, &sender, 1);
assert_eq!(client.get_creator_holder_count(&creator), 1);

// Out and back again. A decrement that did not pair with its increment
// would leave the count drifting after repeated transfers, which is the
// failure mode this guards.
client.transfer_keys(&creator, &sender, &recipient, &1);
assert_eq!(client.get_creator_holder_count(&creator), 1);

client.transfer_keys(&creator, &recipient, &sender, &1);
assert_eq!(client.get_creator_holder_count(&creator), 1);

assert_eq!(client.get_key_balance(&creator, &sender), 1);
assert_eq!(client.get_key_balance(&creator, &recipient), 0);
}

#[test]
fn test_transfer_keys_holder_count_never_exceeds_distinct_holders() {
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 creator = Address::generate(&env);
let sender = Address::generate(&env);
let first = Address::generate(&env);
let second = Address::generate(&env);

setup_creator_with_keys(&env, &client, &admin, &creator, &sender, 4);

// Two first-time recipients, sender retains a balance throughout.
client.transfer_keys(&creator, &sender, &first, &1);
client.transfer_keys(&creator, &sender, &second, &1);

// Three wallets hold keys, so the count is three — a second transfer to
// an existing holder must not count them twice.
client.transfer_keys(&creator, &sender, &first, &1);

assert_eq!(client.get_creator_holder_count(&creator), 3);
assert_eq!(client.get_key_balance(&creator, &sender), 1);
assert_eq!(client.get_key_balance(&creator, &first), 2);
assert_eq!(client.get_key_balance(&creator, &second), 1);
}
Loading