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
184 changes: 184 additions & 0 deletions creator-keys/tests/fee_rounding_invariants.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
//! Invariant tests for fee rounding behavior at boundary values.
//!
//! These tests verify that fee split logic maintains key invariants:
//! - Balance conservation: creator_amount + protocol_amount == total
//! - Rounding direction: remainder from integer division goes to creator
//! - Boundary handling: zero, negative, and minimum amounts
//! - Maximum input values near i128 limits

use creator_keys::fee;

#[test]
fn fee_split_preserves_balance_for_boundary_value_one() {
let (creator, protocol) = fee::compute_fee_split(1, 9000, 1000);
assert_eq!(creator + protocol, 1, "balance conservation at amount=1");
}

#[test]
fn fee_split_preserves_balance_for_boundary_value_two() {
let (creator, protocol) = fee::compute_fee_split(2, 9000, 1000);
assert_eq!(creator + protocol, 2, "balance conservation at amount=2");
}

#[test]
fn fee_split_preserves_balance_for_boundary_value_ten() {
let (creator, protocol) = fee::compute_fee_split(10, 5000, 5000);
assert_eq!(
creator + protocol,
10,
"balance conservation at 50/50 split"
);
}

#[test]
fn fee_split_remainder_favors_creator_at_boundary_999() {
let (creator, protocol) = fee::compute_fee_split(999, 9000, 1000);
// 999 * 1000 / 10000 = 99.9 -> 99 protocol; creator gets remainder
assert_eq!(protocol, 99);
assert_eq!(creator, 900);
assert_eq!(creator + protocol, 999);
}

#[test]
fn fee_split_remainder_favors_creator_at_boundary_1001() {
let (creator, protocol) = fee::compute_fee_split(1001, 9000, 1000);
// 1001 * 1000 / 10000 = 100.1 -> 100 protocol; creator gets remainder
assert_eq!(protocol, 100);
assert_eq!(creator, 901);
assert_eq!(creator + protocol, 1001);
}

#[test]
fn fee_split_dust_price_creates_zero_protocol_fee() {
let (creator, protocol) = fee::compute_fee_split(1, 1000, 9000);
// 1 * 9000 / 10000 = 0.9 -> 0 protocol; creator gets full amount
assert_eq!(protocol, 0);
assert_eq!(creator, 1);
assert_eq!(creator + protocol, 1);
}

#[test]
fn fee_split_equal_split_50_50_even_amount() {
let (creator, protocol) = fee::compute_fee_split(100, 5000, 5000);
assert_eq!(creator, 50);
assert_eq!(protocol, 50);
assert_eq!(creator + protocol, 100);
}

#[test]
fn fee_split_equal_split_50_50_odd_amount_remainder_to_creator() {
let (creator, protocol) = fee::compute_fee_split(101, 5000, 5000);
// 101 * 5000 / 10000 = 50.5 -> 50 protocol; creator gets 51
assert_eq!(protocol, 50);
assert_eq!(creator, 51);
assert_eq!(creator + protocol, 101);
}

#[test]
fn fee_split_preserves_balance_for_large_amount() {
let large_amount = 1_000_000_000_000i128;
let (creator, protocol) = fee::compute_fee_split(large_amount, 9000, 1000);
assert_eq!(creator + protocol, large_amount);
}

#[test]
fn fee_split_preserves_balance_for_max_safe_amount() {
// Near i128::MAX but safe for multiplication by protocol_bps
let safe_amount = 9_223_372_036_854_775i128; // (i128::MAX / 10000) approx
let (creator, protocol) = fee::compute_fee_split(safe_amount, 9000, 1000);
assert_eq!(creator + protocol, safe_amount);
}

#[test]
fn fee_split_zero_amount_returns_zero_split() {
let (creator, protocol) = fee::compute_fee_split(0, 9000, 1000);
assert_eq!(creator, 0);
assert_eq!(protocol, 0);
}

#[test]
fn fee_split_negative_amount_returns_zero_split() {
let (creator, protocol) = fee::compute_fee_split(-100, 9000, 1000);
assert_eq!(creator, 0);
assert_eq!(protocol, 0);
}

