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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
174 changes: 173 additions & 1 deletion creator-keys/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3803,6 +3803,33 @@ fn assert_sell_proceeds_slippage(
Ok(())
}

fn assert_max_slippage_bps(
env: &Env,
creator: &Address,
reference_price: i128,
execution_price_after_fees: i128,
max_slippage_bps: Option<u32>,
) -> Result<(), ContractError> {
if let Some(max_bps) = max_slippage_bps {
if max_bps > 0 && reference_price > 0 {
let diff = if execution_price_after_fees > reference_price {
execution_price_after_fees - reference_price
} else {
reference_price - execution_price_after_fees
};
let actual_bps = ((diff * 10_000) / reference_price) as u32;
if actual_bps > max_bps {
env.events().publish(
(soroban_sdk::symbol_short!("imp_ex"), creator.clone()),
(actual_bps, max_bps),
);
return Err(ContractError::SlippageExceeded);
}
}
}
Ok(())
}

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.
Expand Down Expand Up @@ -5814,6 +5841,17 @@ impl CreatorKeysContract {
Self::buy_key(env, key_id, buyer, payment, max_price)
}

pub fn buy_with_slippage(
env: Env,
key_id: Address,
buyer: Address,
payment: i128,
max_price: Option<i128>,
max_slippage_bps: Option<u32>,
) -> Result<u32, ContractError> {
Self::buy_key_with_slippage(env, key_id, buyer, payment, max_price, max_slippage_bps)
}

