Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions contracts/async-vault/src/deposit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,27 @@ use crate::state::{self, DepositRequest, EpochStatus};
use crate::treasury;
use crate::wind_down;

fn refuse_if_exceeds_cap(e: &Env, amount: i128) {
let Some(cap) = state::deposit_cap(e) else {
return;
};
let current = treasury::deposited_capital(e);
let new_total = current
.checked_add(amount)
.unwrap_or_else(|| panic_with_error!(e, VaultError::AmountTooLarge));
if new_total > cap {
panic_with_error!(e, VaultError::DepositCapExceeded);
}
}

pub(crate) fn request(e: &Env, from: &Address, amount: i128) -> u64 {
wind_down::refuse_if_active(e);
from.require_auth();

if amount <= 0 {
panic_with_error!(e, VaultError::InvalidAmount);
}
refuse_if_exceeds_cap(e, amount);

let epoch_id = state::current_epoch(e);

Expand Down
2 changes: 2 additions & 0 deletions contracts/async-vault/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,4 +109,6 @@ pub enum VaultError {
/// The notice exceeds the upgrade delay, so an investor could not complete
/// an exit before an upgrade lands.
NoticeAboveUpgradeDelay = 6061,
/// A deposit request would breach the vault's configured deposit cap.
DepositCapExceeded = 6062,
}
6 changes: 6 additions & 0 deletions contracts/async-vault/src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,9 @@ pub struct WindDownClaimed {
pub surrendered: i128,
pub assets: i128,
}

#[contractevent]
pub struct DepositCapUpdated {
pub old_cap: Option<i128>,
pub new_cap: Option<i128>,
}
1 change: 1 addition & 0 deletions contracts/async-vault/src/keys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,5 @@ pub(crate) enum DataKey {
WindDownAcc,
WindDownOwed,
WindDownPosition(Address),
DepositCap,
}
30 changes: 26 additions & 4 deletions contracts/async-vault/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@ use state::FIRST_EPOCH;

pub use error::VaultError;
pub use event::{
CustodianSet, Deployed, DepositClaimed, DepositRequested, EpochClosed, EpochFulfilled, Funded,
NoticeSet, RedeemClaimed, RedeemRequested, UpgradeCancelled, UpgradeDelayProposed,
UpgradeDelaySet, UpgradeProposed, Upgraded, WindDownActivated, WindDownClaimed,
WindDownDelaySet, WindDownProposalCancelled, WindDownProposed, WindDownRoundFinalized,
CustodianSet, Deployed, DepositCapUpdated, DepositClaimed, DepositRequested, EpochClosed,
EpochFulfilled, Funded, NoticeSet, RedeemClaimed, RedeemRequested, UpgradeCancelled,
UpgradeDelayProposed, UpgradeDelaySet, UpgradeProposed, Upgraded, WindDownActivated,
WindDownClaimed, WindDownDelaySet, WindDownProposalCancelled, WindDownProposed,
WindDownRoundFinalized,
};
pub use pricing::{DirectUnitPricing, PricingScheme};
pub use roles::VaultRoles;
Expand Down Expand Up @@ -164,6 +165,27 @@ impl AsyncVault {
.unwrap_or_else(|| panic_with_error!(e, VaultError::AmountTooLarge))
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR body names total_economic_assets(), which is not in the diff. Leftover from before the metric was simplified?


pub fn deposit_cap(e: &Env) -> Option<i128> {
state::deposit_cap(e)
}

#[only_admin]
pub fn set_deposit_cap(e: &Env, cap: Option<i128>, caller: Address) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New governance parameter, but §8.8's Configuration row still lists only bounds, freshness, timelock and wind-down delay. Worth adding the cap there?

caller.require_auth();
if let Some(c) = cap {
if c < 0 {
panic_with_error!(e, VaultError::InvalidAmount);
}
}
let old_cap = state::deposit_cap(e);
state::set_deposit_cap(e, cap);
DepositCapUpdated {
old_cap,
new_cap: cap,
}
.publish(e);
}

#[only_admin]
pub fn set_custodian(e: &Env, custodian: Address, caller: Address) {
caller.require_auth();
Expand Down
11 changes: 11 additions & 0 deletions contracts/async-vault/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,3 +170,14 @@ pub(crate) fn set_pending_burn_shares(e: &Env, shares: i128) {
pub(crate) fn pending_burn_shares(e: &Env) -> i128 {
storage::get_instance(e, &DataKey::PendingBurnShares).unwrap_or(0)
}

pub(crate) fn deposit_cap(e: &Env) -> Option<i128> {
storage::get_instance(e, &DataKey::DepositCap)
}

pub(crate) fn set_deposit_cap(e: &Env, cap: Option<i128>) {
match cap {
Some(val) => storage::set_instance(e, &DataKey::DepositCap, &val),
None => e.storage().instance().remove(&DataKey::DepositCap),
}
}
188 changes: 188 additions & 0 deletions contracts/async-vault/src/test/deposit_cap.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
use super::*;

#[test]
fn deposit_cap_defaults_to_none_unbounded() {
let f = setup();
assert_eq!(f.vault.deposit_cap(), None);

let investor = f.investor(50_000);
let epoch = f.vault.request_deposit(&investor, &50_000);
assert_eq!(f.vault.get_epoch(&epoch).unwrap().total_deposited, 50_000);
}

#[test]
fn governance_sets_updates_and_clears_cap() {
let f = setup();

// Set cap
f.vault.set_deposit_cap(&Some(1_000), &f.admin);
assert_eq!(f.vault.deposit_cap(), Some(1_000));

// Update cap
f.vault.set_deposit_cap(&Some(2_000), &f.admin);
assert_eq!(f.vault.deposit_cap(), Some(2_000));

// Clear cap
f.vault.set_deposit_cap(&None, &f.admin);
assert_eq!(f.vault.deposit_cap(), None);
}

#[test]
fn negative_cap_rejected() {
let f = setup();
refused(
f.vault.try_set_deposit_cap(&Some(-1), &f.admin),
VaultError::InvalidAmount,
);
}

#[test]
fn unauthorized_caller_rejected() {
let f = setup();
let rando = Address::generate(&f.e);

f.e.set_auths(&[]);
assert!(f.vault.try_set_deposit_cap(&Some(1_000), &rando).is_err());
assert!(f
.vault
.try_set_deposit_cap(&Some(1_000), &f.manager)
.is_err());
assert!(f
.vault
.try_set_deposit_cap(&Some(1_000), &f.guardian)
.is_err());
assert!(f
.vault
.try_set_deposit_cap(&Some(1_000), &f.treasury)
.is_err());
}

#[test]
fn boundary_value_enforcement() {
let f = setup();
f.vault.set_deposit_cap(&Some(1_000), &f.admin);

let investor1 = f.investor(2_000);
let investor2 = f.investor(2_000);

// Deposit exceeding cap from empty vault fails
refused(
f.vault.try_request_deposit(&investor1, &1_001),
VaultError::DepositCapExceeded,
);

// Deposit exactly equal to cap succeeds
let epoch = f.vault.request_deposit(&investor1, &1_000);
assert_eq!(f.vault.get_epoch(&epoch).unwrap().total_deposited, 1_000);

// Any subsequent deposit fails
refused(
f.vault.try_request_deposit(&investor2, &1),
VaultError::DepositCapExceeded,
);
}

#[test]
fn cancellation_frees_capacity() {
let f = setup();
f.vault.set_deposit_cap(&Some(1_000), &f.admin);

let investor1 = f.investor(1_000);
let investor2 = f.investor(1_000);

let epoch_id = f.vault.request_deposit(&investor1, &1_000);

// Blocked while request is pending
refused(
f.vault.try_request_deposit(&investor2, &500),
VaultError::DepositCapExceeded,
);

// Cancel refund restores capacity
f.vault.cancel_deposit(&investor1, &epoch_id);
assert_eq!(f.vault.get_epoch(&epoch_id).unwrap().total_deposited, 0);

// Now investor2 can deposit
f.vault.request_deposit(&investor2, &500);
assert_eq!(f.vault.get_epoch(&epoch_id).unwrap().total_deposited, 500);
}

#[test]
fn deployed_capital_and_multi_epoch_accounting() {
let f = setup();
f.vault.set_deposit_cap(&Some(1_000), &f.admin);

let investor1 = f.investor(1_000);
let investor2 = f.investor(1_000);

// Epoch 1 deposit
f.vault.request_deposit(&investor1, &1_000);
f.vault.close_epoch(&f.manager);
f.attest(wad(1));
f.vault.fulfill_epoch(&1);

// Deploy 800 to custodian
f.vault.set_custodian(&f.custodian, &f.admin);
f.vault.deploy_to_custodian(&f.treasury, &800);
assert_eq!(f.vault.net_deployed(), 800);

// In Epoch 2, deposit of 1 still breaches cap because deployed capital is counted
refused(
f.vault.try_request_deposit(&investor2, &1),
VaultError::DepositCapExceeded,
);

// Governance raises cap to 1_500
f.vault.set_deposit_cap(&Some(1_500), &f.admin);

// Now investor2 can deposit 500
let epoch2 = f.vault.request_deposit(&investor2, &500);
assert_eq!(f.vault.get_epoch(&epoch2).unwrap().total_deposited, 500);
}

#[test]
fn redemptions_free_capacity() {
let f = setup();
f.vault.set_deposit_cap(&Some(1_000), &f.admin);

let investor1 = f.investor(1_000);
let investor2 = f.investor(1_000);

// Epoch 1: investor1 deposits 1_000 and claims 1_000 shares
f.vault.request_deposit(&investor1, &1_000);
f.vault.close_epoch(&f.manager);
f.attest(wad(1));
f.vault.fulfill_epoch(&1);
f.vault.claim_deposit(&investor1, &1);

// In Epoch 2, deposited capital is 1_000, so new deposit of 500 fails
refused(
f.vault.try_request_deposit(&investor2, &500),
VaultError::DepositCapExceeded,
);

// Investor 1 requests redemption of 500 shares
f.vault.request_redeem(&investor1, &500);
f.vault.close_epoch(&f.manager);
f.attest(wad(1));
f.vault.fulfill_epoch(&2);

// Redemption is now priced and committed (500 committed to exit)
assert_eq!(f.vault.committed(), 500);

// In Epoch 3, headroom has opened up by 500, so investor2 can deposit 500
let epoch3 = f.vault.request_deposit(&investor2, &500);
assert_eq!(f.vault.get_epoch(&epoch3).unwrap().total_deposited, 500);
}

#[test]
fn zero_cap_blocks_all_deposits() {
let f = setup();
f.vault.set_deposit_cap(&Some(0), &f.admin);

let investor = f.investor(100);
refused(
f.vault.try_request_deposit(&investor, &1),
VaultError::DepositCapExceeded,
);
}
1 change: 1 addition & 0 deletions contracts/async-vault/src/test/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ mod constructor;
mod controls;
mod conversions;
mod deposit;
mod deposit_cap;
mod epochs;
mod multi_epoch;
mod notice;
Expand Down
6 changes: 6 additions & 0 deletions contracts/async-vault/src/treasury.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ pub(crate) fn uncovered(e: &Env) -> i128 {
(state::committed(e) - liquid_reserve(e)).max(0)
}

/// Capital in the vault and deployed to the custodian, less liabilities already
/// committed to priced exits. Used to evaluate headroom against the deposit cap.
pub(crate) fn deposited_capital(e: &Env) -> i128 {
(held(e) + state::net_deployed(e)).saturating_sub(state::committed(e))
}

pub(crate) fn deploy(e: &Env, assets: i128) -> i128 {
wind_down::refuse_if_active(e);
if assets <= 0 {
Expand Down
Loading