@@ -4,6 +4,7 @@ mod errors;
44mod interface;
55mod storage;
66mod governance;
7+ mod fee;
78
89pub use errors:: VaultError ;
910
@@ -44,13 +45,20 @@ impl AuraVault {
4445 set_token ( & env, & underlying_token) ;
4546 set_total_shares ( & env, 0 ) ;
4647 set_total_deposited ( & env, 0 ) ;
48+ set_version ( & env, 1 ) ;
49+ set_layout_version ( & env, CURRENT_LAYOUT_VERSION ) ;
4750 initialize_governance ( & env, signers) ?;
4851 bump_instance ( & env) ;
4952 Ok ( ( ) )
5053 }
5154
5255 // -----------------------------------------------------------------------
5356 // deposit
57+ //
58+ // Issue requirement: Emit Deposit event with indexed user and amount.
59+ // In Soroban, topics (first tuple) are indexed; data (second value) is not.
60+ // We place `caller` and `amount` in topics so they can be efficiently
61+ // filtered by indexers.
5462 // -----------------------------------------------------------------------
5563 pub fn deposit ( env : Env , caller : Address , amount : i128 ) -> Result < i128 , VaultError > {
5664 caller. require_auth ( ) ;
@@ -81,7 +89,7 @@ impl AuraVault {
8189
8290 let total_shares = get_total_shares ( & env) ;
8391
84- // Compute shares to mint (checked arithmetic, overflow returns MathOverflow)
92+ // Compute shares to mint (checked arithmetic; overflow returns MathOverflow)
8593 let new_shares: i128 = if total_shares == 0 || total_deposited == 0 {
8694 amount
8795 } else {
@@ -102,7 +110,9 @@ impl AuraVault {
102110
103111 // Effects: write state after successful transfer
104112 let old_balance = get_balance ( & env, & caller) ;
105- let new_balance = old_balance + new_shares;
113+ let new_balance = old_balance
114+ . checked_add ( new_shares)
115+ . ok_or ( VaultError :: MathOverflow ) ?;
106116 set_balance ( & env, & caller, new_balance) ;
107117 let new_total_shares = total_shares
108118 . checked_add ( new_shares)
@@ -113,9 +123,11 @@ impl AuraVault {
113123 . ok_or ( VaultError :: MathOverflow ) ?;
114124 set_total_deposited ( & env, new_total_deposited) ;
115125
126+ // Event: topics = (event_name, caller, amount) — indexed for efficient filtering.
127+ // data = (new_shares, new_total_shares, new_total_deposited) — contextual payload.
116128 env. events ( ) . publish (
117- ( Symbol :: new ( & env, "deposit" ) , ) ,
118- ( caller , amount , new_shares, new_total_shares, new_total_deposited) ,
129+ ( Symbol :: new ( & env, "deposit" ) , caller . clone ( ) , amount ) ,
130+ ( new_shares, new_total_shares, new_total_deposited) ,
119131 ) ;
120132
121133 bump_persistent ( & env, & caller) ;
@@ -143,12 +155,6 @@ impl AuraVault {
143155 let token_addr = get_token ( & env) . ok_or ( VaultError :: NotInitialized ) ?;
144156 let token = token:: Client :: new ( & env, & token_addr) ;
145157
146- // Withdrawal queue guard (flash-loan prevention): the vault's actual
147- // on-chain token balance must equal the internally tracked
148- // `total_deposited` before any withdrawal is processed. If an
149- // attacker flash-loans tokens into the vault to inflate
150- // `balance_before`, this check fires and the transaction reverts,
151- // preventing share-price manipulation attacks.
152158 let balance_before = token. balance ( & env. current_contract_address ( ) ) ;
153159 let total_deposited = get_total_deposited ( & env) ;
154160 if balance_before != total_deposited {
@@ -195,9 +201,10 @@ impl AuraVault {
195201 // Interaction: send tokens to caller after state is settled
196202 token. transfer ( & env. current_contract_address ( ) , & caller, & redeem_amount) ;
197203
204+ // Event: topics = (event_name, caller, shares) — indexed for efficient filtering.
198205 env. events ( ) . publish (
199- ( Symbol :: new ( & env, "withdraw" ) , ) ,
200- ( caller , shares , redeem_amount, new_total_shares, new_total_deposited) ,
206+ ( Symbol :: new ( & env, "withdraw" ) , caller . clone ( ) , shares ) ,
207+ ( redeem_amount, new_total_shares, new_total_deposited) ,
201208 ) ;
202209
203210 bump_persistent ( & env, & caller) ;
@@ -232,7 +239,7 @@ impl AuraVault {
232239 let token_addr = get_token ( & env) . ok_or ( VaultError :: NotInitialized ) ?;
233240 let token = token:: Client :: new ( & env, & token_addr) ;
234241
235- // Flash-loan guard: actual token balance must equal tracked state before harvest.
242+ // Flash-loan guard
236243 let balance_before = token. balance ( & env. current_contract_address ( ) ) ;
237244 if balance_before != total_deposited {
238245 env. events ( ) . publish (
@@ -243,15 +250,14 @@ impl AuraVault {
243250 }
244251
245252 let perf_fee_bps = storage:: get_perf_fee_bps ( & env) ;
246- let _mgmt_fee_bps = storage:: get_mgmt_fee_bps ( & env) ;
247- let perf_fee = fee:: calc_perf_fee ( yield_amount, perf_fee_bps) ?;
253+ let fee_amount = fee:: calc_perf_fee ( yield_amount, perf_fee_bps) ?;
248254 let yield_after_fee = yield_amount
249- . checked_sub ( perf_fee )
255+ . checked_sub ( fee_amount )
250256 . ok_or ( VaultError :: MathOverflow ) ?;
251257
252258 let current_fees = storage:: get_total_fee_collected ( & env) ;
253259 let new_fees = current_fees
254- . checked_add ( perf_fee )
260+ . checked_add ( fee_amount )
255261 . ok_or ( VaultError :: MathOverflow ) ?;
256262
257263 let new_total = total_deposited
@@ -266,8 +272,8 @@ impl AuraVault {
266272 storage:: set_total_fee_collected ( & env, new_fees) ;
267273
268274 env. events ( ) . publish (
269- ( Symbol :: new ( & env, "harvest" ) , ) ,
270- ( caller , yield_amount , yield_after_fee, fee_amount) ,
275+ ( Symbol :: new ( & env, "harvest" ) , caller . clone ( ) , yield_amount ) ,
276+ ( yield_after_fee, fee_amount, new_total ) ,
271277 ) ;
272278
273279 bump_instance ( & env) ;
@@ -277,12 +283,6 @@ impl AuraVault {
277283
278284 // -----------------------------------------------------------------------
279285 // harvest_token — multi-yield-token entry point (Issue #48)
280- //
281- // Accepts any SEP-41 token as yield. The vault transfers the alt token
282- // from the caller, converts it to underlying at `exchange_rate`
283- // (underlying_units_per_alt_token, 7-decimal fixed-point), and credits
284- // the converted amount to total_deposited. The admin must have
285- // pre-approved the alt_token address via `register_yield_token`.
286286 // -----------------------------------------------------------------------
287287 pub fn harvest_token (
288288 env : Env ,
@@ -327,7 +327,6 @@ impl AuraVault {
327327 return Err ( VaultError :: BalanceMismatch ) ;
328328 }
329329
330- // Compute fee on the underlying-equivalent amount
331330 let perf_fee_bps = storage:: get_perf_fee_bps ( & env) ;
332331 let fee_amount = fee:: calc_perf_fee ( underlying_amount, perf_fee_bps)
333332 . unwrap_or ( 0 ) ;
@@ -352,8 +351,8 @@ impl AuraVault {
352351 ) ;
353352
354353 env. events ( ) . publish (
355- ( Symbol :: new ( & env, "harvest_token" ) , ) ,
356- ( caller , alt_token , yield_amount, net_underlying, fee_amount) ,
354+ ( Symbol :: new ( & env, "harvest_token" ) , caller , alt_token ) ,
355+ ( yield_amount, net_underlying, fee_amount) ,
357356 ) ;
358357
359358 bump_instance ( & env) ;
@@ -378,18 +377,25 @@ impl AuraVault {
378377
379378 // -----------------------------------------------------------------------
380379 // pause / unpause — admin-only emergency controls
380+ // Takes admin address so the client can require_auth on it.
381381 // -----------------------------------------------------------------------
382- pub fn pause ( env : Env ) -> Result < ( ) , VaultError > {
383- let admin = get_admin ( & env) . ok_or ( VaultError :: NotInitialized ) ?;
382+ pub fn pause ( env : Env , admin : Address ) -> Result < ( ) , VaultError > {
383+ let stored_admin = get_admin ( & env) . ok_or ( VaultError :: NotInitialized ) ?;
384+ if stored_admin != admin {
385+ return Err ( VaultError :: UpgradeUnauthorized ) ;
386+ }
384387 admin. require_auth ( ) ;
385388 set_paused ( & env, true ) ;
386389 env. events ( ) . publish ( ( Symbol :: new ( & env, "paused" ) , ) , ( ) ) ;
387390 bump_instance ( & env) ;
388391 Ok ( ( ) )
389392 }
390393
391- pub fn unpause ( env : Env ) -> Result < ( ) , VaultError > {
392- let admin = get_admin ( & env) . ok_or ( VaultError :: NotInitialized ) ?;
394+ pub fn unpause ( env : Env , admin : Address ) -> Result < ( ) , VaultError > {
395+ let stored_admin = get_admin ( & env) . ok_or ( VaultError :: NotInitialized ) ?;
396+ if stored_admin != admin {
397+ return Err ( VaultError :: UpgradeUnauthorized ) ;
398+ }
393399 admin. require_auth ( ) ;
394400 set_paused ( & env, false ) ;
395401 env. events ( ) . publish ( ( Symbol :: new ( & env, "unpaused" ) , ) , ( ) ) ;
@@ -402,19 +408,111 @@ impl AuraVault {
402408 }
403409
404410 // -----------------------------------------------------------------------
405- // total_assets (read-only — no bumps, no writes)
411+ // Fee administration — admin-only
412+ // -----------------------------------------------------------------------
413+
414+ /// Set performance and management fee rates (basis points).
415+ pub fn set_fees ( env : Env , admin : Address , perf_fee_bps : u32 , mgmt_fee_bps : u32 ) -> Result < ( ) , VaultError > {
416+ let stored_admin = get_admin ( & env) . ok_or ( VaultError :: NotInitialized ) ?;
417+ if stored_admin != admin {
418+ return Err ( VaultError :: UpgradeUnauthorized ) ;
419+ }
420+ admin. require_auth ( ) ;
421+ storage:: set_perf_fee_bps ( & env, perf_fee_bps) ;
422+ storage:: set_mgmt_fee_bps ( & env, mgmt_fee_bps) ;
423+ bump_instance ( & env) ;
424+ Ok ( ( ) )
425+ }
426+
427+ /// Set treasury address where fees are sent on withdrawal.
428+ pub fn set_treasury ( env : Env , admin : Address , treasury : Address ) -> Result < ( ) , VaultError > {
429+ let stored_admin = get_admin ( & env) . ok_or ( VaultError :: NotInitialized ) ?;
430+ if stored_admin != admin {
431+ return Err ( VaultError :: UpgradeUnauthorized ) ;
432+ }
433+ admin. require_auth ( ) ;
434+ storage:: set_treasury ( & env, & treasury) ;
435+ bump_instance ( & env) ;
436+ Ok ( ( ) )
437+ }
438+
439+ /// Withdraw accumulated fees to the treasury. Admin-only.
440+ pub fn withdraw_fees ( env : Env , admin : Address ) -> Result < i128 , VaultError > {
441+ let stored_admin = get_admin ( & env) . ok_or ( VaultError :: NotInitialized ) ?;
442+ if stored_admin != admin {
443+ return Err ( VaultError :: UpgradeUnauthorized ) ;
444+ }
445+ admin. require_auth ( ) ;
446+
447+ let fees = storage:: get_total_fee_collected ( & env) ;
448+ if fees <= 0 {
449+ return Ok ( 0 ) ;
450+ }
451+
452+ let treasury = storage:: get_treasury ( & env) . ok_or ( VaultError :: NotInitialized ) ?;
453+ let token_addr = get_token ( & env) . ok_or ( VaultError :: NotInitialized ) ?;
454+ let token = token:: Client :: new ( & env, & token_addr) ;
455+
456+ // Adjust total_deposited: fees were already excluded from it during harvest,
457+ // so we just transfer from vault balance.
458+ token. transfer ( & env. current_contract_address ( ) , & treasury, & fees) ;
459+ storage:: set_total_fee_collected ( & env, 0 ) ;
460+
461+ env. events ( ) . publish (
462+ ( Symbol :: new ( & env, "fees_withdrawn" ) , admin) ,
463+ ( fees, treasury) ,
464+ ) ;
465+
466+ bump_instance ( & env) ;
467+ Ok ( fees)
468+ }
469+
470+ /// Read total accumulated (unwithdrawn) fees.
471+ pub fn total_fees_collected ( env : Env ) -> i128 {
472+ storage:: get_total_fee_collected ( & env)
473+ }
474+
475+ // -----------------------------------------------------------------------
476+ // total_assets (read-only)
406477 // -----------------------------------------------------------------------
407478 pub fn total_assets ( env : Env ) -> i128 {
408479 get_total_deposited ( & env)
409480 }
410481
411482 // -----------------------------------------------------------------------
412- // balance_of (read-only — no bumps, no writes )
483+ // balance_of (read-only)
413484 // -----------------------------------------------------------------------
414485 pub fn balance_of ( env : Env , address : Address ) -> i128 {
415486 get_balance ( & env, & address)
416487 }
417488
489+ // -----------------------------------------------------------------------
490+ // Upgrade
491+ // -----------------------------------------------------------------------
492+ pub fn upgrade ( env : Env , new_wasm_hash : soroban_sdk:: BytesN < 32 > ) -> Result < ( ) , VaultError > {
493+ let admin = get_admin ( & env) . ok_or ( VaultError :: NotInitialized ) ?;
494+ admin. require_auth ( ) ;
495+
496+ let current_version = get_layout_version ( & env) ;
497+ if current_version != CURRENT_LAYOUT_VERSION {
498+ return Err ( VaultError :: StorageLayoutMismatch ) ;
499+ }
500+
501+ let old_version = get_version ( & env) ;
502+ let new_version = old_version + 1 ;
503+ set_version ( & env, new_version) ;
504+
505+ env. deployer ( ) . update_current_contract_wasm ( new_wasm_hash) ;
506+
507+ env. events ( ) . publish (
508+ ( Symbol :: new ( & env, "upgrade" ) , admin) ,
509+ ( old_version, new_version) ,
510+ ) ;
511+
512+ bump_instance ( & env) ;
513+ Ok ( ( ) )
514+ }
515+
418516 // -----------------------------------------------------------------------
419517 // Governance Methods
420518 // -----------------------------------------------------------------------
@@ -455,13 +553,13 @@ impl AuraVault {
455553 Ok ( ( ) )
456554 }
457555
458- pub fn proposal_status ( env : Env , proposal_id : u64 ) -> Option < String > {
556+ pub fn proposal_status ( env : Env , proposal_id : u64 ) -> Option < soroban_sdk :: String > {
459557 get_proposal_status ( & env, proposal_id) . map ( |status| {
460558 match status {
461- ProposalStatus :: Pending => "Pending" . to_string ( ) ,
462- ProposalStatus :: Approved => "Approved" . to_string ( ) ,
463- ProposalStatus :: Executed => "Executed" . to_string ( ) ,
464- ProposalStatus :: Rejected => "Rejected" . to_string ( ) ,
559+ ProposalStatus :: Pending => soroban_sdk :: String :: from_str ( & env , "Pending" ) ,
560+ ProposalStatus :: Approved => soroban_sdk :: String :: from_str ( & env , "Approved" ) ,
561+ ProposalStatus :: Executed => soroban_sdk :: String :: from_str ( & env , "Executed" ) ,
562+ ProposalStatus :: Rejected => soroban_sdk :: String :: from_str ( & env , "Rejected" ) ,
465563 }
466564 } )
467565 }
0 commit comments