Skip to content
Open
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
2 changes: 1 addition & 1 deletion contracts/commitment_core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ crate-type = ["cdylib", "rlib"]
[features]
testutils = ["soroban-sdk/testutils"]
benchmark = []
fuzzing = []
default = []

[dependencies]
Expand All @@ -17,4 +18,3 @@ shared_utils = { path = "../shared_utils" }

[dev-dependencies]
soroban-sdk = { version = "21.0.0", features = ["testutils"] }

73 changes: 55 additions & 18 deletions contracts/commitment_core/src/fee_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,47 @@

use crate::{CommitmentCoreContract, CommitmentCoreContractClient, CommitmentRules};
use soroban_sdk::{
testutils::{Address as _, AuthorizedFunction, AuthorizedInvocation},
token, Address, Env, IntoVal, String, Symbol,
contract, contractimpl,
testutils::{Address as _, Ledger},
token, Address, Env, String,
};

fn create_token_contract<'a>(e: &Env, admin: &Address) -> (Address, token::Client<'a>) {
let addr = e.register_stellar_asset_contract(admin.clone());
(addr.clone(), token::Client::new(e, &addr))
#[contract]
struct FeeMockNftContract;

#[contractimpl]
impl FeeMockNftContract {
pub fn mint(
_e: Env,
_caller: Address,
_owner: Address,
_commitment_id: String,
_duration_days: u32,
_max_loss_percent: u32,
_commitment_type: String,
_initial_amount: i128,
_asset_address: Address,
_early_exit_penalty: u32,
) -> u32 {
1
}

pub fn settle(_e: Env, _caller: Address, _token_id: u32) {}

pub fn mark_inactive(_e: Env, _caller: Address, _token_id: u32) {}
}

fn create_token_contract<'a>(
e: &Env,
admin: &Address,
) -> (Address, token::Client<'a>, token::StellarAssetClient<'a>) {
let token_contract = e.register_stellar_asset_contract_v2(admin.clone());
let addr = token_contract.address();
(
addr.clone(),
token::Client::new(e, &addr),
token::StellarAssetClient::new(e, &addr),
)
}

