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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 45 additions & 27 deletions creator-keys/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,22 @@ pub mod fee {
let creator_amount = total - protocol_amount;
(creator_amount, protocol_amount)
}

/// Computes the fee split safely, returning `None` if multiplication or subtraction overflows.
pub fn checked_compute_fee_split(
total: i128,
_creator_bps: u32,
protocol_bps: u32,
) -> Option<(i128, i128)> {
if total <= 0 {
return Some((0, 0));
}
let protocol_amount = total
.checked_mul(protocol_bps as i128)?
.checked_div(BPS_MAX as i128)?;
let creator_amount = total.checked_sub(protocol_amount)?;
Some((creator_amount, protocol_amount))
}
}

/// Stable, non-optional view of the protocol fee configuration.
Expand Down Expand Up @@ -152,6 +168,7 @@ pub struct CreatorProfile {
pub handle: String,
pub supply: u32,
pub holder_count: u32,
pub fee_recipient: Address,
}

/// Reads a creator profile from storage, returning `None` for unregistered creators.
Expand All @@ -175,27 +192,31 @@ pub fn read_key_balance(env: &Env, creator: &Address) -> u32 {
.unwrap_or(0)
}

/// Formats a quote response with consistent total amount calculation.
/// Formats a quote response with overflow-safe total amount calculation.
///
/// For buys, total_amount = price + fees.
/// For sells, total_amount = price - fees.
fn format_quote_response(
/// Returns `Err(ContractError::Overflow)` if any addition or subtraction would overflow.
fn checked_format_quote_response(
price: i128,
creator_fee: i128,
protocol_fee: i128,
is_buy: bool,
) -> QuoteResponse {
) -> Result<QuoteResponse, ContractError> {
let fees = creator_fee
.checked_add(protocol_fee)
.ok_or(ContractError::Overflow)?;

let total_amount = if is_buy {
price + creator_fee + protocol_fee
price.checked_add(fees).ok_or(ContractError::Overflow)?
} else {
price - creator_fee - protocol_fee
price.checked_sub(fees).ok_or(ContractError::Overflow)?
};
QuoteResponse {

Ok(QuoteResponse {
price,
creator_fee,
protocol_fee,
total_amount,
}
})
}

#[contract]
Expand All @@ -216,10 +237,11 @@ impl CreatorKeysContract {
}

let profile = CreatorProfile {
creator,
creator: creator.clone(),
handle,
supply: 0,
holder_count: 0,
fee_recipient: creator.clone(),
};

env.storage().persistent().set(&key, &profile);
Expand Down Expand Up @@ -376,6 +398,15 @@ impl CreatorKeysContract {
read_creator_profile(&env, &creator).is_some()
}

/// Read-only view: returns the creator fee recipient address.
///
/// Fails with [`ContractError::NotRegistered`] if the creator is not registered.
/// Reuses current creator storage access patterns.
pub fn get_creator_fee_recipient(env: Env, creator: Address) -> Result<Address, ContractError> {
let profile = read_creator_profile(&env, &creator).ok_or(ContractError::NotRegistered)?;
Ok(profile.fee_recipient)
}

pub fn set_fee_config(
env: Env,
admin: Address,
Expand Down Expand Up @@ -458,11 +489,8 @@ impl CreatorKeysContract {
.persistent()
.get(&DataKey::FeeConfig)
.ok_or(ContractError::FeeConfigNotSet)?;
Ok(fee::compute_fee_split(
total,
config.creator_bps,
config.protocol_bps,
))
fee::checked_compute_fee_split(total, config.creator_bps, config.protocol_bps)
.ok_or(ContractError::Overflow)
}

/// Read-only view: returns the fee configuration for a specific creator.
Expand Down Expand Up @@ -521,12 +549,7 @@ impl CreatorKeysContract {

let (creator_fee, protocol_fee) = Self::compute_fees_for_payment(env.clone(), price)?;

Ok(format_quote_response(
price,
creator_fee,
protocol_fee,
true,
))
checked_format_quote_response(price, creator_fee, protocol_fee, true)
}

/// Read-only view: returns a quote for selling a key.
Expand Down Expand Up @@ -556,12 +579,7 @@ impl CreatorKeysContract {

let (creator_fee, protocol_fee) = Self::compute_fees_for_payment(env.clone(), price)?;

Ok(format_quote_response(
price,
creator_fee,
protocol_fee,
false,
))
checked_format_quote_response(price, creator_fee, protocol_fee, false)
}
}

Expand Down
68 changes: 68 additions & 0 deletions creator-keys/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -362,3 +362,71 @@ fn test_get_quote_fails_if_fee_not_set() {
let result = client.try_get_buy_quote(&creator);
assert_eq!(result, Err(Ok(ContractError::FeeConfigNotSet)));
}

#[test]
fn test_get_buy_quote_fails_if_not_registered() {
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);
client.set_key_price(&admin, &1000);

let unregistered_creator = Address::generate(&env);
let result = client.try_get_buy_quote(&unregistered_creator);
assert_eq!(result, Err(Ok(ContractError::NotRegistered)));
}

