Skip to content

Commit 73aaf15

Browse files
authored
Merge pull request #85 from OpadijoIdris/feat/stellar-benji-connector
feat: integrate BENJI strategy connector for YieldVault
2 parents ae90b63 + 302e7ca commit 73aaf15

4 files changed

Lines changed: 259 additions & 0 deletions

File tree

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
use soroban_sdk::{contract, contractimpl, contracttype, token, Address, Env};
2+
use crate::strategy::StrategyTrait;
3+
4+
#[contracttype]
5+
#[derive(Clone, Debug, Eq, PartialEq)]
6+
pub enum StrategyDataKey {
7+
Vault,
8+
Asset,
9+
BenjiToken,
10+
}
11+
12+
#[contract]
13+
pub struct BenjiStrategy;
14+
15+
#[contractimpl]
16+
impl BenjiStrategy {
17+
pub fn initialize(env: Env, vault: Address, asset: Address, benji_token: Address) {
18+
if env.storage().instance().has(&StrategyDataKey::Vault) {
19+
panic!("already initialized");
20+
}
21+
env.storage().instance().set(&StrategyDataKey::Vault, &vault);
22+
env.storage().instance().set(&StrategyDataKey::Asset, &asset);
23+
env.storage().instance().set(&StrategyDataKey::BenjiToken, &benji_token);
24+
}
25+
}
26+
27+
#[contractimpl]
28+
impl StrategyTrait for BenjiStrategy {
29+
fn deposit(env: Env, amount: i128) {
30+
let vault: Address = env.storage().instance().get(&StrategyDataKey::Vault).unwrap();
31+
vault.require_auth();
32+
33+
let asset_addr: Address = env.storage().instance().get(&StrategyDataKey::Asset).unwrap();
34+
let benji_addr: Address = env.storage().instance().get(&StrategyDataKey::BenjiToken).unwrap();
35+
36+
let asset_client = token::Client::new(&env, &asset_addr);
37+
let benji_client = token::Client::new(&env, &benji_addr);
38+
39+
// Transfer USDC from Vault to Strategy
40+
asset_client.transfer(&vault, &env.current_contract_address(), &amount);
41+
42+
// In a real BENJI integration, we would swap USDC for BENJI or call a specific mint function.
43+
// For this connector, we assume BENJI tokens are issued 1:1 for the underlying asset.
44+
// BENJI is a "Fund Token" on Stellar.
45+
46+
// Simulating the purchase of BENJI tokens
47+
// logic: benji_client.mint(&env.current_contract_address(), &amount);
48+
// Note: Real BENJI has a patent-pending daily accrual mechanism.
49+
}
50+
51+
fn withdraw(env: Env, amount: i128) {
52+
let vault: Address = env.storage().instance().get(&StrategyDataKey::Vault).unwrap();
53+
vault.require_auth();
54+
55+
let asset_addr: Address = env.storage().instance().get(&StrategyDataKey::Asset).unwrap();
56+
let asset_client = token::Client::new(&env, &asset_addr);
57+
58+
// Transfer USDC from Strategy back to Vault
59+
asset_client.transfer(&env.current_contract_address(), &vault, &amount);
60+
}
61+
62+
fn total_value(env: Env) -> i128 {
63+
let benji_addr: Address = env.storage().instance().get(&StrategyDataKey::BenjiToken).unwrap();
64+
let benji_client = token::Client::new(&env, &benji_addr);
65+
66+
// The value is the balance of BENJI tokens held by this contract.
67+
// Assumes 1 BENJI = 1 USD/USDC.
68+
benji_client.balance(&env.current_contract_address())
69+
}
70+
71+
fn asset(env: Env) -> Address {
72+
env.storage().instance().get(&StrategyDataKey::Asset).unwrap()
73+
}
74+
}

contracts/vault/src/lib.rs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
#![no_std]
22

33
mod test;
4+
pub mod strategy;
5+
pub mod benji_strategy;
46

57
use soroban_sdk::{
68
contract, contractclient, contracterror, contractimpl, contracttype, symbol_short, token,
79
Address, Env, Vec,
810
};
11+
use crate::strategy::StrategyClient;
912

1013
const MAX_PAGE_SIZE: u32 = 50;
1114

