Skip to content

Commit 4e05feb

Browse files
authored
Merge pull request soterika#200 from Onyii1234/feature/vault-deposit-function
feat: enhance vault deposit function with indexed events and admin co…
2 parents 77b39bd + 0db2e4e commit 4e05feb

5 files changed

Lines changed: 262 additions & 142 deletions

File tree

aura-vault/src/errors.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ pub enum VaultError {
1414
MathOverflow = 6,
1515
InvalidAddress = 7,
1616
ZeroShares = 8,
17-
TimelockNotExpired = 9,
18-
NotApproved = 10,
19-
AlreadyVoted = 11,
17+
UpgradeUnauthorized = 9,
18+
StorageLayoutMismatch = 10,
19+
VaultPaused = 11,
20+
BalanceMismatch = 12,
2021
}

aura-vault/src/interface.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use soroban_sdk::{contractspecentry, Address, Env, Vec, Symbol};
1+
use soroban_sdk::{Address, Env, Vec, Symbol, BytesN, String};
22
use crate::errors::VaultError;
33

44
/// Public ABI for AuraVault. Implemented by the contract in lib.rs.
@@ -8,11 +8,16 @@ pub trait AuraVaultTrait {
88
fn deposit(env: Env, caller: Address, amount: i128) -> Result<i128, VaultError>;
99
fn withdraw(env: Env, caller: Address, shares: i128) -> Result<i128, VaultError>;
1010
fn harvest(env: Env, caller: Address, yield_amount: i128) -> Result<(), VaultError>;
11-
fn pause(env: Env) -> Result<(), VaultError>;
12-
fn unpause(env: Env) -> Result<(), VaultError>;
11+
fn pause(env: Env, admin: Address) -> Result<(), VaultError>;
12+
fn unpause(env: Env, admin: Address) -> Result<(), VaultError>;
1313
fn is_paused(env: Env) -> bool;
14+
fn set_fees(env: Env, admin: Address, perf_fee_bps: u32, mgmt_fee_bps: u32) -> Result<(), VaultError>;
15+
fn set_treasury(env: Env, admin: Address, treasury: Address) -> Result<(), VaultError>;
16+
fn withdraw_fees(env: Env, admin: Address) -> Result<i128, VaultError>;
17+
fn total_fees_collected(env: Env) -> i128;
1418
fn total_assets(env: Env) -> i128;
1519
fn balance_of(env: Env, address: Address) -> i128;
20+
fn upgrade(env: Env, new_wasm_hash: BytesN<32>) -> Result<(), VaultError>;
1621
fn propose_update_admin(env: Env, proposer: Address, new_admin: Address) -> Result<u64, VaultError>;
1722
fn propose_update_token(env: Env, proposer: Address, new_token: Address) -> Result<u64, VaultError>;
1823
fn propose_parameter_update(env: Env, proposer: Address, name: Symbol, value: i128) -> Result<u64, VaultError>;

aura-vault/src/lib.rs

Lines changed: 137 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ mod errors;
44
mod interface;
55
mod storage;
66
mod governance;
7+
mod fee;
78

89
pub 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
}

aura-vault/src/storage.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ pub enum DataKey {
1616
MgmtFeeBps,
1717
TotalFeeCollected,
1818
LastMgmtFeeTime,
19+
/// Whitelisted alternative yield tokens (Issue #48)
20+
YieldToken(Address),
1921
}
2022

2123
pub const DAY_IN_LEDGERS: u32 = 17_280;

0 commit comments

Comments
 (0)