fn setup_test() -> (
Expand All @@ -33,12 +67,12 @@ fn setup_test() -> (
e.mock_all_auths();

let admin = Address::generate(&e);
let nft_contract = Address::generate(&e);
let nft_contract = e.register_contract(None, FeeMockNftContract);
let user = Address::generate(&e);
let (token_address, token_client) = create_token_contract(&e, &admin);
let (token_address, token_client, token_admin_client) = create_token_contract(&e, &admin);

// Mint tokens to user
token_client.mint(&user, &10_000_000);
token_admin_client.mint(&user, &10_000_000);

let contract_id = e.register_contract(None, CommitmentCoreContract);
let client = CommitmentCoreContractClient::new(&e, &contract_id);
Expand Down Expand Up @@ -221,12 +255,13 @@ fn test_early_exit_penalty_retained_as_fee() {

let expected_penalty = 100_000i128; // 10% of 1,000,000
let expected_returned = amount - expected_penalty;
let expected_user_balance = 10_000_000i128 - amount + expected_returned;

// Verify penalty was added to collected fees
assert_eq!(client.get_collected_fees(&token_address), expected_penalty);

// Verify user received net amount
assert_eq!(token_client.balance(&user), expected_returned);
assert_eq!(token_client.balance(&user), expected_user_balance);
}

#[test]
Expand All @@ -251,12 +286,13 @@ fn test_early_exit_with_creation_fee_and_penalty() {
let exit_penalty = 99_000i128; // 10% of 990,000
let expected_returned = net_amount - exit_penalty;
let total_fees = creation_fee + exit_penalty;
let expected_user_balance = 10_000_000i128 - amount + expected_returned;

// Verify both fees were collected
assert_eq!(client.get_collected_fees(&token_address), total_fees);

// Verify user received correct amount
assert_eq!(token_client.balance(&user), expected_returned);
assert_eq!(token_client.balance(&user), expected_user_balance);
}

// ============================================================================
Expand Down Expand Up @@ -442,11 +478,11 @@ fn test_get_collected_fees_multiple_assets() {
let (e, admin, _, user, _, _, client) = setup_test();

// Create two different tokens
let (token1, token1_client) = create_token_contract(&e, &admin);
let (token2, token2_client) = create_token_contract(&e, &admin);
let (token1, _token1_client, token1_admin_client) = create_token_contract(&e, &admin);
let (token2, _token2_client, token2_admin_client) = create_token_contract(&e, &admin);

token1_client.mint(&user, &10_000_000);
token2_client.mint(&user, &10_000_000);
token1_admin_client.mint(&user, &10_000_000);
token2_admin_client.mint(&user, &10_000_000);

// Set creation fee
client.set_creation_fee_bps(&admin, &100);
Expand Down Expand Up @@ -476,21 +512,22 @@ fn test_fee_collection_with_settle() {
let amount = 1_000_000i128;
let creation_fee = 10_000i128;
let net_amount = amount - creation_fee;
let expected_user_balance = 10_000_000i128 - amount + net_amount;

let mut rules = default_rules(&e);
rules.duration_days = 0; // Expires immediately
let rules = default_rules(&e);

let commitment_id = client.create_commitment(&user, &amount, &token_address, &rules);

// Settle commitment
e.ledger().with_mut(|li| li.timestamp = li.timestamp + 1);
e.ledger()
.with_mut(|li| li.timestamp = li.timestamp + (rules.duration_days as u64 * 86_400) + 1);
client.settle(&commitment_id);

// Verify creation fee still collected
assert_eq!(client.get_collected_fees(&token_address), creation_fee);

// Verify user got back net amount
assert_eq!(token_client.balance(&user), net_amount);
assert_eq!(token_client.balance(&user), expected_user_balance);
}

#[test]
Expand Down
141 changes: 141 additions & 0 deletions contracts/commitment_core/src/fuzz_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
#![cfg(test)]

use crate::{
fuzzing::{
classify_generated_commitment_id_bytes, observe_amount, observe_commitment_input,
AmountShape, CommitmentIdShape,
},
CommitmentCoreContract, CommitmentCoreContractClient, CommitmentRules,
};
use soroban_sdk::{
contract, contractimpl,
testutils::Address as _,
token::StellarAssetClient,
Address, Env, String,
};

#[contract]
struct FuzzMockNftContract;

#[contractimpl]
impl FuzzMockNftContract {
pub fn mint(
_e: Env,
_caller: Address,
_owner: Address,
_commitment_id: String,
_duration_days: u32,
_max_loss_percent: u32,
_commitment_type: String,
_initial_amount: i128,
_asset_address: Address,
_early_exit_penalty: u32,
) -> u32 {
1
}

pub fn settle(_e: Env, _caller: Address, _token_id: u32) {}

pub fn mark_inactive(_e: Env, _caller: Address, _token_id: u32) {}
}

fn default_rules(e: &Env) -> CommitmentRules {
CommitmentRules {
duration_days: 30,
max_loss_percent: 10,
commitment_type: String::from_str(e, "safe"),
early_exit_penalty: 15,
min_fee_threshold: 0,
grace_period_days: 0,
}
}

#[test]
fn test_fuzz_commitment_id_seed_shapes() {
assert_eq!(
classify_generated_commitment_id_bytes(b""),
CommitmentIdShape::Empty
);
assert_eq!(
classify_generated_commitment_id_bytes(b"user_supplied"),
CommitmentIdShape::InvalidPrefix
);
assert_eq!(
classify_generated_commitment_id_bytes(b"c_"),
CommitmentIdShape::MissingDigits
);
assert_eq!(
classify_generated_commitment_id_bytes(b"c_12x"),
CommitmentIdShape::NonDigitSuffix
);
assert_eq!(
classify_generated_commitment_id_bytes(b"c_18446744073709551615"),
CommitmentIdShape::ValidGenerated
);
}

#[test]
fn test_fuzz_commitment_id_rejects_oversized_seed() {
let oversized = *b"c_1234567890123456789012345678901";
assert_eq!(
classify_generated_commitment_id_bytes(&oversized),
CommitmentIdShape::TooLong
);
}

#[test]
fn test_fuzz_amount_seed_shapes() {
assert_eq!(observe_amount(0, 0).shape, AmountShape::NonPositive);
assert_eq!(observe_amount(-1, 0).shape, AmountShape::NonPositive);
assert_eq!(observe_amount(1, 10_001).shape, AmountShape::InvalidFeeBps);
assert_eq!(observe_amount(i128::MAX, 2).shape, AmountShape::FeeOverflow);

let max_fee = observe_amount(1, 10_000);
assert_eq!(max_fee.shape, AmountShape::Valid);
assert_eq!(max_fee.fee, Some(1));
assert_eq!(max_fee.net, Some(0));

let normal = observe_amount(1_000, 100);
assert_eq!(normal.shape, AmountShape::Valid);
assert_eq!(normal.fee, Some(10));
assert_eq!(normal.net, Some(990));
}

#[test]
fn test_fuzz_observation_combines_id_and_amount_seed() {
let observation = observe_commitment_input(b"c_42", 1_000, 100);
assert_eq!(observation.id_shape, CommitmentIdShape::ValidGenerated);
assert_eq!(observation.amount.shape, AmountShape::Valid);
assert_eq!(observation.amount.fee, Some(10));
assert_eq!(observation.amount.net, Some(990));
}

#[test]
fn test_create_commitment_rejects_fee_math_overflow() {
let e = Env::default();
e.mock_all_auths_allowing_non_root_auth();

let contract_id = e.register_contract(None, CommitmentCoreContract);
let nft_contract = e.register_contract(None, FuzzMockNftContract);
let client = CommitmentCoreContractClient::new(&e, &contract_id);

let admin = Address::generate(&e);
let owner = Address::generate(&e);
let token_admin = Address::generate(&e);
let amount = i128::MAX;

let token_contract = e.register_stellar_asset_contract_v2(token_admin);
let asset_address = token_contract.address();
let token_admin_client = StellarAssetClient::new(&e, &asset_address);
token_admin_client.mint(&owner, &amount);

client.initialize(&admin, &nft_contract);
client.set_creation_fee_bps(&admin, &2);

let result = client.try_create_commitment(&owner, &amount, &asset_address, &default_rules(&e));

assert!(result.is_err());
assert_eq!(client.get_total_commitments(), 0);
assert_eq!(client.get_total_value_locked(), 0);
assert_eq!(client.get_collected_fees(&asset_address), 0);
}
Loading