@@ -40,6 +43,7 @@ pub enum DataKey {
4043
TotalShares,
4144
TotalAssets,
4245
Admin,
46+
Strategy,
4347
State,
4448
DaoThreshold,
4549
ProposalNonce,
@@ -97,6 +101,21 @@ impl YieldVault {
97101

98102
env.storage().instance().set(&DataKey::Admin, &admin);
99103
env.storage().instance().set(&DataKey::TokenAsset, &token);
104+
env.storage().instance().set(&DataKey::TotalAssets, &0i128);
105+
env.storage().instance().set(&DataKey::DaoThreshold, &1i128);
106+
env.storage().instance().set(&DataKey::ProposalNonce, &0u32);
107+
}
108+
109+
/// Set or update the active strategy connector.
110+
pub fn set_strategy(env: Env, strategy: Address) {
111+
let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap();
112+
admin.require_auth();
113+
env.storage().instance().set(&DataKey::Strategy, &strategy);
114+
}
115+
116+
/// Read the active strategy address.
117+
pub fn strategy(env: Env) -> Option<Address> {
118+
env.storage().instance().get(&DataKey::Strategy)
100119

101120
// Initialize the unified state
102121
let state = VaultState {
@@ -144,6 +163,18 @@ impl YieldVault {
144163
Self::get_state(&env).total_shares
145164
}
146165

166+
/// Read the total underlying assets (idle in vault + invested in strategy).
167+
pub fn total_assets(env: Env) -> i128 {
168+
let idle_assets = env.storage().instance().get::<_, i128>(&DataKey::TotalAssets).unwrap_or(0);
169+
170+
let strategy_assets = if let Some(strategy_addr) = Self::strategy(env.clone()) {
171+
let strategy_client = StrategyClient::new(&env, &strategy_addr);
172+
strategy_client.total_value()
173+
} else {
174+
0
175+
};
176+
177+
idle_assets + strategy_assets
147178
pub fn total_assets(env: Env) -> i128 {
148179
Self::get_state(&env).total_assets
149180
}
@@ -483,6 +514,12 @@ impl YieldVault {
483514

484515
token_client.transfer(&user, &env.current_contract_address(), &amount);
485516

517+
// Update idle state
518+
let ta = env.storage().instance().get::<_, i128>(&DataKey::TotalAssets).unwrap_or(0);
519+
env.storage().instance().set(&DataKey::TotalAssets, &(ta + amount));
520+
521+
let ts = Self::total_shares(env.clone());
522+
env.storage().instance().set(&DataKey::TotalShares, &(ts + shares_to_mint));
486523
state.total_assets += amount;
487524
state.total_shares += shares_to_mint;
488525
env.storage().instance().set(&DataKey::State, &state);
@@ -532,6 +569,26 @@ impl YieldVault {
532569

533570
let token_addr = Self::token(env.clone());
534571
let token_client = token::Client::new(&env, &token_addr);
572+
573+
// Check if vault has enough idle assets, otherwise divest from strategy
574+
let mut idle_ta = env.storage().instance().get::<_, i128>(&DataKey::TotalAssets).unwrap_or(0);
575+
if idle_ta < assets_to_return {
576+
let needed = assets_to_return - idle_ta;
577+
Self::divest(env.clone(), needed);
578+
idle_ta = env.storage().instance().get::<_, i128>(&DataKey::TotalAssets).unwrap_or(0);
579+
}
580+
581+
// Transfer assets from vault to user
582+
token_client.transfer(&env.current_contract_address(), &user, &assets_to_return);
583+
584+
// Update state
585+
env.storage().instance().set(&DataKey::TotalAssets, &(idle_ta - assets_to_return));
586+
587+
let ts = Self::total_shares(env.clone());
588+
env.storage().instance().set(&DataKey::TotalShares, &(ts - shares));
589+
590+
let vault_balance = Self::balance(env.clone(), user.clone());
591+
env.storage().instance().set(&DataKey::ShareBalance(user.clone()), &(vault_balance - shares));
535592
token_client.transfer(&env.current_contract_address(), &user, &assets_to_return);
536593

537594
state.total_assets -= assets_to_return;
@@ -549,12 +606,53 @@ impl YieldVault {
549606
Ok(assets_to_return)
550607
}
551608

609+
/// Move idle funds to the strategy.
610+
pub fn invest(env: Env, amount: i128) {
611+
let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap();
612+
admin.require_auth();
613+
614+
let strategy_addr = Self::strategy(env.clone()).expect("no strategy set");
615+
let strategy_client = StrategyClient::new(&env, &strategy_addr);
616+
617+
let mut idle_ta = env.storage().instance().get::<_, i128>(&DataKey::TotalAssets).unwrap_or(0);
618+
if idle_ta < amount { panic!("insufficient idle assets"); }
619+
620+
// Approve and deposit to strategy
621+
let token_addr = Self::token(env.clone());
622+
let token_client = token::Client::new(&env, &token_addr);
623+
token_client.approve(&env.current_contract_address(), &strategy_addr, &amount, &env.ledger().sequence());
624+
625+
strategy_client.deposit(&amount);
626+
627+
// Update idle assets
628+
env.storage().instance().set(&DataKey::TotalAssets, &(idle_ta - amount));
629+
}
630+
631+
/// Recall funds from the strategy.
632+
pub fn divest(env: Env, amount: i128) {
633+
// Can be called by admin or internally by withdraw
634+
let strategy_addr = Self::strategy(env.clone()).expect("no strategy set");
635+
let strategy_client = StrategyClient::new(&env, &strategy_addr);
636+
637+
strategy_client.withdraw(&amount);
638+
639+
// The strategy contract should have transferred funds back to the vault
640+
let idle_ta = env.storage().instance().get::<_, i128>(&DataKey::TotalAssets).unwrap_or(0);
641+
env.storage().instance().set(&DataKey::TotalAssets, &(idle_ta + amount));
642+
}
643+
644+
/// Admin function to artificially accrue yield (legacy, but updated for strategy).
552645
pub fn accrue_yield(env: Env, amount: i128) {
553646
let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap();
554647
admin.require_auth();
555648

556649
let token_addr = Self::token(env.clone());
557650
let token_client = token::Client::new(&env, &token_addr);
651+
652+
token_client.transfer(&admin, &env.current_contract_address(), &amount);
653+
654+
let ta = env.storage().instance().get::<_, i128>(&DataKey::TotalAssets).unwrap_or(0);
655+
env.storage().instance().set(&DataKey::TotalAssets, &(ta + amount));
558656
token_client.transfer(&admin, &env.current_contract_address(), &amount);
559657

560658
let mut state = Self::get_state(&env);

contracts/vault/src/strategy.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
use soroban_sdk::{contractclient, Address, Env};
2+
3+
#[contractclient(name = "StrategyClient")]
4+
pub trait StrategyTrait {
5+
/// Deposits an amount of the underlying asset into the strategy.
6+
fn deposit(env: Env, amount: i128);
7+
8+
/// Withdraws an amount of the underlying asset from the strategy.
9+
fn withdraw(env: Env, amount: i128);
10+
11+
/// Returns the total value of assets held by the strategy (including accrued yield).
12+
fn total_value(env: Env) -> i128;
13+
14+
/// Returns the address of the underlying asset (e.g., USDC).
15+
fn asset(env: Env) -> Address;
16+
}

contracts/vault/src/test.rs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424
#![cfg(test)]
2525

2626
use super::*;
27+
use soroban_sdk::testutils::{Address as _};
28+
use soroban_sdk::{token, Address, Env};
29+
use crate::benji_strategy::{BenjiStrategy, BenjiStrategyClient};
2730
use soroban_sdk::testutils::Address as _;
2831
use soroban_sdk::{token, Address, Env, Vec};
2932

@@ -60,6 +63,69 @@ fn setup_vault(
6063
// ─── 1. initialize ───────────────────────────────────────────────────────────
6164

6265
#[test]
66+
fn test_vault_with_benji_strategy() {
67+
let env = Env::default();
68+
env.mock_all_auths();
69+
70+
let admin = Address::generate(&env);
71+
let user = Address::generate(&env);
72+
73+
// Setup USDC (Underlying Asset)
74+
let token_admin = Address::generate(&env);
75+
let usdc = create_token_contract(&env, &token_admin);
76+
let usdc_admin_client = token::StellarAssetClient::new(&env, &usdc.address);
77+
usdc_admin_client.mint(&user, &1000);
78+
79+
// Setup BENJI Token (Strategy Asset)
80+
let benji_token = create_token_contract(&env, &token_admin);
81+
let benji_admin_client = token::StellarAssetClient::new(&env, &benji_token.address);
82+
83+
// Register Contracts
84+
let vault_id = env.register(YieldVault, ());
85+
let vault = YieldVaultClient::new(&env, &vault_id);
86+
87+
let strategy_id = env.register(BenjiStrategy, ());
88+
let strategy = BenjiStrategyClient::new(&env, &strategy_id);
89+
90+
// 1. Initialize
91+
vault.initialize(&admin, &usdc.address);
92+
strategy.initialize(&vault_id, &usdc.address, &benji_token.address);
93+
vault.set_strategy(&strategy_id);
94+
95+
// 2. User Deposits 100 USDC
96+
vault.deposit(&user, &100);
97+
assert_eq!(vault.total_assets(), 100);
98+
assert_eq!(usdc.balance(&vault_id), 100);
99+
assert_eq!(strategy.total_value(), 0);
100+
101+
// 3. Invest 60 USDC into BENJI Strategy
102+
vault.invest(&60);
103+
assert_eq!(usdc.balance(&vault_id), 40);
104+
assert_eq!(usdc.balance(&strategy_id), 60);
105+
106+
// In our mock, strategy value depends on BENJI tokens held by contract
107+
// Let's simulate the strategy contract "buying" BENJI tokens
108+
benji_admin_client.mint(&strategy_id, &60);
109+
assert_eq!(strategy.total_value(), 60);
110+
assert_eq!(vault.total_assets(), 100); // 40 idle + 60 in strategy
111+
112+
// 4. Yield Accrues in BENJI (Daily return)
113+
benji_admin_client.mint(&strategy_id, &6); // 10% yield
114+
assert_eq!(strategy.total_value(), 66);
115+
assert_eq!(vault.total_assets(), 106); // 40 idle + 66 in strategy
116+
117+
// 5. User Withdraws some shares.
118+
// Vault has 40 idle assets, but user wants to withdraw 50 shares (value ~53 USDC)
119+
// This should trigger an internal divestment
120+
let withdrawn = vault.withdraw(&user, &50);
121+
assert_eq!(withdrawn, 53); // 50 shares * 106 assets / 100 shares = 53
122+
123+
assert_eq!(vault.total_shares(), 50);
124+
assert_eq!(vault.total_assets(), 53);
125+
}
126+
127+
#[test]
128+
fn test_vault_flow_legacy() {
63129
fn test_initialize_sets_state() {
64130
let env = Env::default();
65131
env.mock_all_auths();
@@ -130,6 +196,7 @@ fn test_deposit_second_user_proportional_shares() {
130196
}
131197

132198
#[test]
199+
fn test_governance_sets_benji_strategy() {
133200
fn test_deposit_zero_returns_invalid_amount_error() {
134201
let env = Env::default();
135202
env.mock_all_auths();
@@ -178,12 +245,14 @@ fn test_deposit_tiny_amount_after_large_yield_mints_zero_shares() {
178245
// ─── 3. withdraw ─────────────────────────────────────────────────────────────
179246

180247
#[test]
248+
fn test_benji_connector_reports_yield() {
181249
fn test_withdraw_happy_path_receives_correct_assets() {
182250
let env = Env::default();
183251
env.mock_all_auths();
184252

185253
let (vault, usdc, usdc_sa, admin) = setup_vault(&env);
186254
let user = Address::generate(&env);
255+
let benji_strategy = Address::generate(&env);
187256
usdc_sa.mint(&user, &200);
188257
usdc_sa.mint(&admin, &100);
189258

@@ -208,6 +277,8 @@ fn test_withdraw_zero_shares_returns_error() {
208277
usdc_sa.mint(&user, &100);
209278
vault.deposit(&user, &100);
210279

280+
vault.report_benji_yield(&benji_strategy, &40);
281+
assert_eq!(vault.total_assets(), 540);
211282
let result = vault.try_withdraw(&user, &0);
212283
assert!(result.is_err());
213284
}

0 commit comments

Comments
 (0)