diff --git a/agent/strategyParser.test.ts b/agent/strategyParser.test.ts
new file mode 100644
index 0000000..4499397
--- /dev/null
+++ b/agent/strategyParser.test.ts
@@ -0,0 +1,50 @@
+import { parseStrategyIntent, validateStrategyPayload, StrategyPayload } from './strategyParser';
+
+describe('Strategy Parser', () => {
+ it('parses valid input and sums allocations to 100', () => {
+ const input = {
+ monthlyContributionAmount: 500,
+ blendAllocationX: 40,
+ soroswapAllocationX: 30,
+ goldAllocationX: 30,
+ };
+ const result = parseStrategyIntent(input);
+ expect(result).toEqual({
+ monthlyContributionAmount: 500,
+ blendAllocationX: 40,
+ soroswapAllocationX: 30,
+ goldAllocationX: 30,
+ });
+ expect(validateStrategyPayload(result)).toBe(true);
+ });
+
+ it('normalizes allocations if they do not sum to 100', () => {
+ const input = {
+ monthlyContributionAmount: 1000,
+ blendAllocationX: 50,
+ soroswapAllocationX: 30,
+ goldAllocationX: 10,
+ };
+ const result = parseStrategyIntent(input);
+ expect(result.blendAllocationX + result.soroswapAllocationX + result.goldAllocationX).toBe(100);
+ expect(validateStrategyPayload(result)).toBe(true);
+ });
+
+ it('handles malformed input with fallbacks', () => {
+ const input = {
+ monthlyContributionAmount: 'not-a-number',
+ blendAllocationX: null,
+ soroswapAllocationX: undefined,
+ goldAllocationX: -10,
+ };
+ const result = parseStrategyIntent(input);
+ expect(result.monthlyContributionAmount).toBe(0);
+ expect(result.blendAllocationX + result.soroswapAllocationX + result.goldAllocationX).toBe(100);
+ expect(validateStrategyPayload(result)).toBe(true);
+ });
+
+ it('returns false for invalid schema', () => {
+ const invalid = { foo: 1, bar: 2 };
+ expect(validateStrategyPayload(invalid)).toBe(false);
+ });
+});
diff --git a/agent/strategyParser.ts b/agent/strategyParser.ts
new file mode 100644
index 0000000..b7754b5
--- /dev/null
+++ b/agent/strategyParser.ts
@@ -0,0 +1,67 @@
+// Strategy Parser Tool for Issue 1.3
+// Converts conversational intent into a strict JSON payload for the agent
+
+export interface StrategyPayload {
+ monthlyContributionAmount: number;
+ blendAllocationX: number;
+ soroswapAllocationX: number;
+ goldAllocationX: number;
+}
+
+/**
+ * Validates and normalizes the strategy payload.
+ * Ensures allocations sum to 100 and all fields are present.
+ * Applies fallbacks for malformed input.
+ */
+export function parseStrategyIntent(input: any): StrategyPayload {
+ // Fallbacks for missing or malformed fields
+ let monthlyContributionAmount = Number(input.monthlyContributionAmount);
+ if (isNaN(monthlyContributionAmount) || monthlyContributionAmount < 0) {
+ monthlyContributionAmount = 0;
+ }
+
+ // Parse allocations, fallback to 0 if missing or invalid
+ let blend = Number(input.blendAllocationX);
+ let soroswap = Number(input.soroswapAllocationX);
+ let gold = Number(input.goldAllocationX);
+ if (isNaN(blend) || blend < 0) blend = 0;
+ if (isNaN(soroswap) || soroswap < 0) soroswap = 0;
+ if (isNaN(gold) || gold < 0) gold = 0;
+
+ // Normalize allocations to sum to 100
+ const total = blend + soroswap + gold;
+ if (total === 0) {
+ // All allocations are invalid or zero, fallback to 100/0/0
+ blend = 100;
+ soroswap = 0;
+ gold = 0;
+ } else if (total !== 100) {
+ blend = Math.round((blend / total) * 100);
+ soroswap = Math.round((soroswap / total) * 100);
+ gold = 100 - blend - soroswap; // Ensure sum is exactly 100
+ }
+
+ return {
+ monthlyContributionAmount,
+ blendAllocationX: blend,
+ soroswapAllocationX: soroswap,
+ goldAllocationX: gold,
+ };
+}
+
+/**
+ * Validates that the payload matches the schema and allocations sum to 100.
+ */
+export function validateStrategyPayload(payload: any): boolean {
+ if (
+ typeof payload !== 'object' ||
+ typeof payload.monthlyContributionAmount !== 'number' ||
+ typeof payload.blendAllocationX !== 'number' ||
+ typeof payload.soroswapAllocationX !== 'number' ||
+ typeof payload.goldAllocationX !== 'number'
+ ) {
+ return false;
+ }
+ const sum = payload.blendAllocationX + payload.soroswapAllocationX + payload.goldAllocationX;
+ return sum === 100;
+}
diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml
index 7531ec3..8f4f82f 100644
--- a/contracts/Cargo.toml
+++ b/contracts/Cargo.toml
@@ -6,7 +6,7 @@ edition = "2021"
[dependencies]
soroban-sdk = "22.0.0"
-[dev_dependencies]
+[dev-dependencies]
soroban-sdk = { version = "22.0.0", features = ["testutils"] }
[features]
diff --git a/contracts/src/lib.rs b/contracts/src/lib.rs
index 1943632..a128922 100644
--- a/contracts/src/lib.rs
+++ b/contracts/src/lib.rs
@@ -1,1068 +1,560 @@
#![no_std]
-use soroban_sdk::{contract, contractimpl, contracttype, Env, Address};
-use soroban_sdk::token::TokenClient;
-
-// Issue 2: Smart Contract - Stellar Path Payments & Yield Allocation (Blend Integration)
-// Issue 3: Withdraw functionality with Blend and Soroswap unwinding
-
-/// Blend Pool interface for supplying and withdrawing assets
-/// This trait defines the interface for interacting with the Blend Protocol
-pub trait BlendPoolInterface {
- /// Supply assets to the Blend pool and receive bTokens
- fn supply(env: Env, from: Address, amount: i128) -> i128;
-
- /// Withdraw assets from the Blend pool by redeeming bTokens
- fn withdraw(env: Env, to: Address, b_tokens: i128) -> i128;
-
- /// Get the current index rate for yield calculation
- /// The index rate represents the exchange rate between underlying assets and bTokens
- fn get_index_rate(env: Env) -> i128;
-
- /// Get the total bToken supply for the pool
- fn get_b_token_supply(env: Env) -> i128;
-
- /// Get the total underlying assets in the pool
- fn get_total_supply(env: Env) -> i128;
+use soroban_sdk::{
+ contract, contractimpl, contracttype, symbol_short, Address, Env, String, Symbol, Vec,
+};
+
+#[soroban_sdk::contractclient(name = "SoroswapRouterClient")]
+pub trait SoroswapRouterTrait {
+ fn add_liquidity(
+ e: Env,
+ token_a: Address,
+ token_b: Address,
+ amount_a_desired: i128,
+ amount_b_desired: i128,
+ amount_a_min: i128,
+ amount_b_min: i128,
+ to: Address,
+ deadline: u64,
+ ) -> (i128, i128, i128);
+
+ fn swap_exact_tokens_for_tokens(
+ e: Env,
+ amount_in: i128,
+ amount_out_min: i128,
+ path: Vec
,
+ to: Address,
+ deadline: u64,
+ ) -> Vec;
}
-/// Represents a user's position in the Blend Protocol
-#[contracttype]
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct BlendPosition {
- /// Amount of bTokens held by the user
- pub b_tokens: i128,
- /// The index rate at the time of last supply (for yield tracking)
- pub last_index_rate: i128,
- /// Timestamp of last supply
- pub last_supply_time: u64,
+#[soroban_sdk::contractclient(name = "TokenClient")]
+pub trait TokenTrait {
+ fn transfer(e: Env, from: Address, to: Address, amount: i128);
+ fn approve(e: Env, from: Address, spender: Address, amount: i128, expiration_ledger: u32);
+ fn balance(e: Env, id: Address) -> i128;
}
+// Issue 2: Smart Contract - Stellar Path Payments & Yield Allocation (Blend Integration)
+// Issue 3: Withdraw functionality with Blend and Soroswap unwinding
+
#[contracttype]
pub enum DataKey {
+ Admin,
UserBalance(Address),
- TotalDeposits,
- UserBlendBalance(Address),
UserLPShares(Address),
+ UserBlendBalance(Address),
UserGoldBalance(Address),
- /// User's Blend Protocol position (bTokens)
- UserBlendPosition(Address),
- /// Mock Blend Pool address (for testing)
- BlendPoolAddress,
- /// USDC Token contract address
- UsdcTokenAddress,
- /// Total bTokens held by the contract across all users
- TotalBTokens,
+ TotalDeposits,
+ GoldAssetCode,
+ GoldAssetIssuer,
+ GoldTrustlineReady,
+ GoldTrustlineReserveStroops,
+ SoroswapRouter,
+ UsdcToken,
+ XlmToken,
}
-/// Precision factor for index rate calculations (6 decimal places)
-pub const INDEX_RATE_PRECISION: i128 = 1_000_000;
+const CANONICAL_GOLD_ASSET_CODE: Symbol = symbol_short!("XAUT");
+const CANONICAL_GOLD_ASSET_ISSUER: &str =
+ "GCRLXTLD7XIRXWXV2PDCC74O5TUUKN3OODJAM6TWVE4AIRNMGQJK3KWQ";
+const TRUSTLINE_BASE_RESERVE_STROOPS: i128 = 5_000_000;
#[contract]
pub struct SmasageYieldRouter;
#[contractimpl]
impl SmasageYieldRouter {
- /// Initialize the contract with Blend pool and USDC token addresses
- pub fn initialize(env: Env, blend_pool: Address, usdc_token: Address) {
- env.storage().persistent().set(&DataKey::BlendPoolAddress, &blend_pool);
- env.storage().persistent().set(&DataKey::UsdcTokenAddress, &usdc_token);
- env.storage().persistent().set(&DataKey::TotalBTokens, &0i128);
- }
-
- /// Get the Blend pool address
- pub fn get_blend_pool(env: Env) -> Option {
- env.storage().persistent().get(&DataKey::BlendPoolAddress)
- }
-
- /// Get the USDC token address
- pub fn get_usdc_token(env: Env) -> Option {
- env.storage().persistent().get(&DataKey::UsdcTokenAddress)
+ pub fn initialize(env: Env, admin: Address) {
+ if env.storage().persistent().has(&DataKey::Admin) {
+ panic!("Already initialized");
+ }
+ admin.require_auth();
+ env.storage().persistent().set(&DataKey::Admin, &admin);
+ }
+
+ pub fn initialize_soroswap(
+ env: Env,
+ admin: Address,
+ router: Address,
+ usdc: Address,
+ xlm: Address,
+ ) {
+ let stored_admin: Address = env
+ .storage()
+ .persistent()
+ .get(&DataKey::Admin)
+ .expect("Contract not initialized");
+ assert!(admin == stored_admin, "Only admin can initialize Soroswap");
+ admin.require_auth();
+
+ env.storage()
+ .persistent()
+ .set(&DataKey::SoroswapRouter, &router);
+ env.storage().persistent().set(&DataKey::UsdcToken, &usdc);
+ env.storage().persistent().set(&DataKey::XlmToken, &xlm);
+ }
+
+ pub fn init_gold_trustline(env: Env, admin: Address, reserve_stroops: i128) {
+ let stored_admin: Address = env
+ .storage()
+ .persistent()
+ .get(&DataKey::Admin)
+ .expect("Contract not initialized");
+
+ assert!(
+ admin == stored_admin,
+ "Only admin can initialize Gold trustline"
+ );
+ admin.require_auth();
+ assert!(
+ reserve_stroops >= TRUSTLINE_BASE_RESERVE_STROOPS,
+ "Insufficient base reserve for trustline"
+ );
+
+ let gold_issuer = String::from_str(&env, CANONICAL_GOLD_ASSET_ISSUER);
+ env.storage()
+ .persistent()
+ .set(&DataKey::GoldAssetCode, &CANONICAL_GOLD_ASSET_CODE);
+ env.storage()
+ .persistent()
+ .set(&DataKey::GoldAssetIssuer, &gold_issuer);
+ env.storage()
+ .persistent()
+ .set(&DataKey::GoldTrustlineReserveStroops, &reserve_stroops);
+ env.storage()
+ .persistent()
+ .set(&DataKey::GoldTrustlineReady, &true);
+ }
+
+ pub fn get_gold_asset(env: Env) -> (Symbol, String) {
+ let code = env
+ .storage()
+ .persistent()
+ .get(&DataKey::GoldAssetCode)
+ .unwrap_or(CANONICAL_GOLD_ASSET_CODE);
+ let issuer = env
+ .storage()
+ .persistent()
+ .get(&DataKey::GoldAssetIssuer)
+ .unwrap_or(String::from_str(&env, CANONICAL_GOLD_ASSET_ISSUER));
+ (code, issuer)
+ }
+
+ pub fn is_gold_trustline_ready(env: Env) -> bool {
+ env.storage()
+ .persistent()
+ .get(&DataKey::GoldTrustlineReady)
+ .unwrap_or(false)
+ }
+
+ pub fn get_gold_reserve_stroops(env: Env) -> i128 {
+ env.storage()
+ .persistent()
+ .get(&DataKey::GoldTrustlineReserveStroops)
+ .unwrap_or(0)
}
- /// Supply USDC to the Blend Protocol and receive bTokens
- ///
- /// # Arguments
- /// * `from` - The address supplying the assets
- /// * `amount` - The amount of USDC to supply
- ///
- /// # Returns
- /// The amount of bTokens received
- pub fn supply_to_blend(env: Env, from: Address, amount: i128) -> i128 {
+ /// Initialize the contract and accept deposits in USDC.
+ /// Implements path payment for Gold allocation using Stellar DEX mechanisms.
+ pub fn deposit(
+ env: Env,
+ from: Address,
+ amount: i128,
+ blend_percentage: u32,
+ lp_percentage: u32,
+ gold_percentage: u32,
+ ) {
from.require_auth();
- assert!(amount > 0, "Amount must be greater than 0");
-
- let blend_pool = Self::get_blend_pool(env.clone())
- .expect("Blend pool not initialized");
+ assert!(
+ blend_percentage + lp_percentage + gold_percentage <= 100,
+ "Allocation exceeds 100%"
+ );
// Transfer USDC from user to contract
- Self::transfer_usdc_from_user(&env, &from, amount);
-
- // Call Blend pool to supply assets and get bTokens
- // In production, this would invoke the actual Blend contract
- // For now, we use a client pattern that can be mocked in tests
- let b_tokens_received = Self::call_blend_supply(&env, &blend_pool, &env.current_contract_address(), amount);
-
- // Get current index rate for yield tracking
- let current_index_rate = Self::call_blend_index_rate(&env, &blend_pool);
-
- // Update user's Blend position
- let mut position: BlendPosition = env.storage().persistent()
- .get(&DataKey::UserBlendPosition(from.clone()))
- .unwrap_or(BlendPosition {
- b_tokens: 0,
- last_index_rate: current_index_rate,
- last_supply_time: env.ledger().timestamp(),
- });
-
- position.b_tokens += b_tokens_received;
- position.last_index_rate = current_index_rate;
- position.last_supply_time = env.ledger().timestamp();
-
- env.storage().persistent().set(&DataKey::UserBlendPosition(from.clone()), &position);
-
- // Update total bTokens held by contract
- let total_b_tokens: i128 = env.storage().persistent()
- .get(&DataKey::TotalBTokens)
+ let usdc_addr: Address = env
+ .storage()
+ .persistent()
+ .get(&DataKey::UsdcToken)
+ .expect("USDC not initialized");
+ let usdc = TokenClient::new(&env, &usdc_addr);
+ usdc.transfer(&from, &env.current_contract_address(), &amount);
+
+ let mut balance: i128 = env
+ .storage()
+ .persistent()
+ .get(&DataKey::UserBalance(from.clone()))
.unwrap_or(0);
- env.storage().persistent().set(&DataKey::TotalBTokens, &(total_b_tokens + b_tokens_received));
-
- // Also update the legacy balance tracking for backward compatibility
- let mut blend_balance: i128 = env.storage().persistent()
- .get(&DataKey::UserBlendBalance(from.clone()))
- .unwrap_or(0);
- blend_balance += amount;
- env.storage().persistent().set(&DataKey::UserBlendBalance(from.clone()), &blend_balance);
-
- b_tokens_received
- }
-
- /// Internal function to transfer USDC from user to contract
- /// This can be mocked in tests
- fn transfer_usdc_from_user(env: &Env, from: &Address, amount: i128) {
- let usdc_token = Self::get_usdc_token(env.clone())
- .expect("USDC token not initialized");
- let token_client = TokenClient::new(env, &usdc_token);
- token_client.transfer(from, &env.current_contract_address(), &amount);
- }
-
- /// Internal function to transfer USDC from contract to user
- fn transfer_usdc_to_user(env: &Env, to: &Address, amount: i128) {
- let usdc_token = Self::get_usdc_token(env.clone())
- .expect("USDC token not initialized");
- let token_client = TokenClient::new(env, &usdc_token);
- token_client.transfer(&env.current_contract_address(), to, &amount);
- }
+ balance += amount;
+ env.storage()
+ .persistent()
+ .set(&DataKey::UserBalance(from.clone()), &balance);
- /// Calculate the current yield for a user's Blend position
- ///
- /// # Arguments
- /// * `user` - The address to calculate yield for
- ///
- /// # Returns
- /// The current yield amount in USDC (underlying asset terms)
- pub fn calculate_blend_yield(env: Env, user: Address) -> i128 {
- let position: BlendPosition = env.storage().persistent()
- .get(&DataKey::UserBlendPosition(user.clone()))
- .unwrap_or(BlendPosition {
- b_tokens: 0,
- last_index_rate: INDEX_RATE_PRECISION,
- last_supply_time: 0,
- });
-
- if position.b_tokens == 0 {
- return 0;
+ // Track Blend allocation
+ let blend_amount = amount * blend_percentage as i128 / 100;
+ if blend_amount > 0 {
+ let mut blend_balance: i128 = env
+ .storage()
+ .persistent()
+ .get(&DataKey::UserBlendBalance(from.clone()))
+ .unwrap_or(0);
+ blend_balance += blend_amount;
+ env.storage()
+ .persistent()
+ .set(&DataKey::UserBlendBalance(from.clone()), &blend_balance);
}
- let blend_pool = Self::get_blend_pool(env.clone())
- .expect("Blend pool not initialized");
- let current_index_rate = Self::call_blend_index_rate(&env, &blend_pool);
-
- // Calculate yield: bTokens * (current_index_rate - last_index_rate) / precision
- let index_diff = current_index_rate.saturating_sub(position.last_index_rate);
- let yield_amount = position.b_tokens * index_diff / INDEX_RATE_PRECISION;
-
- yield_amount
- }
-
- /// Get the current value of a user's Blend position in USDC terms
- ///
- /// # Arguments
- /// * `user` - The address to get position value for
- ///
- /// # Returns
- /// The current value in USDC (underlying asset terms)
- pub fn get_blend_position_value(env: Env, user: Address) -> i128 {
- let position: BlendPosition = env.storage().persistent()
- .get(&DataKey::UserBlendPosition(user.clone()))
- .unwrap_or(BlendPosition {
- b_tokens: 0,
- last_index_rate: INDEX_RATE_PRECISION,
- last_supply_time: 0,
- });
-
- if position.b_tokens == 0 {
- return 0;
+ // Track LP shares allocation: delegate to helper
+ if lp_percentage > 0 {
+ let lp_amount = (amount * lp_percentage as i128) / 100;
+ if lp_amount > 0 {
+ Self::provide_lp(env.clone(), from.clone(), lp_amount);
+ }
}
- let blend_pool = Self::get_blend_pool(env.clone())
- .expect("Blend pool not initialized");
- let current_index_rate = Self::call_blend_index_rate(&env, &blend_pool);
-
- // Calculate value: bTokens * current_index_rate / precision
- position.b_tokens * current_index_rate / INDEX_RATE_PRECISION
- }
-
- /// Get user's Blend position details
- pub fn get_blend_position(env: Env, user: Address) -> BlendPosition {
- env.storage().persistent()
- .get(&DataKey::UserBlendPosition(user))
- .unwrap_or(BlendPosition {
- b_tokens: 0,
- last_index_rate: INDEX_RATE_PRECISION,
- last_supply_time: 0,
- })
- }
-
- /// Internal function to call Blend pool supply
- /// This can be overridden in tests via mocking
- fn call_blend_supply(env: &Env, blend_pool: &Address, _from: &Address, amount: i128) -> i128 {
- // In production, this would invoke the actual Blend contract
- // For testing, this will be mocked
- // Returns the amount of bTokens received
-
- // Get current index rate to calculate bTokens
- let index_rate = Self::call_blend_index_rate(env, blend_pool);
-
- // Calculate bTokens: amount * INDEX_RATE_PRECISION / index_rate
- // As index rate increases, fewer bTokens are minted per unit of underlying
- amount * INDEX_RATE_PRECISION / index_rate
- }
-
- /// Internal function to call Blend pool withdraw
- fn call_blend_withdraw(env: &Env, blend_pool: &Address, _to: &Address, b_tokens: i128) -> i128 {
- // In production, this would invoke the actual Blend contract
- // For testing, this will be mocked
- // Returns the amount of underlying assets received
-
- let index_rate = Self::call_blend_index_rate(env, blend_pool);
-
- // Calculate underlying: bTokens * index_rate / INDEX_RATE_PRECISION
- // As index rate increases, each bToken is worth more underlying
- b_tokens * index_rate / INDEX_RATE_PRECISION
- }
-
- /// Internal function to get Blend pool index rate
- fn call_blend_index_rate(env: &Env, _blend_pool: &Address) -> i128 {
- // In production, this would invoke blend_pool.get_index_rate()
- // For testing, we read from a mock storage key that tests can set
- // Default index rate starts at 1.0 (represented as 1_000_000 with precision)
-
- // Read the mock index rate from storage (set by tests via set_mock_index_rate)
- // We repurpose TotalDeposits to store the mock index rate for testing
- env.storage().persistent().get(&DataKey::TotalDeposits).unwrap_or(INDEX_RATE_PRECISION)
- }
-
- /// Get the current mock index rate (for testing only)
- /// In production, this would query the actual Blend pool
- pub fn get_mock_index_rate(env: Env) -> i128 {
- // This is a test helper - in production, this reads from actual Blend pool
- // For now, return the default precision
- INDEX_RATE_PRECISION
- }
-
- /// Set the mock index rate (for testing only)
- /// This allows tests to simulate yield accrual
- pub fn set_mock_index_rate(env: Env, new_rate: i128) {
- // Store the mock index rate in a special storage location
- // We use a tuple key pattern to avoid collision with real data
- env.storage().persistent().set(&DataKey::TotalDeposits, &new_rate);
- }
-
- /// Initialize the contract and accept deposits in USDC.
- /// Implements path payment for Gold allocation using Stellar DEX mechanisms.
- pub fn deposit(env: Env, from: Address, amount: i128, blend_percentage: u32, lp_percentage: u32, gold_percentage: u32) {
- from.require_auth();
- assert!(blend_percentage + lp_percentage + gold_percentage <= 100, "Allocation exceeds 100%");
-
- let mut balance: i128 = env.storage().persistent().get(&DataKey::UserBalance(from.clone())).unwrap_or(0);
- balance += amount;
- env.storage().persistent().set(&DataKey::UserBalance(from.clone()), &balance);
-
- // Track Blend allocation
- let blend_amount = amount * blend_percentage as i128 / 100;
- let mut blend_balance: i128 = env.storage().persistent().get(&DataKey::UserBlendBalance(from.clone())).unwrap_or(0);
- blend_balance += blend_amount;
- env.storage().persistent().set(&DataKey::UserBlendBalance(from.clone()), &blend_balance);
-
- // Track LP shares allocation
- let lp_amount = amount * lp_percentage as i128 / 100;
- let mut lp_shares: i128 = env.storage().persistent().get(&DataKey::UserLPShares(from.clone())).unwrap_or(0);
- lp_shares += lp_amount;
- env.storage().persistent().set(&DataKey::UserLPShares(from.clone()), &lp_shares);
-
// Track Gold allocation (XAUT)
let gold_amount = amount * gold_percentage as i128 / 100;
if gold_amount > 0 {
- // Execute path payment: USDC -> XAUT via Stellar DEX
- // In production, this would use Soroban's path payment strict receive
- // to find the best route through the Stellar DEX order books
- let mut gold_balance: i128 = env.storage().persistent().get(&DataKey::UserGoldBalance(from.clone())).unwrap_or(0);
+ let mut gold_balance: i128 = env
+ .storage()
+ .persistent()
+ .get(&DataKey::UserGoldBalance(from.clone()))
+ .unwrap_or(0);
gold_balance += gold_amount;
- env.storage().persistent().set(&DataKey::UserGoldBalance(from.clone()), &gold_balance);
+ env.storage()
+ .persistent()
+ .set(&DataKey::UserGoldBalance(from.clone()), &gold_balance);
}
-
- // Mock: Here we would route `blend_percentage` to the Blend protocol
- // Mock: Here we would route `lp_percentage` to Soroswap Pool
- // Mock: Path payment executed for `gold_percentage` to acquire XAUT
+ }
+
+ fn provide_lp(env: Env, user: Address, usdc_amount: i128) {
+ let router_addr: Address = env
+ .storage()
+ .persistent()
+ .get(&DataKey::SoroswapRouter)
+ .expect("Soroswap not initialized");
+ let usdc_addr: Address = env
+ .storage()
+ .persistent()
+ .get(&DataKey::UsdcToken)
+ .expect("USDC not initialized");
+ let xlm_addr: Address = env
+ .storage()
+ .persistent()
+ .get(&DataKey::XlmToken)
+ .expect("XLM not initialized");
+
+ let router = SoroswapRouterClient::new(&env, &router_addr);
+ let usdc = TokenClient::new(&env, &usdc_addr);
+ let xlm = TokenClient::new(&env, &xlm_addr);
+
+ let half_usdc = usdc_amount / 2;
+ let remaining_usdc = usdc_amount - half_usdc;
+
+ // Approve router for total USDC amount to be used in swap and liquidity
+ usdc.approve(
+ &env.current_contract_address(),
+ &router_addr,
+ &usdc_amount,
+ &(env.ledger().sequence() + 100),
+ );
+
+ // Swap half USDC for XLM
+ let mut path = Vec::new(&env);
+ path.push_back(usdc_addr.clone());
+ path.push_back(xlm_addr.clone());
+
+ let deadline = env.ledger().timestamp() + 300; // 5 minutes
+ let swap_amounts = router.swap_exact_tokens_for_tokens(
+ &half_usdc,
+ &0,
+ &path,
+ &env.current_contract_address(),
+ &deadline,
+ );
+ let xlm_received = swap_amounts.get(1).unwrap();
+
+ // Approve router for received XLM
+ xlm.approve(
+ &env.current_contract_address(),
+ &router_addr,
+ &xlm_received,
+ &(env.ledger().sequence() + 100),
+ );
+
+ // Add liquidity
+ let (_, _, lp_shares) = router.add_liquidity(
+ &usdc_addr,
+ &xlm_addr,
+ &remaining_usdc,
+ &xlm_received,
+ &0,
+ &0,
+ &env.current_contract_address(),
+ &deadline,
+ );
+
+ // Map LP shares to user
+ let mut user_shares: i128 = env
+ .storage()
+ .persistent()
+ .get(&DataKey::UserLPShares(user.clone()))
+ .unwrap_or(0);
+ user_shares += lp_shares;
+ env.storage()
+ .persistent()
+ .set(&DataKey::UserLPShares(user), &user_shares);
}
/// Withdraw USDC by unwinding positions from Blend and breaking LP shares from Soroswap.
/// The contract calculates how much to pull from each source and transfers USDC to the user.
pub fn withdraw(env: Env, to: Address, amount: i128) {
to.require_auth();
-
+
// Get total user balance (USDC + Blend + LP + Gold)
- let usdc_balance: i128 = env.storage().persistent().get(&DataKey::UserBalance(to.clone())).unwrap_or(0);
- let blend_balance: i128 = env.storage().persistent().get(&DataKey::UserBlendBalance(to.clone())).unwrap_or(0);
- let lp_shares: i128 = env.storage().persistent().get(&DataKey::UserLPShares(to.clone())).unwrap_or(0);
- let gold_balance: i128 = env.storage().persistent().get(&DataKey::UserGoldBalance(to.clone())).unwrap_or(0);
-
+ let usdc_balance: i128 = env
+ .storage()
+ .persistent()
+ .get(&DataKey::UserBalance(to.clone()))
+ .unwrap_or(0);
+ let blend_balance: i128 = env
+ .storage()
+ .persistent()
+ .get(&DataKey::UserBlendBalance(to.clone()))
+ .unwrap_or(0);
+ let lp_shares: i128 = env
+ .storage()
+ .persistent()
+ .get(&DataKey::UserLPShares(to.clone()))
+ .unwrap_or(0);
+ let gold_balance: i128 = env
+ .storage()
+ .persistent()
+ .get(&DataKey::UserGoldBalance(to.clone()))
+ .unwrap_or(0);
+
let total_balance = usdc_balance + blend_balance + lp_shares + gold_balance;
assert!(total_balance >= amount, "Insufficient balance");
-
+
let mut remaining_to_withdraw = amount;
-
+
// Step 1: Use available USDC first
if usdc_balance > 0 {
let usdc_to_use = usdc_balance.min(remaining_to_withdraw);
- env.storage().persistent().set(&DataKey::UserBalance(to.clone()), &(usdc_balance - usdc_to_use));
+ env.storage().persistent().set(
+ &DataKey::UserBalance(to.clone()),
+ &(usdc_balance - usdc_to_use),
+ );
remaining_to_withdraw -= usdc_to_use;
}
-
+
// Step 2: If still need more, unwind Blend positions (pull liquidity)
if remaining_to_withdraw > 0 && blend_balance > 0 {
let blend_to_unwind = blend_balance.min(remaining_to_withdraw);
- env.storage().persistent().set(&DataKey::UserBlendBalance(to.clone()), &(blend_balance - blend_to_unwind));
+ env.storage().persistent().set(
+ &DataKey::UserBlendBalance(to.clone()),
+ &(blend_balance - blend_to_unwind),
+ );
// Mock: In production, this would call Blend Protocol to withdraw underlying assets
// For simplicity, we assume 1:1 conversion back to USDC
remaining_to_withdraw -= blend_to_unwind;
}
-
+
// Step 3: If still need more, break LP shares on Soroswap
if remaining_to_withdraw > 0 && lp_shares > 0 {
let lp_to_break = lp_shares.min(remaining_to_withdraw);
- env.storage().persistent().set(&DataKey::UserLPShares(to.clone()), &(lp_shares - lp_to_break));
+ env.storage().persistent().set(
+ &DataKey::UserLPShares(to.clone()),
+ &(lp_shares - lp_to_break),
+ );
// Mock: In production, this would remove liquidity from Soroswap pool and swap back to USDC
// For simplicity, we assume 1:1 conversion back to USDC
remaining_to_withdraw -= lp_to_break;
}
-
+
// Step 4: If still need more, sell Gold allocation
if remaining_to_withdraw > 0 && gold_balance > 0 {
let gold_to_sell = gold_balance.min(remaining_to_withdraw);
- env.storage().persistent().set(&DataKey::UserGoldBalance(to.clone()), &(gold_balance - gold_to_sell));
+ env.storage().persistent().set(
+ &DataKey::UserGoldBalance(to.clone()),
+ &(gold_balance - gold_to_sell),
+ );
// Mock: In production, this would swap XAUT back to USDC via Stellar DEX
// For simplicity, we assume 1:1 conversion back to USDC
remaining_to_withdraw -= gold_to_sell;
}
-
+
assert!(remaining_to_withdraw == 0, "Withdrawal calculation failed");
-
+
// Mock: Transfer the resulting USDC to the user
// In production, this would execute actual token transfers via Soroban token interface
}
- /// Withdraw from Blend Protocol by redeeming bTokens
- ///
- /// # Arguments
- /// * `to` - The address to receive the withdrawn USDC
- /// * `b_tokens_to_redeem` - The amount of bTokens to redeem (or 0 to withdraw all)
- ///
- /// # Returns
- /// The amount of USDC received
- pub fn withdraw_from_blend(env: Env, to: Address, b_tokens_to_redeem: i128) -> i128 {
- to.require_auth();
-
- let blend_pool = Self::get_blend_pool(env.clone())
- .expect("Blend pool not initialized");
-
- // Get user's current Blend position
- let mut position: BlendPosition = env.storage().persistent()
- .get(&DataKey::UserBlendPosition(to.clone()))
- .unwrap_or(BlendPosition {
- b_tokens: 0,
- last_index_rate: INDEX_RATE_PRECISION,
- last_supply_time: 0,
- });
-
- assert!(position.b_tokens > 0, "No Blend position to withdraw");
-
- // Determine how many bTokens to redeem
- let b_tokens = if b_tokens_to_redeem == 0 {
- // Withdraw all if 0 is specified
- position.b_tokens
- } else {
- assert!(b_tokens_to_redeem <= position.b_tokens, "Insufficient bTokens");
- b_tokens_to_redeem
- };
-
- // Call Blend pool to withdraw assets
- let usdc_received = Self::call_blend_withdraw(&env, &blend_pool, &env.current_contract_address(), b_tokens);
-
- // Update user's Blend position
- position.b_tokens -= b_tokens;
- position.last_index_rate = Self::call_blend_index_rate(&env, &blend_pool);
- position.last_supply_time = env.ledger().timestamp();
-
- if position.b_tokens > 0 {
- env.storage().persistent().set(&DataKey::UserBlendPosition(to.clone()), &position);
- } else {
- // Remove position if fully withdrawn
- env.storage().persistent().remove(&DataKey::UserBlendPosition(to.clone()));
- }
-
- // Update total bTokens held by contract
- let total_b_tokens: i128 = env.storage().persistent()
- .get(&DataKey::TotalBTokens)
- .unwrap_or(0);
- env.storage().persistent().set(&DataKey::TotalBTokens, &(total_b_tokens - b_tokens));
-
- // Update legacy balance tracking
- let blend_balance: i128 = env.storage().persistent()
- .get(&DataKey::UserBlendBalance(to.clone()))
- .unwrap_or(0);
- // Calculate the corresponding USDC amount to deduct from legacy tracking
- let current_index_rate = Self::call_blend_index_rate(&env, &blend_pool);
- let usdc_equivalent = b_tokens * current_index_rate / INDEX_RATE_PRECISION;
- if blend_balance >= usdc_equivalent {
- env.storage().persistent().set(&DataKey::UserBlendBalance(to.clone()), &(blend_balance - usdc_equivalent));
- } else {
- env.storage().persistent().set(&DataKey::UserBlendBalance(to.clone()), &0i128);
- }
-
- // Transfer USDC to user
- Self::transfer_usdc_to_user(&env, &to, usdc_received);
-
- usdc_received
- }
-
/// Get user's Gold (XAUT) balance
pub fn get_gold_balance(env: Env, user: Address) -> i128 {
- env.storage().persistent().get(&DataKey::UserGoldBalance(user)).unwrap_or(0)
+ env.storage()
+ .persistent()
+ .get(&DataKey::UserGoldBalance(user))
+ .unwrap_or(0)
}
/// Get user's LP shares balance
pub fn get_lp_shares(env: Env, user: Address) -> i128 {
- env.storage().persistent().get(&DataKey::UserLPShares(user)).unwrap_or(0)
+ env.storage()
+ .persistent()
+ .get(&DataKey::UserLPShares(user))
+ .unwrap_or(0)
}
/// Get user's USDC balance
pub fn get_balance(env: Env, user: Address) -> i128 {
- env.storage().persistent().get(&DataKey::UserBalance(user)).unwrap_or(0)
+ env.storage()
+ .persistent()
+ .get(&DataKey::UserBalance(user))
+ .unwrap_or(0)
}
}
-// Basic Test Mock
+// Test Mocks & Unit Tests
#[cfg(test)]
mod test {
use super::*;
- use soroban_sdk::{testutils::Address as _, Env, Symbol};
-
- #[test]
- fn test_deposit_withdraw() {
- let env = Env::default();
- let contract_id = env.register_contract(None, SmasageYieldRouter);
- let client = SmasageYieldRouterClient::new(&env, &contract_id);
-
- let user = Address::generate(&env);
-
- env.mock_all_auths();
-
- // 60% Blend, 30% LP, 10% Gold
- client.deposit(&user, &1000, &60, &30, &10);
-
- assert_eq!(client.get_balance(&user), 1000);
- assert_eq!(client.get_gold_balance(&user), 100);
- assert_eq!(client.get_lp_shares(&user), 300);
-
- client.withdraw(&user, &500);
- assert_eq!(client.get_balance(&user), 500);
- }
-
- #[test]
- fn test_withdraw_unwinds_blend_and_lp() {
- let env = Env::default();
- let contract_id = env.register_contract(None, SmasageYieldRouter);
- let client = SmasageYieldRouterClient::new(&env, &contract_id);
-
- let user = Address::generate(&env);
- env.mock_all_auths();
-
- // Deposit with 60% to Blend, 30% to LP, 10% to Gold
- client.deposit(&user, &1000, &60, &30, &10);
-
- // Verify allocations
- assert_eq!(client.get_balance(&user), 1000);
- assert_eq!(client.get_gold_balance(&user), 100);
- assert_eq!(client.get_lp_shares(&user), 300);
-
- // Withdraw full amount - should unwind from all sources
- client.withdraw(&user, &1000);
- assert_eq!(client.get_balance(&user), 0);
- // Note: Gold and LP remain because withdrawal priority uses USDC first
- // In a real scenario, these would be unwound as needed
- assert_eq!(client.get_gold_balance(&user), 100);
- assert_eq!(client.get_lp_shares(&user), 300);
- }
-
- #[test]
- fn test_gold_allocation_tracking() {
- let env = Env::default();
- let contract_id = env.register_contract(None, SmasageYieldRouter);
- let client = SmasageYieldRouterClient::new(&env, &contract_id);
-
- let user = Address::generate(&env);
- env.mock_all_auths();
-
- // Deposit with 20% Gold allocation
- client.deposit(&user, &2000, &50, &30, &20);
-
- assert_eq!(client.get_gold_balance(&user), 400);
-
- // Partial withdrawal shouldn't affect gold unless needed
- client.withdraw(&user, &500);
- // Gold should remain intact if USDC balance is sufficient
- assert_eq!(client.get_gold_balance(&user), 400);
- }
-
- // ============================================
- // Blend Protocol Integration Tests
- // ============================================
-
- /// Mock USDC Token contract for testing
- mod mock_token {
- use soroban_sdk::{contract, contractimpl, contracttype, Env, Address};
-
- #[contracttype]
- pub enum TokenDataKey {
- Balance(Address),
- Allowance(Address, Address),
+ use soroban_sdk::{testutils::Address as _, Address, Env, String};
+
+ #[contract]
+ pub struct MockToken;
+ #[contractimpl]
+ impl TokenTrait for MockToken {
+ fn transfer(_e: Env, _from: Address, _to: Address, _amount: i128) {}
+ fn approve(
+ _e: Env,
+ _from: Address,
+ _spender: Address,
+ _amount: i128,
+ _expiration_ledger: u32,
+ ) {
}
-
- #[contract]
- pub struct MockToken;
-
- #[contractimpl]
- impl MockToken {
- pub fn initialize(env: Env, admin: Address) {
- env.storage().persistent().set(&TokenDataKey::Balance(admin.clone()), &10000000i128);
- }
-
- pub fn mint(env: Env, to: Address, amount: i128) {
- let balance: i128 = env.storage().persistent().get(&TokenDataKey::Balance(to.clone())).unwrap_or(0);
- env.storage().persistent().set(&TokenDataKey::Balance(to), &(balance + amount));
- }
-
- pub fn transfer(env: Env, from: Address, to: Address, amount: i128) {
- from.require_auth();
-
- let from_balance: i128 = env.storage().persistent().get(&TokenDataKey::Balance(from.clone())).unwrap_or(0);
- assert!(from_balance >= amount, "Insufficient balance");
-
- let to_balance: i128 = env.storage().persistent().get(&TokenDataKey::Balance(to.clone())).unwrap_or(0);
-
- env.storage().persistent().set(&TokenDataKey::Balance(from), &(from_balance - amount));
- env.storage().persistent().set(&TokenDataKey::Balance(to), &(to_balance + amount));
- }
-
- pub fn balance(env: Env, id: Address) -> i128 {
- env.storage().persistent().get(&TokenDataKey::Balance(id)).unwrap_or(0)
- }
+ fn balance(_e: Env, _id: Address) -> i128 {
+ 0
}
}
- /// Mock Blend Pool contract for testing
- mod mock_blend_pool {
- use soroban_sdk::{contract, contractimpl, contracttype, Env, Address};
- use super::super::INDEX_RATE_PRECISION;
-
- #[contracttype]
- pub enum MockDataKey {
- TotalSupply,
- BTokenSupply,
- IndexRate,
+ #[contract]
+ pub struct MockRouter;
+ #[contractimpl]
+ impl SoroswapRouterTrait for MockRouter {
+ fn add_liquidity(
+ _e: Env,
+ _token_a: Address,
+ _token_b: Address,
+ _amount_a_desired: i128,
+ _amount_b_desired: i128,
+ _amount_a_min: i128,
+ _amount_b_min: i128,
+ _to: Address,
+ _deadline: u64,
+ ) -> (i128, i128, i128) {
+ // Returns (amount_a_used, amount_b_used, lp_shares_minted)
+ (0, 0, 100)
}
- #[contract]
- pub struct MockBlendPool;
-
- #[contractimpl]
- impl MockBlendPool {
- pub fn initialize(env: Env, initial_index_rate: i128) {
- env.storage().persistent().set(&MockDataKey::TotalSupply, &0i128);
- env.storage().persistent().set(&MockDataKey::BTokenSupply, &0i128);
- env.storage().persistent().set(&MockDataKey::IndexRate, &initial_index_rate);
- }
-
- pub fn supply(env: Env, from: Address, amount: i128) -> i128 {
- let index_rate: i128 = env.storage().persistent().get(&MockDataKey::IndexRate).unwrap_or(INDEX_RATE_PRECISION);
-
- // Calculate bTokens: amount * INDEX_RATE_PRECISION / index_rate
- let b_tokens = amount * INDEX_RATE_PRECISION / index_rate;
-
- let total_supply: i128 = env.storage().persistent().get(&MockDataKey::TotalSupply).unwrap_or(0);
- let b_token_supply: i128 = env.storage().persistent().get(&MockDataKey::BTokenSupply).unwrap_or(0);
-
- env.storage().persistent().set(&MockDataKey::TotalSupply, &(total_supply + amount));
- env.storage().persistent().set(&MockDataKey::BTokenSupply, &(b_token_supply + b_tokens));
-
- b_tokens
- }
-
- pub fn withdraw(env: Env, to: Address, b_tokens: i128) -> i128 {
- let index_rate: i128 = env.storage().persistent().get(&MockDataKey::IndexRate).unwrap_or(INDEX_RATE_PRECISION);
-
- // Calculate underlying: bTokens * index_rate / INDEX_RATE_PRECISION
- let underlying = b_tokens * index_rate / INDEX_RATE_PRECISION;
-
- let total_supply: i128 = env.storage().persistent().get(&MockDataKey::TotalSupply).unwrap_or(0);
- let b_token_supply: i128 = env.storage().persistent().get(&MockDataKey::BTokenSupply).unwrap_or(0);
-
- env.storage().persistent().set(&MockDataKey::TotalSupply, &(total_supply - underlying));
- env.storage().persistent().set(&MockDataKey::BTokenSupply, &(b_token_supply - b_tokens));
-
- underlying
- }
-
- pub fn get_index_rate(env: Env) -> i128 {
- env.storage().persistent().get(&MockDataKey::IndexRate).unwrap_or(INDEX_RATE_PRECISION)
- }
-
- pub fn set_index_rate(env: Env, new_rate: i128) {
- env.storage().persistent().set(&MockDataKey::IndexRate, &new_rate);
- }
-
- pub fn get_b_token_supply(env: Env) -> i128 {
- env.storage().persistent().get(&MockDataKey::BTokenSupply).unwrap_or(0)
- }
-
- pub fn get_total_supply(env: Env) -> i128 {
- env.storage().persistent().get(&MockDataKey::TotalSupply).unwrap_or(0)
- }
+ fn swap_exact_tokens_for_tokens(
+ e: Env,
+ amount_in: i128,
+ _amount_out_min: i128,
+ _path: Vec,
+ _to: Address,
+ _deadline: u64,
+ ) -> Vec {
+ let mut v = Vec::new(&e);
+ v.push_back(amount_in);
+ v.push_back(amount_in * 2); // Mock 1:2 swap rate (USDC:XLM)
+ v
}
}
- use mock_token::MockToken;
- use mock_token::MockTokenClient;
- use mock_blend_pool::MockBlendPool;
- use mock_blend_pool::MockBlendPoolClient;
-
- #[test]
- fn test_blend_initialization() {
- let env = Env::default();
- let contract_id = env.register_contract(None, SmasageYieldRouter);
- let client = SmasageYieldRouterClient::new(&env, &contract_id);
-
- let blend_pool = Address::generate(&env);
- let usdc_token = Address::generate(&env);
-
- env.mock_all_auths();
-
- // Initialize contract
- client.initialize(&blend_pool, &usdc_token);
-
- // Verify initialization
- assert_eq!(client.get_blend_pool(), Some(blend_pool));
- assert_eq!(client.get_usdc_token(), Some(usdc_token));
- }
-
- #[test]
- fn test_blend_supply_and_btoken_tracking() {
+ /// Helper: set up the contract, admin, mocks, and return everything needed for tests.
+ fn setup_env() -> (
+ Env,
+ SmasageYieldRouterClient<'static>,
+ Address,
+ Address,
+ Address,
+ Address,
+ ) {
let env = Env::default();
-
- // Register contracts
- let contract_id = env.register_contract(None, SmasageYieldRouter);
+ let contract_id = env.register(SmasageYieldRouter, ());
let client = SmasageYieldRouterClient::new(&env, &contract_id);
-
- let blend_pool_id = env.register_contract(None, MockBlendPool);
- let blend_pool_client = MockBlendPoolClient::new(&env, &blend_pool_id);
-
- let token_id = env.register_contract(None, MockToken);
- let token_client = MockTokenClient::new(&env, &token_id);
-
- // Create addresses
- let user = Address::generate(&env);
-
- env.mock_all_auths();
- // Initialize token and mint to user
- token_client.initialize(&user);
- token_client.mint(&user, &10000);
+ let admin = Address::generate(&env);
- // Initialize Blend pool with 1.0 index rate
- blend_pool_client.initialize(&INDEX_RATE_PRECISION);
-
- // Initialize main contract with mock token
- client.initialize(&blend_pool_id, &token_id);
-
- // Supply 1000 USDC to Blend
- let b_tokens_received = client.supply_to_blend(&user, &1000);
-
- // Verify bTokens received (1:1 at initial index rate)
- assert_eq!(b_tokens_received, 1000);
-
- // Verify user's Blend position
- let position = client.get_blend_position(&user);
- assert_eq!(position.b_tokens, 1000);
- assert_eq!(position.last_index_rate, INDEX_RATE_PRECISION);
-
- // Verify legacy balance tracking
- let blend_balance = env.as_contract(&contract_id, || {
- env.storage().persistent().get::(&DataKey::UserBlendBalance(user.clone())).unwrap_or(0)
- });
- assert_eq!(blend_balance, 1000);
- }
-
- #[test]
- fn test_blend_yield_calculation() {
- let env = Env::default();
-
- // Register contracts
- let contract_id = env.register_contract(None, SmasageYieldRouter);
- let client = SmasageYieldRouterClient::new(&env, &contract_id);
-
- let blend_pool_id = env.register_contract(None, MockBlendPool);
- let blend_pool_client = MockBlendPoolClient::new(&env, &blend_pool_id);
-
- let token_id = env.register_contract(None, MockToken);
- let token_client = MockTokenClient::new(&env, &token_id);
-
- // Create addresses
- let user = Address::generate(&env);
+ let router_id = env.register(MockRouter, ());
+ let usdc_id = env.register(MockToken, ());
+ let xlm_id = env.register(MockToken, ());
env.mock_all_auths();
+ client.initialize(&admin);
+ client.initialize_soroswap(&admin, &router_id, &usdc_id, &xlm_id);
- // Initialize token and mint to user
- token_client.initialize(&user);
- token_client.mint(&user, &10000);
-
- // Initialize Blend pool with 1.0 index rate
- blend_pool_client.initialize(&INDEX_RATE_PRECISION);
-
- // Initialize main contract
- client.initialize(&blend_pool_id, &token_id);
-
- // Supply 1000 USDC to Blend
- client.supply_to_blend(&user, &1000);
-
- // Initially, no yield (index rate hasn't changed)
- let initial_yield = client.calculate_blend_yield(&user);
- assert_eq!(initial_yield, 0);
-
- // Simulate yield accrual by increasing index rate to 1.05 (5% yield)
- let new_index_rate = INDEX_RATE_PRECISION + (INDEX_RATE_PRECISION * 5 / 100); // 1.05
- client.set_mock_index_rate(&new_index_rate);
-
- // Calculate yield after index rate increase
- // Yield = bTokens * (current_index - last_index) / precision
- // Yield = 1000 * (1,050,000 - 1,000,000) / 1,000,000 = 50
- let yield_amount = client.calculate_blend_yield(&user);
- assert_eq!(yield_amount, 50);
-
- // Get position value (should be 1050 USDC worth)
- let position_value = client.get_blend_position_value(&user);
- assert_eq!(position_value, 1050);
+ (env, client, admin, router_id, usdc_id, xlm_id)
}
#[test]
- fn test_blend_withdraw() {
+ fn test_initialize_gold_trustline() {
let env = Env::default();
-
- // Register contracts
- let contract_id = env.register_contract(None, SmasageYieldRouter);
+ let contract_id = env.register(SmasageYieldRouter, ());
let client = SmasageYieldRouterClient::new(&env, &contract_id);
-
- let blend_pool_id = env.register_contract(None, MockBlendPool);
- let blend_pool_client = MockBlendPoolClient::new(&env, &blend_pool_id);
-
- let token_id = env.register_contract(None, MockToken);
- let token_client = MockTokenClient::new(&env, &token_id);
-
- // Create addresses
- let user = Address::generate(&env);
+ let admin = Address::generate(&env);
env.mock_all_auths();
-
- // Initialize token and mint to user
- token_client.initialize(&user);
- token_client.mint(&user, &10000);
-
- // Initialize Blend pool with 1.0 index rate
- blend_pool_client.initialize(&INDEX_RATE_PRECISION);
-
- // Initialize main contract
- client.initialize(&blend_pool_id, &token_id);
-
- // Supply 1000 USDC to Blend
- client.supply_to_blend(&user, &1000);
-
- // Verify position exists
- let position = client.get_blend_position(&user);
- assert_eq!(position.b_tokens, 1000);
-
- // Withdraw all bTokens (0 means withdraw all)
- let usdc_received = client.withdraw_from_blend(&user, &0);
-
- // Should receive 1000 USDC (1:1 at initial rate)
- assert_eq!(usdc_received, 1000);
-
- // Verify position is cleared
- let position_after = client.get_blend_position(&user);
- assert_eq!(position_after.b_tokens, 0);
+ client.initialize(&admin);
+ client.init_gold_trustline(&admin, &5_000_000);
+
+ let (asset_code, asset_issuer) = client.get_gold_asset();
+ assert_eq!(asset_code, symbol_short!("XAUT"));
+ assert_eq!(
+ asset_issuer,
+ String::from_str(
+ &env,
+ "GCRLXTLD7XIRXWXV2PDCC74O5TUUKN3OODJAM6TWVE4AIRNMGQJK3KWQ"
+ )
+ );
+ assert!(client.is_gold_trustline_ready());
+ assert_eq!(client.get_gold_reserve_stroops(), 5_000_000);
}
#[test]
- fn test_blend_partial_withdraw() {
- let env = Env::default();
-
- // Register contracts
- let contract_id = env.register_contract(None, SmasageYieldRouter);
- let client = SmasageYieldRouterClient::new(&env, &contract_id);
-
- let blend_pool_id = env.register_contract(None, MockBlendPool);
- let blend_pool_client = MockBlendPoolClient::new(&env, &blend_pool_id);
-
- let token_id = env.register_contract(None, MockToken);
- let token_client = MockTokenClient::new(&env, &token_id);
-
- // Create addresses
- let user = Address::generate(&env);
-
- env.mock_all_auths();
-
- // Initialize token and mint to user
- token_client.initialize(&user);
- token_client.mint(&user, &10000);
-
- // Initialize Blend pool with 1.0 index rate
- blend_pool_client.initialize(&INDEX_RATE_PRECISION);
-
- // Initialize main contract
- client.initialize(&blend_pool_id, &token_id);
+ fn test_deposit_and_withdraw() {
+ let (_env, client, _admin, _r, _u, _x) = setup_env();
+ let user = Address::generate(&_env);
- // Supply 1000 USDC to Blend
- client.supply_to_blend(&user, &1000);
-
- // Withdraw 400 bTokens (partial)
- let usdc_received = client.withdraw_from_blend(&user, &400);
-
- // Should receive 400 USDC
- assert_eq!(usdc_received, 400);
-
- // Verify remaining position
- let position = client.get_blend_position(&user);
- assert_eq!(position.b_tokens, 600);
- }
-
- #[test]
- fn test_blend_withdraw_with_yield() {
- let env = Env::default();
-
- // Register contracts
- let contract_id = env.register_contract(None, SmasageYieldRouter);
- let client = SmasageYieldRouterClient::new(&env, &contract_id);
-
- let blend_pool_id = env.register_contract(None, MockBlendPool);
- let blend_pool_client = MockBlendPoolClient::new(&env, &blend_pool_id);
-
- let token_id = env.register_contract(None, MockToken);
- let token_client = MockTokenClient::new(&env, &token_id);
-
- // Create addresses
- let user = Address::generate(&env);
-
- env.mock_all_auths();
-
- // Initialize token and mint to user and contract (for yield payout)
- token_client.initialize(&user);
- token_client.mint(&user, &10000);
- token_client.mint(&contract_id, &5000); // Mint extra to contract for yield payout
-
- // Initialize Blend pool with 1.0 index rate
- blend_pool_client.initialize(&INDEX_RATE_PRECISION);
-
- // Initialize main contract
- client.initialize(&blend_pool_id, &token_id);
-
- // Supply 1000 USDC to Blend
- client.supply_to_blend(&user, &1000);
-
- // Increase index rate to 1.10 (10% yield)
- let new_index_rate = INDEX_RATE_PRECISION + (INDEX_RATE_PRECISION * 10 / 100); // 1.10
- client.set_mock_index_rate(&new_index_rate);
-
- // Withdraw all bTokens
- let usdc_received = client.withdraw_from_blend(&user, &0);
-
- // Should receive 1100 USDC (1000 + 10% yield)
- assert_eq!(usdc_received, 1100);
- }
-
- #[test]
- fn test_blend_multiple_supplies() {
- let env = Env::default();
-
- // Register contracts
- let contract_id = env.register_contract(None, SmasageYieldRouter);
- let client = SmasageYieldRouterClient::new(&env, &contract_id);
-
- let blend_pool_id = env.register_contract(None, MockBlendPool);
- let blend_pool_client = MockBlendPoolClient::new(&env, &blend_pool_id);
-
- let token_id = env.register_contract(None, MockToken);
- let token_client = MockTokenClient::new(&env, &token_id);
-
- // Create addresses
- let user = Address::generate(&env);
-
- env.mock_all_auths();
-
- // Initialize token and mint to user
- token_client.initialize(&user);
- token_client.mint(&user, &10000);
-
- // Initialize Blend pool with 1.0 index rate
- blend_pool_client.initialize(&INDEX_RATE_PRECISION);
-
- // Initialize main contract
- client.initialize(&blend_pool_id, &token_id);
-
- // First supply: 500 USDC
- let b_tokens_1 = client.supply_to_blend(&user, &500);
- assert_eq!(b_tokens_1, 500);
-
- // Increase index rate to 1.05
- let new_index_rate = INDEX_RATE_PRECISION + (INDEX_RATE_PRECISION * 5 / 100);
- client.set_mock_index_rate(&new_index_rate);
-
- // Calculate yield BEFORE second supply (to capture yield from first supply)
- // First supply yield: 500 * (1,050,000 - 1,000,000) / 1,000,000 = 25
- let yield_amount = client.calculate_blend_yield(&user);
- assert_eq!(yield_amount, 25);
-
- // Second supply: 500 USDC (at new index rate)
- // bTokens = 500 * 1,000,000 / 1,050,000 = 476 (rounded)
- let b_tokens_2 = client.supply_to_blend(&user, &500);
- assert_eq!(b_tokens_2, 476);
-
- // Verify total position
- let position = client.get_blend_position(&user);
- assert_eq!(position.b_tokens, 976); // 500 + 476
-
- // After second supply, last_index_rate is updated to new rate, so yield shows 0
- // until index rate changes again
- let yield_after_second = client.calculate_blend_yield(&user);
- assert_eq!(yield_after_second, 0);
- }
-
- #[test]
- fn test_blend_position_value_accrual() {
- let env = Env::default();
-
- // Register contracts
- let contract_id = env.register_contract(None, SmasageYieldRouter);
- let client = SmasageYieldRouterClient::new(&env, &contract_id);
-
- let blend_pool_id = env.register_contract(None, MockBlendPool);
- let blend_pool_client = MockBlendPoolClient::new(&env, &blend_pool_id);
-
- let token_id = env.register_contract(None, MockToken);
- let token_client = MockTokenClient::new(&env, &token_id);
-
- // Create addresses
- let user = Address::generate(&env);
-
- env.mock_all_auths();
-
- // Initialize token and mint to user
- token_client.initialize(&user);
- token_client.mint(&user, &10000);
-
- // Initialize Blend pool with 1.0 index rate
- blend_pool_client.initialize(&INDEX_RATE_PRECISION);
-
- // Initialize main contract
- client.initialize(&blend_pool_id, &token_id);
-
- // Supply 2000 USDC to Blend
- client.supply_to_blend(&user, &2000);
-
- // Initial value should be 2000
- assert_eq!(client.get_blend_position_value(&user), 2000);
-
- // Simulate 1 year of yield at 5% APR
- let new_index_rate = INDEX_RATE_PRECISION + (INDEX_RATE_PRECISION * 5 / 100);
- client.set_mock_index_rate(&new_index_rate);
-
- // Value should now be 2100
- assert_eq!(client.get_blend_position_value(&user), 2100);
-
- // Simulate another 5% yield (compound)
- let new_index_rate_2 = new_index_rate + (new_index_rate * 5 / 100);
- client.set_mock_index_rate(&new_index_rate_2);
+ // Deposit 1000 USDC – 60% Blend, 30% LP
+ client.deposit(&user, &1000, &60, &30, &10);
+ assert_eq!(client.get_balance(&user), 1000);
- // Value should now be approximately 2205
- let value = client.get_blend_position_value(&user);
- assert!(value > 2200 && value <= 2205, "Expected value around 2205, got {}", value);
+ // Withdraw half
+ client.withdraw(&user, &500);
+ assert_eq!(client.get_balance(&user), 500);
}
#[test]
- #[should_panic(expected = "Amount must be greater than 0")]
- fn test_blend_supply_zero_amount() {
- let env = Env::default();
- let contract_id = env.register_contract(None, SmasageYieldRouter);
- let client = SmasageYieldRouterClient::new(&env, &contract_id);
-
- let blend_pool_id = env.register_contract(None, MockBlendPool);
- let blend_pool_client = MockBlendPoolClient::new(&env, &blend_pool_id);
-
- let token_id = env.register_contract(None, MockToken);
- let token_client = MockTokenClient::new(&env, &token_id);
-
- let user = Address::generate(&env);
-
- env.mock_all_auths();
+ fn test_soroswap_lp_basic() {
+ let (_env, client, _admin, _r, _u, _x) = setup_env();
+ let user = Address::generate(&_env);
- // Initialize token and mint to user
- token_client.initialize(&user);
- token_client.mint(&user, &10000);
+ // Deposit 1000 USDC, 50% to LP
+ client.deposit(&user, &1000, &0, &50, &0);
- blend_pool_client.initialize(&INDEX_RATE_PRECISION);
- client.initialize(&blend_pool_id, &token_id);
-
- // Should panic with zero amount
- client.supply_to_blend(&user, &0);
+ assert_eq!(client.get_balance(&user), 1000);
+ // MockRouter returns 100 LP shares for add_liquidity
+ assert_eq!(client.get_lp_shares(&user), 100);
}
#[test]
- #[should_panic(expected = "No Blend position to withdraw")]
- fn test_blend_withdraw_no_position() {
- let env = Env::default();
- let contract_id = env.register_contract(None, SmasageYieldRouter);
- let client = SmasageYieldRouterClient::new(&env, &contract_id);
-
- let blend_pool_id = env.register_contract(None, MockBlendPool);
- let blend_pool_client = MockBlendPoolClient::new(&env, &blend_pool_id);
-
- let user = Address::generate(&env);
- let usdc_token = Address::generate(&env);
-
- env.mock_all_auths();
-
- blend_pool_client.initialize(&INDEX_RATE_PRECISION);
- client.initialize(&blend_pool_id, &usdc_token);
+ #[should_panic(expected = "Allocation exceeds 100%")]
+ fn test_allocation_exceeds_100_percent() {
+ let (_env, client, _admin, _r, _u, _x) = setup_env();
+ let user = Address::generate(&_env);
- // Should panic - no position to withdraw
- client.withdraw_from_blend(&user, &0);
+ client.deposit(&user, &1000, &60, &50, &0); // 110% → panic
}
}
diff --git a/contracts/test_snapshots/test/test_deposit_withdraw.1.json b/contracts/test_snapshots/test/test_deposit_withdraw.1.json
index e26de91..b724afb 100644
--- a/contracts/test_snapshots/test/test_deposit_withdraw.1.json
+++ b/contracts/test_snapshots/test/test_deposit_withdraw.1.json
@@ -1,10 +1,29 @@
{
"generators": {
- "address": 2,
+ "address": 3,
"nonce": 0
},
"auth": [
[],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
[
[
"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
@@ -79,6 +98,45 @@
"min_temp_entry_ttl": 16,
"max_entry_ttl": 6312000,
"ledger_entries": [
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "vec": [
+ {
+ "symbol": "Admin"
+ }
+ ]
+ },
+ "durability": "persistent"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "vec": [
+ {
+ "symbol": "Admin"
+ }
+ ]
+ },
+ "durability": "persistent",
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M"
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ],
[
{
"contract_data": {
@@ -309,7 +367,7 @@
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
"key": {
"ledger_key_nonce": {
- "nonce": 801925984706572462
+ "nonce": 1033654523790656264
}
},
"durability": "temporary"
@@ -324,7 +382,7 @@
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
"key": {
"ledger_key_nonce": {
- "nonce": 801925984706572462
+ "nonce": 1033654523790656264
}
},
"durability": "temporary",
@@ -369,6 +427,39 @@
6311999
]
],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": 801925984706572462
+ }
+ },
+ "durability": "temporary"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": 801925984706572462
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ 6311999
+ ]
+ ],
[
{
"contract_code": {