#[test]
fn fee_split_minimum_protocol_bps_zero() {
let (creator, protocol) = fee::compute_fee_split(1000, 10000, 0);
assert_eq!(creator, 1000);
assert_eq!(protocol, 0);
assert_eq!(creator + protocol, 1000);
}

#[test]
fn fee_split_maximum_protocol_bps_5000() {
let (creator, protocol) = fee::compute_fee_split(10000, 5000, 5000);
assert_eq!(creator, 5000);
assert_eq!(protocol, 5000);
assert_eq!(creator + protocol, 10000);
}

#[test]
fn fee_split_invariant_holds_across_typical_amounts() {
for total in [1i128, 10, 99, 100, 999, 1000, 10000, 100000, 1_000_000] {
let (creator, protocol) = fee::compute_fee_split(total, 9000, 1000);
assert_eq!(
creator + protocol,
total,
"balance conservation fails at amount={}",
total
);
}
}

#[test]
fn fee_split_invariant_holds_for_all_valid_fee_configs() {
let total = 1000i128;
for protocol_bps in [0u32, 1, 100, 1000, 5000, 9999, 10000] {
let creator_bps = 10000u32.saturating_sub(protocol_bps);
let (creator, protocol) = fee::compute_fee_split(total, creator_bps, protocol_bps);
assert_eq!(
creator + protocol,
total,
"balance conservation fails at protocol_bps={}",
protocol_bps
);
}
}

#[test]
fn checked_fee_split_boundary_prevents_overflow() {
// Test near-maximum safe amounts
let safe_amount = 9_223_372_036_854_775i128;
let result = fee::checked_compute_fee_split(safe_amount, 9000, 1000);
assert!(
result.is_some(),
"checked split should succeed for safe amount"
);
let (creator, protocol) = result.unwrap();
assert_eq!(creator + protocol, safe_amount);
}

#[test]
fn checked_fee_split_rejects_overflow() {
// Amounts that would overflow during multiplication
let unsafe_amount = i128::MAX;
let result = fee::checked_compute_fee_split(unsafe_amount, 9000, 1000);
assert!(
result.is_none(),
"checked split should fail for unsafe amount"
);
}

#[test]
fn checked_fee_split_zero_amount_succeeds() {
let result = fee::checked_compute_fee_split(0, 9000, 1000);
assert_eq!(result, Some((0, 0)));
}

#[test]
fn checked_fee_split_negative_amount_succeeds() {
let result = fee::checked_compute_fee_split(-100, 9000, 1000);
assert_eq!(result, Some((0, 0)));
}
145 changes: 145 additions & 0 deletions creator-keys/tests/max_amount_inputs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
//! Tests for near-maximum amount input values in buy and sell paths.
//!
//! These tests verify overflow-safe behavior and clear error handling when
//! amounts approach or exceed i128 limits.

mod contract_test_env;

use contract_test_env::{
register_creator_keys, register_test_creator, set_stored_key_price, test_env_with_auths,
};
use creator_keys::CreatorKeysContractClient;
use soroban_sdk::{testutils::Address as _, Address, Env};

fn register_holder_with_one_key(
env: &Env,
client: &CreatorKeysContractClient<'_>,
creator: &Address,
) -> Address {
let holder = Address::generate(env);
let price = client.get_buy_quote(creator).price;
client.buy_key(creator, &holder, &price);
holder
}

#[test]
fn test_buy_key_with_large_safe_amount_succeeds() {
let env = test_env_with_auths();
let (client, contract_id) = register_creator_keys(&env);
let large_price = 1_000_000_000_000i128;
set_stored_key_price(&env, &contract_id, large_price);
let creator = register_test_creator(&env, &client, "creator1");
let buyer = Address::generate(&env);

let supply = client.buy_key(&creator, &buyer, &large_price);
assert_eq!(supply, 1, "buy should succeed with large amount");
}

