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
30 changes: 30 additions & 0 deletions contracts/split/src/calc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -307,4 +307,34 @@ mod tests {
assert_exact(&env, total, ratios, denom);
}
}

// -----------------------------------------------------------------------
// calc_platform_fee tests
// -----------------------------------------------------------------------

#[test]
fn test_calc_platform_fee_normal() {
// 1_000_000 funded at 250 bps (2.5%) → fee = 25_000
let fee = calc_platform_fee(1_000_000, 250).unwrap();
assert_eq!(fee, 25_000);
}

#[test]
fn test_calc_platform_fee_zero_bps() {
// Zero fee rate → always zero fee regardless of funded amount
assert_eq!(calc_platform_fee(999_999_999, 0).unwrap(), 0);
}

#[test]
fn test_calc_platform_fee_max_bps() {
// 10_000 bps = 100% → fee equals funded
assert_eq!(calc_platform_fee(500, 10_000).unwrap(), 500);
}

#[test]
fn test_calc_platform_fee_overflow() {
// i128::MAX * any fee_bps > 0 will overflow the intermediate multiplication
let result = calc_platform_fee(i128::MAX, 1);
assert_eq!(result, Err(crate::error::ContractError::ArithmeticOverflow));
}
}
11 changes: 6 additions & 5 deletions contracts/split/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -278,25 +278,26 @@ pub fn invoice_archived(env: &Env, invoice_id: u64) {

/// Emitted when a delegate is assigned to an invoice.
/// Topics: (split, delegated, invoice_id)
/// Data: delegate
/// Data: (delegate, event_seq)
pub fn delegate_set(env: &Env, invoice_id: u64, delegate: &Address) {
let event_seq = next_seq(env, invoice_id);
env.events().publish(
(
symbol_short!("split"),
symbol_short!("delegated"),
invoice_id,
),
delegate.clone(),
(delegate.clone(), event_seq),
);
}

/// Emitted when a delegate is revoked from an invoice.
/// Topics: (split, revoked, invoice_id)
/// Data: ()
pub fn delegate_revoked(env: &Env, invoice_id: u64) {
/// Data: (revoker, ledger_sequence)
pub fn delegate_revoked(env: &Env, invoice_id: u64, revoker: &Address) {
env.events().publish(
(symbol_short!("split"), symbol_short!("revoked"), invoice_id),
(),
(revoker.clone(), env.ledger().sequence()),
);
}

Expand Down
30 changes: 22 additions & 8 deletions contracts/split/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1947,6 +1947,22 @@ fn load_invoice(env: &Env, id: u64) -> Invoice {
invoice
}

/// Guard: return `Err(ContractError::InvalidStatus)` unless the invoice is
/// still in the `Pending` state. Calling this at the top of any mutating
/// function replaces the repeated inline pattern:
///
/// ```rust,ignore
/// if invoice.status != InvoiceStatus::Pending {
/// return Err(ContractError::InvalidStatus);
/// }
/// ```
fn assert_invoice_pending(invoice: &Invoice) -> Result<(), ContractError> {
if invoice.status != InvoiceStatus::Pending {
return Err(ContractError::InvalidStatus);
}
Ok(())
}

/// Estimates the serialised size (in bytes) of an invoice's persisted
/// representation (issue #425). Sums the XDR-encoded length of the three
/// pieces `save_invoice` actually writes to storage (`InvoiceCore`,
Expand Down Expand Up @@ -3041,9 +3057,7 @@ impl SplitContract {
let mut invoice = load_invoice(&env, invoice_id);

// Only allow withdrawal while invoice is in Pending (Open) status.
if invoice.status != InvoiceStatus::Pending {
return Err(ContractError::InvalidStatus);
}
assert_invoice_pending(&invoice)?;

let contrib_key = contribution_key(invoice_id, &payer);
let amount: i128 = env
Expand Down Expand Up @@ -9152,7 +9166,7 @@ impl SplitContract {
let fee = if is_waived {
0
} else {
(proportional as u128 * platform_fee_bps as u128 / 10_000u128) as i128
calc_platform_fee(proportional, platform_fee_bps).expect("ArithmeticOverflow")
};
let tax = (proportional as u128 * invoice.tax_bps as u128 / 10_000u128) as i128;
let payout = proportional - fee - tax;
Expand Down Expand Up @@ -9716,7 +9730,7 @@ impl SplitContract {
let fee = if is_waived {
0
} else {
(payout_raw as u128 * platform_fee_bps as u128 / 10_000u128) as i128
calc_platform_fee(payout_raw, platform_fee_bps).expect("ArithmeticOverflow")
};
let tax = (payout_raw as u128 * invoice.tax_bps as u128 / 10_000u128) as i128;
let payout = payout_raw - fee - tax;
Expand Down Expand Up @@ -10446,8 +10460,8 @@ impl SplitContract {
(amount as u128 * member_funded as u128 / member_total as u128)
as i128
};
let fee = (proportional as u128 * platform_fee_bps as u128 / 10_000u128)
as i128;
let fee = calc_platform_fee(proportional, platform_fee_bps)
.expect("ArithmeticOverflow");
let tax = (proportional as u128 * member.tax_bps as u128 / 10_000u128)
as i128;
let payout = proportional - fee - tax;
Expand Down Expand Up @@ -13100,7 +13114,7 @@ impl SplitContract {

env.storage().persistent().remove(&delegate_key(invoice_id));

events::delegate_revoked(&env, invoice_id);
events::delegate_revoked(&env, invoice_id, &invoice.creator);
append_audit_entry(&env, invoice_id, symbol_short!("rvk_del"), &invoice.creator);
}

Expand Down