From fed974984d185b8920ea99a9bb2d78a3ca8949da Mon Sep 17 00:00:00 2001 From: aishagbadamosi <125849753+aishagbadamosi@users.noreply.github.com> Date: Sat, 17 Oct 2026 18:41:56 +0100 Subject: [PATCH 1/2] feat: slippage-protected bonding curve swap with revert on breach Add swap() and get_price_impact() entrypoints that execute a bonding curve buy or sell and revert with SlippageExceeded when price impact exceeds max_slippage_bps. Emit SwapSlippageExceededEvent with actual and max bps. Closes #1052 --- creator-keys/src/events.rs | 29 ++ creator-keys/src/lib.rs | 206 +++++++++++++ .../tests/bonding_curve_swap_slippage.rs | 291 ++++++++++++++++++ docs/slippage-protection.md | 4 + 4 files changed, 530 insertions(+) create mode 100644 creator-keys/tests/bonding_curve_swap_slippage.rs diff --git a/creator-keys/src/events.rs b/creator-keys/src/events.rs index d12df836..4d8132ff 100644 --- a/creator-keys/src/events.rs +++ b/creator-keys/src/events.rs @@ -2260,6 +2260,35 @@ pub fn slippage_check_passed_topics(creator: &Address) -> (Symbol, Address) { (SLIPPAGE_CHECK_PASSED_EVENT_NAME, creator.clone()) } +/// Event name for a bonding-curve swap rejected by its price-impact guard. +pub const SWAP_SLIPPAGE_EXCEEDED_EVENT_NAME: Symbol = symbol_short!("swap_slp"); + +/// Emitted when a bonding-curve `swap` is rejected because the computed +/// price impact exceeds the caller's `max_slippage_bps` tolerance. The +/// payload carries both the actual impact and the requested ceiling so +/// clients and indexers can surface the exact breach magnitude. +/// +/// Event shape: +/// - topics: `(SWAP_SLIPPAGE_EXCEEDED_EVENT_NAME, creator)` +/// - data: `SwapSlippageExceededEvent` +#[derive(Clone, Debug, Eq, PartialEq)] +#[contracttype] +pub struct SwapSlippageExceededEvent { + /// Creator whose bonding curve was being swapped against. + pub creator_id: Address, + /// Computed price impact of the rejected swap, in basis points. + pub actual_bps: u32, + /// Caller-supplied slippage ceiling that the impact breached. + pub max_bps: u32, + /// Ledger sequence at which the breach was detected. + pub ledger: u32, +} + +/// Shared swap-slippage-exceeded event topics tuple. +pub fn swap_slippage_exceeded_topics(creator: &Address) -> (Symbol, Address) { + (SWAP_SLIPPAGE_EXCEEDED_EVENT_NAME, creator.clone()) +} + // --- Max buy quantity per transaction (#828) --- /// Event name emitted when the creator updates the max buy quantity per transaction. diff --git a/creator-keys/src/lib.rs b/creator-keys/src/lib.rs index 4ac833d1..8498c58d 100644 --- a/creator-keys/src/lib.rs +++ b/creator-keys/src/lib.rs @@ -1509,6 +1509,19 @@ pub enum CurvePreset { Flat = 2, } +/// Direction of a slippage-protected bonding-curve swap. +/// +/// Consumed by [`CreatorKeysContract::swap`] and +/// [`CreatorKeysContract::get_price_impact`]. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[contracttype] +pub enum SwapDirection { + /// Purchase `amount` keys, walking the curve up from the current supply. + Buy = 0, + /// Sell `amount` keys, walking the curve down from the current supply. + Sell = 1, +} + /// Archive partition strategy for retention management. #[derive(Clone, Copy, Debug, Eq, PartialEq)] #[contracttype] @@ -3379,6 +3392,70 @@ fn assert_sell_proceeds_slippage( Ok(()) } +/// Computes the bonding-curve spot price before and after a swap of `amount` +/// keys in `direction`, together with the impact between them in basis points. +/// +/// The pre-swap price is the current spot price at the live supply. The +/// post-swap price is the spot price the curve would report once the whole +/// trade has moved supply: `supply + amount` for a buy, `supply - amount` +/// for a sell. Impact is the absolute relative move between the two prices, +/// scaled by 10 000 (see [`compute_slippage_bps`]). +/// +/// Returns `(pre_price, post_price, impact_bps)`. +/// +/// # Errors +/// +/// - [`ContractError::KeyPriceNotSet`] if no base key price is configured. +/// - [`ContractError::NotRegistered`] if the creator is not registered. +/// - [`ContractError::InsufficientSupply`] for a sell larger than the live supply. +/// - [`ContractError::Overflow`] on arithmetic overflow or a zero pre-swap price. +fn compute_swap_price_impact( + env: &Env, + creator: &Address, + amount: u32, + direction: SwapDirection, +) -> Result<(i128, i128, u32), ContractError> { + let base_price: i128 = env + .storage() + .persistent() + .get(&constants::storage::KEY_PRICE) + .ok_or(ContractError::KeyPriceNotSet)?; + + let profile = read_registered_creator_profile(env, creator)?; + let supply = profile.supply; + + let post_supply = match direction { + SwapDirection::Buy => supply.checked_add(amount).ok_or(ContractError::Overflow)?, + SwapDirection::Sell => supply + .checked_sub(amount) + .ok_or(ContractError::InsufficientSupply)?, + }; + + let pre_price = compute_bonding_curve_price(env, creator, base_price, supply)?; + let post_price = compute_bonding_curve_price(env, creator, base_price, post_supply)?; + let impact_bps = compute_slippage_bps(pre_price, post_price)?; + Ok((pre_price, post_price, impact_bps)) +} + +/// Sums the bonding-curve price of each key a `amount`-key buy would mint, +/// walking the curve from `start_supply` upwards. This is the exact budget +/// [`CreatorKeysContract::buy_keys`] needs to fill the whole order. +fn compute_swap_buy_cost( + env: &Env, + creator: &Address, + base_price: i128, + start_supply: u32, + amount: u32, +) -> Result { + let mut total: i128 = 0; + for i in 0..amount { + let supply = start_supply.checked_add(i).ok_or(ContractError::Overflow)?; + let price = compute_bonding_curve_price(env, creator, base_price, supply)?; + total = total.checked_add(price).ok_or(ContractError::Overflow)?; + } + Ok(total) +} + fn accrue_sell_trade_fees(env: &Env, creator: &Address, price: i128) -> Result<(), ContractError> { // Deduct the protocol trade fee first so the treasury is paid before the // creator/seller split is computed on the remainder. @@ -9148,6 +9225,135 @@ impl CreatorKeysContract { checked_format_quote_response(price, creator_fee, protocol_fee, false) } + /// Read-only view: returns the bonding-curve price impact, in basis points, + /// of swapping `amount` keys in `direction` against the current supply. + /// + /// The impact is the absolute relative move of the curve's spot price + /// caused by the whole trade: the spot price at the live supply versus the + /// spot price once supply has shifted by `amount`. On a flat curve the + /// impact is always `0`; on a rising curve a buy moves the price up and a + /// sell moves it down. Use this view to derive a `max_slippage_bps` bound + /// before calling [`Self::swap`]. + /// + /// # Errors + /// + /// - [`ContractError::KeyPriceNotSet`] if no base key price is configured. + /// - [`ContractError::NotRegistered`] if the creator is not registered. + /// - [`ContractError::InsufficientSupply`] for a sell larger than the live supply. + /// - [`ContractError::Overflow`] on arithmetic overflow. + pub fn get_price_impact( + env: Env, + creator: Address, + amount: u32, + direction: SwapDirection, + ) -> Result { + let (_, _, impact_bps) = compute_swap_price_impact(&env, &creator, amount, direction)?; + Ok(impact_bps) + } + + /// Executes a slippage-protected swap of `amount` keys in `direction` + /// against the creator's bonding curve. + /// + /// The pre-swap spot price and the post-swap spot price are both computed + /// before any state is committed. When `max_slippage_bps` is non-zero and + /// the resulting price impact exceeds it, the call reverts with + /// [`ContractError::SlippageExceeded`] after emitting a + /// [`events::SwapSlippageExceededEvent`] carrying the actual and maximum + /// basis-point values. A `max_slippage_bps` of `0` skips the check + /// entirely, letting advanced callers opt out of the guard. An impact + /// exactly equal to the tolerance is accepted; only a strictly larger + /// impact reverts. + /// + /// A buy fills completely or not at all: the exact curve cost of the whole + /// order is pre-computed and used as the payment budget for + /// [`Self::buy_keys`], and a partial fill caused by a per-creator cap + /// reverts with [`ContractError::SupplyCapExceeded`]. A sell is executed + /// through [`Self::batch_sell`] with a single order, so it inherits that + /// path's pre-flight balance checks, flash-loan guard, launch penalty and + /// the [`MAX_BATCH_SELL_SIZE`] ceiling on `amount`. + /// + /// Returns the gross curve cost paid for a buy, or the net seller proceeds + /// reported by the batch-sell path for a sell. + /// + /// # Errors + /// + /// - [`ContractError::NotPositiveAmount`] if `amount` is zero. + /// - [`ContractError::SlippageExceeded`] if the impact breaches + /// `max_slippage_bps` (and the bound is non-zero). + /// - [`ContractError::KeyPriceNotSet`] if no base key price is configured. + /// - [`ContractError::NotRegistered`] if the creator is not registered. + /// - [`ContractError::InsufficientSupply`] for a sell larger than the live supply. + /// - [`ContractError::SupplyCapExceeded`] if a cap stopped the buy short of `amount`. + /// - [`ContractError::BatchSizeExceeded`] for a sell of more than + /// [`MAX_BATCH_SELL_SIZE`] keys. + /// - Errors from the underlying buy/sell paths (pause, blacklist, balance, + /// cooldown, circuit breaker, …) propagate unchanged. + pub fn swap( + env: Env, + creator: Address, + trader: Address, + amount: u32, + direction: SwapDirection, + max_slippage_bps: u32, + ) -> Result { + if amount == 0 { + return Err(ContractError::NotPositiveAmount); + } + + // Record the pre-swap price and compute the post-swap price before any + // state is committed, so a breach reverts without side effects. + let (_, _, impact_bps) = compute_swap_price_impact(&env, &creator, amount, direction)?; + + // A zero tolerance skips the check entirely for advanced callers. + if max_slippage_bps > 0 && impact_bps > max_slippage_bps { + env.events().publish( + events::swap_slippage_exceeded_topics(&creator), + events::SwapSlippageExceededEvent { + creator_id: creator.clone(), + actual_bps: impact_bps, + max_bps: max_slippage_bps, + ledger: env.ledger().sequence(), + }, + ); + return Err(ContractError::SlippageExceeded); + } + + match direction { + SwapDirection::Buy => { + let base_price: i128 = env + .storage() + .persistent() + .get(&constants::storage::KEY_PRICE) + .ok_or(ContractError::KeyPriceNotSet)?; + let profile = read_registered_creator_profile(&env, &creator)?; + let start_supply = profile.supply; + + let cost = compute_swap_buy_cost(&env, &creator, base_price, start_supply, amount)?; + let new_supply = + Self::buy_keys(env.clone(), creator.clone(), trader, amount, cost, None)?; + let expected_supply = start_supply + .checked_add(amount) + .ok_or(ContractError::Overflow)?; + // A per-creator cap can break the fill short inside buy_keys; + // surface a hard error so the whole swap reverts atomically. + if new_supply != expected_supply { + return Err(ContractError::SupplyCapExceeded); + } + Ok(cost) + } + SwapDirection::Sell => { + // batch_sell authorizes the seller once at its own frame, which + // avoids Soroban's one-`require_auth`-per-address-per-invocation + // limit that a loop of sell_key calls would trip on. + let mut orders = Vec::new(&env); + orders.push_back((creator.clone(), amount)); + let results = Self::batch_sell(env.clone(), trader, orders)?; + let order = results.get(0).ok_or(ContractError::InsufficientSupply)?; + Ok(order.proceeds) + } + } + } + /// Deposits `amount` as a dividend for all current key holders of `creator`. /// /// The protocol fee is deducted first; the remainder is distributed proportionally diff --git a/creator-keys/tests/bonding_curve_swap_slippage.rs b/creator-keys/tests/bonding_curve_swap_slippage.rs new file mode 100644 index 00000000..4ec696fc --- /dev/null +++ b/creator-keys/tests/bonding_curve_swap_slippage.rs @@ -0,0 +1,291 @@ +//! Slippage-protected bonding-curve `swap` (price-impact guard) and the +//! `get_price_impact` view. + +mod contract_test_env; + +use contract_test_env::{ + capture_snapshot, register_creator_keys, register_test_creator, set_curve_slope, + set_pricing_and_fees, test_env_with_auths, +}; +use creator_keys::{events, ContractError, SwapDirection}; +use soroban_sdk::{ + testutils::{Address as _, Events, Ledger as _}, + Address, Env, IntoVal, Symbol, +}; + +const KEY_PRICE: i128 = 1_000; +const SLOPE: i128 = 100; +const CREATOR_BPS: u32 = 9000; +const PROTOCOL_BPS: u32 = 1000; + +/// price(supply) = KEY_PRICE + SLOPE * supply +fn curve_price(supply: u32) -> i128 { + KEY_PRICE + SLOPE * supply as i128 +} + +fn setup( + env: &Env, +) -> ( + creator_keys::CreatorKeysContractClient<'_>, + Address, + Address, +) { + let (client, _) = register_creator_keys(env); + let _admin = set_pricing_and_fees(env, &client, KEY_PRICE, CREATOR_BPS, PROTOCOL_BPS); + set_curve_slope(env, &client, SLOPE); + let creator = register_test_creator(env, &client, "alice"); + let buyer = Address::generate(env); + (client, creator, buyer) +} + +/// Bumps the ledger sequence so a sell is not blocked by the same-ledger +/// flash-loan guard after a prior buy. +fn next_ledger(env: &Env) { + let mut l = env.ledger().get(); + l.sequence_number += 1; + env.ledger().set(l); +} + +// --------------------------------------------------------------------------- +// Acceptance: swap reverts with SlippageExceeded when impact exceeds tolerance +// --------------------------------------------------------------------------- + +#[test] +fn test_swap_reverts_with_slippage_exceeded_when_impact_exceeds_tolerance() { + let env = test_env_with_auths(); + let (client, creator, buyer) = setup(&env); + + // supply 0 -> 5: spot moves 1000 -> 1500, impact = 5000 bps. + let before = capture_snapshot(&client, &creator, &buyer); + let result = client.try_swap(&creator, &buyer, &5u32, &SwapDirection::Buy, &4_000u32); + let after = capture_snapshot(&client, &creator, &buyer); + + assert_eq!(result, Err(Ok(ContractError::SlippageExceeded))); + before.assert_unchanged(&after); +} + +#[test] +fn test_swap_sell_reverts_with_slippage_exceeded_when_impact_exceeds_tolerance() { + let env = test_env_with_auths(); + let (client, creator, holder) = setup(&env); + + let cost: i128 = (0..10u32).map(curve_price).sum(); + client.buy_keys(&creator, &holder, &10u32, &cost, &None); + next_ledger(&env); + + // supply 10 -> 5: spot moves 2000 -> 1500, impact = 2500 bps. + let before = capture_snapshot(&client, &creator, &holder); + let result = client.try_swap(&creator, &holder, &5u32, &SwapDirection::Sell, &2_499u32); + let after = capture_snapshot(&client, &creator, &holder); + + assert_eq!(result, Err(Ok(ContractError::SlippageExceeded))); + before.assert_unchanged(&after); +} + +// --------------------------------------------------------------------------- +// Acceptance: swap succeeds when impact is within tolerance +// --------------------------------------------------------------------------- + +#[test] +fn test_swap_succeeds_when_impact_equals_tolerance() { + let env = test_env_with_auths(); + let (client, creator, buyer) = setup(&env); + + // Impact is exactly 5000 bps; equality with the bound must pass. + let expected_cost: i128 = (0..5u32).map(curve_price).sum(); + let notional = client.swap(&creator, &buyer, &5u32, &SwapDirection::Buy, &5_000u32); + + assert_eq!(notional, expected_cost); + assert_eq!(client.get_total_key_supply(&creator), 5); + assert_eq!(client.get_key_balance(&creator, &buyer), 5); +} + +#[test] +fn test_swap_succeeds_when_impact_below_tolerance() { + let env = test_env_with_auths(); + let (client, creator, buyer) = setup(&env); + + // One key from supply 0: 1000 -> 1100, impact = 1000 bps. + let notional = client.swap(&creator, &buyer, &1u32, &SwapDirection::Buy, &1_000u32); + + assert_eq!(notional, curve_price(0)); + assert_eq!(client.get_total_key_supply(&creator), 1); +} + +#[test] +fn test_swap_sell_succeeds_within_tolerance_and_reports_net_proceeds() { + let env = test_env_with_auths(); + let (client, creator, holder) = setup(&env); + + let cost: i128 = (0..10u32).map(curve_price).sum(); + client.buy_keys(&creator, &holder, &10u32, &cost, &None); + next_ledger(&env); + + // supply 10 -> 8: spot moves 2000 -> 1800, impact = 1000 bps. + let sell_quote = client.get_sell_quote(&creator, &holder); + assert_eq!(sell_quote.price, curve_price(9)); + let notional = client.swap(&creator, &holder, &2u32, &SwapDirection::Sell, &1_000u32); + + // In the standard 9000/1000 split the fee share consumes the whole key + // price, so net seller proceeds are zero — matching get_sell_quote. + assert_eq!(sell_quote.total_amount, 0); + assert_eq!(notional, 0); + + assert_eq!(client.get_total_key_supply(&creator), 8); + assert_eq!(client.get_key_balance(&creator, &holder), 8); +} + +// --------------------------------------------------------------------------- +// Acceptance: get_price_impact returns accurate bps for any amount +// --------------------------------------------------------------------------- + +#[test] +fn test_get_price_impact_matches_curve_math_for_various_amounts() { + let env = test_env_with_auths(); + let (client, creator, holder) = setup(&env); + + // Zero amount never moves the spot price. + assert_eq!( + client.get_price_impact(&creator, &0u32, &SwapDirection::Buy), + 0 + ); + + // supply 0 -> 1: 1000 -> 1100 = 1000 bps. + assert_eq!( + client.get_price_impact(&creator, &1u32, &SwapDirection::Buy), + 1_000 + ); + + // supply 0 -> 5: 1000 -> 1500 = 5000 bps. + assert_eq!( + client.get_price_impact(&creator, &5u32, &SwapDirection::Buy), + 5_000 + ); + + // After a buy of 10, supply is 10. Sell 5: 2000 -> 1500 = 2500 bps. + let cost: i128 = (0..10u32).map(curve_price).sum(); + client.buy_keys(&creator, &holder, &10u32, &cost, &None); + assert_eq!( + client.get_price_impact(&creator, &5u32, &SwapDirection::Sell), + 2_500 + ); + + // Sell the full supply: 2000 -> 1000 = 5000 bps. + assert_eq!( + client.get_price_impact(&creator, &10u32, &SwapDirection::Sell), + 5_000 + ); +} + +#[test] +fn test_get_price_impact_flat_curve_is_always_zero() { + let env = test_env_with_auths(); + let (client, creator, _) = setup(&env); + let admin = Address::generate(&env); + client.set_curve_slope(&admin, &0i128); + + assert_eq!( + client.get_price_impact(&creator, &1u32, &SwapDirection::Buy), + 0 + ); + assert_eq!( + client.get_price_impact(&creator, &100u32, &SwapDirection::Buy), + 0 + ); +} + +#[test] +fn test_get_price_impact_sell_exceeding_supply_reverts() { + let env = test_env_with_auths(); + let (client, creator, holder) = setup(&env); + + let buy_quote = client.get_buy_quote(&creator); + client.buy_key(&creator, &holder, &buy_quote.total_amount, &None); + + let result = client.try_get_price_impact(&creator, &2u32, &SwapDirection::Sell); + assert_eq!(result, Err(Ok(ContractError::InsufficientSupply))); +} + +// --------------------------------------------------------------------------- +// Acceptance: zero slippage param bypasses check without error +// --------------------------------------------------------------------------- + +#[test] +fn test_swap_zero_slippage_param_bypasses_check() { + let env = test_env_with_auths(); + let (client, creator, buyer) = setup(&env); + + // Impact would be 5000 bps, far above any sane bound, but a zero + // tolerance skips the check entirely. + let expected_cost: i128 = (0..5u32).map(curve_price).sum(); + let notional = client.swap(&creator, &buyer, &5u32, &SwapDirection::Buy, &0u32); + + assert_eq!(notional, expected_cost); + assert_eq!(client.get_total_key_supply(&creator), 5); +} + +// --------------------------------------------------------------------------- +// Acceptance: SlippageExceeded includes actual and max bps values +// --------------------------------------------------------------------------- + +#[test] +fn test_slippage_exceeded_event_reports_actual_and_max_bps() { + let env = test_env_with_auths(); + let (client, contract_id) = register_creator_keys(&env); + let _admin = set_pricing_and_fees(&env, &client, KEY_PRICE, CREATOR_BPS, PROTOCOL_BPS); + set_curve_slope(&env, &client, SLOPE); + let creator = register_test_creator(&env, &client, "alice"); + let buyer = Address::generate(&env); + + let result = client.try_swap(&creator, &buyer, &5u32, &SwapDirection::Buy, &4_000u32); + assert_eq!(result, Err(Ok(ContractError::SlippageExceeded))); + + let mut found = false; + for (contract, topics, data) in env.events().all().iter() { + if contract != contract_id { + continue; + } + let name: Symbol = topics.get(0).unwrap().into_val(&env); + if name == events::SWAP_SLIPPAGE_EXCEEDED_EVENT_NAME { + let payload: events::SwapSlippageExceededEvent = data.clone().into_val(&env); + assert_eq!(payload.creator_id, creator); + assert_eq!(payload.actual_bps, 5_000); + assert_eq!(payload.max_bps, 4_000); + found = true; + } + } + assert!( + found, + "expected a SwapSlippageExceeded event carrying actual and max bps" + ); +} + +// --------------------------------------------------------------------------- +// Swap input validation +// --------------------------------------------------------------------------- + +#[test] +fn test_swap_zero_amount_reverts() { + let env = test_env_with_auths(); + let (client, creator, buyer) = setup(&env); + + let result = client.try_swap(&creator, &buyer, &0u32, &SwapDirection::Buy, &0u32); + assert_eq!(result, Err(Ok(ContractError::NotPositiveAmount))); +} + +#[test] +fn test_swap_buy_charges_exact_curve_cost_for_multi_key_order() { + let env = test_env_with_auths(); + let (client, creator, buyer) = setup(&env); + + let expected_cost: i128 = (0..3u32).map(curve_price).sum(); + let quote = client.get_buy_quote(&creator); + assert_eq!(quote.price, curve_price(0)); + + let notional = client.swap(&creator, &buyer, &3u32, &SwapDirection::Buy, &0u32); + assert_eq!(notional, expected_cost); + + // The next spot price reflects the moved supply. + let quote_after = client.get_buy_quote(&creator); + assert_eq!(quote_after.price, curve_price(3)); +} diff --git a/docs/slippage-protection.md b/docs/slippage-protection.md index e7551974..73599307 100644 --- a/docs/slippage-protection.md +++ b/docs/slippage-protection.md @@ -10,6 +10,10 @@ When submitting a buy, supply a maximum acceptable total price. The contract eva When submitting a sell, supply a minimum acceptable proceeds value. The contract rejects the operation with `SlippageExceeded` when the current proceeds are lower than that bound. A bound equal to the quoted proceeds is accepted. +## Bonding curve swap protection + +`swap` accepts a `max_slippage_bps` tolerance alongside `amount` and a buy/sell `direction`. The contract computes the curve's spot price before and after the whole trade and reverts with `SlippageExceeded` when the impact in basis points is strictly above the tolerance; an impact exactly at the tolerance is accepted. The breach emits a `SwapSlippageExceeded` event carrying both the actual and maximum bps values. Passing `0` skips the check entirely for advanced callers. The `get_price_impact(amount, direction)` view returns the same bps impact without executing anything, so clients can derive a tolerance before building the transaction. + ## Client workflow 1. Read a fresh quote immediately before building the transaction. From 55884d66484b573ccd683fcd7d2a9c3be805b133 Mon Sep 17 00:00:00 2001 From: aishagbadamosi <125849753+aishagbadamosi@users.noreply.github.com> Date: Sun, 18 Oct 2026 12:02:21 +0100 Subject: [PATCH 2/2] fix: restore closing brace dropped by merge in compute_swap_buy_cost --- creator-keys/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/creator-keys/src/lib.rs b/creator-keys/src/lib.rs index 3a5ef21a..10d34903 100644 --- a/creator-keys/src/lib.rs +++ b/creator-keys/src/lib.rs @@ -3903,6 +3903,8 @@ fn compute_swap_buy_cost( total = total.checked_add(price).ok_or(ContractError::Overflow)?; } Ok(total) +} + fn assert_max_slippage_bps( env: &Env, creator: &Address,