From d7e507b0e2bcfbf373ef2e4bfba0e9748a9475e1 Mon Sep 17 00:00:00 2001 From: jamilahmadzai Date: Sat, 11 Jul 2026 17:08:53 +0200 Subject: [PATCH] test: cover farming pool boost credit invariants --- soroban/contracts/farming-pool/src/test.rs | 179 +++++++++++++++++++++ 1 file changed, 179 insertions(+) diff --git a/soroban/contracts/farming-pool/src/test.rs b/soroban/contracts/farming-pool/src/test.rs index 95f4704..87ede70 100644 --- a/soroban/contracts/farming-pool/src/test.rs +++ b/soroban/contracts/farming-pool/src/test.rs @@ -146,6 +146,20 @@ fn test_effective_stake_full_allocation_2x() { assert_eq!(stake, 2_000); } +#[test] +fn test_effective_stake_full_allocation_3x() { + // 100% allocation at 3× multiplier: all principal is boosted. + let stake = compute_total_stake(1_000, 100, 3); + assert_eq!(stake, 3_000); +} + +#[test] +fn test_effective_stake_full_allocation_1x_has_no_gain() { + // 100% allocation at 1× multiplier should behave like no boost. + let stake = compute_total_stake(1_000, 100, 1); + assert_eq!(stake, 1_000); +} + #[test] fn test_effective_stake_half_allocation_2x() { // 50% allocation at 2×: principal = 500, virtual = 500*2 = 1000. total = 1500. @@ -233,6 +247,25 @@ fn test_boost_update_preserves_previously_earned_credits() { assert_eq!(t.client.get_credits(&t.user), 12_500); } +#[test] +fn test_boost_change_checkpoints_old_rate_across_long_window() { + // This mirrors the bounty invariant: 500 ledgers unboosted, then 500 ledgers + // at 50% allocation with a 2× multiplier. Credits from the first window must + // not be retroactively recalculated after the boost is applied. + let t = setup(2, 1); + t.client.stake(&t.user, &1_000); + advance_ledgers(&t.env, 500); + t.client.set_boost(&t.user, &50u32); + advance_ledgers(&t.env, 500); + + let unboosted_window = 1_000 * 500; + let boosted_window = 1_500 * 500; + assert_eq!( + t.client.get_credits(&t.user), + unboosted_window + boosted_window + ); +} + #[test] fn test_boost_can_be_updated_repeatedly_without_losing_credits() { // 10 ledgers at 50% boost (effective 1500), then 10 at 100% (effective 2000). @@ -260,6 +293,28 @@ fn test_set_boost_rejects_over_100_allocation() { assert!(t.client.try_set_boost(&t.user, &101u32).is_err()); } +#[test] +fn test_set_boost_emits_boost_applied_event() { + let t = setup(2, 1); + t.client.set_boost(&t.user, &50u32); + + assert_eq!( + t.env.events().all(), + soroban_sdk::vec![ + &t.env, + ( + t.contract_id.clone(), + soroban_sdk::vec![ + &t.env, + soroban_sdk::symbol_short!("boost").into_val(&t.env), + soroban_sdk::symbol_short!("applied").into_val(&t.env) + ], + (t.user.clone(), 50u32, 2u32).into_val(&t.env), + ) + ] + ); +} + #[test] fn test_admin_sets_global_multiplier() { let t = setup(2, 1); @@ -271,6 +326,50 @@ fn test_admin_sets_global_multiplier() { assert_eq!(cfg.multiplier, 3); } +#[test] +fn test_set_global_multiplier_emits_mult_set_event() { + let t = setup(2, 1); + t.client.set_global_multiplier(&3u32); + + assert_eq!( + t.env.events().all(), + soroban_sdk::vec![ + &t.env, + ( + t.contract_id.clone(), + soroban_sdk::vec![ + &t.env, + soroban_sdk::symbol_short!("boost").into_val(&t.env), + soroban_sdk::symbol_short!("mult_set").into_val(&t.env) + ], + 3u32.into_val(&t.env), + ) + ] + ); +} + +#[test] +fn test_set_global_multiplier_requires_admin_auth() { + let (env, contract_id, client, _admin, user) = setup_without_mocked_auth(); + + let result = client + .mock_auths(&[MockAuth { + address: &user, + invoke: &MockAuthInvoke { + contract: &contract_id, + fn_name: "set_global_multiplier", + args: (&3u32,).into_val(&env), + sub_invokes: &[], + }, + }]) + .try_set_global_multiplier(&3u32); + + assert!( + result.is_err(), + "non-admin set_global_multiplier must be rejected" + ); +} + #[test] fn test_set_credit_rate_updates_public_getters() { let t = setup_with_lock_period(2, 1, 12); @@ -413,6 +512,21 @@ fn test_admin_multiplier_change_applies_from_next_checkpoint() { assert_eq!(t.client.get_credits(&t.user), 35_000); } +#[test] +fn test_global_multiplier_change_does_not_retroactively_alter_banked_credits() { + let t = setup(2, 1); + t.client.stake(&t.user, &1_000); + t.client.set_boost(&t.user, &50u32); + advance_ledgers(&t.env, 20); + + t.client.set_boost(&t.user, &50u32); // checkpoints 30_000 at 2× + t.client.set_global_multiplier(&3u32); + assert_eq!(t.client.get_credits(&t.user), 30_000); + + advance_ledgers(&t.env, 1); + assert_eq!(t.client.get_credits(&t.user), 32_000); +} + #[test] #[should_panic(expected = "multiplier must be >= 1")] fn test_admin_multiplier_rejects_zero() { @@ -454,6 +568,71 @@ fn test_get_credits_zero_without_stake() { assert_eq!(t.client.get_credits(&t.user), 0); } +#[test] +fn test_get_credits_increases_monotonically_with_ledger_advancement() { + let t = setup(2, 1); + t.client.stake(&t.user, &1_000); + + advance_ledgers(&t.env, 3); + let first = t.client.get_credits(&t.user); + advance_ledgers(&t.env, 7); + let second = t.client.get_credits(&t.user); + + assert_eq!(first, 3_000); + assert_eq!(second, 10_000); + assert!(second > first); +} + +#[test] +fn test_boosted_stake_earns_more_than_unboosted_same_amount() { + let t = setup(2, 1); + let boosted = t.user.clone(); + let unboosted = Address::generate(&t.env); + t.token_sac.mint(&unboosted, &1_000_000i128); + + t.client.stake(&boosted, &1_000); + t.client.set_boost(&boosted, &50u32); + t.client.stake(&unboosted, &1_000); + advance_ledgers(&t.env, 10); + + assert_eq!(t.client.get_credits(&unboosted), 10_000); + assert_eq!(t.client.get_credits(&boosted), 15_000); + assert!(t.client.get_credits(&boosted) > t.client.get_credits(&unboosted)); +} + +#[test] +fn test_get_credits_and_calculate_credits_track_separate_position_types() { + // `get_credits` reports stake/unstake credit state. `calculate_credits` + // reports lock/unlock position credit state. A user can have both, but each + // view must only include its own position type. + let t = setup(2, 1); + + t.client.lock_assets(&t.user, &1_000); + advance_ledgers(&t.env, 10); + assert_eq!(t.client.calculate_credits(&t.user), 10_000); + assert_eq!(t.client.get_credits(&t.user), 0); + + t.client.stake(&t.user, &500); + advance_ledgers(&t.env, 10); + assert_eq!(t.client.calculate_credits(&t.user), 20_000); + assert_eq!(t.client.get_credits(&t.user), 5_000); +} + +#[test] +fn test_stake_unstake_round_trip_restores_balance_and_clears_stake() { + let t = setup(2, 1); + let initial_balance = t.token.balance(&t.user); + + t.client.stake(&t.user, &2_500); + assert_eq!(t.token.balance(&t.user), initial_balance - 2_500); + advance_ledgers(&t.env, 4); + + let credits = t.client.unstake(&t.user); + assert_eq!(credits, 10_000); + assert_eq!(t.token.balance(&t.user), initial_balance); + assert!(t.client.get_stake(&t.user).is_none()); +} + // ── lock_assets tests ───────────────────────────────────────────────────────── #[test]