diff --git a/creator-keys/src/events.rs b/creator-keys/src/events.rs index 4935b31..c74648b 100644 --- a/creator-keys/src/events.rs +++ b/creator-keys/src/events.rs @@ -181,6 +181,8 @@ pub struct KeysSoldEvent { pub quantity: u32, /// Net proceeds received by the seller after fees. pub proceeds: i128, + /// Total supply of keys for this creator after the sale. + pub new_supply: u32, /// Ledger sequence number at the time of the sale. pub ledger: u32, } diff --git a/creator-keys/src/lib.rs b/creator-keys/src/lib.rs index 764be7a..7df38ba 100644 --- a/creator-keys/src/lib.rs +++ b/creator-keys/src/lib.rs @@ -2062,6 +2062,7 @@ impl CreatorKeysContract { creator_id: creator.clone(), quantity: 1, proceeds, + new_supply: profile.supply, ledger: env.ledger().sequence(), }; diff --git a/creator-keys/tests/buy_updates_total_supply.rs b/creator-keys/tests/buy_updates_total_supply.rs new file mode 100644 index 0000000..f9b2466 --- /dev/null +++ b/creator-keys/tests/buy_updates_total_supply.rs @@ -0,0 +1,265 @@ +//! Unit tests for `buy_key` keeping the creator's stored total supply correct (#739). +//! +//! Supply is the number every price quote, cap check and dividend split is +//! computed from, so the field written on a buy has to equal the pre-buy supply +//! plus what was bought — and the `get_total_key_supply` view has to report the +//! same number the buy returned. A buy that fails must leave it alone entirely. +//! +//! `buy_key` buys one key per call, so "buying 5 keys" below is five calls. + +mod contract_test_env; + +use contract_test_env::{ + register_creator_keys, register_test_creator, set_pricing_and_fees, test_env_with_auths, +}; +use creator_keys::{ContractError, CreatorKeysContractClient, RegisterCreatorParams}; +use soroban_sdk::{testutils::Address as _, Address, Env, String}; + +const KEY_PRICE: i128 = 1_000; +const CREATOR_BPS: u32 = 9_000; +const PROTOCOL_BPS: u32 = 1_000; + +/// Deploy the contract with pricing, fees, a protocol admin and one creator. +fn setup(env: &Env) -> (CreatorKeysContractClient<'_>, Address, Address) { + let (client, _) = register_creator_keys(env); + let admin = set_pricing_and_fees(env, &client, KEY_PRICE, CREATOR_BPS, PROTOCOL_BPS); + let creator = register_test_creator(env, &client, "alice"); + (client, admin, creator) +} + +/// Register a creator carrying a hard supply cap. +/// +/// The cap can only be set at registration — there is no setter for it — so a +/// capped creator has to be registered separately from the fixture one. +fn register_capped_creator( + env: &Env, + client: &CreatorKeysContractClient<'_>, + handle: &str, + max_supply: u32, +) -> Address { + let creator = Address::generate(env); + client.register_creator( + &RegisterCreatorParams { + creator: creator.clone(), + handle: String::from_str(env, handle), + }, + &None, + &Some(max_supply), + &None, + &None, + &None, + &None, + ); + creator +} + +/// Buy one key at the current quote, returning the supply the call reports. +fn buy_one(client: &CreatorKeysContractClient<'_>, creator: &Address, buyer: &Address) -> u32 { + let quote = client.get_buy_quote(creator); + client.buy_key(creator, buyer, "e.total_amount, &None) +} + +/// Buy `count` keys for `buyer`, asserting the view agrees with the return +/// value after every single call. +fn buy_keys_checking_supply( + client: &CreatorKeysContractClient<'_>, + creator: &Address, + buyer: &Address, + count: u32, +) -> u32 { + let mut last_supply = client.get_total_key_supply(creator); + for _ in 0..count { + let before = last_supply; + last_supply = buy_one(client, creator, buyer); + assert_eq!( + last_supply, + before + 1, + "each buy must increment supply by exactly one" + ); + assert_eq!( + client.get_total_key_supply(creator), + last_supply, + "get_total_key_supply must agree with the value buy_key returned" + ); + } + last_supply +} + +// --------------------------------------------------------------------------- +// Supply grows by exactly what was bought +// --------------------------------------------------------------------------- + +#[test] +fn test_buying_one_key_from_zero_supply_sets_supply_to_one() { + let env = test_env_with_auths(); + let (client, _admin, creator) = setup(&env); + let buyer = Address::generate(&env); + + assert_eq!( + client.get_total_key_supply(&creator), + 0, + "a freshly registered creator starts at zero supply" + ); + + assert_eq!(buy_one(&client, &creator, &buyer), 1); + assert_eq!(client.get_total_key_supply(&creator), 1); +} + +#[test] +fn test_buying_five_keys_from_supply_ten_sets_supply_to_fifteen() { + let env = test_env_with_auths(); + let (client, _admin, creator) = setup(&env); + let early_buyer = Address::generate(&env); + let buyer = Address::generate(&env); + + // Establish the starting supply of 10. + buy_keys_checking_supply(&client, &creator, &early_buyer, 10); + assert_eq!(client.get_total_key_supply(&creator), 10); + + assert_eq!( + buy_keys_checking_supply(&client, &creator, &buyer, 5), + 15, + "five more buys on a supply of ten must land on fifteen" + ); + assert_eq!(client.get_total_key_supply(&creator), 15); +} + +#[test] +fn test_sequential_buys_from_different_wallets_each_increment_supply() { + let env = test_env_with_auths(); + let (client, _admin, creator) = setup(&env); + let first = Address::generate(&env); + let second = Address::generate(&env); + + assert_eq!(buy_one(&client, &creator, &first), 1); + assert_eq!(client.get_total_key_supply(&creator), 1); + + assert_eq!( + buy_one(&client, &creator, &second), + 2, + "a second wallet's buy must continue the same supply counter" + ); + assert_eq!(client.get_total_key_supply(&creator), 2); + + // Supply counts keys in circulation, not distinct holders. + assert_eq!(client.get_key_balance(&creator, &first), 1); + assert_eq!(client.get_key_balance(&creator, &second), 1); +} + +#[test] +fn test_interleaved_buys_across_wallets_accumulate_supply_in_order() { + let env = test_env_with_auths(); + let (client, _admin, creator) = setup(&env); + let first = Address::generate(&env); + let second = Address::generate(&env); + + let wallets = [&first, &second, &first, &second, &first]; + for (index, wallet) in wallets.iter().enumerate() { + let expected = index as u32 + 1; + assert_eq!(buy_one(&client, &creator, wallet), expected); + assert_eq!(client.get_total_key_supply(&creator), expected); + } + + assert_eq!(client.get_key_balance(&creator, &first), 3); + assert_eq!(client.get_key_balance(&creator, &second), 2); +} + +// --------------------------------------------------------------------------- +// A rejected buy leaves supply exactly where it was +// --------------------------------------------------------------------------- + +#[test] +fn test_supply_unchanged_after_a_buy_rejected_on_underpayment() { + let env = test_env_with_auths(); + let (client, _admin, creator) = setup(&env); + let buyer = Address::generate(&env); + + buy_keys_checking_supply(&client, &creator, &buyer, 3); + let supply_before = client.get_total_key_supply(&creator); + let balance_before = client.get_key_balance(&creator, &buyer); + + // One stroop short of the quoted price. + let quote = client.get_buy_quote(&creator); + assert_eq!( + client.try_buy_key(&creator, &buyer, &(quote.price - 1), &None), + Err(Ok(ContractError::InsufficientPayment)) + ); + + assert_eq!( + client.get_total_key_supply(&creator), + supply_before, + "a rejected buy must not move supply" + ); + assert_eq!(client.get_key_balance(&creator, &buyer), balance_before); +} + +#[test] +fn test_supply_unchanged_after_a_buy_rejected_on_non_positive_payment() { + let env = test_env_with_auths(); + let (client, _admin, creator) = setup(&env); + let buyer = Address::generate(&env); + + buy_keys_checking_supply(&client, &creator, &buyer, 2); + let supply_before = client.get_total_key_supply(&creator); + + assert_eq!( + client.try_buy_key(&creator, &buyer, &0, &None), + Err(Ok(ContractError::NotPositiveAmount)) + ); + assert_eq!(client.get_total_key_supply(&creator), supply_before); + + assert_eq!( + client.try_buy_key(&creator, &buyer, &-1, &None), + Err(Ok(ContractError::NotPositiveAmount)) + ); + assert_eq!(client.get_total_key_supply(&creator), supply_before); +} + +#[test] +fn test_supply_unchanged_after_a_buy_rejected_on_the_supply_cap() { + let env = test_env_with_auths(); + let (client, _admin, _creator) = setup(&env); + let capped = register_capped_creator(&env, &client, "capped", 3); + let buyer = Address::generate(&env); + + // Fill the creator right up to their cap. + buy_keys_checking_supply(&client, &capped, &buyer, 3); + let supply_before = client.get_total_key_supply(&capped); + assert_eq!(supply_before, 3); + + let quote = client.get_buy_quote(&capped); + assert_eq!( + client.try_buy_key(&capped, &buyer, "e.total_amount, &None), + Err(Ok(ContractError::SupplyCapExceeded)) + ); + + assert_eq!( + client.get_total_key_supply(&capped), + supply_before, + "a buy rejected on the supply cap must not move supply" + ); + assert_eq!(client.get_key_balance(&capped, &buyer), 3); +} + +#[test] +fn test_supply_unchanged_after_a_buy_for_an_unregistered_creator() { + let env = test_env_with_auths(); + let (client, _admin, creator) = setup(&env); + let buyer = Address::generate(&env); + let unregistered = Address::generate(&env); + + buy_keys_checking_supply(&client, &creator, &buyer, 2); + let supply_before = client.get_total_key_supply(&creator); + + assert_eq!( + client.try_buy_key(&unregistered, &buyer, &(KEY_PRICE * 2), &None), + Err(Ok(ContractError::NotRegistered)) + ); + + assert_eq!(client.get_total_key_supply(&creator), supply_before); + assert_eq!( + client.get_total_key_supply(&unregistered), + 0, + "an unregistered creator must report zero supply" + ); +} diff --git a/creator-keys/tests/sell_event_seller_and_supply.rs b/creator-keys/tests/sell_event_seller_and_supply.rs new file mode 100644 index 0000000..352f919 --- /dev/null +++ b/creator-keys/tests/sell_event_seller_and_supply.rs @@ -0,0 +1,244 @@ +//! Integration tests for the `KeysSold` event carrying the seller and the +//! post-sell supply (#736). +//! +//! Downstream indexers rebuild supply from the trade event stream, so the sell +//! event has to name who sold and what the supply became — and that figure has +//! to agree with what the contract reports afterwards, or the indexed supply +//! drifts from chain state. +//! +//! `sell_key` sells one key per call, so the issue's "sell of 3 keys" is three +//! calls; the event asserted below is the one emitted by the third. + +mod contract_test_env; + +use contract_test_env::{ + register_creator_keys, register_test_creator, set_pricing_and_fees, test_env_with_auths, +}; +use creator_keys::{events, ContractError, CreatorKeysContractClient}; +use soroban_sdk::{ + testutils::{Address as _, Events}, + Address, Env, IntoVal, Symbol, +}; + +const KEY_PRICE: i128 = 1_000; +const CREATOR_BPS: u32 = 9_000; +const PROTOCOL_BPS: u32 = 1_000; + +const STARTING_SUPPLY: u32 = 10; +const KEYS_SOLD: u32 = 3; +const EXPECTED_SUPPLY_AFTER: u32 = STARTING_SUPPLY - KEYS_SOLD; + +/// Deploy the contract with pricing, fees, a protocol admin and one creator. +fn setup(env: &Env) -> (CreatorKeysContractClient<'_>, Address) { + 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"); + (client, creator) +} + +/// Buy `count` keys for `buyer`, one call per key. +fn buy_keys( + client: &CreatorKeysContractClient<'_>, + creator: &Address, + buyer: &Address, + count: u32, +) { + for _ in 0..count { + let quote = client.get_buy_quote(creator); + client.buy_key(creator, buyer, "e.total_amount, &None); + } +} + +/// Decode the single `KeysSold` event in the log, failing if there is not exactly one. +fn expect_one_sell_event(env: &Env) -> events::KeysSoldEvent { + let log = env.events().all(); + let sell_events: std::vec::Vec<_> = log + .iter() + .filter(|(_, topics, _)| { + topics + .get(events::TOPIC_EVENT_NAME_INDEX) + .map(|v| { + let name: Symbol = v.into_val(env); + name == events::SELL_EVENT_NAME + }) + .unwrap_or(false) + }) + .collect(); + + assert_eq!( + sell_events.len(), + 1, + "exactly one sell event must be emitted per sell" + ); + let (_, _, data) = sell_events[0].clone(); + data.into_val(env) +} + +/// Count the `KeysSold` events currently in the log. +fn sell_event_count(env: &Env) -> u32 { + env.events() + .all() + .iter() + .filter(|(_, topics, _)| { + topics + .get(events::TOPIC_EVENT_NAME_INDEX) + .map(|v| { + let name: Symbol = v.into_val(env); + name == events::SELL_EVENT_NAME + }) + .unwrap_or(false) + }) + .count() as u32 +} + +// --------------------------------------------------------------------------- +// The event names the seller and the supply the sell produced +// --------------------------------------------------------------------------- + +#[test] +fn test_sell_event_reports_seller_and_new_supply_after_selling_three_of_ten() { + let env = test_env_with_auths(); + let (client, creator) = setup(&env); + let seller = Address::generate(&env); + + buy_keys(&client, &creator, &seller, STARTING_SUPPLY); + assert_eq!(client.get_total_key_supply(&creator), STARTING_SUPPLY); + assert_eq!(client.get_key_balance(&creator, &seller), STARTING_SUPPLY); + + // Sell the first two keys, then clear the log so only the third sell's + // event is under assertion. + client.sell_key(&creator, &seller, &None); + client.sell_key(&creator, &seller, &None); + env.events().all(); + + let returned_supply = client.sell_key(&creator, &seller, &None); + let event = expect_one_sell_event(&env); + + assert_eq!( + event.seller, seller, + "seller field must be the wallet that sold" + ); + assert_eq!(event.creator_id, creator, "creator_id field must match"); + assert_eq!(event.quantity, 1, "each sell_key call sells one key"); + assert_eq!( + event.new_supply, EXPECTED_SUPPLY_AFTER, + "new_supply must be 7 after selling 3 of 10" + ); + + // The event, the call's return value and the on-chain view must not disagree. + assert_eq!( + event.new_supply, + client.get_total_key_supply(&creator), + "new_supply must match the supply the contract reports after the sell" + ); + assert_eq!( + event.new_supply, returned_supply, + "new_supply must match the value sell_key returned" + ); +} + +#[test] +fn test_sell_event_new_supply_tracks_every_sell_down_to_zero() { + let env = test_env_with_auths(); + let (client, creator) = setup(&env); + let seller = Address::generate(&env); + + buy_keys(&client, &creator, &seller, STARTING_SUPPLY); + + // Each successive sell must report the supply that sell produced. + for expected_supply in (0..STARTING_SUPPLY).rev() { + env.events().all(); + client.sell_key(&creator, &seller, &None); + + let event = expect_one_sell_event(&env); + assert_eq!( + event.new_supply, expected_supply, + "new_supply must be {expected_supply} after this sell" + ); + assert_eq!(event.new_supply, client.get_total_key_supply(&creator)); + } +} + +#[test] +fn test_sell_event_names_the_selling_wallet_not_another_holder() { + let env = test_env_with_auths(); + let (client, creator) = setup(&env); + let seller = Address::generate(&env); + let other_holder = Address::generate(&env); + + buy_keys(&client, &creator, &seller, 5); + buy_keys(&client, &creator, &other_holder, 5); + + env.events().all(); + client.sell_key(&creator, &seller, &None); + + let event = expect_one_sell_event(&env); + assert_eq!(event.seller, seller); + assert_ne!( + event.seller, other_holder, + "the event must not attribute the sale to an uninvolved holder" + ); + assert_eq!( + event.new_supply, 9, + "new_supply counts all holders' keys, not just the seller's" + ); +} + +// --------------------------------------------------------------------------- +// A rejected sell emits nothing +// --------------------------------------------------------------------------- + +#[test] +fn test_no_sell_event_emitted_when_the_sell_is_rejected() { + let env = test_env_with_auths(); + let (client, creator) = setup(&env); + let holder = Address::generate(&env); + let stranger = Address::generate(&env); + + buy_keys(&client, &creator, &holder, STARTING_SUPPLY); + + env.events().all(); + let supply_before = client.get_total_key_supply(&creator); + + // A wallet holding nothing cannot sell. + assert_eq!( + client.try_sell_key(&creator, &stranger, &None), + Err(Ok(ContractError::InsufficientBalance)) + ); + + assert_eq!( + sell_event_count(&env), + 0, + "a rejected sell must not emit a sell event" + ); + assert_eq!( + client.get_total_key_supply(&creator), + supply_before, + "a rejected sell must not move supply" + ); +} + +#[test] +fn test_no_sell_event_emitted_when_the_sell_misses_its_slippage_floor() { + let env = test_env_with_auths(); + let (client, creator) = setup(&env); + let holder = Address::generate(&env); + + buy_keys(&client, &creator, &holder, STARTING_SUPPLY); + + env.events().all(); + + // Demand more than the sale can possibly return. + assert_eq!( + client.try_sell_key(&creator, &holder, &Some(i128::MAX)), + Err(Ok(ContractError::SlippageExceeded)) + ); + + assert_eq!( + sell_event_count(&env), + 0, + "a sell rejected on slippage must not emit a sell event" + ); + assert_eq!(client.get_total_key_supply(&creator), STARTING_SUPPLY); + assert_eq!(client.get_key_balance(&creator, &holder), STARTING_SUPPLY); +} diff --git a/creator-keys/tests/sell_updates_total_supply.rs b/creator-keys/tests/sell_updates_total_supply.rs new file mode 100644 index 0000000..ecf5f63 --- /dev/null +++ b/creator-keys/tests/sell_updates_total_supply.rs @@ -0,0 +1,268 @@ +//! Unit tests for `sell_key` keeping the creator's stored total supply correct (#738). +//! +//! Every successful sell must decrement supply by exactly what was sold, and +//! supply must never be able to go negative — it is a `u32`, so an underflow +//! would wrap to a huge number and corrupt every price quote and dividend split +//! computed from it. A rejected sell must leave supply exactly where it was. +//! +//! # A note on `insufficient_supply` +//! +//! `sell_key` sells one key per call and has no `amount` parameter, so a wallet +//! cannot ask to sell more than the supply through it — the balance guard fires +//! first and returns [`ContractError::InsufficientBalance`]. The contract's +//! `InsufficientSupply` error belongs to the sell-side path that *does* take an +//! amount, `buyback`, which burns N keys and rejects `N > supply`. Both are +//! asserted below so the "cannot sell more than exists" rule is covered on +//! whichever entrypoint enforces it. + +mod contract_test_env; + +use contract_test_env::{ + register_creator_keys, register_test_creator, set_pricing_and_fees, test_env_with_auths, +}; +use creator_keys::{ContractError, CreatorKeysContractClient}; +use soroban_sdk::{testutils::Address as _, Address, Env}; + +const KEY_PRICE: i128 = 1_000; +const CREATOR_BPS: u32 = 9_000; +const PROTOCOL_BPS: u32 = 1_000; + +/// Deploy the contract with pricing, fees, a protocol admin and one creator. +fn setup(env: &Env) -> (CreatorKeysContractClient<'_>, Address, Address) { + let (client, _) = register_creator_keys(env); + let admin = set_pricing_and_fees(env, &client, KEY_PRICE, CREATOR_BPS, PROTOCOL_BPS); + let creator = register_test_creator(env, &client, "alice"); + (client, admin, creator) +} + +/// Buy `count` keys for `buyer`, one call per key. +fn buy_keys( + client: &CreatorKeysContractClient<'_>, + creator: &Address, + buyer: &Address, + count: u32, +) { + for _ in 0..count { + let quote = client.get_buy_quote(creator); + client.buy_key(creator, buyer, "e.total_amount, &None); + } +} + +// --------------------------------------------------------------------------- +// Supply shrinks by exactly what was sold +// --------------------------------------------------------------------------- + +#[test] +fn test_selling_one_key_from_supply_five_sets_supply_to_four() { + let env = test_env_with_auths(); + let (client, _admin, creator) = setup(&env); + let seller = Address::generate(&env); + + buy_keys(&client, &creator, &seller, 5); + assert_eq!(client.get_total_key_supply(&creator), 5); + + let returned = client.sell_key(&creator, &seller, &None); + + assert_eq!(returned, 4, "sell_key must return the post-sell supply"); + assert_eq!( + client.get_total_key_supply(&creator), + 4, + "get_total_key_supply must agree with the value sell_key returned" + ); + assert_eq!(client.get_key_balance(&creator, &seller), 4); +} + +#[test] +fn test_selling_all_keys_from_supply_ten_sets_supply_to_zero() { + let env = test_env_with_auths(); + let (client, _admin, creator) = setup(&env); + let seller = Address::generate(&env); + + buy_keys(&client, &creator, &seller, 10); + assert_eq!(client.get_total_key_supply(&creator), 10); + + // Walk the whole position down, checking the view after every sell. + for expected_supply in (0..10u32).rev() { + let returned = client.sell_key(&creator, &seller, &None); + assert_eq!(returned, expected_supply); + assert_eq!(client.get_total_key_supply(&creator), expected_supply); + } + + assert_eq!( + client.get_total_key_supply(&creator), + 0, + "selling every key must leave supply at zero" + ); + assert_eq!(client.get_key_balance(&creator, &seller), 0); + assert_eq!(client.get_creator_holder_count(&creator), 0); +} + +#[test] +fn test_supply_tracks_sells_across_two_holders() { + let env = test_env_with_auths(); + let (client, _admin, creator) = setup(&env); + let first = Address::generate(&env); + let second = Address::generate(&env); + + buy_keys(&client, &creator, &first, 4); + buy_keys(&client, &creator, &second, 6); + assert_eq!(client.get_total_key_supply(&creator), 10); + + // Supply is the shared total: it drops regardless of which holder sells. + assert_eq!(client.sell_key(&creator, &first, &None), 9); + assert_eq!(client.sell_key(&creator, &second, &None), 8); + assert_eq!(client.sell_key(&creator, &second, &None), 7); + + assert_eq!(client.get_total_key_supply(&creator), 7); + assert_eq!(client.get_key_balance(&creator, &first), 3); + assert_eq!(client.get_key_balance(&creator, &second), 4); +} + +// --------------------------------------------------------------------------- +// Supply can never go below zero +// --------------------------------------------------------------------------- + +#[test] +fn test_selling_past_zero_supply_is_rejected_and_never_underflows() { + let env = test_env_with_auths(); + let (client, _admin, creator) = setup(&env); + let seller = Address::generate(&env); + + buy_keys(&client, &creator, &seller, 2); + client.sell_key(&creator, &seller, &None); + client.sell_key(&creator, &seller, &None); + assert_eq!(client.get_total_key_supply(&creator), 0); + + // The wallet now holds nothing, so the next sell is refused rather than + // wrapping the u32 supply around. + assert_eq!( + client.try_sell_key(&creator, &seller, &None), + Err(Ok(ContractError::InsufficientBalance)) + ); + + assert_eq!( + client.get_total_key_supply(&creator), + 0, + "supply must stay at zero, not wrap to u32::MAX" + ); +} + +#[test] +fn test_selling_more_than_the_wallet_holds_is_rejected_on_balance() { + let env = test_env_with_auths(); + let (client, _admin, creator) = setup(&env); + let seller = Address::generate(&env); + + buy_keys(&client, &creator, &seller, 3); + + // Three sells drain the position exactly. + for _ in 0..3 { + client.sell_key(&creator, &seller, &None); + } + + // `sell_key` takes no amount, so "more than I hold" surfaces as a fourth + // call against an empty balance. + assert_eq!( + client.try_sell_key(&creator, &seller, &None), + Err(Ok(ContractError::InsufficientBalance)) + ); + assert_eq!(client.get_total_key_supply(&creator), 0); +} + +#[test] +fn test_burning_more_keys_than_supply_is_rejected_with_insufficient_supply() { + let env = test_env_with_auths(); + let (client, _admin, creator) = setup(&env); + + // `buyback` is the sell-side path that takes an amount, so it is the one + // that can be asked to remove more keys than exist. + buy_keys(&client, &creator, &creator, 5); + let supply_before = client.get_total_key_supply(&creator); + assert_eq!(supply_before, 5); + + let over_supply = supply_before + 1; + assert_eq!( + client.try_get_buyback_quote(&creator, &over_supply), + Err(Ok(ContractError::InsufficientSupply)), + "quoting a burn larger than supply must be rejected" + ); + assert_eq!( + client.try_buyback(&creator, &creator, &over_supply, &(KEY_PRICE * 100), &None), + Err(Ok(ContractError::InsufficientSupply)), + "burning more keys than exist must be rejected" + ); + + assert_eq!( + client.get_total_key_supply(&creator), + supply_before, + "a rejected burn must not move supply" + ); +} + +// --------------------------------------------------------------------------- +// A rejected sell leaves supply exactly where it was +// --------------------------------------------------------------------------- + +#[test] +fn test_supply_unchanged_after_a_sell_from_a_wallet_holding_nothing() { + let env = test_env_with_auths(); + let (client, _admin, creator) = setup(&env); + let holder = Address::generate(&env); + let stranger = Address::generate(&env); + + buy_keys(&client, &creator, &holder, 6); + let supply_before = client.get_total_key_supply(&creator); + + assert_eq!( + client.try_sell_key(&creator, &stranger, &None), + Err(Ok(ContractError::InsufficientBalance)) + ); + + assert_eq!(client.get_total_key_supply(&creator), supply_before); + assert_eq!(client.get_key_balance(&creator, &holder), 6); +} + +#[test] +fn test_supply_unchanged_after_a_sell_rejected_on_slippage() { + let env = test_env_with_auths(); + let (client, _admin, creator) = setup(&env); + let seller = Address::generate(&env); + + buy_keys(&client, &creator, &seller, 4); + let supply_before = client.get_total_key_supply(&creator); + + // Demand more proceeds than the sale can return. + assert_eq!( + client.try_sell_key(&creator, &seller, &Some(i128::MAX)), + Err(Ok(ContractError::SlippageExceeded)) + ); + + assert_eq!( + client.get_total_key_supply(&creator), + supply_before, + "a sell rejected on slippage must not move supply" + ); + assert_eq!(client.get_key_balance(&creator, &seller), 4); + + // The position is still sellable on the very next call. + assert_eq!(client.sell_key(&creator, &seller, &None), supply_before - 1); +} + +#[test] +fn test_supply_unchanged_after_a_sell_for_an_unregistered_creator() { + let env = test_env_with_auths(); + let (client, _admin, creator) = setup(&env); + let seller = Address::generate(&env); + let unregistered = Address::generate(&env); + + buy_keys(&client, &creator, &seller, 3); + let supply_before = client.get_total_key_supply(&creator); + + assert_eq!( + client.try_sell_key(&unregistered, &seller, &None), + Err(Ok(ContractError::NotRegistered)) + ); + + assert_eq!(client.get_total_key_supply(&creator), supply_before); + assert_eq!(client.get_total_key_supply(&unregistered), 0); +}