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
35 changes: 35 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 26 additions & 0 deletions contracts/async-vault/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[package]
name = "async-vault"
version = "0.1.0"
edition.workspace = true
license.workspace = true
publish = false

[lib]
crate-type = ["lib", "cdylib"]
doctest = false

[dependencies]
soroban-sdk = { workspace = true }
storage = { path = "../../crates/storage" }
stellar-access = { workspace = true }
stellar-macros = { workspace = true }
stellar-contract-utils = { workspace = true }
bindings = { path = "../../crates/bindings" }


[dev-dependencies]
soroban-sdk = { workspace = true, features = ["testutils"] }
nav-oracle = { path = "../nav-oracle" }
share-token = { path = "../share-token" }
compliance = { path = "../compliance" }
identity-verifier = { path = "../identity-verifier" }
94 changes: 94 additions & 0 deletions contracts/async-vault/src/deposit.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
use bindings::ShareClient;
use soroban_sdk::{panic_with_error, token::TokenClient, Address, Env};
use stellar_contract_utils::math::{i128_fixed_point::checked_mul_div_floor, wad::WAD_SCALE};

use crate::error::VaultError;
use crate::event::{DepositClaimed, DepositRequested};
use crate::keys::DataKey;
use crate::state::{self, DepositRequest, EpochStatus};

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

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

let epoch_id = state::current_epoch(e);

if state::get_deposit_request(e, epoch_id, from).is_some() {
panic_with_error!(e, VaultError::RequestOutstanding);
}

let mut epoch = state::get_epoch(e, epoch_id)
.unwrap_or_else(|| panic_with_error!(e, VaultError::EpochNotFound));

epoch.total_deposited = epoch
.total_deposited
.checked_add(amount)
.unwrap_or_else(|| panic_with_error!(e, VaultError::AmountTooLarge));

state::set_deposit_request(
e,
epoch_id,
from,
&DepositRequest {
amount,
claimed: false,
},
);
state::set_epoch(e, epoch_id, &epoch);

let asset = state::get_addr(e, &DataKey::Asset);
TokenClient::new(e, &asset).transfer(from, e.current_contract_address(), &amount);
Comment thread
hpmaxi marked this conversation as resolved.

DepositRequested {
controller: from.clone(),
epoch: epoch_id,
amount,
}
.publish(e);

epoch_id
}

pub(crate) fn claim(e: &Env, caller: &Address, epoch_id: u64) -> i128 {
caller.require_auth();

let epoch = state::get_epoch(e, epoch_id)
.unwrap_or_else(|| panic_with_error!(e, VaultError::EpochNotFound));

if epoch.status != EpochStatus::Fulfilled {
panic_with_error!(e, VaultError::EpochNotFulfilled);
}

let mut request = state::get_deposit_request(e, epoch_id, caller)
.unwrap_or_else(|| panic_with_error!(e, VaultError::RequestNotFound));

if request.claimed {
panic_with_error!(e, VaultError::AlreadyClaimed);
}

let shares = checked_mul_div_floor(e, &request.amount, &WAD_SCALE, &epoch.share_price)
.unwrap_or_else(|| panic_with_error!(e, VaultError::AmountTooLarge));

if shares == 0 {
panic_with_error!(e, VaultError::NothingToClaim);
}

request.claimed = true;
state::set_deposit_request(e, epoch_id, caller, &request);

let share_token = state::get_addr(e, &DataKey::ShareToken);
ShareClient::new(e, &share_token).mint(caller, &shares, &e.current_contract_address());
Comment thread
hpmaxi marked this conversation as resolved.

DepositClaimed {
controller: caller.clone(),
epoch: epoch_id,
amount: request.amount,
shares,
}
.publish(e);

shares
}
88 changes: 88 additions & 0 deletions contracts/async-vault/src/epoch.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
use bindings::OracleFeedClient;
use soroban_sdk::{panic_with_error, token::TokenClient, Env};
use stellar_contract_utils::math::{i128_fixed_point::checked_mul_div_floor, wad::WAD_SCALE};

use crate::error::VaultError;
use crate::event::{EpochClosed, EpochFulfilled};
use crate::keys::DataKey;
use crate::state::{self, EpochInfo, EpochStatus};

pub(crate) fn open(total_deposited: i128) -> EpochInfo {
EpochInfo {
status: EpochStatus::Open,
total_deposited,
total_shares_redeeming: 0,
share_price: 0,
}
}

pub(crate) fn close(e: &Env) -> u64 {
let current = state::current_epoch(e);
let mut epoch = state::get_epoch(e, current)
.unwrap_or_else(|| panic_with_error!(e, VaultError::EpochNotFound));

if epoch.status != EpochStatus::Open {
panic_with_error!(e, VaultError::EpochNotOpen);
}

let next = current
.checked_add(1)
.unwrap_or_else(|| panic_with_error!(e, VaultError::EpochOverflow));

epoch.status = EpochStatus::Pending;
state::set_epoch(e, current, &epoch);

state::set_epoch(e, next, &open(0));
state::set_current_epoch(e, next);

EpochClosed {
epoch: current,
total_deposited: epoch.total_deposited,
total_shares_redeeming: epoch.total_shares_redeeming,
}
.publish(e);

current
}