#[test]
fn test_get_creator_fee_recipient_success() {
let env = Env::default();
env.mock_all_auths();
let contract_id = env.register(CreatorKeysContract, ());
let client = CreatorKeysContractClient::new(&env, &contract_id);

let creator = Address::generate(&env);
let handle = String::from_str(&env, "alice");
client.register_creator(&creator, &handle);

let recipient = client.get_creator_fee_recipient(&creator);
assert_eq!(recipient, creator);
}

#[test]
fn test_get_creator_fee_recipient_fails_if_not_registered() {
let env = Env::default();
let contract_id = env.register(CreatorKeysContract, ());
let client = CreatorKeysContractClient::new(&env, &contract_id);

let unregistered_creator = Address::generate(&env);
let result = client.try_get_creator_fee_recipient(&unregistered_creator);
assert_eq!(result, Err(Ok(ContractError::NotRegistered)));
}

#[test]
fn test_quote_overflow_guards() {
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);
// Set a massive price that will cause overflow when fees are added
let max_price = i128::MAX - 1;
client.set_key_price(&admin, &max_price);
client.set_fee_config(&admin, &9000, &1000); // 90/10 split

let creator = Address::generate(&env);
let handle = String::from_str(&env, "alice");
client.register_creator(&creator, &handle);

// Buy quote: price + fees (will overflow)
let result = client.try_get_buy_quote(&creator);
assert_eq!(result, Err(Ok(ContractError::Overflow)));

// Sell quote: price - fees (won't overflow if price is large, but let's test sub overflow)
// Actually price - fees is safe if price > fees.
// To test subtraction overflow, we need fees > price.
// Price must be positive per contract constraint.
}
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,14 @@
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M"
}
},
{
"key": {
"symbol": "fee_recipient"
},
"val": {
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M"
}
},
{
"key": {
"symbol": "handle"
Expand All @@ -117,6 +125,14 @@
"string": "alice"
}
},
{
"key": {
"symbol": "holder_count"
},
"val": {
"u32": 0
}
},
{
"key": {
"symbol": "supply"
Expand Down
16 changes: 16 additions & 0 deletions creator-keys/test_snapshots/test/test_buy_key_success.1.json
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,14 @@
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M"
}
},
{
"key": {
"symbol": "fee_recipient"
},
"val": {
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M"
}
},
{
"key": {
"symbol": "handle"
Expand All @@ -145,6 +153,14 @@
"string": "alice"
}
},
{
"key": {
"symbol": "holder_count"
},
"val": {
"u32": 1
}
},
{
"key": {
"symbol": "supply"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,14 @@
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4"
}
},
{
"key": {
"symbol": "fee_recipient"
},
"val": {
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4"
}
},
{
"key": {
"symbol": "handle"
Expand All @@ -92,6 +100,14 @@
"string": "alice"
}
},
{
"key": {
"symbol": "holder_count"
},
"val": {
"u32": 0
}
},
{
"key": {
"symbol": "supply"
Expand Down
Loading
Loading