@@ -1207,6 +1207,84 @@ fn test_reputation_score_never_goes_below_zero() {
12071207 assert_eq ! ( score, 0 , "Score should floor at 0, not go negative" ) ;
12081208}
12091209
1210+ // ----------------------------------------------------------------
1211+ // Regression tests — issue #601: bound reputation decay loop
1212+ // ----------------------------------------------------------------
1213+
1214+ #[ test]
1215+ fn test_reputation_decay_bounded_for_extremely_long_inactivity ( ) {
1216+ let t = setup ( ) ;
1217+
1218+ t. env . as_contract ( & t. contract . address , || {
1219+ invoice:: set_payer_score ( & t. env , & t. payer , 80 ) ;
1220+ } ) ;
1221+
1222+ let config = Config {
1223+ high_rep_threshold : 80 ,
1224+ bonus_bps : 200 ,
1225+ min_discount_rate_bps : 100 ,
1226+ decay_rate_bps : 100 ,
1227+ decay_period_ledgers : 2 ,
1228+ dispute_timeout_ledgers : 100 ,
1229+ xlm_sac_address : Address :: generate ( & t. env ) ,
1230+ usdc_sac_address : Address :: generate ( & t. env ) ,
1231+ eurc_sac_address : Address :: generate ( & t. env ) ,
1232+ price_oracle : None ,
1233+ max_oracle_age_ledgers : 17280 ,
1234+ } ;
1235+ t. env . as_contract ( & t. contract . address , || {
1236+ crate :: storage:: set_config ( & t. env , & config) ;
1237+ t. env . storage ( ) . instance ( ) . extend_ttl ( 1_000_000 , 2_000_000 ) ;
1238+ } ) ;
1239+
1240+ // periods_passed = 2,500 / 2 = 1,250 (1.25x the cap) — sustained
1241+ // long-term inactivity under a normal (non-griefing) decay period.
1242+ let mut ledger = t. env . ledger ( ) . get ( ) ;
1243+ ledger. sequence_number += 2_500 ;
1244+ t. env . ledger ( ) . set ( ledger) ;
1245+
1246+ let score = t. contract . payer_score ( & t. payer ) ;
1247+
1248+ assert_eq ! ( score, 0 , "Score for a long-inactive payer should floor at 0, not hang or panic" ) ;
1249+ }
1250+
1251+ #[ test]
1252+ fn test_reputation_decay_bounded_when_decay_period_is_one_ledger ( ) {
1253+ let t = setup ( ) ;
1254+
1255+ t. env . as_contract ( & t. contract . address , || {
1256+ invoice:: set_payer_score ( & t. env , & t. payer , 80 ) ;
1257+ } ) ;
1258+
1259+ // The exact griefing scenario from issue #601: decay_period_ledgers=1.
1260+ let config = Config {
1261+ high_rep_threshold : 80 ,
1262+ bonus_bps : 200 ,
1263+ min_discount_rate_bps : 100 ,
1264+ decay_rate_bps : 100 ,
1265+ decay_period_ledgers : 1 ,
1266+ dispute_timeout_ledgers : 100 ,
1267+ xlm_sac_address : Address :: generate ( & t. env ) ,
1268+ usdc_sac_address : Address :: generate ( & t. env ) ,
1269+ eurc_sac_address : Address :: generate ( & t. env ) ,
1270+ price_oracle : None ,
1271+ max_oracle_age_ledgers : 17280 ,
1272+ } ;
1273+ t. env . as_contract ( & t. contract . address , || {
1274+ crate :: storage:: set_config ( & t. env , & config) ;
1275+ t. env . storage ( ) . instance ( ) . extend_ttl ( 1_000_000 , 2_000_000 ) ;
1276+ } ) ;
1277+
1278+ // periods_passed = 1,500 (1.5x the cap) with decay_period_ledgers=1.
1279+ let mut ledger = t. env . ledger ( ) . get ( ) ;
1280+ ledger. sequence_number += 1_500 ;
1281+ t. env . ledger ( ) . set ( ledger) ;
1282+
1283+ let score = t. contract . payer_score ( & t. payer ) ;
1284+
1285+ assert_eq ! ( score, 0 , "decay_period_ledgers=1 with a large gap should floor at 0, not hang or panic" ) ;
1286+ }
1287+
12101288#[ test]
12111289fn test_reputation_score_never_exceeds_100 ( ) {
12121290 let t = setup ( ) ;
0 commit comments