pub(crate) fn fulfill(e: &Env, epoch_id: u64) -> i128 {
let mut epoch = state::get_epoch(e, epoch_id)
.unwrap_or_else(|| panic_with_error!(e, VaultError::EpochNotFound));

if epoch.status != EpochStatus::Pending {
panic_with_error!(e, VaultError::EpochNotPending);
}

let feed = OracleFeedClient::new(e, &state::get_addr(e, &DataKey::Oracle));
feed.ensure_consumable();

let share_price = feed.nav_per_share();
Comment thread
hpmaxi marked this conversation as resolved.
if share_price <= 0 {
panic_with_error!(e, VaultError::InvalidSharePrice);
}

let owed = checked_mul_div_floor(e, &epoch.total_shares_redeeming, &share_price, &WAD_SCALE)
.unwrap_or_else(|| panic_with_error!(e, VaultError::AmountTooLarge));
let pending = state::pending_redeem_assets(e)
.checked_add(owed)
.unwrap_or_else(|| panic_with_error!(e, VaultError::AmountTooLarge));

let asset = state::get_addr(e, &DataKey::Asset);
if TokenClient::new(e, &asset).balance(&e.current_contract_address()) < pending {
Comment thread
hpmaxi marked this conversation as resolved.
panic_with_error!(e, VaultError::InsufficientLiquidity);
}
state::set_pending_redeem_assets(e, pending);

epoch.status = EpochStatus::Fulfilled;
epoch.share_price = share_price;
state::set_epoch(e, epoch_id, &epoch);

EpochFulfilled {
epoch: epoch_id,
share_price,
total_deposited: epoch.total_deposited,
}
.publish(e);

share_price
}
55 changes: 55 additions & 0 deletions contracts/async-vault/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
use soroban_sdk::contracterror;

/// Vault failures. Codes are stable once assigned and are never reused; a
/// removed variant leaves its number retired.
#[contracterror]
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[repr(u32)]
pub enum VaultError {
/// The constructor was given the same address for two authorities that
/// must be held separately.
RolesNotDistinct = 6000,
/// The controller has no request in the given epoch.
RequestNotFound = 6001,
/// An amount or share quantity of zero or less was specified.
InvalidAmount = 6007,
/// The controller already holds an unclaimed request in this epoch. A
/// second request is rejected rather than added to the first, so that at
/// most one request per controller per epoch holds by construction.
RequestOutstanding = 6009,
/// The epoch total would exceed `i128::MAX`.
AmountTooLarge = 6014,
/// No epoch is stored under the given id.
EpochNotFound = 6029,
/// An entry the constructor writes is absent from instance storage, which
/// means the instance was archived or the contract was never constructed.
NotInitialized = 6030,
/// The oracle returned a share price of zero or less. Unreachable with a
/// correctly configured feed; kept because the oracle sits behind a
/// settable address.
InvalidSharePrice = 6031,
/// The epoch is not `Open`, so it cannot be closed or take new requests.
EpochNotOpen = 6032,
/// The epoch is not `Pending`, so it cannot be priced. An
/// epoch must be closed before it can be fulfilled.
EpochNotPending = 6038,
/// The epoch counter would exceed `u64::MAX`.
EpochOverflow = 6033,
/// The epoch has not been fulfilled yet, so it has no share price to
/// claim against.
EpochNotFulfilled = 6034,
/// The request was already claimed. Claiming is idempotent by rejection,
/// not by silently minting nothing twice.
AlreadyClaimed = 6035,
/// The deposit is smaller than one share at the epoch's price, so it would
/// mint zero. Rejected rather than burning the deposit to dust.
NothingToClaim = 6036,
/// The vault does not hold enough assets to settle the epoch's redemptions
/// at the attested price, so the epoch is not fulfilled at all.
InsufficientLiquidity = 6037,
/// The amount would deploy assets already owed to holders whose exit has
/// been priced but not yet claimed.
ReserveCommittedToExits = 6005,
/// No custodian has been set, so capital has nowhere to go.
CustodianNotSet = 6012,
}
73 changes: 73 additions & 0 deletions contracts/async-vault/src/event.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
use soroban_sdk::{contractevent, Address};

#[contractevent]
pub struct DepositRequested {
#[topic]
pub controller: Address,
pub epoch: u64,
pub amount: i128,
}

#[contractevent]
pub struct EpochFulfilled {
#[topic]
pub epoch: u64,
pub share_price: i128,
pub total_deposited: i128,
}

#[contractevent]
pub struct DepositClaimed {
#[topic]
pub controller: Address,
pub epoch: u64,
pub amount: i128,
pub shares: i128,
}

#[contractevent]
pub struct RedeemRequested {
#[topic]
pub controller: Address,
pub epoch: u64,
pub shares: i128,
}

#[contractevent]
pub struct RedeemClaimed {
#[topic]
pub controller: Address,
pub epoch: u64,
pub shares: i128,
pub assets: i128,
}

#[contractevent]
pub struct EpochClosed {
#[topic]
pub epoch: u64,
pub total_deposited: i128,
pub total_shares_redeeming: i128,
}

#[contractevent]
pub struct CustodianSet {
#[topic]
pub custodian: Address,
}

#[contractevent]
pub struct Deployed {
#[topic]
pub custodian: Address,
pub assets: i128,
pub net_deployed: i128,
}

#[contractevent]
pub struct Funded {
#[topic]
pub from: Address,
pub assets: i128,
pub net_deployed: i128,
}
Loading