#[test]
fn test_buy_key_with_maximum_safe_i128_succeeds() {
let env = test_env_with_auths();
let (client, contract_id) = register_creator_keys(&env);
let max_safe_amount = 9_223_372_036_854_775i128; // (i128::MAX / 10000) approx
set_stored_key_price(&env, &contract_id, max_safe_amount);
let creator = register_test_creator(&env, &client, "creator2");
let buyer = Address::generate(&env);

let supply = client.buy_key(&creator, &buyer, &max_safe_amount);
assert_eq!(supply, 1, "buy should succeed at safe maximum");
}

#[test]
fn test_buy_quote_with_large_amount_succeeds() {
let env = test_env_with_auths();
let (client, contract_id) = register_creator_keys(&env);
let large_price = 500_000_000_000i128;
set_stored_key_price(&env, &contract_id, large_price);
let admin = Address::generate(&env);
client.set_fee_config(&admin, &9000u32, &1000u32);
let creator = register_test_creator(&env, &client, "creator3");

let q = client.get_buy_quote(&creator);
assert_eq!(q.price, large_price);
}

#[test]
fn test_buy_quote_with_maximum_safe_amount_succeeds() {
let env = test_env_with_auths();
let (client, contract_id) = register_creator_keys(&env);
let max_safe_amount = 9_223_372_036_854_775i128;
set_stored_key_price(&env, &contract_id, max_safe_amount);
let admin = Address::generate(&env);
client.set_fee_config(&admin, &9000u32, &1000u32);
let creator = register_test_creator(&env, &client, "creator4");

let q = client.get_buy_quote(&creator);
assert_eq!(q.price, max_safe_amount);
assert!(q.total_amount > 0, "total amount should include fees");
}

#[test]
fn test_sell_quote_with_large_amount_succeeds() {
let env = test_env_with_auths();
let (client, contract_id) = register_creator_keys(&env);
let large_price = 500_000_000_000i128;
set_stored_key_price(&env, &contract_id, large_price);
let admin = Address::generate(&env);
client.set_fee_config(&admin, &9000u32, &1000u32);
let creator = register_test_creator(&env, &client, "creator5");
let holder = register_holder_with_one_key(&env, &client, &creator);

let q = client.get_sell_quote(&creator, &holder);
assert_eq!(q.price, large_price);
}

#[test]
fn test_sell_quote_with_maximum_safe_amount_succeeds() {
let env = test_env_with_auths();
let (client, contract_id) = register_creator_keys(&env);
let max_safe_amount = 9_223_372_036_854_775i128;
set_stored_key_price(&env, &contract_id, max_safe_amount);
let admin = Address::generate(&env);
client.set_fee_config(&admin, &9000u32, &1000u32);
let creator = register_test_creator(&env, &client, "creator6");
let holder = register_holder_with_one_key(&env, &client, &creator);

let q = client.get_sell_quote(&creator, &holder);
assert_eq!(q.price, max_safe_amount);
}

#[test]
fn test_buy_quote_with_maximum_safe_amount_50_50_fees_succeeds() {
let env = test_env_with_auths();
let (client, contract_id) = register_creator_keys(&env);
let max_safe_amount = 9_223_372_036_854_775i128;
set_stored_key_price(&env, &contract_id, max_safe_amount);
let admin = Address::generate(&env);
client.set_fee_config(&admin, &5000u32, &5000u32);
let creator = register_test_creator(&env, &client, "creator7");

let q = client.get_buy_quote(&creator);
assert_eq!(q.price, max_safe_amount);
assert!(
q.total_amount > q.price,
"buy total should exceed base price with fees"
);
}

#[test]
fn test_sell_quote_with_maximum_safe_amount_50_50_fees_succeeds() {
let env = test_env_with_auths();
let (client, contract_id) = register_creator_keys(&env);
let max_safe_amount = 9_223_372_036_854_775i128;
set_stored_key_price(&env, &contract_id, max_safe_amount);
let admin = Address::generate(&env);
client.set_fee_config(&admin, &5000u32, &5000u32);
let creator = register_test_creator(&env, &client, "creator8");
let holder = register_holder_with_one_key(&env, &client, &creator);

let q = client.get_sell_quote(&creator, &holder);
assert_eq!(q.price, max_safe_amount);
assert!(
q.total_amount < q.price,
"sell total should be less than base price"
);
}
51 changes: 51 additions & 0 deletions docs/error-codes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Contract Error Codes

