Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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);
}
}

Expand All @@ -65,28 +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
// Repeated reads should keep TTL alive
for _ in 0..200 {
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);
}
}
Expand All @@ -98,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);
}

Expand All @@ -136,35 +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
// Simulate long usage period
for _ in 0..50 {
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);
}
}
95 changes: 20 additions & 75 deletions soroban-contracts/contracts/single_rwa_vault/src/test_helpers.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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]
Expand All @@ -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));
Expand All @@ -74,28 +43,22 @@ impl MockUsdc {

// ─────────────────────────────────────────────────────────────────────────────
// Mock zkMe verifier
// Maintains a per-user approval flag settable by test code.
// ─────────────────────────────────────────────────────────────────────────────

#[contract]
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};

Expand All @@ -111,8 +74,6 @@ mod _bypass {
}
pub use _bypass::AlwaysApproveZkme;

// ─────────────────────────────────────────────────────────────────────────────
// TestContext — returned by setup() and setup_with_kyc_bypass()
// ─────────────────────────────────────────────────────────────────────────────

pub struct TestContext {
Expand All @@ -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();
Expand All @@ -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,
Expand All @@ -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();
Expand All @@ -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,
Expand All @@ -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(
Expand All @@ -259,18 +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"),
expected_apy: 500u32, // 5 %
timelock_delay: 172800u64, // 48 hours
yield_vesting_period: 0u64, // Default to 0 for instant claiming (backward compatibility)
expected_apy: 500u32,
timelock_delay: 172800u64,
yield_vesting_period: 0u64,
}
}
}
Loading
Loading