pub fn buy_key(
env: Env,
creator: Address,
Expand All @@ -5824,6 +5862,25 @@ impl CreatorKeysContract {
Self::buy_key_with_referrer(env, creator, buyer, payment, max_price, None)
}

pub fn buy_key_with_slippage(
env: Env,
creator: Address,
buyer: Address,
payment: i128,
max_price: Option<i128>,
max_slippage_bps: Option<u32>,
) -> Result<u32, ContractError> {
Self::buy_key_with_ref_and_slip(
env,
creator,
buyer,
payment,
max_price,
None,
max_slippage_bps,
)
}

/// Purchases multiple keys in a single transaction, subject to the creator's
/// per-transaction max buy quantity limit if configured.
///
Expand All @@ -5841,6 +5898,27 @@ impl CreatorKeysContract {
Self::buy_keys_with_referrer(env, creator, buyer, quantity, payment, max_price, None)
}

pub fn buy_keys_with_slippage(
env: Env,
creator: Address,
buyer: Address,
quantity: u32,
payment: i128,
max_price: Option<i128>,
max_slippage_bps: Option<u32>,
) -> Result<u32, ContractError> {
Self::buy_keys_with_ref_and_slip(
env,
creator,
buyer,
quantity,
payment,
max_price,
None,
max_slippage_bps,
)
}

pub fn buy_keys_with_referrer(
env: Env,
creator: Address,
Expand All @@ -5849,6 +5927,22 @@ impl CreatorKeysContract {
payment: i128,
max_price: Option<i128>,
referrer: Option<Address>,
) -> Result<u32, ContractError> {
Self::buy_keys_with_ref_and_slip(
env, creator, buyer, quantity, payment, max_price, referrer, None,
)
}

#[allow(clippy::too_many_arguments)]
pub fn buy_keys_with_ref_and_slip(
env: Env,
creator: Address,
buyer: Address,
quantity: u32,
payment: i128,
max_price: Option<i128>,
referrer: Option<Address>,
max_slippage_bps: Option<u32>,
) -> Result<u32, ContractError> {
buyer.require_auth();
emergency_pause::assert_trading_allowed(&env, &creator)?;
Expand Down Expand Up @@ -6007,6 +6101,29 @@ impl CreatorKeysContract {
pre_price
};

if let Some(max_bps) = max_slippage_bps {
if max_bps > 0 {
let reference_price = if in_auction {
key_price
} else if profile.supply == 0 {
base_price
} else {
compute_bonding_curve_price(&env, &creator, base_price, profile.supply - 1)?
};
let net_for_slippage = {
let trade_fee = compute_trade_fee(&env, key_price)?;
fee::checked_sub_i128(key_price, trade_fee).unwrap_or(key_price)
};
assert_max_slippage_bps(
&env,
&creator,
reference_price,
net_for_slippage,
Some(max_bps),
)?;
}
}

// Check max supply cap if set
if let Some(max_supply) = env
.storage()
Expand Down Expand Up @@ -6348,9 +6465,30 @@ impl CreatorKeysContract {
payment: i128,
max_price: Option<i128>,
referrer: Option<Address>,
) -> Result<u32, ContractError> {
Self::buy_key_with_ref_and_slip(env, creator, buyer, payment, max_price, referrer, None)
}

#[allow(clippy::too_many_arguments)]
pub fn buy_key_with_ref_and_slip(
env: Env,
creator: Address,
buyer: Address,
payment: i128,
max_price: Option<i128>,
referrer: Option<Address>,
max_slippage_bps: Option<u32>,
) -> Result<u32, ContractError> {
buyer.require_auth();
Self::execute_buy(env, creator, buyer, payment, max_price, referrer)
Self::execute_buy(
env,
creator,
buyer,
payment,
max_price,
referrer,
max_slippage_bps,
)
}

/// Executes a single-key purchase for `buyer` without checking the
Expand All @@ -6368,6 +6506,7 @@ impl CreatorKeysContract {
payment: i128,
max_price: Option<i128>,
referrer: Option<Address>,
max_slippage_bps: Option<u32>,
) -> Result<u32, ContractError> {
emergency_pause::assert_trading_allowed(&env, &creator)?;
assert_global_trading_not_halted(&env)?;
Expand Down Expand Up @@ -6504,6 +6643,22 @@ impl CreatorKeysContract {
};

assert_buy_price_slippage(&env, &creator, price, max_price)?;
if let Some(max_bps) = max_slippage_bps {
if max_bps > 0 {
let reference_price = if in_auction {
price
} else if profile.supply == 0 {
base_price
} else {
compute_bonding_curve_price(&env, &creator, base_price, profile.supply - 1)?
};
let net_price = {
let trade_fee = compute_trade_fee(&env, price)?;
fee::checked_sub_i128(price, trade_fee).unwrap_or(price)
};
assert_max_slippage_bps(&env, &creator, reference_price, net_price, Some(max_bps))?;
}
}

if payment < price {
return Err(ContractError::InsufficientPayment);
Expand Down Expand Up @@ -6846,6 +7001,16 @@ impl CreatorKeysContract {
creator: Address,
seller: Address,
min_proceeds: Option<i128>,
) -> Result<u32, ContractError> {
Self::sell_key_with_slippage(env, creator, seller, min_proceeds, None)
}

pub fn sell_key_with_slippage(
env: Env,
creator: Address,
seller: Address,
min_proceeds: Option<i128>,
max_slippage_bps: Option<u32>,
) -> Result<u32, ContractError> {
seller.require_auth();
emergency_pause::assert_trading_allowed(&env, &creator)?;
Expand Down Expand Up @@ -6961,6 +7126,12 @@ impl CreatorKeysContract {
settle_holder_dividends(&env, &creator, &seller, current_balance)?;

assert_sell_proceeds_slippage(&env, &creator, price, min_proceeds)?;
if let Some(max_bps) = max_slippage_bps {
if max_bps > 0 {
let proceeds = compute_sell_proceeds(&env, price)?;
assert_max_slippage_bps(&env, &creator, curve_price, proceeds, Some(max_bps))?;
}
}

// A sell is market activity too, so it feeds the same rolling volume window
// as a buy. Recorded before the fee split so the tier reflects this trade.
Expand Down Expand Up @@ -17478,6 +17649,7 @@ impl CreatorKeysContract {
payment,
None,
None,
None,
)?;

// Emit a forwarded-buy event for downstream indexers.
Expand Down
51 changes: 51 additions & 0 deletions creator-keys/test_snapshots/buy_extends_instance_ttl.1.json
Original file line number Diff line number Diff line change
Expand Up @@ -746,6 +746,57 @@
12623039
]
],
[
{
"contract_data": {
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
"key": {
"vec": [
{
"symbol": "LastTradeTimestamp"
},
{
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M"
},
{
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
}
]
},
"durability": "persistent"
}
},
[
{
"last_modified_ledger_seq": 0,
"data": {
"contract_data": {
"ext": "v0",
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
"key": {
"vec": [
{
"symbol": "LastTradeTimestamp"
},
{
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M"
},
{
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
}
]
},
"durability": "persistent",
"val": {
"u64": 0
}
}
},
"ext": "v0"
},
12623039
]
],
[
{
"contract_data": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1097,6 +1097,57 @@
6311520
]
],
[
{
"contract_data": {
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
"key": {
"vec": [
{
"symbol": "LastTradeTimestamp"
},
{
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M"
},
{
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
}
]
},
"durability": "persistent"
}
},
[
{
"last_modified_ledger_seq": 0,
"data": {
"contract_data": {
"ext": "v0",
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
"key": {
"vec": [
{
"symbol": "LastTradeTimestamp"
},
{
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M"
},
{
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
}
]
},
"durability": "persistent",
"val": {
"u64": 0
}
}
},
"ext": "v0"
},
6311520
]
],
[
{
"contract_data": {
Expand Down
Loading
Loading