This document maps `ContractError` enum values to their meanings, likely causes, and expected caller behavior.

Error codes are defined in [`creator-keys/src/lib.rs`](../creator-keys/src/lib.rs) as a `#[contracterror]` enum with numeric values for on-chain serialization.

## Error Reference

| Code | Name | Numeric | Likely Cause | Expected Caller Behavior |
|------|------|---------|--------------|--------------------------|
| 1 | `AlreadyRegistered` | 1 | Attempt to register a creator address that is already registered in the contract | Retrieve existing creator profile or contact creator to select a different address |
| 2 | `NotRegistered` | 2 | Attempt to query or transact with a creator address that has not been registered | Register the creator first or verify the creator address is correct |
| 3 | `Overflow` | 3 | Integer arithmetic would overflow or underflow (e.g., supply > u32::MAX or fee math exceeds i128 bounds) | Check input amounts are within safe ranges or wait for supply to decrease; for quotes, verify key price is not near i128::MAX |
| 4 | `InsufficientPayment` | 4 | Payment amount is less than the current key price for a buy operation | Provide payment at least equal to the key price; call `get_buy_quote` to check current price and fees |
| 5 | `KeyPriceNotSet` | 5 | Attempt to use pricing operations (buy, sell, quote) before an admin has set a key price | Contact protocol admin to set a key price via `set_key_price` |
| 6 | `NotPositiveAmount` | 6 | Amount is zero or negative where a positive value is required (e.g., `set_key_price`, `buy_key` payment) | Use a positive amount; for buy/sell, check user input for negative or zero values |
| 7 | `FeeConfigNotSet` | 7 | Attempt to compute or query fees before an admin has set a protocol fee configuration | Contact protocol admin to set fee config via `set_fee_config` |
| 8 | `InvalidFeeConfig` | 8 | Proposed fee configuration violates constraints: `creator_bps + protocol_bps != 10000` or `protocol_bps > 5000` | Ensure basis points sum to 10,000 and protocol share does not exceed 50%; see `fee::validate_fee_bps` for validation rules |
| 9 | `InsufficientBalance` | 9 | Holder has no keys for the given creator (sell attempt with zero balance) | Verify holder owns keys before attempting a sell; call `get_key_balance` to check holdings |

## Integration Notes

### Authorization and Registration

- Code 1 (`AlreadyRegistered`) is raised only if the same address calls `register_creator` twice. This is a guard against accidental re-registration; intended behavior should call `get_creator` first or handle registration state off-chain.
- Code 2 (`NotRegistered`) applies to both reads and writes. Callers must always register a creator before buy/sell/quote operations.

### Pricing and Fees

- Code 5 (`KeyPriceNotSet`) and Code 7 (`FeeConfigNotSet`) are initialization gates. Clients should detect these and display a message like "Pricing has not been configured yet" rather than retrying immediately.
- Code 8 (`InvalidFeeConfig`) is raised when basis points are invalid. Always validate client-side before sending transactions: `creator_bps + protocol_bps == 10000` and `protocol_bps <= 5000`.

### Buy and Sell

- Code 4 (`InsufficientPayment`) applies only to buy operations. Sellers use code 9 (`InsufficientBalance`).
- Code 6 (`NotPositiveAmount`) can be raised by `set_key_price` (amount must be > 0) or `buy_key` (payment must be > 0).

### Overflow Handling

- Code 3 (`Overflow`) should be rare in typical use. It can arise if:
- Protocol supply exceeds ~4 billion keys (unlikely in practice).
- A key price approaches i128::MAX and fee math tries to add fees on top (call `get_buy_quote` first to validate).
- Arithmetic during division or subtraction overflows (internal invariant violation; report if encountered).

## Event Emission

The contract does not emit error events. Errors are returned to the caller synchronously. Clients should log and handle error codes as part of transaction response validation.

## Version Stability

Error codes are stable and will not change. New error codes may be added in future contract versions without breaking existing error handling. Consumers should handle unexpected error codes gracefully (e.g., log and retry or display a generic error message).
Loading
Loading