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
10 changes: 9 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,15 @@ jobs:
- name: Install tarpaulin
run: cargo install cargo-tarpaulin
- name: Run coverage
run: cargo tarpaulin --all-features --workspace --timeout 120 --out Stdout
run: cargo tarpaulin --all-features --workspace --timeout 120 --out Stdout --fail-under 70

module-reachability:
name: Rust Module Reachability
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check Rust module reachability
run: python3 scripts/check_rust_modules.py

sdk-parity:
name: SDK Error Code Parity
Expand Down
10 changes: 10 additions & 0 deletions contracts/marketx/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ use soroban_sdk::contracterror;
pub enum ContractError {
NotAdmin = 1,
Unauthorized = 2,
/// Caller is not the proposed administrator.
NotProposedAdmin = 3,
/// Caller is not the configured oracle.
NotOracle = 4,
EscrowNotFound = 10,
InvalidEscrowState = 11,
Expand All @@ -22,13 +24,21 @@ pub enum ContractError {
ItemAmountInvalid = 83,
EscrowNotExpired = 90,
EscrowAlreadyFunded = 91,
/// The requested milestone does not exist.
MilestoneNotFound = 100,
/// The requested milestone was already completed.
MilestoneAlreadyCompleted = 101,
/// The escrow timelock has not been reached.
TimeLockNotReached = 110,
/// Timelock release is not enabled for this escrow.
TimeLockNotEnabled = 111,
/// The group buy has not received enough funding.
GroupBuyNotFunded = 120,
/// The group buy has already been funded.
GroupBuyAlreadyFunded = 121,
/// The group buy deadline has passed.
GroupBuyDeadlinePassed = 122,
/// The group buy amount is invalid.
InvalidGroupBuyAmount = 123,

// ── Dispute Resolution V2 (#201-204) ─────────────────────────────────────
Expand Down
7 changes: 7 additions & 0 deletions contracts/marketx/src/escrow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ impl Contract {
env.storage()
.persistent()
.set(&DataKey::TotalFeesCollected, &0i128);
env.storage().persistent().set(
&DataKey::VolumeTiers,
&VolumeTierConfig {
reset_ledger: env.ledger().sequence(),
..VolumeTierConfig::default()
},
);

Ok(())
}
Expand Down
32 changes: 32 additions & 0 deletions contracts/marketx/src/fees.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,38 @@ impl Contract {
.unwrap_or(0)
}

pub fn get_total_fees_collected(env: Env) -> i128 {
env.storage()
.persistent()
.get(&DataKey::TotalFeesCollected)
.unwrap_or(0)
}

pub fn get_buyer_volume(env: Env, buyer: Address) -> i128 {
let config: VolumeTierConfig = env
.storage()
.persistent()
.get(&DataKey::VolumeTiers)
.unwrap_or_default();
Self::buyer_volume_internal(&env, &buyer, &config)
}

pub fn get_buyer_tier(env: Env, buyer: Address) -> u32 {
let config: VolumeTierConfig = env
.storage()
.persistent()
.get(&DataKey::VolumeTiers)
.unwrap_or_default();
config.tier(Self::buyer_volume_internal(&env, &buyer, &config))
}

pub fn get_volume_tiers(env: Env) -> VolumeTierConfig {
env.storage()
.persistent()
.get(&DataKey::VolumeTiers)
.unwrap_or_default()
}

/// Add an address to the fee exemption whitelist. Admin only.
pub fn add_fee_whitelist(env: Env, address: Address) -> Result<(), ContractError> {
let admin = Self::assert_admin(&env)?;
Expand Down
4 changes: 4 additions & 0 deletions contracts/marketx/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ pub use types::*;

#[cfg(test)]
mod test;
#[cfg(test)]
mod test_integer_safety;
#[cfg(test)]
mod test_volume;

/// The MarketX escrow contract.
///
Expand Down
113 changes: 56 additions & 57 deletions contracts/marketx/src/test_integer_safety.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Integer Safety Tests for Fee Basis Points Calculations
//!
//!
//! Tests to verify overflow-safe fee calculations
//!
//!
//! Test Cases:
//! - Zero amount returns zero fee
//! - Amount less than 10,000 returns 0 (rounds down)
Expand All @@ -12,81 +12,74 @@

#![cfg(test)]
mod integer_safety_tests {
use soroban_sdk::{testutils::Address as _, token::Client as TokenClient, Address, Env, IntoVal};
use crate::{Client, Contract};
use crate::{Contract, ContractClient};
use soroban_sdk::{
testutils::Address as _, token::Client as TokenClient, token::StellarAssetClient, Address,
Env,
};

const FEE_BPS: u32 = 500; // 5%

fn setup() -> (Env, Address, Address, TokenClient, Address, Client) {
fn setup<'a>() -> (
Env,
Address,
Address,
TokenClient<'a>,
Address,
ContractClient<'a>,
) {
let env = Env::default();
env.mock_all_auths();
let admin = Address::random(&env);
let buyer = Address::random(&env);

let admin = Address::generate(&env);
let buyer = Address::generate(&env);

let sac = env.register_stellar_asset_contract_v2(admin.clone());
let token = TokenClient::new(&env, &sac);
let token = TokenClient::new(&env, &sac.address());
let token_admin = StellarAssetClient::new(&env, &sac.address());
let token_id = sac.address();

let contract_id = env.register_contract(None, Contract);
let client = Client::new(&env, &contract_id);
let contract_id = env.register(Contract, ());
let client = ContractClient::new(&env, &contract_id);

client.initialize(&admin, &admin, &FEE_BPS, &0i128, &0i128);
token.approve(&admin, &contract_id, &i128::MAX);
token_admin.mint(&buyer, &2_000_000_000);
token.approve(&buyer, &contract_id, &i128::MAX, &1000);

(env, buyer, admin, token, token_id, client)
}

#[test]
fn test_zero_amount_returns_zero_fee() {
let (env, buyer, seller, _token, token_id, client) = setup();

// Create and release escrow with 0 amount - should not panic
let escrow_id = client.create_escrow(
&buyer,
&seller,
&token_id,
&0i128,
&None,
&None,
&None,
);
let (_env, buyer, seller, _token, token_id, client) = setup();

client.fund_escrow(&escrow_id);
let result = client.try_release_escrow(&escrow_id);

// Should handle gracefully (either success or appropriate error)
assert!(result.is_ok() || result.is_err());
let result = client.try_create_escrow(
&buyer, &seller, &token_id, &0i128, &None, &None, &None, &None,
);
assert_eq!(result, Err(Ok(crate::ContractError::InvalidEscrowAmount)));
}

#[test]
fn test_small_amount_rounds_down() {
let (env, buyer, seller, _token, token_id, client) = setup();

// Amount 9,999 should give 0 fee with 500 bps
// (9999 * 500) / 10000 = 499.95 -> rounds to 0
let (_env, buyer, seller, _token, token_id, client) = setup();

// Amount 9,999 floors to a 499 fee with 500 bps.
let escrow_id = client.create_escrow(
&buyer,
&seller,
&token_id,
&9_999i128,
&None,
&None,
&None,
&buyer, &seller, &token_id, &9_999i128, &None, &None, &None, &None,
);

client.fund_escrow(&escrow_id);
client.release_escrow(&escrow_id);

// Get total fees collected - should be 0 (rounded down)
// Get total fees collected.
let total_fees = client.get_total_fees_collected();
assert_eq!(total_fees, 0);
assert_eq!(total_fees, 499);
}

#[test]
fn test_exact_division() {
let (env, buyer, seller, _token, token_id, client) = setup();
let (_env, buyer, seller, _token, token_id, client) = setup();

// Amount 10,000 with 500 bps = 500 fee exactly
let escrow_id = client.create_escrow(
&buyer,
Expand All @@ -96,6 +89,7 @@ mod integer_safety_tests {
&None,
&None,
&None,
&None,
);

client.fund_escrow(&escrow_id);
Expand All @@ -108,8 +102,8 @@ mod integer_safety_tests {

#[test]
fn test_remainder_handled_correctly() {
let (env, buyer, seller, _token, token_id, client) = setup();
let (_env, buyer, seller, _token, token_id, client) = setup();

// Amount 10,001 with 500 bps
// Fee = (10001 * 500) / 10000 = 500.05 -> floors to 500
let escrow_id = client.create_escrow(
Expand All @@ -120,6 +114,7 @@ mod integer_safety_tests {
&None,
&None,
&None,
&None,
);

client.fund_escrow(&escrow_id);
Expand All @@ -132,11 +127,11 @@ mod integer_safety_tests {

#[test]
fn test_large_amount_no_overflow() {
let (env, buyer, seller, _token, token_id, client) = setup();
let (_env, buyer, seller, _token, token_id, client) = setup();

// Very large amount - should not cause overflow
let large_amount = 1_000_000_000i128; // 100 XLM

let escrow_id = client.create_escrow(
&buyer,
&seller,
Expand All @@ -145,10 +140,11 @@ mod integer_safety_tests {
&None,
&None,
&None,
&None,
);

client.fund_escrow(&escrow_id);

// This should not panic
let result = client.try_release_escrow(&escrow_id);
assert!(result.is_ok());
Expand All @@ -157,14 +153,15 @@ mod integer_safety_tests {
#[test]
fn test_multiple_escrows_accumulate_safely() {
let (env, buyer, seller, _token, token_id, client) = setup();

// Create multiple escrows
for amount in [1000i128, 2000, 3000, 4000, 5000] {
for (index, amount) in [1000i128, 2000, 3000, 4000, 5000].into_iter().enumerate() {
let escrow_id = client.create_escrow(
&buyer,
&seller,
&token_id,
&amount,
&Some(soroban_sdk::Bytes::from_slice(&env, &[index as u8])),
&None,
&None,
&None,
Expand All @@ -181,10 +178,11 @@ mod integer_safety_tests {

#[test]
fn test_zero_fee_bps_returns_zero() {
let (env, admin, buyer, seller, _token, token_id, client) = setup();

let (env, buyer, _admin, _token, token_id, client) = setup();
let seller = Address::generate(&env);

// Set zero fee bps
client.set_fee_percentage(&admin, &0);
client.set_fee_percentage(&0);

let escrow_id = client.create_escrow(
&buyer,
Expand All @@ -194,6 +192,7 @@ mod integer_safety_tests {
&None,
&None,
&None,
&None,
);

client.fund_escrow(&escrow_id);
Expand All @@ -203,4 +202,4 @@ mod integer_safety_tests {
let total_fees = client.get_total_fees_collected();
assert_eq!(total_fees, 0);
}
}
}
Loading
Loading