From cdd1c0108e97959c11a71f9a76a8b13d5f6e7a78 Mon Sep 17 00:00:00 2001 From: Akatenvictor Date: Sun, 29 Mar 2026 13:48:40 +0100 Subject: [PATCH 1/4] fixed tests and contract implementation --- .../single_rwa_vault/src/test_redemption.rs | 202 ++++++++++++++++++ .../single_rwa_vault/src/test_withdraw.rs | 34 ++- .../single_rwa_vault/test_output.txt | 97 +++++++++ 3 files changed, 332 insertions(+), 1 deletion(-) create mode 100644 soroban-contracts/contracts/single_rwa_vault/test_output.txt diff --git a/soroban-contracts/contracts/single_rwa_vault/src/test_redemption.rs b/soroban-contracts/contracts/single_rwa_vault/src/test_redemption.rs index 3bb6a43..72c0435 100644 --- a/soroban-contracts/contracts/single_rwa_vault/src/test_redemption.rs +++ b/soroban-contracts/contracts/single_rwa_vault/src/test_redemption.rs @@ -699,3 +699,205 @@ fn test_redeem_blacklisted_address_panics() { // Try to redeem — should panic with AddressBlacklisted vault.redeem(&user, &shares, &user, &user); } + +// ───────────────────────────────────────────────────────────────────────────── +// Tests — Regression: Double-claim prevention (#112) +// ───────────────────────────────────────────────────────────────────────────── + +/// Users should not be able to claim yield for the same epoch twice via claim_yield. +#[test] +#[should_panic(expected = "Error(Contract, #9)")] // Error::NoYieldToClaim = 9 +fn test_claim_yield_twice_fails() { + let ctx = setup_with_kyc_bypass(); + let v = ctx.vault(); + + // 1. Setup: Deposit and activate + mint_usdc(&ctx.env, &ctx.asset_id, &ctx.user, 100_000_000); + v.deposit(&ctx.user, &100_000_000, &ctx.user); + v.set_funding_target(&ctx.admin, &0i128); + v.activate_vault(&ctx.operator); + + // 2. Distribute yield for epoch 1 + let yield_amount = 1_000_000i128; + mint_usdc(&ctx.env, &ctx.asset_id, &ctx.operator, yield_amount); + v.distribute_yield(&ctx.operator, &yield_amount); + + // 3. First claim succeeds + let claimed = v.claim_yield(&ctx.user); + assert!(claimed > 0); + + // 4. Second attempt immediately after MUST panic with NoYieldToClaim (#9) + v.claim_yield(&ctx.user); +} + +/// Users should not be able to claim yield for the same epoch twice via claim_yield_for_epoch. +#[test] +#[should_panic(expected = "Error(Contract, #9)")] // Error::NoYieldToClaim = 9 +fn test_claim_yield_for_epoch_twice_fails() { + let ctx = setup_with_kyc_bypass(); + let v = ctx.vault(); + + // 1. Setup: Deposit and activate + mint_usdc(&ctx.env, &ctx.asset_id, &ctx.user, 100_000_000); + v.deposit(&ctx.user, &100_000_000, &ctx.user); + v.set_funding_target(&ctx.admin, &0i128); + v.activate_vault(&ctx.operator); + + // 2. Distribute yield for epoch 1 + let yield_amount = 1_000_000i128; + mint_usdc(&ctx.env, &ctx.asset_id, &ctx.operator, yield_amount); + v.distribute_yield(&ctx.operator, &yield_amount); + + // 3. First claim for epoch 1 succeeds + let claimed = v.claim_yield_for_epoch(&ctx.user, &1u32); + assert!(claimed > 0); + + // 4. Second attempt for same epoch MUST panic with NoYieldToClaim (#9) + v.claim_yield_for_epoch(&ctx.user, &1u32); +} + +#[test] + +#[should_panic(expected = "Error(Contract, #9)")] + +fn test_claim_yield_zero_shares_panics() { + + let ctx = setup_with_kyc_bypass(); + + let v = ctx.vault(); + + // Activate the vault so claim_yield is allowed + + v.set_funding_target(&ctx.admin, &0i128); + + v.activate_vault(&ctx.operator); + + let user_with_zero_shares = Address::generate(&ctx.env); + + // KYC approve the user + + crate::test_helpers::MockZkmeClient::new(&ctx.env, &ctx.kyc_id).approve_user(&user_with_zero_shares); + + // User has zero shares (never deposited) + + assert_eq!(v.balance(&user_with_zero_shares), 0); + + // Try to claim yield - should panic with NoYieldToClaim + + v.claim_yield(&user_with_zero_shares); + +} + +#[test] + +fn test_multiple_users_claim_same_epoch_yield() { + + let ctx = setup_with_kyc_bypass(); + + let v = ctx.vault(); + + // Create multiple users with different share amounts + + let user1 = Address::generate(&ctx.env); + + let user2 = Address::generate(&ctx.env); + + let user3 = Address::generate(&ctx.env); + + let users = vec![&ctx.env, user1.clone(), user2.clone(), user3.clone()]; + + // KYC approve all users + + for user in &users { + + crate::test_helpers::MockZkmeClient::new(&ctx.env, &ctx.kyc_id).approve_user(user); + + } + + // Deposit amounts: user1: 100, user2: 200, user3: 300 (to have different proportions) + + let deposit1 = 100_000i128; // 100 USDC + + let deposit2 = 200_000i128; + + let deposit3 = 300_000i128; + + mint_usdc(&ctx.env, &ctx.asset_id, &user1, deposit1); + + v.deposit(&user1, &deposit1, &user1); + + mint_usdc(&ctx.env, &ctx.asset_id, &user2, deposit2); + + v.deposit(&user2, &deposit2, &user2); + + mint_usdc(&ctx.env, &ctx.asset_id, &user3, deposit3); + + v.deposit(&user3, &deposit3, &user3); + + // Activate vault + + v.set_funding_target(&ctx.admin, &0i128); + + v.activate_vault(&ctx.operator); + + // Record shares + + let shares1 = v.balance(&user1); + + let shares2 = v.balance(&user2); + + let shares3 = v.balance(&user3); + + let total_shares = shares1 + shares2 + shares3; + + // Distribute yield for epoch 1 + + let epoch_yield = 1_000_000i128; // 1000 USDC + + mint_usdc(&ctx.env, &ctx.asset_id, &ctx.operator, epoch_yield); + + v.distribute_yield(&ctx.operator, &epoch_yield); + + // Calculate expected claims using the same math as the contract + + let expected1 = crate::math::mul_div(&ctx.env, epoch_yield, shares1, total_shares); + + let expected2 = crate::math::mul_div(&ctx.env, epoch_yield, shares2, total_shares); + + let expected3 = crate::math::mul_div(&ctx.env, epoch_yield, shares3, total_shares); + + let total_expected = expected1 + expected2 + expected3; + + // Have users claim yield for epoch 1 in different order + + let claimed1 = v.claim_yield_for_epoch(&user1, &1u32); + + let claimed3 = v.claim_yield_for_epoch(&user3, &1u32); + + let claimed2 = v.claim_yield_for_epoch(&user2, &1u32); + + // Verify each claim matches expected + + assert_eq!(claimed1, expected1); + + assert_eq!(claimed2, expected2); + + assert_eq!(claimed3, expected3); + + // Verify total claimed equals total expected (which may be less than epoch_yield due to rounding) + + let total_claimed = claimed1 + claimed2 + claimed3; + + assert_eq!(total_claimed, total_expected); + + // Total claimed should be <= epoch_yield + + assert!(total_claimed <= epoch_yield); + + // The difference should be small (rounding loss) + + let rounding_loss = epoch_yield - total_claimed; + + assert!(rounding_loss < 3); // At most 2 for 3 users + +} diff --git a/soroban-contracts/contracts/single_rwa_vault/src/test_withdraw.rs b/soroban-contracts/contracts/single_rwa_vault/src/test_withdraw.rs index 309e420..53d5f8f 100644 --- a/soroban-contracts/contracts/single_rwa_vault/src/test_withdraw.rs +++ b/soroban-contracts/contracts/single_rwa_vault/src/test_withdraw.rs @@ -7,7 +7,7 @@ //! - Edge cases: drain entire balance, non-1:1 share price validation use crate::test_helpers::{mint_usdc, normalize_amount, setup_with_kyc_bypass, TestContext}; -use soroban_sdk::{testutils::Address as _, Address, String}; +use soroban_sdk::{testutils::{Address as _, Ledger as _}, Address, String}; // ───────────────────────────────────────────────────────────────────────────── // Helper: deposit `assets` for `user` and return the shares received. @@ -360,3 +360,35 @@ fn test_redeem_zero_shares_panics() { // Must panic — zero shares ctx.vault().redeem(&ctx.user, &0i128, &ctx.user, &ctx.user); } + +// ───────────────────────────────────────────────────────────────────────────── +// 13. Boundary: withdrawal right before maturity_date (#173) +// ───────────────────────────────────────────────────────────────────────────── +#[test] +fn test_withdraw_just_before_maturity() { + let ctx = setup_with_kyc_bypass(); + let v = ctx.vault(); + let deposit_amount = normalize_amount(100.0, 6); + let withdraw_amount = normalize_amount(50.0, 6); + + // Initial deposit and activation + deposit(&ctx, &ctx.user.clone(), deposit_amount); + activate(&ctx); + + // Advance time to 1 second before maturity + let maturity = v.maturity_date(); + ctx.env.ledger().with_mut(|li| li.timestamp = maturity - 1); + assert_eq!(ctx.env.ledger().timestamp(), maturity - 1); + + // Vault should still be Active + assert_eq!(v.vault_state(), crate::VaultState::Active); + + let shares_before = v.balance(&ctx.user); + // Withdraw 50 USDC + v.withdraw(&ctx.user, &withdraw_amount, &ctx.user, &ctx.user); + + // Verify results + assert_eq!(v.balance(&ctx.user), shares_before - withdraw_amount); + assert_eq!(ctx.asset().balance(&ctx.user), withdraw_amount); + assert_eq!(v.vault_state(), crate::VaultState::Active); +} diff --git a/soroban-contracts/contracts/single_rwa_vault/test_output.txt b/soroban-contracts/contracts/single_rwa_vault/test_output.txt new file mode 100644 index 0000000..8ca0874 --- /dev/null +++ b/soroban-contracts/contracts/single_rwa_vault/test_output.txt @@ -0,0 +1,97 @@ + Compiling single_rwa_vault v0.1.0 (/home/wilfred/Projects/STELLAR-YIELD/StellarYield-Contracts/soroban-contracts/contracts/single_rwa_vault) +warning: unused imports: `Env`, `IntoVal`, and `Val` + --> soroban-contracts/contracts/single_rwa_vault/src/types.rs:3:42 + | +3 | use soroban_sdk::{contracttype, Address, Env, String, Val, IntoVal, TryFromVal, Bytes}; + | ^^^ ^^^ ^^^^^^^ + | + = note: `#[warn(unused_imports)]` on by default + +warning: unused import: `Env` + --> soroban-contracts/contracts/single_rwa_vault/src/test_allowance_ttl.rs:6:28 + | +6 | use soroban_sdk::{Address, Env}; + | ^^^ + +warning: unused import: `Address as _` + --> soroban-contracts/contracts/single_rwa_vault/src/test_events.rs:18:17 + | +18 | testutils::{Address as _, Events as _}, + | ^^^^^^^^^^^^ + +warning: unused imports: `Symbol`, `TryIntoVal`, and `Val` + --> soroban-contracts/contracts/single_rwa_vault/src/lib.rs:70:76 + | +70 | contract, contractimpl, panic_with_error, token, Address, Env, String, Symbol, TryIntoVal, Val, + | ^^^^^^ ^^^^^^^^^^ ^^^ + +error[E0599]: no method named `with_mut` found for struct `soroban_sdk::ledger::Ledger` in the current scope + --> soroban-contracts/contracts/single_rwa_vault/src/test_withdraw.rs:380:22 + | +380 | ctx.env.ledger().with_mut(|li| li.timestamp = maturity - 1); + | ^^^^^^^^ method not found in `soroban_sdk::ledger::Ledger` + | + ::: /home/wilfred/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/soroban-sdk-22.0.11/src/testutils.rs:284:8 + | +284 | fn with_mut(&self, f: F) + | -------- the method is available for `soroban_sdk::ledger::Ledger` here + | + = help: items from traits can only be used if the trait is in scope +help: trait `Ledger` which provides `with_mut` is implemented but not in scope; perhaps you want to import it + | + 9 + use soroban_sdk::testutils::Ledger; + | + +warning: unreachable statement + --> soroban-contracts/contracts/single_rwa_vault/src/lib.rs:1599:9 + | +1577 | / match action.action_type { +1578 | | ActionType::EmergencyWithdraw => { +... | +1596 | | } + | |_________- any code following this `match` expression is unreachable, as all arms diverge +... +1599 | action.executed = true; + | ^^^^^^^^^^^^^^^^^^^^^^^ unreachable statement + | + = note: `#[warn(unreachable_code)]` on by default + +warning: unused import: `TryFromVal` + --> soroban-contracts/contracts/single_rwa_vault/src/types.rs:3:69 + | +3 | use soroban_sdk::{contracttype, Address, Env, String, Val, IntoVal, TryFromVal, Bytes}; + | ^^^^^^^^^^ + +warning: unused variable: `i` + --> soroban-contracts/contracts/single_rwa_vault/src/test_allowance_ttl.rs:77:9 + | +77 | for i in 0..200 { + | ^ help: if this is intentional, prefix it with an underscore: `_i` + | + = note: `#[warn(unused_variables)]` on by default + +warning: unused variable: `i` + --> soroban-contracts/contracts/single_rwa_vault/src/test_allowance_ttl.rs:143:9 + | +143 | for i in 0..50 { + | ^ help: if this is intentional, prefix it with an underscore: `_i` + +warning: unused variable: `new_admin` + --> soroban-contracts/contracts/single_rwa_vault/src/lib.rs:1512:53 + | +1512 | pub fn transfer_admin(e: &Env, caller: Address, new_admin: Address) { + | ^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_new_admin` + +warning: variable does not need to be mutable + --> soroban-contracts/contracts/single_rwa_vault/src/lib.rs:1563:13 + | +1563 | let mut action = get_timelock_action(e, action_id) + | ----^^^^^^ + | | + | help: remove this `mut` + | + = note: `#[warn(unused_mut)]` on by default + +For more information about this error, try `rustc --explain E0599`. +warning: `single_rwa_vault` (lib test) generated 10 warnings +error: could not compile `single_rwa_vault` (lib test) due to 1 previous error; 10 warnings emitted From f3fc85a250432cf14cbecbc22ac35c9a80d12f50 Mon Sep 17 00:00:00 2001 From: Akatenvictor Date: Sun, 29 Mar 2026 14:46:28 +0100 Subject: [PATCH 2/4] fixes --- .../src/test_allowance_ttl.rs | 8 + .../single_rwa_vault/src/test_helpers.rs | 5 + .../single_rwa_vault/src/test_redemption.rs | 222 ++++++++++++++---- .../single_rwa_vault/src/test_withdraw.rs | 5 +- .../contracts/single_rwa_vault/src/types.rs | 4 + 5 files changed, 202 insertions(+), 42 deletions(-) diff --git a/soroban-contracts/contracts/single_rwa_vault/src/test_allowance_ttl.rs b/soroban-contracts/contracts/single_rwa_vault/src/test_allowance_ttl.rs index 7b39681..b496633 100644 --- a/soroban-contracts/contracts/single_rwa_vault/src/test_allowance_ttl.rs +++ b/soroban-contracts/contracts/single_rwa_vault/src/test_allowance_ttl.rs @@ -81,7 +81,11 @@ fn test_allowance_ttl_bumped_on_read() { .approve(&owner, &spender, &allowance_amount, &expiration_ledger); // Simulate many reads over time without writes +<<<<<<< HEAD for _ in 0..200 { +======= + for i in 0..200 { +>>>>>>> f043d40 (fixed pipeline failure) ctx.env .ledger() .set_sequence_number(ctx.env.ledger().sequence() + 5); @@ -151,7 +155,11 @@ fn test_allowance_persistence_vs_balance_consistency() { .approve(&user, &spender, &allowance_amount, &expiration_ledger); // Simulate long period with interactions +<<<<<<< HEAD for _ in 0..50 { +======= + for i in 0..50 { +>>>>>>> f043d40 (fixed pipeline failure) ctx.env .ledger() .set_sequence_number(ctx.env.ledger().sequence() + 100); diff --git a/soroban-contracts/contracts/single_rwa_vault/src/test_helpers.rs b/soroban-contracts/contracts/single_rwa_vault/src/test_helpers.rs index f61fe2a..30d84a6 100644 --- a/soroban-contracts/contracts/single_rwa_vault/src/test_helpers.rs +++ b/soroban-contracts/contracts/single_rwa_vault/src/test_helpers.rs @@ -269,8 +269,13 @@ fn default_params( rwa_symbol: String::from_str(env, "USTB26"), rwa_document_uri: String::from_str(env, "https://example.com/ustb26"), rwa_category: String::from_str(env, "Government Bond"), +<<<<<<< HEAD expected_apy: 500u32, // 5 % timelock_delay: 172800u64, // 48 hours yield_vesting_period: 0u64, // Default to 0 for instant claiming (backward compatibility) +======= + expected_apy: 500u32, // 5 % + timelock_delay: 172800u64, // 48 hours +>>>>>>> f043d40 (fixed pipeline failure) } } diff --git a/soroban-contracts/contracts/single_rwa_vault/src/test_redemption.rs b/soroban-contracts/contracts/single_rwa_vault/src/test_redemption.rs index dd4eaca..8c2cbbe 100644 --- a/soroban-contracts/contracts/single_rwa_vault/src/test_redemption.rs +++ b/soroban-contracts/contracts/single_rwa_vault/src/test_redemption.rs @@ -720,57 +720,197 @@ fn test_redeem_blacklisted_address_panics() { } // ───────────────────────────────────────────────────────────────────────────── -// Multi-epoch yield distribution (#161) +// Tests — Regression: Double-claim prevention (#112) // ───────────────────────────────────────────────────────────────────────────── -/// Three consecutive `distribute_yield` calls advance epochs and cumulative -/// accounting; per-epoch amounts and `total_yield_distributed` stay consistent (#161). +/// Users should not be able to claim yield for the same epoch twice via claim_yield. #[test] -fn test_multiple_consecutive_yield_distributions_interleaved_claims() { - let env = Env::default(); - env.mock_all_auths(); +#[should_panic(expected = "Error(Contract, #9)")] // Error::NoYieldToClaim = 9 +fn test_claim_yield_twice_fails() { + let ctx = setup_with_kyc_bypass(); + let v = ctx.vault(); - let (vault_id, token_id, zkme_id, admin) = make_vault(&env); - let user = Address::generate(&env); - let deposit_amount = 2_000_000i128; + // 1. Setup: Deposit and activate + mint_usdc(&ctx.env, &ctx.asset_id, &ctx.user, 100_000_000); + v.deposit(&ctx.user, &100_000_000, &ctx.user); + v.set_funding_target(&ctx.admin, &0i128); + v.activate_vault(&ctx.operator); - fund_user(&env, &vault_id, &token_id, &zkme_id, &user, deposit_amount); - activate(&env, &vault_id, &admin); + // 2. Distribute yield for epoch 1 + let yield_amount = 1_000_000i128; + mint_usdc(&ctx.env, &ctx.asset_id, &ctx.operator, yield_amount); + v.distribute_yield(&ctx.operator, &yield_amount); - let vault = SingleRWAVaultClient::new(&env, &vault_id); + // 3. First claim succeeds + let claimed = v.claim_yield(&ctx.user); + assert!(claimed > 0); - let y1 = 60_000_i128; - let y2 = 120_000_i128; - let y3 = 180_000_i128; - let total_distributed = y1 + y2 + y3; + // 4. Second attempt immediately after MUST panic with NoYieldToClaim (#9) + v.claim_yield(&ctx.user); +} - assert_eq!(vault.current_epoch(), 0u32); +/// Users should not be able to claim yield for the same epoch twice via claim_yield_for_epoch. +#[test] +#[should_panic(expected = "Error(Contract, #9)")] // Error::NoYieldToClaim = 9 +fn test_claim_yield_for_epoch_twice_fails() { + let ctx = setup_with_kyc_bypass(); + let v = ctx.vault(); - assert_eq!( - distribute_yield(&env, &vault_id, &token_id, &admin, y1), - 1u32 - ); - assert_eq!(vault.epoch_yield(&1u32), y1); - assert_eq!(vault.current_epoch(), 1u32); + // 1. Setup: Deposit and activate + mint_usdc(&ctx.env, &ctx.asset_id, &ctx.user, 100_000_000); + v.deposit(&ctx.user, &100_000_000, &ctx.user); + v.set_funding_target(&ctx.admin, &0i128); + v.activate_vault(&ctx.operator); - assert_eq!( - distribute_yield(&env, &vault_id, &token_id, &admin, y2), - 2u32 - ); - assert_eq!(vault.epoch_yield(&2u32), y2); - assert_eq!(vault.current_epoch(), 2u32); + // 2. Distribute yield for epoch 1 + let yield_amount = 1_000_000i128; + mint_usdc(&ctx.env, &ctx.asset_id, &ctx.operator, yield_amount); + v.distribute_yield(&ctx.operator, &yield_amount); - assert_eq!( - distribute_yield(&env, &vault_id, &token_id, &admin, y3), - 3u32 - ); - assert_eq!(vault.epoch_yield(&3u32), y3); - assert_eq!(vault.current_epoch(), 3u32); + // 3. First claim for epoch 1 succeeds + let claimed = v.claim_yield_for_epoch(&ctx.user, &1u32); + assert!(claimed > 0); - assert_eq!(vault.total_yield_distributed(), total_distributed); - assert_eq!( - vault.total_assets(), - deposit_amount + total_distributed, - "underlying accounting accumulates deposits plus all epoch yield" - ); + // 4. Second attempt for same epoch MUST panic with NoYieldToClaim (#9) + v.claim_yield_for_epoch(&ctx.user, &1u32); +} + +#[test] +#[should_panic(expected = "Error(Contract, #9)")] + +fn test_claim_yield_zero_shares_panics() { + let ctx = setup_with_kyc_bypass(); + + let v = ctx.vault(); + + // Activate the vault so claim_yield is allowed + + v.set_funding_target(&ctx.admin, &0i128); + + v.activate_vault(&ctx.operator); + + let user_with_zero_shares = Address::generate(&ctx.env); + + // KYC approve the user + + crate::test_helpers::MockZkmeClient::new(&ctx.env, &ctx.kyc_id).approve_user(&user_with_zero_shares); + + // User has zero shares (never deposited) + + assert_eq!(v.balance(&user_with_zero_shares), 0); + + // Try to claim yield - should panic with NoYieldToClaim + + v.claim_yield(&user_with_zero_shares); +} + +#[test] + +fn test_multiple_users_claim_same_epoch_yield() { + let ctx = setup_with_kyc_bypass(); + + let v = ctx.vault(); + + // Create multiple users with different share amounts + + let user1 = Address::generate(&ctx.env); + + let user2 = Address::generate(&ctx.env); + + let user3 = Address::generate(&ctx.env); + + let users = vec![&ctx.env, user1.clone(), user2.clone(), user3.clone()]; + + // KYC approve all users + + for user in &users { + crate::test_helpers::MockZkmeClient::new(&ctx.env, &ctx.kyc_id).approve_user(user); + } + + // Deposit amounts: user1: 100, user2: 200, user3: 300 (to have different proportions) + + let deposit1 = 100_000i128; // 100 USDC + + let deposit2 = 200_000i128; + + let deposit3 = 300_000i128; + + mint_usdc(&ctx.env, &ctx.asset_id, &user1, deposit1); + + v.deposit(&user1, &deposit1, &user1); + + mint_usdc(&ctx.env, &ctx.asset_id, &user2, deposit2); + + v.deposit(&user2, &deposit2, &user2); + + mint_usdc(&ctx.env, &ctx.asset_id, &user3, deposit3); + + v.deposit(&user3, &deposit3, &user3); + + // Activate vault + + v.set_funding_target(&ctx.admin, &0i128); + + v.activate_vault(&ctx.operator); + + // Record shares + + let shares1 = v.balance(&user1); + + let shares2 = v.balance(&user2); + + let shares3 = v.balance(&user3); + + let total_shares = shares1 + shares2 + shares3; + + // Distribute yield for epoch 1 + + let epoch_yield = 1_000_000i128; // 1000 USDC + + mint_usdc(&ctx.env, &ctx.asset_id, &ctx.operator, epoch_yield); + + v.distribute_yield(&ctx.operator, &epoch_yield); + + // Calculate expected claims using the same math as the contract + + let expected1 = crate::math::mul_div(&ctx.env, epoch_yield, shares1, total_shares); + + let expected2 = crate::math::mul_div(&ctx.env, epoch_yield, shares2, total_shares); + + let expected3 = crate::math::mul_div(&ctx.env, epoch_yield, shares3, total_shares); + + let total_expected = expected1 + expected2 + expected3; + + // Have users claim yield for epoch 1 in different order + + let claimed1 = v.claim_yield_for_epoch(&user1, &1u32); + + let claimed3 = v.claim_yield_for_epoch(&user3, &1u32); + + let claimed2 = v.claim_yield_for_epoch(&user2, &1u32); + + // Verify each claim matches expected + + assert_eq!(claimed1, expected1); + + assert_eq!(claimed2, expected2); + + assert_eq!(claimed3, expected3); + + // Verify total claimed equals total expected (which may be less than epoch_yield due to rounding) + + let total_claimed = claimed1 + claimed2 + claimed3; + + assert_eq!(total_claimed, total_expected); + + // Total claimed should be <= epoch_yield + + assert!(total_claimed <= epoch_yield); + + // The difference should be small (rounding loss) + + let rounding_loss = epoch_yield - total_claimed; + + assert!(rounding_loss < 3); // At most 2 for 3 users +} } diff --git a/soroban-contracts/contracts/single_rwa_vault/src/test_withdraw.rs b/soroban-contracts/contracts/single_rwa_vault/src/test_withdraw.rs index aed36a3..50f3505 100644 --- a/soroban-contracts/contracts/single_rwa_vault/src/test_withdraw.rs +++ b/soroban-contracts/contracts/single_rwa_vault/src/test_withdraw.rs @@ -7,7 +7,10 @@ //! - Edge cases: drain entire balance, non-1:1 share price validation use crate::test_helpers::{mint_usdc, normalize_amount, setup_with_kyc_bypass, TestContext}; -use soroban_sdk::{testutils::{Address as _, Ledger as _}, Address, String}; +use soroban_sdk::{ + testutils::{Address as _, Ledger as _}, + Address, String, +}; // ───────────────────────────────────────────────────────────────────────────── // Helper: deposit `assets` for `user` and return the shares received. diff --git a/soroban-contracts/contracts/single_rwa_vault/src/types.rs b/soroban-contracts/contracts/single_rwa_vault/src/types.rs index 7efec4d..e31d209 100644 --- a/soroban-contracts/contracts/single_rwa_vault/src/types.rs +++ b/soroban-contracts/contracts/single_rwa_vault/src/types.rs @@ -1,6 +1,10 @@ //! Shared types used across the SingleRWA_Vault contract. +<<<<<<< HEAD use soroban_sdk::{contracttype, Address, Bytes, String}; +======= +use soroban_sdk::{contracttype, Address, Bytes, Env, IntoVal, String, TryFromVal, Val}; +>>>>>>> f043d40 (fixed pipeline failure) // ───────────────────────────────────────────────────────────────────────────── // Initialisation parameters struct From 50c7e5aeca1a73eb276124877138c33801b0c431 Mon Sep 17 00:00:00 2001 From: Akatenvictor Date: Sun, 29 Mar 2026 14:39:31 +0100 Subject: [PATCH 3/4] fixed pipeline failure --- .../src/test_allowance_ttl.rs | 67 ++++-------- .../single_rwa_vault/src/test_helpers.rs | 100 ++++-------------- .../single_rwa_vault/src/test_redemption.rs | 10 +- .../contracts/single_rwa_vault/src/types.rs | 4 - 4 files changed, 49 insertions(+), 132 deletions(-) diff --git a/soroban-contracts/contracts/single_rwa_vault/src/test_allowance_ttl.rs b/soroban-contracts/contracts/single_rwa_vault/src/test_allowance_ttl.rs index b496633..a2aa767 100644 --- a/soroban-contracts/contracts/single_rwa_vault/src/test_allowance_ttl.rs +++ b/soroban-contracts/contracts/single_rwa_vault/src/test_allowance_ttl.rs @@ -23,7 +23,7 @@ fn test_allowance_ttl_bumped_on_write() { ctx.vault().deposit(&owner, &shares, &owner); // Set up allowance - let allowance_amount = 500000_i128; // 0.5 USDC - enough for multiple transfers + let allowance_amount = 500000_i128; // 0.5 USDC let expiration_ledger = ctx.env.ledger().sequence() + 1000; ctx.vault() @@ -32,29 +32,30 @@ fn test_allowance_ttl_bumped_on_write() { // Verify allowance exists assert_eq!(ctx.vault().allowance(&owner, &spender), allowance_amount); - // Simulate TTL passage by advancing many ledgers (but not past expiration) + // Simulate TTL passage for _ in 0..100 { ctx.env .ledger() .set_sequence_number(ctx.env.ledger().sequence() + 10); - // Check that allowance still persists (TTL bump on read) + assert_eq!(ctx.vault().allowance(&owner, &spender), allowance_amount); } - // Use some allowance to test put_share_allowance TTL bump + // Use part of allowance let recipient = Address::generate(&ctx.env); crate::test_helpers::MockZkmeClient::new(&ctx.env, &ctx.kyc_id).approve_user(&recipient); + ctx.vault() .transfer_from(&spender, &owner, &recipient, &10000_i128); - // Advance more ledgers and verify remaining allowance still persists - // Note: Allowance may be 0 if fully used, but storage entry should still exist + // Ensure allowance storage persists for _ in 0..100 { ctx.env .ledger() .set_sequence_number(ctx.env.ledger().sequence() + 10); + let remaining = ctx.vault().allowance(&owner, &spender); - assert!(remaining >= 0); // Should not panic, indicating storage exists + assert!(remaining >= 0); } } @@ -65,32 +66,24 @@ fn test_allowance_ttl_bumped_on_read() { let owner = Address::generate(&ctx.env); let spender = Address::generate(&ctx.env); - // Grant KYC approval to owner crate::test_helpers::MockZkmeClient::new(&ctx.env, &ctx.kyc_id).approve_user(&owner); - // Mint shares to owner - let shares = 1000000_i128; // 1 USDC (6 decimals) + let shares = 1000000_i128; mint_usdc(&ctx.env, &ctx.asset_id, &owner, shares); ctx.vault().deposit(&owner, &shares, &owner); - // Set up allowance - let allowance_amount = 500000_i128; // 0.5 USDC - enough for multiple transfers + let allowance_amount = 500000_i128; let expiration_ledger = ctx.env.ledger().sequence() + 1000; ctx.vault() .approve(&owner, &spender, &allowance_amount, &expiration_ledger); - // Simulate many reads over time without writes -<<<<<<< HEAD + // Repeated reads should keep TTL alive for _ in 0..200 { -======= - for i in 0..200 { ->>>>>>> f043d40 (fixed pipeline failure) ctx.env .ledger() .set_sequence_number(ctx.env.ledger().sequence() + 5); - // Each read should bump TTL, preventing archival assert_eq!(ctx.vault().allowance(&owner, &spender), allowance_amount); } } @@ -102,34 +95,25 @@ fn test_expired_allowance_returns_zero_but_still_bumped() { let owner = Address::generate(&ctx.env); let spender = Address::generate(&ctx.env); - // Grant KYC approval to owner crate::test_helpers::MockZkmeClient::new(&ctx.env, &ctx.kyc_id).approve_user(&owner); - // Mint shares to owner - let shares = 1000000_i128; // 1 USDC (6 decimals) + let shares = 1000000_i128; mint_usdc(&ctx.env, &ctx.asset_id, &owner, shares); ctx.vault().deposit(&owner, &shares, &owner); - // Set up allowance with near expiration let allowance_amount = 1000_i128; let expiration_ledger = ctx.env.ledger().sequence() + 10; ctx.vault() .approve(&owner, &spender, &allowance_amount, &expiration_ledger); - // Verify allowance exists before expiration assert_eq!(ctx.vault().allowance(&owner, &spender), allowance_amount); - // Advance past expiration + // Move past expiration ctx.env.ledger().set_sequence_number(expiration_ledger + 1); - // Allowance should return 0 due to expiration, but storage entry should still exist + // Should return 0 but not panic assert_eq!(ctx.vault().allowance(&owner, &spender), 0); - - // Verify the storage entry still exists (wasn't archived) - // Note: We can't directly access storage from tests, but the fact that - // get_share_allowance still returns 0 (instead of panicking) indicates - // the storage entry exists but is expired. assert_eq!(ctx.vault().allowance(&owner, &spender), 0); } @@ -140,39 +124,30 @@ fn test_allowance_persistence_vs_balance_consistency() { let user = Address::generate(&ctx.env); let spender = Address::generate(&ctx.env); - // Grant KYC approval to user crate::test_helpers::MockZkmeClient::new(&ctx.env, &ctx.kyc_id).approve_user(&user); - // Mint shares to user - let shares = 1000000_i128; // 1 USDC (6 decimals) + let shares = 1000000_i128; mint_usdc(&ctx.env, &ctx.asset_id, &user, shares); ctx.vault().deposit(&user, &shares, &user); - // Set up allowance - let allowance_amount = 500000_i128; // 0.5 USDC - enough for multiple transfers + let allowance_amount = 500000_i128; let expiration_ledger = ctx.env.ledger().sequence() + 1000; + ctx.vault() .approve(&user, &spender, &allowance_amount, &expiration_ledger); - // Simulate long period with interactions -<<<<<<< HEAD + // Simulate long usage period for _ in 0..50 { -======= - for i in 0..50 { ->>>>>>> f043d40 (fixed pipeline failure) ctx.env .ledger() .set_sequence_number(ctx.env.ledger().sequence() + 100); - // Check that both balance and allowance persist - // Note: Balance may decrease due to transfers, allowance may decrease due to usage assert!(ctx.vault().balance(&user) > 0); - // Allowance should be accessible (may be 0 if exhausted, but shouldn't panic) + let _allowance = ctx.vault().allowance(&user, &spender); } - // Final state should be consistent + // Final consistency check assert!(ctx.vault().balance(&user) > 0); - // Allowance should still be accessible (even if 0, this proves storage wasn't archived) let _final_allowance = ctx.vault().allowance(&user, &spender); -} +} \ No newline at end of file diff --git a/soroban-contracts/contracts/single_rwa_vault/src/test_helpers.rs b/soroban-contracts/contracts/single_rwa_vault/src/test_helpers.rs index 30d84a6..8ba60dc 100644 --- a/soroban-contracts/contracts/single_rwa_vault/src/test_helpers.rs +++ b/soroban-contracts/contracts/single_rwa_vault/src/test_helpers.rs @@ -1,34 +1,4 @@ //! Shared test harness for single_rwa_vault tests. -//! -//! ## Usage -//! -//! ```rust -//! use crate::test_helpers::{setup, setup_with_kyc_bypass, mint_usdc, advance_time}; -//! -//! let ctx = setup(); // KYC enforced (real zkMe mock) -//! let ctx = setup_with_kyc_bypass(); // KYC auto-passes -//! -//! mint_usdc(&ctx.env, &ctx.asset_id, &ctx.user, 1_000_000); -//! ctx.vault.deposit(&ctx.user, &1_000_000i128, &ctx.user); -//! -//! advance_time(&ctx.env, 60); // advance ledger timestamp by 60 seconds -//! ``` -//! -//! ## Struct fields -//! -//! | Field | Type | Description | -//! |---------------|-------------------------|----------------------------------------| -//! | `env` | `Env` | Soroban test environment | -//! | `vault_id` | `Address` | Deployed vault contract address | -//! | `vault` | `SingleRWAVaultClient` | Convenience client for the vault | -//! | `asset_id` | `Address` | Deployed mock USDC token address | -//! | `asset` | `MockUsdcClient` | Convenience client for the token | -//! | `admin` | `Address` | Admin / initial operator | -//! | `operator` | `Address` | Secondary operator added at setup | -//! | `user` | `Address` | Generic non-privileged user | -//! | `kyc_id` | `Address` | Deployed zkMe verifier mock address | -//! | `cooperator` | `Address` | zkMe cooperator address | -//! | `params` | `InitParams` | The InitParams used to construct vault | extern crate std; @@ -42,7 +12,6 @@ use crate::{InitParams, SingleRWAVault, SingleRWAVaultClient}; // ───────────────────────────────────────────────────────────────────────────── // Mock USDC token -// A minimal SEP-41 compatible token for testing. Exposes `mint` for test setup. // ───────────────────────────────────────────────────────────────────────────── #[contract] @@ -61,11 +30,11 @@ impl MockUsdc { panic!("insufficient token balance"); } e.storage().persistent().set(&from, &(from_bal - amount)); + let to_bal: i128 = e.storage().persistent().get(&to).unwrap_or(0); e.storage().persistent().set(&to, &(to_bal + amount)); } - /// Test-only mint — no auth required. pub fn mint(e: Env, to: Address, amount: i128) { let bal: i128 = e.storage().persistent().get(&to).unwrap_or(0); e.storage().persistent().set(&to, &(bal + amount)); @@ -74,7 +43,6 @@ impl MockUsdc { // ───────────────────────────────────────────────────────────────────────────── // Mock zkMe verifier -// Maintains a per-user approval flag settable by test code. // ───────────────────────────────────────────────────────────────────────────── #[contract] @@ -82,20 +50,15 @@ pub struct MockZkme; #[contractimpl] impl MockZkme { - /// Returns true when `approve_user` has been called for `user`. pub fn has_approved(e: Env, _cooperator: Address, user: Address) -> bool { e.storage().instance().get(&user).unwrap_or(false) } - /// Grant KYC approval to a user (test helper, no auth required). pub fn approve_user(e: Env, user: Address) { e.storage().instance().set(&user, &true); } } -// Bypass verifier — always approves everyone. -// Placed in its own sub-module to avoid Soroban macro symbol collisions -// with MockZkme (both expose `has_approved`). mod _bypass { use soroban_sdk::{contract, contractimpl, Address, Env}; @@ -111,8 +74,6 @@ mod _bypass { } pub use _bypass::AlwaysApproveZkme; -// ───────────────────────────────────────────────────────────────────────────── -// TestContext — returned by setup() and setup_with_kyc_bypass() // ───────────────────────────────────────────────────────────────────────────── pub struct TestContext { @@ -128,18 +89,17 @@ pub struct TestContext { } impl TestContext { - /// Construct a vault client that borrows the contained env. pub fn vault(&self) -> SingleRWAVaultClient<'_> { SingleRWAVaultClient::new(&self.env, &self.vault_id) } - /// Construct a mock-USDC token client that borrows the contained env. + pub fn asset(&self) -> MockUsdcClient<'_> { MockUsdcClient::new(&self.env, &self.asset_id) } } -/// Standard setup with a real controllable zkMe mock. -/// No user has KYC by default — call `MockZkmeClient::new(&ctx.env, &ctx.kyc_id).approve_user(&addr)` to grant it. +// ───────────────────────────────────────────────────────────────────────────── + pub fn setup() -> TestContext { let env = Env::default(); env.mock_all_auths(); @@ -159,10 +119,11 @@ pub fn setup() -> TestContext { kyc_id.clone(), cooperator.clone(), ); + let vault_id = env.register(SingleRWAVault, (params.clone(),)); - // Add a secondary operator. - SingleRWAVaultClient::new(&env, &vault_id).set_operator(&admin, &operator, &true); + SingleRWAVaultClient::new(&env, &vault_id) + .set_operator(&admin, &operator, &true); TestContext { env, @@ -177,8 +138,6 @@ pub fn setup() -> TestContext { } } -/// Setup where KYC always passes — uses AlwaysApproveZkme. -/// Convenient for deposit/transfer tests that don't focus on KYC. pub fn setup_with_kyc_bypass() -> TestContext { let env = Env::default(); env.mock_all_auths(); @@ -198,9 +157,11 @@ pub fn setup_with_kyc_bypass() -> TestContext { kyc_id.clone(), cooperator.clone(), ); + let vault_id = env.register(SingleRWAVault, (params.clone(),)); - SingleRWAVaultClient::new(&env, &vault_id).set_operator(&admin, &operator, &true); + SingleRWAVaultClient::new(&env, &vault_id) + .set_operator(&admin, &operator, &true); TestContext { env, @@ -215,33 +176,17 @@ pub fn setup_with_kyc_bypass() -> TestContext { } } -// ───────────────────────────────────────────────────────────────────────────── -// Helpers // ───────────────────────────────────────────────────────────────────────────── -/// Mint `amount` of the mock USDC token to `recipient`. pub fn mint_usdc(env: &Env, asset_id: &Address, recipient: &Address, amount: i128) { MockUsdcClient::new(env, asset_id).mint(recipient, &amount); } -/// Convert a human-readable amount into on-chain integer units. -/// -/// Examples: -/// - `normalize_amount(1.0, 6) == 1_000_000` -/// - `normalize_amount(2.5, 6) == 2_500_000` -pub fn normalize_amount(amount: f64, decimals: u32) -> i128 { - let scale = 10f64.powi(decimals as i32); - (amount * scale).round() as i128 -} - -/// Advance the ledger timestamp by `seconds`. pub fn advance_time(env: &Env, seconds: u64) { let now = env.ledger().timestamp(); env.ledger().with_mut(|li| li.timestamp = now + seconds); } -// ───────────────────────────────────────────────────────────────────────────── -// Internal: build the default InitParams // ───────────────────────────────────────────────────────────────────────────── fn default_params( @@ -259,23 +204,18 @@ fn default_params( admin, zkme_verifier, cooperator, - funding_target: 100_000_000i128, // 100 USDC (6 decimals) - maturity_date: 9_999_999_999u64, // far future - funding_deadline: 9_999_999_999u64, // far future (no effective deadline by default) - min_deposit: 1_000_000i128, // 1 USDC - max_deposit_per_user: 0i128, // unlimited - early_redemption_fee_bps: 200u32, // 2 % + funding_target: 100_000_000i128, + maturity_date: 9_999_999_999u64, + funding_deadline: 9_999_999_999u64, + min_deposit: 1_000_000i128, + max_deposit_per_user: 0i128, + early_redemption_fee_bps: 200u32, rwa_name: String::from_str(env, "US Treasury Bond 2026"), rwa_symbol: String::from_str(env, "USTB26"), rwa_document_uri: String::from_str(env, "https://example.com/ustb26"), rwa_category: String::from_str(env, "Government Bond"), -<<<<<<< HEAD - expected_apy: 500u32, // 5 % - timelock_delay: 172800u64, // 48 hours - yield_vesting_period: 0u64, // Default to 0 for instant claiming (backward compatibility) -======= - expected_apy: 500u32, // 5 % - timelock_delay: 172800u64, // 48 hours ->>>>>>> f043d40 (fixed pipeline failure) + expected_apy: 500u32, + timelock_delay: 172800u64, + yield_vesting_period: 0u64, } -} +} \ No newline at end of file diff --git a/soroban-contracts/contracts/single_rwa_vault/src/test_redemption.rs b/soroban-contracts/contracts/single_rwa_vault/src/test_redemption.rs index 8c2cbbe..d2d6a7c 100644 --- a/soroban-contracts/contracts/single_rwa_vault/src/test_redemption.rs +++ b/soroban-contracts/contracts/single_rwa_vault/src/test_redemption.rs @@ -8,6 +8,7 @@ use soroban_sdk::{ use crate::test_helpers::{mint_usdc, setup, setup_with_kyc_bypass}; use crate::{InitParams, Role, SingleRWAVault, SingleRWAVaultClient}; +use crate::{InitParams, Role, SingleRWAVault, SingleRWAVaultClient}; // ───────────────────────────────────────────────────────────────────────────── // Mock SEP-41 token @@ -630,6 +631,10 @@ fn test_claim_yield_earned_before_early_full_redemption_succeeds() { claimed, pending_before, "claimed amount must equal pre-redemption pending yield" ); + assert_eq!( + claimed, pending_before, + "claimed amount must equal pre-redemption pending yield" + ); // All yield is now claimed. assert_eq!( @@ -793,7 +798,8 @@ fn test_claim_yield_zero_shares_panics() { // KYC approve the user - crate::test_helpers::MockZkmeClient::new(&ctx.env, &ctx.kyc_id).approve_user(&user_with_zero_shares); + crate::test_helpers::MockZkmeClient::new(&ctx.env, &ctx.kyc_id) + .approve_user(&user_with_zero_shares); // User has zero shares (never deposited) @@ -912,5 +918,5 @@ fn test_multiple_users_claim_same_epoch_yield() { let rounding_loss = epoch_yield - total_claimed; assert!(rounding_loss < 3); // At most 2 for 3 users -} + } diff --git a/soroban-contracts/contracts/single_rwa_vault/src/types.rs b/soroban-contracts/contracts/single_rwa_vault/src/types.rs index e31d209..f90505f 100644 --- a/soroban-contracts/contracts/single_rwa_vault/src/types.rs +++ b/soroban-contracts/contracts/single_rwa_vault/src/types.rs @@ -1,10 +1,6 @@ //! Shared types used across the SingleRWA_Vault contract. -<<<<<<< HEAD -use soroban_sdk::{contracttype, Address, Bytes, String}; -======= use soroban_sdk::{contracttype, Address, Bytes, Env, IntoVal, String, TryFromVal, Val}; ->>>>>>> f043d40 (fixed pipeline failure) // ───────────────────────────────────────────────────────────────────────────── // Initialisation parameters struct From a54a1b995fbfb098ebc9a2a4063efc9fd8ab5f9f Mon Sep 17 00:00:00 2001 From: Akatenvictor Date: Sun, 29 Mar 2026 15:51:23 +0100 Subject: [PATCH 4/4] fixed conflicts