|
| 1 | +//! Integration test confirming the bonding curve price increases monotonically |
| 2 | +//! across ten sequential single-key buys, and decreases after a sell (Issue #697). |
| 3 | +//! |
| 4 | +//! Covers all four acceptance criteria: |
| 5 | +//! AC-1 Price strictly increases after each sequential buy. |
| 6 | +//! AC-2 Price strictly decreases after a sell. |
| 7 | +//! AC-3 Price at supply 0 equals the configured base price. |
| 8 | +//! AC-4 All ten price snapshots are monotonically increasing. |
| 9 | +
|
| 10 | +mod contract_test_env; |
| 11 | + |
| 12 | +use contract_test_env::{ |
| 13 | + register_creator_keys, register_test_creator, set_curve_slope, set_pricing_and_fees, |
| 14 | + test_env_with_auths, |
| 15 | +}; |
| 16 | +use soroban_sdk::testutils::Address as _; |
| 17 | + |
| 18 | +const BASE_PRICE: i128 = 5_000; |
| 19 | +const CURVE_SLOPE: i128 = 100; |
| 20 | +const NUM_BUYS: usize = 10; |
| 21 | + |
| 22 | +/// AC-3: `get_buy_quote` at supply 0 returns a price equal to BASE_PRICE. |
| 23 | +/// AC-1 + AC-4: all ten buy-quote prices are strictly increasing. |
| 24 | +/// AC-2: one sell after ten buys yields a price strictly below the last buy price. |
| 25 | +#[test] |
| 26 | +fn test_ten_sequential_buys_price_strictly_increases_and_sell_decreases() { |
| 27 | + let env = test_env_with_auths(); |
| 28 | + let (client, _) = register_creator_keys(&env); |
| 29 | + |
| 30 | + set_pricing_and_fees(&env, &client, BASE_PRICE, 9000, 1000); |
| 31 | + set_curve_slope(&env, &client, CURVE_SLOPE); |
| 32 | + |
| 33 | + let creator = register_test_creator(&env, &client, "alice"); |
| 34 | + let holder = soroban_sdk::Address::generate(&env); |
| 35 | + |
| 36 | + // AC-3: price at supply 0 must equal BASE_PRICE. |
| 37 | + let price_at_zero = client.get_buy_quote(&creator).price; |
| 38 | + assert_eq!( |
| 39 | + price_at_zero, BASE_PRICE, |
| 40 | + "price at supply 0 must equal the configured base price" |
| 41 | + ); |
| 42 | + |
| 43 | + // AC-1 + AC-4: record the quote price before each buy (price at supply i), |
| 44 | + // execute the buy, then assert each successive quote is strictly greater. |
| 45 | + // Buy 0 goes to `holder` so they hold a key for the sell assertion below. |
| 46 | + let mut buy_prices: Vec<i128> = Vec::new(); |
| 47 | + |
| 48 | + for i in 0..NUM_BUYS { |
| 49 | + let quote = client.get_buy_quote(&creator); |
| 50 | + |
| 51 | + if let Some(&prev_price) = buy_prices.last() { |
| 52 | + assert!( |
| 53 | + quote.price > prev_price, |
| 54 | + "buy {} price ({}) must be strictly greater than buy {} price ({})", |
| 55 | + i + 1, |
| 56 | + quote.price, |
| 57 | + i, |
| 58 | + prev_price |
| 59 | + ); |
| 60 | + } |
| 61 | + |
| 62 | + buy_prices.push(quote.price); |
| 63 | + |
| 64 | + let buyer = if i == 0 { |
| 65 | + holder.clone() |
| 66 | + } else { |
| 67 | + soroban_sdk::Address::generate(&env) |
| 68 | + }; |
| 69 | + |
| 70 | + client.buy_key(&creator, &buyer, "e.total_amount, &None); |
| 71 | + } |
| 72 | + |
| 73 | + assert_eq!(buy_prices.len(), NUM_BUYS); |
| 74 | + |
| 75 | + // AC-4: full monotonicity check across the collected snapshots. |
| 76 | + for (index, window) in buy_prices.windows(2).enumerate() { |
| 77 | + assert!( |
| 78 | + window[1] > window[0], |
| 79 | + "price snapshot {} ({}) must be strictly greater than snapshot {} ({})", |
| 80 | + index + 2, |
| 81 | + window[1], |
| 82 | + index + 1, |
| 83 | + window[0] |
| 84 | + ); |
| 85 | + } |
| 86 | + |
| 87 | + // AC-2: price after one sell must be strictly less than the last buy price. |
| 88 | + let price_before_sell = client.get_buy_quote(&creator).price; |
| 89 | + client.sell_key(&creator, &holder, &None); |
| 90 | + let price_after_sell = client.get_buy_quote(&creator).price; |
| 91 | + |
| 92 | + assert!( |
| 93 | + price_after_sell < price_before_sell, |
| 94 | + "price after sell ({}) must be strictly less than price before sell ({})", |
| 95 | + price_after_sell, |
| 96 | + price_before_sell |
| 97 | + ); |
| 98 | +} |
| 99 | + |
| 100 | +/// Regression guard: a flat (zero-slope) curve does not produce strictly |
| 101 | +/// increasing prices, confirming the main test above is actually discriminating. |
| 102 | +#[test] |
| 103 | +fn test_flat_curve_prices_do_not_strictly_increase_across_ten_buys() { |
| 104 | + let env = test_env_with_auths(); |
| 105 | + let (client, _) = register_creator_keys(&env); |
| 106 | + |
| 107 | + // No set_curve_slope call — defaults to 0 (flat). |
| 108 | + set_pricing_and_fees(&env, &client, BASE_PRICE, 9000, 1000); |
| 109 | + |
| 110 | + let creator = register_test_creator(&env, &client, "bob"); |
| 111 | + |
| 112 | + let mut prices: Vec<i128> = Vec::new(); |
| 113 | + |
| 114 | + for _ in 0..NUM_BUYS { |
| 115 | + let quote = client.get_buy_quote(&creator); |
| 116 | + prices.push(quote.price); |
| 117 | + let buyer = soroban_sdk::Address::generate(&env); |
| 118 | + client.buy_key(&creator, &buyer, "e.total_amount, &None); |
| 119 | + } |
| 120 | + |
| 121 | + let strictly_increasing = prices.windows(2).all(|w| w[1] > w[0]); |
| 122 | + assert!( |
| 123 | + !strictly_increasing, |
| 124 | + "flat pricing must not produce strictly increasing prices across ten buys" |
| 125 | + ); |
| 126 | +} |
| 127 | + |
| 128 | +/// Validates the ten price snapshots against the bonding curve formula |
| 129 | +/// `price = base_price + slope * supply` for every step. |
| 130 | +#[test] |
| 131 | +fn test_ten_buy_prices_match_bonding_curve_formula() { |
| 132 | + let env = test_env_with_auths(); |
| 133 | + let (client, _) = register_creator_keys(&env); |
| 134 | + |
| 135 | + set_pricing_and_fees(&env, &client, BASE_PRICE, 9000, 1000); |
| 136 | + set_curve_slope(&env, &client, CURVE_SLOPE); |
| 137 | + |
| 138 | + let creator = register_test_creator(&env, &client, "carol"); |
| 139 | + |
| 140 | + for i in 0..NUM_BUYS { |
| 141 | + let quote = client.get_buy_quote(&creator); |
| 142 | + |
| 143 | + let expected = BASE_PRICE + CURVE_SLOPE * i as i128; |
| 144 | + assert_eq!( |
| 145 | + quote.price, expected, |
| 146 | + "price at supply {} must be {} (base_price + slope * supply)", |
| 147 | + i, expected |
| 148 | + ); |
| 149 | + |
| 150 | + let buyer = soroban_sdk::Address::generate(&env); |
| 151 | + client.buy_key(&creator, &buyer, "e.total_amount, &None); |
| 152 | + } |
| 153 | + |
| 154 | + assert_eq!( |
| 155 | + client.get_total_key_supply(&creator), |
| 156 | + NUM_BUYS as u32, |
| 157 | + "supply must equal ten after ten buys" |
| 158 | + ); |
| 159 | +} |
0 commit comments