-
Notifications
You must be signed in to change notification settings - Fork 0
Build the async vault: epochs, claims, treasury and roles #70
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
hpmaxi
wants to merge
21
commits into
main
Choose a base branch
from
feat/async-vault-contract
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
14d4bc5
feat: nav oracle contracts and guards
hpmaxi 703f235
test(nav-oracle): assert the guards attest has
hpmaxi 8b0fd7e
chore: add nav oracle to environments
hpmaxi 057a34a
style: format the stub contractmeta calls
hpmaxi b93c2d3
feat(async-vault): add contract skeleton
hpmaxi 51c506f
chore: remove old docs
hpmaxi da22f2f
feat: add storage crate
hpmaxi d6b544a
feat: use storage crate in nav-oracle
hpmaxi 0c2f206
feat: async-vault initial skeleton
hpmaxi 606e6ee
feat: deposit
hpmaxi 058f245
feat: epoch fulfill
hpmaxi 54cd30b
feat: claim methods and WAD price
hpmaxi 1da54e9
chore: remove old noun-verb mismatch file
hpmaxi 6e7b960
feat: wire oracle, add redeem
hpmaxi d12c05d
chore: refactor tests
hpmaxi 7d78f6d
feat: add claim
hpmaxi 3ac5208
feat: add pending state
hpmaxi 5c32c26
feat: treasury fund
hpmaxi b577008
feat: complete auth roles
hpmaxi 42653c0
fix: env variables for CI
hpmaxi 4f1d2ba
fix: pr review fixes and remove unused module pricing
hpmaxi File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
|
|
||
| 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()); | ||
|
hpmaxi marked this conversation as resolved.
|
||
|
|
||
| DepositClaimed { | ||
| controller: caller.clone(), | ||
| epoch: epoch_id, | ||
| amount: request.amount, | ||
| shares, | ||
| } | ||
| .publish(e); | ||
|
|
||
| shares | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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(); | ||
|
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 { | ||
|
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 | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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, | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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, | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.