Skip to content

Commit 66db721

Browse files
authored
Merge pull request #659 from Chronos-III/feature/issues-611-612-613-614
fix(split): resolve issues #611 #612 #613 #614
2 parents 6080d8b + d2e7e10 commit 66db721

4 files changed

Lines changed: 49 additions & 98 deletions

File tree

contracts/split/src/calc.rs

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
use crate::types::BASIS_POINTS_TOTAL;
99
use soroban_sdk::{Env, Vec};
1010

11+
use crate::error::ContractError;
12+
1113
/// Distribute `total` among recipients according to their `ratios` out of
1214
/// `denom`, using the largest-remainder method to handle rounding.
1315
///
@@ -35,9 +37,13 @@ pub fn distribute_with_remainder(
3537
total: i128,
3638
ratios: &Vec<i128>,
3739
denom: i128,
38-
) -> Vec<i128> {
39-
assert!(!ratios.is_empty(), "ratios must not be empty");
40-
assert!(denom > 0, "denom must be positive");
40+
) -> Result<Vec<i128>, ContractError> {
41+
if ratios.is_empty() {
42+
return Err(ContractError::InvalidAmount);
43+
}
44+
if denom <= 0 {
45+
return Err(ContractError::InvalidAmount);
46+
}
4147

4248
let n = ratios.len() as usize;
4349

@@ -59,7 +65,9 @@ pub fn distribute_with_remainder(
5965
// Contracts with more than 64 recipients would need a larger cap, but
6066
// 64 is a reasonable upper bound for on-chain use.
6167
const MAX_RECIPIENTS: usize = 64;
62-
assert!(n <= MAX_RECIPIENTS, "too many recipients (max 64)");
68+
if n > MAX_RECIPIENTS {
69+
return Err(ContractError::InvalidAmount);
70+
}
6371

6472
let mut indices = [0usize; MAX_RECIPIENTS];
6573
for i in 0..n {
@@ -95,7 +103,7 @@ pub fn distribute_with_remainder(
95103
shares_mut.set(idx, current + 1);
96104
}
97105

98-
shares_mut
106+
Ok(shares_mut)
99107
}
100108

101109
// ---------------------------------------------------------------------------
@@ -169,7 +177,8 @@ mod tests {
169177
/// Assert sum equals total and return shares.
170178
fn assert_exact(env: &Env, total: i128, ratios: &[i128], denom: i128) -> Vec<i128> {
171179
let r_vec = make_ratios(env, ratios);
172-
let result = distribute_with_remainder(env, total, &r_vec, denom);
180+
let result = distribute_with_remainder(env, total, &r_vec, denom)
181+
.expect("distribute_with_remainder should not fail for valid inputs");
173182
let sum: i128 = result.iter().sum();
174183
assert_eq!(
175184
sum, total,

contracts/split/src/events.rs

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1060,15 +1060,30 @@ pub fn tranche_released(env: &Env, invoice_id: u64, tranche_index: u32, amount:
10601060

10611061
/// Issue #349: Emitted when an address's reputation score is updated.
10621062
/// Topics: (split, rep_upd, address)
1063-
/// Data: score
1064-
pub fn rep_updated(env: &Env, address: &Address, score: &RepScore) {
1063+
/// Data: (score_struct, computed_score)
1064+
///
1065+
/// # Computed score formula
1066+
///
1067+
/// The `computed_score` is a single `u32` summary of the raw `RepScore`
1068+
/// counters. The formula rewards consistent on-time payment and successful
1069+
/// invoice completion while penalising late payments and refunds:
1070+
///
1071+
/// ```text
1072+
/// base = paid_on_time * 10 + invoices_released * 5
1073+
/// deductions = late_pays * 5 + invoices_refunded * 2
1074+
/// computed = base.saturating_sub(deductions)
1075+
/// ```
1076+
///
1077+
/// Indexers are encouraged to use `computed_score` directly rather than
1078+
/// re-implementing the formula off-chain.
1079+
pub fn rep_updated(env: &Env, address: &Address, score: &RepScore, computed_score: u32) {
10651080
env.events().publish(
10661081
(
10671082
symbol_short!("split"),
10681083
symbol_short!("rep_upd"),
10691084
address.clone(),
10701085
),
1071-
score.clone(),
1086+
(score.clone(), computed_score),
10721087
);
10731088
}
10741089

@@ -1625,7 +1640,10 @@ pub fn child_invoice_unblocked(env: &Env, child_id: u64, parent_id: u64) {
16251640
/// Emitted every time a late-payment penalty is charged.
16261641
///
16271642
/// Topics: `("late_pen", invoice_id)`
1628-
/// Data: `(payer, penalty_amount)`
1643+
/// Data: `(invoice_id, payer, penalty_amount)`
1644+
///
1645+
/// `invoice_id` is included in both the topics (for indexer filtering) and
1646+
/// the data payload (for data-only decoders that do not inspect topics).
16291647
#[allow(dead_code)]
16301648
pub fn late_payment_penalty_charged(
16311649
env: &Env,
@@ -1635,7 +1653,7 @@ pub fn late_payment_penalty_charged(
16351653
) {
16361654
env.events().publish(
16371655
(symbol_short!("late_pen"), invoice_id),
1638-
(payer.clone(), penalty_amount),
1656+
(invoice_id, payer.clone(), penalty_amount),
16391657
);
16401658
}
16411659

contracts/split/src/lib.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -457,7 +457,17 @@ where
457457
let mut score = get_rep_internal(env, address);
458458
update_fn(&mut score);
459459
env.storage().persistent().set(&rep_key(address), &score);
460-
events::rep_updated(env, address, &score);
460+
// Compute the derived score integer before emitting the event.
461+
// Formula: (paid_on_time * 10 + invoices_released * 5)
462+
// .saturating_sub(late_pays * 5 + invoices_refunded * 2)
463+
let base = (score.paid_on_time as u32)
464+
.saturating_mul(10)
465+
.saturating_add((score.invoices_released as u32).saturating_mul(5));
466+
let deductions = (score.late_pays as u32)
467+
.saturating_mul(5)
468+
.saturating_add((score.invoices_refunded as u32).saturating_mul(2));
469+
let computed_score = base.saturating_sub(deductions);
470+
events::rep_updated(env, address, &score, computed_score);
461471
score
462472
}
463473

contracts/split/src/stats.rs

Lines changed: 0 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -13,92 +13,6 @@ const TOTAL_VOLUME: &str = "stats_total_volume";
1313
const TOTAL_RECIPIENTS_PAID: &str = "stats_total_recipients_paid";
1414
const STATS_UPDATED: &str = "StatsUpdated";
1515

16-
pub type Stats = (u64, i128, u64);
17-
18-
fn invoices_key(env: &Env) -> Symbol {
19-
Symbol::new(env, TOTAL_INVOICES)
20-
}
21-
22-
fn volume_key(env: &Env) -> Symbol {
23-
Symbol::new(env, TOTAL_VOLUME)
24-
}
25-
26-
fn recipients_paid_key(env: &Env) -> Symbol {
27-
Symbol::new(env, TOTAL_RECIPIENTS_PAID)
28-
}
29-
30-
pub fn get_stats(env: &Env) -> Stats {
31-
let invoices = env
32-
.storage()
33-
.instance()
34-
.get::<Symbol, u64>(&invoices_key(env))
35-
.unwrap_or(0);
36-
let volume = env
37-
.storage()
38-
.instance()
39-
.get::<Symbol, i128>(&volume_key(env))
40-
.unwrap_or(0);
41-
let recipients_paid = env
42-
.storage()
43-
.instance()
44-
.get::<Symbol, u64>(&recipients_paid_key(env))
45-
.unwrap_or(0);
46-
47-
(invoices, volume, recipients_paid)
48-
}
49-
50-
fn publish_updated(env: &Env, stats: Stats) {
51-
env.events().publish(
52-
(Symbol::new(env, STATS_UPDATED),),
53-
(
54-
stats.0,
55-
stats.1,
56-
stats.2,
57-
env.ledger().sequence(),
58-
),
59-
);
60-
}
61-
62-
pub fn record_invoice_created(env: &Env) -> Result<(), ContractError> {
63-
let (invoices, volume, recipients_paid) = get_stats(env);
64-
let updated_invoices = invoices
65-
.checked_add(1)
66-
.ok_or(ContractError::StatsOverflow)?;
67-
68-
env.storage()
69-
.instance()
70-
.set(&invoices_key(env), &updated_invoices);
71-
72-
publish_updated(env, (updated_invoices, volume, recipients_paid));
73-
Ok(())
74-
}
75-
76-
pub fn record_volume(env: &Env, amount: i128) -> Result<(), ContractError> {
77-
let (invoices, volume, recipients_paid) = get_stats(env);
78-
let updated_volume = volume
79-
.checked_add(amount)
80-
.ok_or(ContractError::StatsOverflow)?;
81-
82-
env.storage()
83-
.instance()
84-
.set(&volume_key(env), &updated_volume);
85-
86-
publish_updated(env, (invoices, updated_volume, recipients_paid));
87-
Ok(())
88-
}
89-
90-
pub fn record_recipients_paid(env: &Env, count: u64) -> Result<(), ContractError> {
91-
let (invoices, volume, recipients_paid) = get_stats(env);
92-
let updated_recipients_paid = recipients_paid
93-
.checked_add(count)
94-
.ok_or(ContractError::StatsOverflow)?;
95-
96-
env.storage()
97-
.instance()
98-
.set(&recipients_paid_key(env), &updated_recipients_paid);
99-
100-
publish_updated(env, (invoices, volume, updated_recipients_paid));
101-
Ok(())
10216
fn total_invoices_key(env: &Env) -> Symbol {
10317
Symbol::new(env, TOTAL_INVOICES)
10418
}

0 commit comments

Comments
 (0)