44//! across recipients proportionally, ensuring every stroop is accounted for
55//! (i.e. `sum(result) == total` always holds).
66
7+ #[ allow( unused_imports) ]
8+ use crate :: types:: BASIS_POINTS_TOTAL ;
79use soroban_sdk:: { Env , Vec } ;
810
911use crate :: error:: ContractError ;
@@ -15,16 +17,21 @@ use crate::error::ContractError;
1517/// * `env` – Soroban environment (needed to allocate the result `Vec`)
1618/// * `total` – total amount to distribute (stroops); must be ≥ 0
1719/// * `ratios` – relative weight of each recipient (must be non-empty, all ≥ 0)
18- /// * `denom` – sum of all ratios (must be > 0)
20+ /// * `denom` – sum of all ratios (must be > 0); typically [`BASIS_POINTS_TOTAL`]
1921///
2022/// # Guarantees
2123/// * `result.iter().sum::<i128>() == total` always
2224/// * recipients with larger fractional remainders receive an extra stroop
2325/// * pure function: no side effects, no storage access
2426///
25- /// # Errors
26- /// * [`ContractError::InvalidAmount`] if `ratios` is empty
27- /// * [`ContractError::InvalidAmount`] if `denom` is zero or negative
27+ /// # Panics
28+ /// * if `ratios` is empty
29+ /// * if `denom` is zero
30+ // NOTE: if you call this function and ignore its return value the Rust
31+ // compiler will emit a `#[must_use]` warning:
32+ // warning: unused return value of `distribute_with_remainder` that must be used
33+ // This ensures callers never silently drop the distribution result.
34+ #[ must_use = "the distribution result must be applied to recipients" ]
2835pub fn distribute_with_remainder (
2936 env : & Env ,
3037 total : i128 ,
@@ -260,6 +267,34 @@ mod tests {
260267 assert_exact ( & env, 1_000_000_000 , & [ 100_000 , 200_000 , 300_000 ] , 600_000 ) ;
261268 }
262269
270+ #[ test]
271+ fn single_recipient_gets_full_amount ( ) {
272+ let env = Env :: default ( ) ;
273+ let r = distribute_with_remainder ( & env, 12345 , & make_ratios ( & env, & [ 1 ] ) , 1 ) ;
274+ assert_eq ! ( r. len( ) , 1 ) ;
275+ assert_eq ! ( r. get( 0 ) , Some ( 12345 ) ) ;
276+ }
277+
278+ #[ test]
279+ fn sum_invariant_holds_with_unequal_ratios ( ) {
280+ let env = Env :: default ( ) ;
281+ // Case 1: 3 recipients with ratios [1, 1, 1] and total=10
282+ // Total is not evenly divisible by denom (10 % 3 != 0)
283+ let r1 = distribute_with_remainder ( & env, 10 , & make_ratios ( & env, & [ 1 , 1 , 1 ] ) , 3 ) ;
284+ let sum1: i128 = r1. iter ( ) . sum ( ) ;
285+ assert_eq ! ( sum1, 10 ) ;
286+
287+ // Case 2: 4 recipients with ratios [2, 3, 1, 4] and total=100
288+ let r2 = distribute_with_remainder ( & env, 100 , & make_ratios ( & env, & [ 2 , 3 , 1 , 4 ] ) , 10 ) ;
289+ let sum2: i128 = r2. iter ( ) . sum ( ) ;
290+ assert_eq ! ( sum2, 100 ) ;
291+
292+ // Case 3: 2 recipients with ratios [1, 3] and total=999
293+ let r3 = distribute_with_remainder ( & env, 999 , & make_ratios ( & env, & [ 1 , 3 ] ) , 4 ) ;
294+ let sum3: i128 = r3. iter ( ) . sum ( ) ;
295+ assert_eq ! ( sum3, 999 ) ;
296+ }
297+
263298 /// Property-based style test: exhaustively verify sum == total for many inputs.
264299 #[ test]
265300 fn test_property_sum_equals_total ( ) {
@@ -282,29 +317,33 @@ mod tests {
282317 }
283318 }
284319
285- /// Confirms that an empty ratios list returns Err(InvalidAmount) rather than panicking.
320+ // -----------------------------------------------------------------------
321+ // calc_platform_fee tests
322+ // -----------------------------------------------------------------------
323+
286324 #[ test]
287- fn test_empty_ratios_returns_err ( ) {
288- let env = Env :: default ( ) ;
289- let empty = Vec :: new ( & env) ;
290- let result = distribute_with_remainder ( & env, 1000 , & empty, 1 ) ;
291- assert_eq ! (
292- result,
293- Err ( ContractError :: InvalidAmount ) ,
294- "expected Err(InvalidAmount) for empty ratios"
295- ) ;
325+ fn test_calc_platform_fee_normal ( ) {
326+ // 1_000_000 funded at 250 bps (2.5%) → fee = 25_000
327+ let fee = calc_platform_fee ( 1_000_000 , 250 ) . unwrap ( ) ;
328+ assert_eq ! ( fee, 25_000 ) ;
296329 }
297330
298- /// Confirms that denom == 0 returns Err(InvalidAmount) rather than panicking.
299331 #[ test]
300- fn test_zero_denom_returns_err ( ) {
301- let env = Env :: default ( ) ;
302- let ratios = make_ratios ( & env, & [ 1 , 2 , 3 ] ) ;
303- let result = distribute_with_remainder ( & env, 1000 , & ratios, 0 ) ;
304- assert_eq ! (
305- result,
306- Err ( ContractError :: InvalidAmount ) ,
307- "expected Err(InvalidAmount) for zero denom"
308- ) ;
332+ fn test_calc_platform_fee_zero_bps ( ) {
333+ // Zero fee rate → always zero fee regardless of funded amount
334+ assert_eq ! ( calc_platform_fee( 999_999_999 , 0 ) . unwrap( ) , 0 ) ;
335+ }
336+
337+ #[ test]
338+ fn test_calc_platform_fee_max_bps ( ) {
339+ // 10_000 bps = 100% → fee equals funded
340+ assert_eq ! ( calc_platform_fee( 500 , 10_000 ) . unwrap( ) , 500 ) ;
341+ }
342+
343+ #[ test]
344+ fn test_calc_platform_fee_overflow ( ) {
345+ // i128::MAX * any fee_bps > 0 will overflow the intermediate multiplication
346+ let result = calc_platform_fee ( i128:: MAX , 1 ) ;
347+ assert_eq ! ( result, Err ( crate :: error:: ContractError :: ArithmeticOverflow ) ) ;
309348 }
310349}
0 commit comments