diff --git a/apps/smart-contracts/contracts/vault-contract/src/events.rs b/apps/smart-contracts/contracts/vault-contract/src/events.rs new file mode 100644 index 0000000..ff8b703 --- /dev/null +++ b/apps/smart-contracts/contracts/vault-contract/src/events.rs @@ -0,0 +1,44 @@ +use soroban_sdk::{contracttype, Address, Env}; + +/// Event emitted when a beneficiary successfully claims their ROI. +/// This enables indexers and explorers to track claim activity. +#[contracttype] +#[derive(Clone, Debug)] +pub struct ClaimEvent { + /// The address that claimed the ROI + pub beneficiary: Address, + /// Amount of participation tokens redeemed + pub tokens_redeemed: i128, + /// Amount of USDC received (including ROI) + pub usdc_received: i128, + /// The ROI percentage at the time of claim + pub roi_percentage: i128, +} + +/// Event emitted when the vault availability is changed by admin. +#[contracttype] +#[derive(Clone, Debug)] +pub struct AvailabilityChangedEvent { + /// The admin who made the change + pub admin: Address, + /// The new enabled status + pub enabled: bool, +} + +/// Helper functions for publishing events +pub mod events { + use super::*; + use soroban_sdk::symbol_short; + + /// Publishes a ClaimEvent to the blockchain event log. + pub fn emit_claim(env: &Env, event: ClaimEvent) { + env.events() + .publish((symbol_short!("claim"),), event); + } + + /// Publishes an AvailabilityChangedEvent to the blockchain event log. + pub fn emit_availability_changed(env: &Env, event: AvailabilityChangedEvent) { + env.events() + .publish((symbol_short!("avail"),), event); + } +} diff --git a/apps/smart-contracts/contracts/vault-contract/src/lib.rs b/apps/smart-contracts/contracts/vault-contract/src/lib.rs index 90f153d..4277f37 100644 --- a/apps/smart-contracts/contracts/vault-contract/src/lib.rs +++ b/apps/smart-contracts/contracts/vault-contract/src/lib.rs @@ -1,10 +1,14 @@ #![no_std] mod error; +mod events; +mod storage_types; mod vault; pub use crate::error::ContractError; -pub use crate::vault::VaultContract; +pub use crate::events::{AvailabilityChangedEvent, ClaimEvent}; +pub use crate::storage_types::DataKey; +pub use crate::vault::{ClaimPreview, VaultContract, VaultOverview}; #[cfg(test)] -mod test; \ No newline at end of file +mod test; diff --git a/apps/smart-contracts/contracts/vault-contract/src/storage_types.rs b/apps/smart-contracts/contracts/vault-contract/src/storage_types.rs new file mode 100644 index 0000000..a352678 --- /dev/null +++ b/apps/smart-contracts/contracts/vault-contract/src/storage_types.rs @@ -0,0 +1,20 @@ +use soroban_sdk::contracttype; + +/// Typed storage keys for the vault contract. +/// Using an enum instead of raw strings improves type safety and readability. +#[derive(Clone)] +#[contracttype] +pub enum DataKey { + /// The admin address that can enable/disable the vault + Admin, + /// Whether claiming is currently enabled + Enabled, + /// The ROI percentage (e.g., 5 means 5% return) + RoiPercentage, + /// The participation token contract address + TokenAddress, + /// The USDC stablecoin contract address + UsdcAddress, + /// Total tokens that have been redeemed through the vault + TotalTokensRedeemed, +} diff --git a/apps/smart-contracts/contracts/vault-contract/src/test.rs b/apps/smart-contracts/contracts/vault-contract/src/test.rs index aad2c59..95d8977 100644 --- a/apps/smart-contracts/contracts/vault-contract/src/test.rs +++ b/apps/smart-contracts/contracts/vault-contract/src/test.rs @@ -3,7 +3,7 @@ extern crate std; use crate::error::ContractError; use crate::vault::{VaultContract, VaultContractClient}; -use soroban_sdk::{testutils::Address as _, token, Address, Env, String}; +use soroban_sdk::{testutils::Address as _, testutils::Events as _, token, Address, Env, String}; use soroban_token_contract::{Token as FactoryToken, TokenClient as FactoryTokenClient}; use token::Client as TokenClient; use token::StellarAssetClient as TokenAdminClient; @@ -33,17 +33,25 @@ fn create_vault<'a>( e: &Env, admin: &Address, enabled: bool, - price: i128, + roi_percentage: i128, token: &Address, usdc: &Address, ) -> VaultContractClient<'a> { let contract_id = e.register( VaultContract, - (admin.clone(), enabled, price, token.clone(), usdc.clone()), + ( + admin.clone(), + enabled, + roi_percentage, + token.clone(), + usdc.clone(), + ), ); VaultContractClient::new(e, &contract_id) } +// ============ Original Tests (Updated) ============ + #[test] fn test_vault_deployment_and_availability() { let env = Env::default(); @@ -80,7 +88,7 @@ fn test_claim_success() { token.mint(&beneficiary, &100); - // price = 2 means 2% premium -> rate = 1.02 + // roi_percentage = 2 means 2% premium -> rate = 1.02 // 100 tokens * 1.02 = 102 USDC usdc_admin.mint(&vault.address, &300); @@ -134,7 +142,7 @@ fn test_claim_insufficient_vault_balance() { token.mint(&beneficiary, &100); - // price = 5 means 5% premium -> rate = 1.05 + // roi_percentage = 5 means 5% premium -> rate = 1.05 // 100 tokens * 1.05 = 105 USDC, but vault only has 100 usdc_admin.mint(&vault.address, &100); @@ -174,7 +182,7 @@ fn test_claim_with_6_percent_premium() { let token = create_token_factory(&env, &token_admin); - // price = 6 means 6% premium -> rate = 1.06 + // roi_percentage = 6 means 6% premium -> rate = 1.06 let vault = create_vault(&env, &admin, true, 6, &token.address, &usdc_client.address); token.mint(&beneficiary, &100); @@ -188,4 +196,417 @@ fn test_claim_with_6_percent_premium() { assert_eq!(token.balance(&vault.address), 100); assert_eq!(usdc_client.balance(&beneficiary), 106); assert_eq!(usdc_client.balance(&vault.address), 94); -} \ No newline at end of file +} + +// ============ New Getter Function Tests ============ + +#[test] +fn test_get_admin() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let token_admin = Address::generate(&env); + + let (usdc_client, _usdc_admin) = create_usdc_token(&env, &admin); + let token = create_token_factory(&env, &token_admin); + + let vault = create_vault(&env, &admin, true, 10, &token.address, &usdc_client.address); + + assert_eq!(vault.get_admin(), admin); +} + +#[test] +fn test_is_enabled() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let token_admin = Address::generate(&env); + + let (usdc_client, _usdc_admin) = create_usdc_token(&env, &admin); + let token = create_token_factory(&env, &token_admin); + + // Test with enabled = false + let vault_disabled = create_vault(&env, &admin, false, 10, &token.address, &usdc_client.address); + assert_eq!(vault_disabled.is_enabled(), false); + + // Test with enabled = true + let vault_enabled = create_vault(&env, &admin, true, 10, &token.address, &usdc_client.address); + assert_eq!(vault_enabled.is_enabled(), true); + + // Test toggling + vault_disabled.availability_for_exchange(&admin, &true); + assert_eq!(vault_disabled.is_enabled(), true); +} + +#[test] +fn test_get_roi_percentage() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let token_admin = Address::generate(&env); + + let (usdc_client, _usdc_admin) = create_usdc_token(&env, &admin); + let token = create_token_factory(&env, &token_admin); + + let vault = create_vault(&env, &admin, true, 15, &token.address, &usdc_client.address); + + assert_eq!(vault.get_roi_percentage(), 15); +} + +#[test] +fn test_get_token_address() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let token_admin = Address::generate(&env); + + let (usdc_client, _usdc_admin) = create_usdc_token(&env, &admin); + let token = create_token_factory(&env, &token_admin); + + let vault = create_vault(&env, &admin, true, 10, &token.address, &usdc_client.address); + + assert_eq!(vault.get_token_address(), token.address); +} + +#[test] +fn test_get_usdc_address() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let token_admin = Address::generate(&env); + + let (usdc_client, _usdc_admin) = create_usdc_token(&env, &admin); + let token = create_token_factory(&env, &token_admin); + + let vault = create_vault(&env, &admin, true, 10, &token.address, &usdc_client.address); + + assert_eq!(vault.get_usdc_address(), usdc_client.address); +} + +#[test] +fn test_get_vault_usdc_balance() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let token_admin = Address::generate(&env); + + let (usdc_client, usdc_admin) = create_usdc_token(&env, &admin); + let token = create_token_factory(&env, &token_admin); + + let vault = create_vault(&env, &admin, true, 10, &token.address, &usdc_client.address); + + // Initially zero + assert_eq!(vault.get_vault_usdc_balance(), 0); + + // After minting + usdc_admin.mint(&vault.address, &500); + assert_eq!(vault.get_vault_usdc_balance(), 500); +} + +#[test] +fn test_get_total_tokens_redeemed() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let token_admin = Address::generate(&env); + let beneficiary1 = Address::generate(&env); + let beneficiary2 = Address::generate(&env); + + let (usdc_client, usdc_admin) = create_usdc_token(&env, &admin); + let token = create_token_factory(&env, &token_admin); + + let vault = create_vault(&env, &admin, true, 5, &token.address, &usdc_client.address); + + // Initially zero + assert_eq!(vault.get_total_tokens_redeemed(), 0); + + // Mint tokens and USDC + token.mint(&beneficiary1, &100); + token.mint(&beneficiary2, &200); + usdc_admin.mint(&vault.address, &1000); + + // First claim + vault.claim(&beneficiary1); + assert_eq!(vault.get_total_tokens_redeemed(), 100); + + // Second claim + vault.claim(&beneficiary2); + assert_eq!(vault.get_total_tokens_redeemed(), 300); +} + +// ============ Preview Function Tests ============ + +#[test] +fn test_preview_claim_basic() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let token_admin = Address::generate(&env); + let beneficiary = Address::generate(&env); + + let (usdc_client, usdc_admin) = create_usdc_token(&env, &admin); + let token = create_token_factory(&env, &token_admin); + + // 10% ROI + let vault = create_vault(&env, &admin, true, 10, &token.address, &usdc_client.address); + + token.mint(&beneficiary, &100); + usdc_admin.mint(&vault.address, &500); + + let preview = vault.preview_claim(&beneficiary); + + assert_eq!(preview.token_balance, 100); + assert_eq!(preview.usdc_amount, 110); // 100 * 1.10 = 110 + assert_eq!(preview.roi_amount, 10); // 110 - 100 = 10 + assert_eq!(preview.vault_has_sufficient_balance, true); + assert_eq!(preview.claim_enabled, true); +} + +#[test] +fn test_preview_claim_insufficient_balance() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let token_admin = Address::generate(&env); + let beneficiary = Address::generate(&env); + + let (usdc_client, usdc_admin) = create_usdc_token(&env, &admin); + let token = create_token_factory(&env, &token_admin); + + // 50% ROI + let vault = create_vault(&env, &admin, true, 50, &token.address, &usdc_client.address); + + token.mint(&beneficiary, &100); + usdc_admin.mint(&vault.address, &100); // Only 100 USDC, but needs 150 + + let preview = vault.preview_claim(&beneficiary); + + assert_eq!(preview.token_balance, 100); + assert_eq!(preview.usdc_amount, 150); // 100 * 1.50 = 150 + assert_eq!(preview.roi_amount, 50); + assert_eq!(preview.vault_has_sufficient_balance, false); // Not enough! + assert_eq!(preview.claim_enabled, true); +} + +#[test] +fn test_preview_claim_disabled_vault() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let token_admin = Address::generate(&env); + let beneficiary = Address::generate(&env); + + let (usdc_client, usdc_admin) = create_usdc_token(&env, &admin); + let token = create_token_factory(&env, &token_admin); + + // Vault is disabled + let vault = create_vault(&env, &admin, false, 10, &token.address, &usdc_client.address); + + token.mint(&beneficiary, &100); + usdc_admin.mint(&vault.address, &500); + + let preview = vault.preview_claim(&beneficiary); + + assert_eq!(preview.token_balance, 100); + assert_eq!(preview.usdc_amount, 110); + assert_eq!(preview.claim_enabled, false); // Disabled! +} + +#[test] +fn test_preview_claim_zero_tokens() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let token_admin = Address::generate(&env); + let beneficiary = Address::generate(&env); + + let (usdc_client, _usdc_admin) = create_usdc_token(&env, &admin); + let token = create_token_factory(&env, &token_admin); + + let vault = create_vault(&env, &admin, true, 10, &token.address, &usdc_client.address); + + // Beneficiary has no tokens + let preview = vault.preview_claim(&beneficiary); + + assert_eq!(preview.token_balance, 0); + assert_eq!(preview.usdc_amount, 0); + assert_eq!(preview.roi_amount, 0); +} + +// ============ Vault Overview Tests ============ + +#[test] +fn test_get_vault_overview() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let token_admin = Address::generate(&env); + let beneficiary = Address::generate(&env); + + let (usdc_client, usdc_admin) = create_usdc_token(&env, &admin); + let token = create_token_factory(&env, &token_admin); + + let vault = create_vault(&env, &admin, true, 25, &token.address, &usdc_client.address); + + token.mint(&beneficiary, &100); + usdc_admin.mint(&vault.address, &1000); + + let overview = vault.get_vault_overview(); + + assert_eq!(overview.admin, admin); + assert_eq!(overview.enabled, true); + assert_eq!(overview.roi_percentage, 25); + assert_eq!(overview.token_address, token.address); + assert_eq!(overview.usdc_address, usdc_client.address); + assert_eq!(overview.vault_usdc_balance, 1000); + assert_eq!(overview.total_tokens_redeemed, 0); +} + +#[test] +fn test_vault_overview_after_claim() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let token_admin = Address::generate(&env); + let beneficiary = Address::generate(&env); + + let (usdc_client, usdc_admin) = create_usdc_token(&env, &admin); + let token = create_token_factory(&env, &token_admin); + + let vault = create_vault(&env, &admin, true, 10, &token.address, &usdc_client.address); + + token.mint(&beneficiary, &100); + usdc_admin.mint(&vault.address, &500); + + // Claim: 100 tokens -> 110 USDC + vault.claim(&beneficiary); + + let overview = vault.get_vault_overview(); + + assert_eq!(overview.vault_usdc_balance, 390); // 500 - 110 = 390 + assert_eq!(overview.total_tokens_redeemed, 100); +} + +// ============ Event Emission Tests ============ + +#[test] +fn test_claim_emits_event() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let token_admin = Address::generate(&env); + let beneficiary = Address::generate(&env); + + let (usdc_client, usdc_admin) = create_usdc_token(&env, &admin); + let token = create_token_factory(&env, &token_admin); + + let vault = create_vault(&env, &admin, true, 5, &token.address, &usdc_client.address); + + token.mint(&beneficiary, &100); + usdc_admin.mint(&vault.address, &200); + + vault.claim(&beneficiary); + + // Verify event was emitted + let events = env.events().all(); + assert!(!events.is_empty(), "Expected claim event to be emitted"); +} + +#[test] +fn test_availability_change_emits_event() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let token_admin = Address::generate(&env); + + let (usdc_client, _usdc_admin) = create_usdc_token(&env, &admin); + let token = create_token_factory(&env, &token_admin); + + let vault = create_vault(&env, &admin, false, 10, &token.address, &usdc_client.address); + + vault.availability_for_exchange(&admin, &true); + + // Verify event was emitted + let events = env.events().all(); + assert!( + !events.is_empty(), + "Expected availability changed event to be emitted" + ); +} + +// ============ Edge Case Tests ============ + +#[test] +fn test_multiple_claims_different_beneficiaries() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let token_admin = Address::generate(&env); + let beneficiary1 = Address::generate(&env); + let beneficiary2 = Address::generate(&env); + let beneficiary3 = Address::generate(&env); + + let (usdc_client, usdc_admin) = create_usdc_token(&env, &admin); + let token = create_token_factory(&env, &token_admin); + + let vault = create_vault(&env, &admin, true, 10, &token.address, &usdc_client.address); + + // Mint different amounts + token.mint(&beneficiary1, &100); + token.mint(&beneficiary2, &200); + token.mint(&beneficiary3, &50); + usdc_admin.mint(&vault.address, &1000); + + // Claims + vault.claim(&beneficiary1); // 110 USDC + vault.claim(&beneficiary2); // 220 USDC + vault.claim(&beneficiary3); // 55 USDC + + assert_eq!(usdc_client.balance(&beneficiary1), 110); + assert_eq!(usdc_client.balance(&beneficiary2), 220); + assert_eq!(usdc_client.balance(&beneficiary3), 55); + assert_eq!(vault.get_total_tokens_redeemed(), 350); + assert_eq!(vault.get_vault_usdc_balance(), 615); // 1000 - 385 = 615 +} + +#[test] +fn test_high_roi_percentage() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let token_admin = Address::generate(&env); + let beneficiary = Address::generate(&env); + + let (usdc_client, usdc_admin) = create_usdc_token(&env, &admin); + let token = create_token_factory(&env, &token_admin); + + // 100% ROI (doubling) + let vault = create_vault(&env, &admin, true, 100, &token.address, &usdc_client.address); + + token.mint(&beneficiary, &100); + usdc_admin.mint(&vault.address, &500); + + let preview = vault.preview_claim(&beneficiary); + assert_eq!(preview.usdc_amount, 200); // 100 * 2.0 = 200 + assert_eq!(preview.roi_amount, 100); + + vault.claim(&beneficiary); + assert_eq!(usdc_client.balance(&beneficiary), 200); +} diff --git a/apps/smart-contracts/contracts/vault-contract/src/vault.rs b/apps/smart-contracts/contracts/vault-contract/src/vault.rs index e72074a..8273246 100644 --- a/apps/smart-contracts/contracts/vault-contract/src/vault.rs +++ b/apps/smart-contracts/contracts/vault-contract/src/vault.rs @@ -1,85 +1,164 @@ -use soroban_sdk::{contract, contractimpl, token, Address, Env}; +use soroban_sdk::{contract, contractimpl, contracttype, token, Address, Env}; use token::Client as TokenClient; use crate::error::ContractError; +use crate::events::{events, AvailabilityChangedEvent, ClaimEvent}; +use crate::storage_types::DataKey; + +/// A complete snapshot of the vault's current state. +/// Useful for dashboards, analytics, and indexer integrations. +#[derive(Clone, Debug)] +#[contracttype] +pub struct VaultOverview { + /// The admin address that controls the vault + pub admin: Address, + /// Whether claiming is currently enabled + pub enabled: bool, + /// The ROI percentage (e.g., 5 = 5% return on investment) + pub roi_percentage: i128, + /// The participation token contract address + pub token_address: Address, + /// The USDC stablecoin contract address + pub usdc_address: Address, + /// Current USDC balance available in the vault + pub vault_usdc_balance: i128, + /// Total participation tokens that have been redeemed + pub total_tokens_redeemed: i128, +} + +/// Information about a beneficiary's claimable ROI. +#[derive(Clone, Debug)] +#[contracttype] +pub struct ClaimPreview { + /// The beneficiary's current token balance + pub token_balance: i128, + /// The amount of USDC the beneficiary would receive + pub usdc_amount: i128, + /// The ROI portion of the USDC amount (profit) + pub roi_amount: i128, + /// Whether the vault has enough USDC to fulfill this claim + pub vault_has_sufficient_balance: bool, + /// Whether claiming is currently enabled + pub claim_enabled: bool, +} #[contract] pub struct VaultContract; #[contractimpl] impl VaultContract { + // ============ Constructor ============ + + /// Initializes the vault contract with the given parameters. + /// + /// # Arguments + /// * `admin` - The address that will control vault availability + /// * `enabled` - Initial state of whether claiming is enabled + /// * `roi_percentage` - The ROI percentage (e.g., 5 for 5% return) + /// * `token` - The participation token contract address + /// * `usdc` - The USDC stablecoin contract address pub fn __constructor( env: Env, admin: Address, enabled: bool, - price: i128, + roi_percentage: i128, token: Address, usdc: Address, ) { + env.storage().instance().set(&DataKey::Admin, &admin); + env.storage().instance().set(&DataKey::Enabled, &enabled); env.storage() .instance() - .set(&"admin", &admin); - - env.storage() - .instance() - .set(&"enabled", &enabled); - - env.storage() - .instance() - .set(&"price", &price); - + .set(&DataKey::RoiPercentage, &roi_percentage); env.storage() .instance() - .set(&"token", &token); - + .set(&DataKey::TokenAddress, &token); + env.storage().instance().set(&DataKey::UsdcAddress, &usdc); env.storage() .instance() - .set(&"usdc", &usdc); + .set(&DataKey::TotalTokensRedeemed, &0_i128); } - pub fn availability_for_exchange(env: Env, admin: Address, enabled: bool) -> Result<(), ContractError> { + // ============ Admin Functions ============ + + /// Enables or disables the vault for ROI claiming. + /// Only the admin can call this function. + /// + /// # Arguments + /// * `admin` - Must be the contract admin address + /// * `enabled` - The new availability state + /// + /// # Errors + /// * `AdminNotFound` - If admin is not set in storage + /// * `OnlyAdminCanChangeAvailability` - If caller is not the admin + pub fn availability_for_exchange( + env: Env, + admin: Address, + enabled: bool, + ) -> Result<(), ContractError> { admin.require_auth(); let stored_admin: Address = env .storage() .instance() - .get(&"admin") + .get(&DataKey::Admin) .ok_or(ContractError::AdminNotFound)?; if admin != stored_admin { return Err(ContractError::OnlyAdminCanChangeAvailability); } - env.storage() - .instance() - .set(&"enabled", &enabled); - + env.storage().instance().set(&DataKey::Enabled, &enabled); + + // Emit availability changed event + events::emit_availability_changed( + &env, + AvailabilityChangedEvent { + admin: admin.clone(), + enabled, + }, + ); + Ok(()) } + // ============ Claim Function ============ + + /// Claims ROI for the beneficiary by exchanging their participation tokens for USDC. + /// The beneficiary receives their tokens' value plus the ROI percentage. + /// + /// Formula: usdc_amount = token_balance * (100 + roi_percentage) / 100 + /// + /// # Arguments + /// * `beneficiary` - The address claiming their ROI (must have tokens) + /// + /// # Errors + /// * `ExchangeIsCurrentlyDisabled` - If vault is disabled + /// * `BeneficiaryHasNoTokensToClaim` - If beneficiary has zero tokens + /// * `VaultDoesNotHaveEnoughUSDC` - If vault cannot cover the claim pub fn claim(env: Env, beneficiary: Address) -> Result<(), ContractError> { beneficiary.require_auth(); let enabled: bool = env .storage() .instance() - .get(&"enabled") + .get(&DataKey::Enabled) .expect("Enabled flag not found"); if !enabled { return Err(ContractError::ExchangeIsCurrentlyDisabled); } - let price: i128 = env + let roi_percentage: i128 = env .storage() .instance() - .get(&"price") - .expect("Price not found"); + .get(&DataKey::RoiPercentage) + .expect("ROI percentage not found"); let token_address: Address = env .storage() .instance() - .get(&"token") + .get(&DataKey::TokenAddress) .expect("Token address not found"); let token_client = TokenClient::new(&env, &token_address); @@ -89,26 +168,221 @@ impl VaultContract { return Err(ContractError::BeneficiaryHasNoTokensToClaim); } - let usdc_amount = (token_balance * (100 + price)) / 100; + let usdc_amount = (token_balance * (100 + roi_percentage)) / 100; let usdc_address: Address = env .storage() .instance() - .get(&"usdc") + .get(&DataKey::UsdcAddress) .expect("USDC address not found"); let usdc_client = TokenClient::new(&env, &usdc_address); - let vault_usdc_balance = usdc_client.balance(&env.current_contract_address()); if vault_usdc_balance < usdc_amount { return Err(ContractError::VaultDoesNotHaveEnoughUSDC); } + // Execute the token exchange token_client.transfer(&beneficiary, &env.current_contract_address(), &token_balance); - usdc_client.transfer(&env.current_contract_address(), &beneficiary, &usdc_amount); - + + // Update total tokens redeemed + let total_redeemed: i128 = env + .storage() + .instance() + .get(&DataKey::TotalTokensRedeemed) + .unwrap_or(0); + env.storage() + .instance() + .set(&DataKey::TotalTokensRedeemed, &(total_redeemed + token_balance)); + + // Emit claim event for indexers and explorers + events::emit_claim( + &env, + ClaimEvent { + beneficiary: beneficiary.clone(), + tokens_redeemed: token_balance, + usdc_received: usdc_amount, + roi_percentage, + }, + ); + Ok(()) } -} \ No newline at end of file + + // ============ View/Getter Functions ============ + + /// Returns the admin address. + pub fn get_admin(env: Env) -> Address { + env.storage() + .instance() + .get(&DataKey::Admin) + .expect("Admin not found") + } + + /// Returns whether claiming is currently enabled. + pub fn is_enabled(env: Env) -> bool { + env.storage() + .instance() + .get(&DataKey::Enabled) + .unwrap_or(false) + } + + /// Returns the ROI percentage (e.g., 5 means 5% return). + pub fn get_roi_percentage(env: Env) -> i128 { + env.storage() + .instance() + .get(&DataKey::RoiPercentage) + .expect("ROI percentage not found") + } + + /// Returns the participation token contract address. + pub fn get_token_address(env: Env) -> Address { + env.storage() + .instance() + .get(&DataKey::TokenAddress) + .expect("Token address not found") + } + + /// Returns the USDC stablecoin contract address. + pub fn get_usdc_address(env: Env) -> Address { + env.storage() + .instance() + .get(&DataKey::UsdcAddress) + .expect("USDC address not found") + } + + /// Returns the current USDC balance held by the vault. + pub fn get_vault_usdc_balance(env: Env) -> i128 { + let usdc_address: Address = env + .storage() + .instance() + .get(&DataKey::UsdcAddress) + .expect("USDC address not found"); + + let usdc_client = TokenClient::new(&env, &usdc_address); + usdc_client.balance(&env.current_contract_address()) + } + + /// Returns the total amount of participation tokens that have been redeemed. + pub fn get_total_tokens_redeemed(env: Env) -> i128 { + env.storage() + .instance() + .get(&DataKey::TotalTokensRedeemed) + .unwrap_or(0) + } + + // ============ Preview Functions ============ + + /// Previews the claim for a beneficiary without executing it. + /// Returns detailed information about what the beneficiary would receive. + /// + /// # Arguments + /// * `beneficiary` - The address to preview the claim for + /// + /// # Returns + /// A `ClaimPreview` struct with all relevant claim information + pub fn preview_claim(env: Env, beneficiary: Address) -> ClaimPreview { + let roi_percentage: i128 = env + .storage() + .instance() + .get(&DataKey::RoiPercentage) + .unwrap_or(0); + + let token_address: Address = env + .storage() + .instance() + .get(&DataKey::TokenAddress) + .expect("Token address not found"); + + let token_client = TokenClient::new(&env, &token_address); + let token_balance = token_client.balance(&beneficiary); + + let usdc_amount = if token_balance > 0 { + (token_balance * (100 + roi_percentage)) / 100 + } else { + 0 + }; + + let roi_amount = usdc_amount - token_balance; + + let usdc_address: Address = env + .storage() + .instance() + .get(&DataKey::UsdcAddress) + .expect("USDC address not found"); + + let usdc_client = TokenClient::new(&env, &usdc_address); + let vault_usdc_balance = usdc_client.balance(&env.current_contract_address()); + + let enabled: bool = env + .storage() + .instance() + .get(&DataKey::Enabled) + .unwrap_or(false); + + ClaimPreview { + token_balance, + usdc_amount, + roi_amount, + vault_has_sufficient_balance: vault_usdc_balance >= usdc_amount, + claim_enabled: enabled, + } + } + + // ============ Overview Functions ============ + + /// Returns a complete snapshot of the vault's current state. + /// Useful for dashboards and analytics integrations. + pub fn get_vault_overview(env: Env) -> VaultOverview { + let admin: Address = env + .storage() + .instance() + .get(&DataKey::Admin) + .expect("Admin not found"); + + let enabled: bool = env + .storage() + .instance() + .get(&DataKey::Enabled) + .unwrap_or(false); + + let roi_percentage: i128 = env + .storage() + .instance() + .get(&DataKey::RoiPercentage) + .unwrap_or(0); + + let token_address: Address = env + .storage() + .instance() + .get(&DataKey::TokenAddress) + .expect("Token address not found"); + + let usdc_address: Address = env + .storage() + .instance() + .get(&DataKey::UsdcAddress) + .expect("USDC address not found"); + + let usdc_client = TokenClient::new(&env, &usdc_address); + let vault_usdc_balance = usdc_client.balance(&env.current_contract_address()); + + let total_tokens_redeemed: i128 = env + .storage() + .instance() + .get(&DataKey::TotalTokensRedeemed) + .unwrap_or(0); + + VaultOverview { + admin, + enabled, + roi_percentage, + token_address, + usdc_address, + vault_usdc_balance, + total_tokens_redeemed, + } + } +}