diff --git a/AGENTS.md b/AGENTS.md index 5fcd374d..20038573 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -114,6 +114,39 @@ Not recorded here on purpose. The reference base kept them in this file and they drifted: its `AGENTS.md` and its generated client pointed at two different vault contracts. The addresses emitted by the deploy script are authoritative. +## Code conventions + +- **Crate names.** Shared crates in `crates/` start with `strata-` + (`strata-timelock`, `strata-storage`, `strata-bindings`), so a path such as + `strata_timelock::DAY_DURATION` reads as the kit's own code. +- **Addresses in entrypoints.** In an entrypoint the kit defines, `caller` is + the account that must sign the call, and `holder` is the account whose + position, request or balance the call is about. When one account is both, the + entrypoint takes `holder` and that account signs. The two ends of a share + transfer are `from` and `to`, and other addresses are named for what they + are (`custodian`, `vault`, the allowlisted `account`). OpenZeppelin trait + methods keep their own names (`operator`, `user_address`). +- **Named errors.** Contract code fails with a named error variant, never with + `unwrap`, `expect` or a bare `panic!`. Tests may unwrap. +- **No boolean switches.** A new entrypoint does one thing. Two behaviours get + two functions, like `freeze_partial_tokens` and `unfreeze_partial_tokens`, + not one function that takes a `bool`. +- **Guards.** Each precondition is a `pub(crate) fn` in the contract's + `guards.rs`. Its name is the condition it checks (`positive_amount`, + `not_winding_down`), and it panics with a named error when the condition + fails. An entrypoint calls its guards first and then does its work, so its + preconditions are the first lines a reader sees. +- **Doc comments.** Every public entrypoint has one, in OpenZeppelin's style: a + one-line summary, then `# Arguments` with units (asset base units, share base + units, WAD prices, seconds or ledgers), `# Errors`, `# Events` and + `# Authorization`. Every error variant's doc says when it happens. Keep them + short: they are stored in the Wasm as part of the contract spec. +- **Design notes.** Each contract's `lib.rs` opens with a `//! # Design` + section: five to ten bullets, each a choice and its trade-off, in plain words + and with no issue numbers or decision ids. +- **Exact pins.** Every crates.io dependency, dev-dependencies included, is + pinned with `=` to the version in `Cargo.lock`. + ## Conventions - Conventional Commits, imperative, subject ≤ 50 chars. **No AI attribution.** @@ -121,8 +154,8 @@ contracts. The addresses emitted by the deploy script are authoritative. `feat/-`, `fix/...`, `chore/...`. - Issue bodies follow the templates in `.github/ISSUE_TEMPLATE/`, 200 words of prose hard cap. Titles are natural language with no prefix. -- Exact dependency pins. Dependencies are never upgraded mid-milestone; each - migration is its own issue in the following one. +- Dependencies are never upgraded mid-milestone; each migration is its own + issue in the following one. - Estimates live in the `Estimate` field of the [project board](https://github.com/orgs/BootNodeDev/projects/29), not in issue bodies. diff --git a/Cargo.lock b/Cargo.lock index 6e8107a6..983faeda 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -184,7 +184,6 @@ checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" name = "async-vault" version = "0.1.0" dependencies = [ - "bindings", "compliance", "identity-verifier", "nav-oracle", @@ -195,8 +194,9 @@ dependencies = [ "stellar-contract-utils", "stellar-macros", "stellar-tokens", - "storage", - "timelock", + "strata-bindings", + "strata-storage", + "strata-timelock", ] [[package]] @@ -223,13 +223,6 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" -[[package]] -name = "bindings" -version = "0.1.0" -dependencies = [ - "soroban-sdk", -] - [[package]] name = "bit-set" version = "0.8.0" @@ -351,13 +344,13 @@ dependencies = [ name = "compliance" version = "0.1.0" dependencies = [ - "bindings", "soroban-sdk", "stellar-access", "stellar-macros", "stellar-tokens", - "storage", - "timelock", + "strata-bindings", + "strata-storage", + "strata-timelock", ] [[package]] @@ -1036,7 +1029,7 @@ dependencies = [ "stellar-access", "stellar-macros", "stellar-tokens", - "storage", + "strata-storage", ] [[package]] @@ -1213,13 +1206,13 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" name = "nav-oracle" version = "0.1.0" dependencies = [ - "bindings", "soroban-sdk", "stellar-access", "stellar-contract-utils", "stellar-macros", - "storage", - "timelock", + "strata-bindings", + "strata-storage", + "strata-timelock", ] [[package]] @@ -1711,7 +1704,6 @@ dependencies = [ name = "share-token" version = "0.1.0" dependencies = [ - "bindings", "compliance", "identity-verifier", "soroban-sdk", @@ -1719,8 +1711,9 @@ dependencies = [ "stellar-contract-utils", "stellar-macros", "stellar-tokens", - "storage", - "timelock", + "strata-bindings", + "strata-storage", + "strata-timelock", ] [[package]] @@ -2062,10 +2055,26 @@ dependencies = [ ] [[package]] -name = "storage" +name = "strata-bindings" +version = "0.1.0" +dependencies = [ + "soroban-sdk", +] + +[[package]] +name = "strata-storage" +version = "0.1.0" +dependencies = [ + "soroban-sdk", +] + +[[package]] +name = "strata-timelock" version = "0.1.0" dependencies = [ + "proptest", "soroban-sdk", + "strata-storage", ] [[package]] @@ -2185,15 +2194,6 @@ dependencies = [ "time-core", ] -[[package]] -name = "timelock" -version = "0.1.0" -dependencies = [ - "proptest", - "soroban-sdk", - "storage", -] - [[package]] name = "tinyvec" version = "1.12.0" diff --git a/contracts/async-vault/Cargo.toml b/contracts/async-vault/Cargo.toml index ecd0b862..64160de6 100644 --- a/contracts/async-vault/Cargo.toml +++ b/contracts/async-vault/Cargo.toml @@ -11,13 +11,13 @@ doctest = false [dependencies] soroban-sdk = { workspace = true } -storage = { path = "../../crates/storage" } +strata-storage = { path = "../../crates/strata-storage" } stellar-access = { workspace = true } stellar-macros = { workspace = true } stellar-contract-utils = { workspace = true } stellar-tokens = { workspace = true } -bindings = { path = "../../crates/bindings" } -timelock = { path = "../../crates/timelock" } +strata-bindings = { path = "../../crates/strata-bindings" } +strata-timelock = { path = "../../crates/strata-timelock" } [dev-dependencies] @@ -26,7 +26,7 @@ nav-oracle = { path = "../nav-oracle" } share-token = { path = "../share-token" } compliance = { path = "../compliance" } identity-verifier = { path = "../identity-verifier" } -proptest = "1" +proptest = "=1.11.0" [features] upgrade_wasm = [] diff --git a/contracts/async-vault/src/deposit.rs b/contracts/async-vault/src/deposit.rs index bc9da0a5..a2d6d685 100644 --- a/contracts/async-vault/src/deposit.rs +++ b/contracts/async-vault/src/deposit.rs @@ -14,8 +14,8 @@ use crate::state::{self, DepositRequest, EpochStatus}; use crate::treasury; use crate::wind_down; -fn refuse_unless_allowed_in(e: &Env, from: &Address) { - let standing = lock_policy::standing(e, from); +fn refuse_unless_allowed_in(e: &Env, holder: &Address) { + let standing = lock_policy::standing(e, holder); if lock_policy::decide(Exit::RequestDeposit, standing, wind_down::is_active(e)) != Decision::Proceed { @@ -36,20 +36,20 @@ fn refuse_if_exceeds_cap(e: &Env, amount: i128) { } } -pub(crate) fn request(e: &Env, from: &Address, amount: i128) -> u64 { +pub(crate) fn request(e: &Env, holder: &Address, amount: i128) -> u64 { wind_down::refuse_if_active(e); governance::refuse_if_setup_open(e); - from.require_auth(); + holder.require_auth(); if amount <= 0 { panic_with_error!(e, VaultError::InvalidAmount); } - refuse_unless_allowed_in(e, from); + refuse_unless_allowed_in(e, holder); refuse_if_exceeds_cap(e, amount); let epoch_id = state::current_epoch(e); - if state::get_deposit_request(e, epoch_id, from).is_some() { + if state::get_deposit_request(e, epoch_id, holder).is_some() { panic_with_error!(e, VaultError::RequestOutstanding); } @@ -64,7 +64,7 @@ pub(crate) fn request(e: &Env, from: &Address, amount: i128) -> u64 { state::set_deposit_request( e, epoch_id, - from, + holder, &DepositRequest { amount, claimed: false, @@ -75,10 +75,10 @@ pub(crate) fn request(e: &Env, from: &Address, amount: i128) -> u64 { settlement::on_deposit_requested(e, epoch_id); let asset = state::get_addr(e, &DataKey::Asset); - TokenClient::new(e, &asset).transfer(from, e.current_contract_address(), &amount); + TokenClient::new(e, &asset).transfer(holder, e.current_contract_address(), &amount); DepositRequested { - controller: from.clone(), + holder: holder.clone(), epoch: epoch_id, amount, } @@ -87,8 +87,8 @@ pub(crate) fn request(e: &Env, from: &Address, amount: i128) -> u64 { epoch_id } -pub(crate) fn claim(e: &Env, caller: &Address, epoch_id: u64) -> i128 { - caller.require_auth(); +pub(crate) fn claim(e: &Env, holder: &Address, epoch_id: u64) -> i128 { + holder.require_auth(); let epoch = state::get_epoch(e, epoch_id) .unwrap_or_else(|| panic_with_error!(e, VaultError::EpochNotFound)); @@ -97,14 +97,14 @@ pub(crate) fn claim(e: &Env, caller: &Address, epoch_id: u64) -> i128 { panic_with_error!(e, VaultError::EpochNotFulfilled); } - let mut request = state::get_deposit_request(e, epoch_id, caller) + let mut request = state::get_deposit_request(e, epoch_id, holder) .unwrap_or_else(|| panic_with_error!(e, VaultError::RequestNotFound)); if request.claimed { panic_with_error!(e, VaultError::AlreadyClaimed); } - let standing = lock_policy::standing(e, caller); + let standing = lock_policy::standing(e, holder); let decision = lock_policy::decide(Exit::ClaimDeposit, standing, wind_down::is_active(e)); match decision { Decision::Proceed | Decision::ConvertToExit | Decision::Surrender => {} @@ -123,25 +123,25 @@ pub(crate) fn claim(e: &Env, caller: &Address, epoch_id: u64) -> i128 { if treasury::free_reserve(e) < request.amount { panic_with_error!(e, VaultError::ClaimNotCovered); } - refund_deposit(e, caller, epoch_id, request.amount); + refund_deposit(e, holder, epoch_id, request.amount); settlement::on_deposit_claimed(e, epoch_id, 0); return 0; } request.claimed = true; - state::set_deposit_request(e, epoch_id, caller, &request); + state::set_deposit_request(e, epoch_id, holder, &request); state::set_pending_mint_shares(e, state::reduce(e, state::pending_mint_shares(e), shares)); if decision != Decision::Proceed { - convert_to_exit(e, caller, epoch_id, shares, standing, decision); + convert_to_exit(e, holder, epoch_id, shares, standing, decision); return 0; } let share_token = state::get_addr(e, &DataKey::ShareToken); - RWATokenClient::new(e, &share_token).mint(caller, &shares, &e.current_contract_address()); + RWATokenClient::new(e, &share_token).mint(holder, &shares, &e.current_contract_address()); DepositClaimed { - controller: caller.clone(), + holder: holder.clone(), epoch: epoch_id, amount: request.amount, shares, @@ -178,7 +178,7 @@ fn convert_to_exit( }; DepositConvertedToExit { - controller: holder.clone(), + holder: holder.clone(), epoch: epoch_id, into_epoch, shares, @@ -189,26 +189,26 @@ fn convert_to_exit( /// Returns the settlement asset and clears the request. Used by cancellation and /// by a claim whose conversion floors to zero. -fn refund_deposit(e: &Env, controller: &Address, epoch_id: u64, amount: i128) { +fn refund_deposit(e: &Env, holder: &Address, epoch_id: u64, amount: i128) { let mut epoch = state::get_epoch(e, epoch_id) .unwrap_or_else(|| panic_with_error!(e, VaultError::EpochNotFound)); epoch.total_deposited = state::reduce(e, epoch.total_deposited, amount); state::set_epoch(e, epoch_id, &epoch); - state::remove_deposit_request(e, epoch_id, controller); + state::remove_deposit_request(e, epoch_id, holder); let asset = state::get_addr(e, &DataKey::Asset); - TokenClient::new(e, &asset).transfer(&e.current_contract_address(), controller, &amount); + TokenClient::new(e, &asset).transfer(&e.current_contract_address(), holder, &amount); DepositCancelled { - controller: controller.clone(), + holder: holder.clone(), epoch: epoch_id, amount, } .publish(e); } -pub(crate) fn cancel(e: &Env, from: &Address, epoch_id: u64) -> i128 { - from.require_auth(); +pub(crate) fn cancel(e: &Env, holder: &Address, epoch_id: u64) -> i128 { + holder.require_auth(); let epoch = state::get_epoch(e, epoch_id) .unwrap_or_else(|| panic_with_error!(e, VaultError::EpochNotFound)); @@ -225,10 +225,10 @@ pub(crate) fn cancel(e: &Env, from: &Address, epoch_id: u64) -> i128 { panic_with_error!(e, VaultError::PriceAvailable); } - let request = state::get_deposit_request(e, epoch_id, from) + let request = state::get_deposit_request(e, epoch_id, holder) .unwrap_or_else(|| panic_with_error!(e, VaultError::RequestNotFound)); - let standing = lock_policy::standing(e, from); + let standing = lock_policy::standing(e, holder); match lock_policy::decide(Exit::CancelDeposit, standing, wind_down::is_active(e)) { Decision::Proceed => {} Decision::Refuse | Decision::Hold | Decision::ConvertToExit | Decision::Surrender => { @@ -241,6 +241,6 @@ pub(crate) fn cancel(e: &Env, from: &Address, epoch_id: u64) -> i128 { state::reduce(e, state::cancellable_escrow(e), request.amount), ); settlement::on_deposit_cancelled(e, epoch_id); - refund_deposit(e, from, epoch_id, request.amount); + refund_deposit(e, holder, epoch_id, request.amount); request.amount } diff --git a/contracts/async-vault/src/epoch.rs b/contracts/async-vault/src/epoch.rs index 2f5293b7..96ad4b03 100644 --- a/contracts/async-vault/src/epoch.rs +++ b/contracts/async-vault/src/epoch.rs @@ -1,6 +1,6 @@ -use bindings::{OracleFeedClient, OracleState}; use soroban_sdk::{panic_with_error, Env}; use stellar_contract_utils::pausable; +use strata_bindings::{OracleFeedClient, OracleState}; use crate::error::VaultError; use crate::event::{EpochClosed, EpochFulfilled}; diff --git a/contracts/async-vault/src/error.rs b/contracts/async-vault/src/error.rs index 36faee2d..5c92a6d4 100644 --- a/contracts/async-vault/src/error.rs +++ b/contracts/async-vault/src/error.rs @@ -9,13 +9,13 @@ 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. + /// The holder 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 + /// The holder already has 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. + /// most one request per holder per epoch holds by construction. RequestOutstanding = 6009, /// The epoch total would exceed `i128::MAX`. AmountTooLarge = 6014, @@ -50,7 +50,7 @@ pub enum VaultError { /// No custodian has been set, so capital has nowhere to go. CustodianNotSet = 6012, /// The epoch is priced, so the request can no longer be cancelled. The - /// controller claims instead. + /// holder claims instead. AlreadyPriced = 6039, /// The vault must always have an admin. Governance hands over in two steps /// rather than stepping down into nobody. @@ -142,7 +142,7 @@ pub enum VaultError { /// The proposed feed does not name this vault as its own, or its setup is /// still open. Its admin could then change its limits with no delay. FeedNotBound = 6072, - /// The controller may not hold shares (delisted, frozen or refused by the + /// The holder may not receive shares (delisted, frozen or refused by the /// compliance rules), so the escrowed shares stay with the vault and the /// request stays open. HolderNotEligible = 6073, diff --git a/contracts/async-vault/src/event.rs b/contracts/async-vault/src/event.rs index d825af74..b2ba60ac 100644 --- a/contracts/async-vault/src/event.rs +++ b/contracts/async-vault/src/event.rs @@ -5,7 +5,7 @@ use crate::governance::VaultChange; #[contractevent] pub struct DepositRequested { #[topic] - pub controller: Address, + pub holder: Address, pub epoch: u64, pub amount: i128, } @@ -21,7 +21,7 @@ pub struct EpochFulfilled { #[contractevent] pub struct DepositClaimed { #[topic] - pub controller: Address, + pub holder: Address, pub epoch: u64, pub amount: i128, pub shares: i128, @@ -30,7 +30,7 @@ pub struct DepositClaimed { #[contractevent] pub struct RedeemRequested { #[topic] - pub controller: Address, + pub holder: Address, pub epoch: u64, pub shares: i128, } @@ -38,7 +38,7 @@ pub struct RedeemRequested { #[contractevent] pub struct RedeemClaimed { #[topic] - pub controller: Address, + pub holder: Address, pub epoch: u64, pub shares: i128, pub assets: i128, @@ -74,7 +74,7 @@ pub struct Deployed { #[contractevent] pub struct Funded { #[topic] - pub from: Address, + pub caller: Address, pub assets: i128, pub net_deployed: i128, } @@ -82,7 +82,7 @@ pub struct Funded { #[contractevent] pub struct DepositCancelled { #[topic] - pub controller: Address, + pub holder: Address, pub epoch: u64, pub amount: i128, } @@ -90,7 +90,7 @@ pub struct DepositCancelled { #[contractevent] pub struct RedeemCancelled { #[topic] - pub controller: Address, + pub holder: Address, pub epoch: u64, pub shares: i128, } @@ -152,11 +152,11 @@ pub struct WindDownClaimed { pub assets: i128, } -/// An escrowed redemption burned into the controller's wind-down entitlement. +/// An escrowed redemption burned into the holder's wind-down entitlement. #[contractevent] pub struct RedemptionSurrendered { #[topic] - pub controller: Address, + pub holder: Address, pub epoch: u64, pub shares: i128, } @@ -165,7 +165,7 @@ pub struct RedemptionSurrendered { #[contractevent] pub struct DepositConvertedToExit { #[topic] - pub controller: Address, + pub holder: Address, pub epoch: u64, pub into_epoch: u64, pub shares: i128, diff --git a/contracts/async-vault/src/governance.rs b/contracts/async-vault/src/governance.rs index 89179d4c..78f3c649 100644 --- a/contracts/async-vault/src/governance.rs +++ b/contracts/async-vault/src/governance.rs @@ -1,4 +1,3 @@ -use bindings::{OracleFeedClient, VaultBindingClient}; use soroban_sdk::{ contracttype, panic_with_error, symbol_short, Address, BytesN, Env, Symbol, Vec, }; @@ -17,7 +16,8 @@ use stellar_tokens::{ RWATokenClient, }, }; -use timelock::{queue, setup, Clock, Refusal}; +use strata_bindings::{OracleFeedClient, VaultBindingClient}; +use strata_timelock::{queue, setup, Clock, Refusal}; use crate::error::VaultError; use crate::event::{ChangeApplied, ChangeCancelled, ChangeProposed, NoticeSet, SetupFinished}; @@ -91,7 +91,7 @@ pub(crate) fn clock(e: &Env) -> Clock { Clock { delay: upgrade::delay(e), exit_window: state::notice(e), - paused_secs: timelock::pause::paused_secs(e), + paused_secs: strata_timelock::pause::paused_secs(e), paused: pausable::paused(e), winding_down: wind_down::is_active(e), } diff --git a/contracts/async-vault/src/health.rs b/contracts/async-vault/src/health.rs index 894f5290..8f7884c7 100644 --- a/contracts/async-vault/src/health.rs +++ b/contracts/async-vault/src/health.rs @@ -1,5 +1,5 @@ -use bindings::{OracleFeedClient, OracleState}; use soroban_sdk::{contracttype, Env}; +use strata_bindings::{OracleFeedClient, OracleState}; use crate::governance; use crate::keys::DataKey; @@ -31,7 +31,7 @@ pub(crate) fn health(e: &Env) -> Health { let feed = OracleFeedClient::new(e, &state::get_addr(e, &DataKey::Oracle)); let notice = state::notice(e); Health { - setup_open: timelock::setup::is_open(e), + setup_open: strata_timelock::setup::is_open(e), token_bound: wiring.token_bound, oracle_bound: wiring.oracle_bound, sole_token_manager: wiring.sole_token_manager, diff --git a/contracts/async-vault/src/lib.rs b/contracts/async-vault/src/lib.rs index fcdb44e7..895e84c0 100644 --- a/contracts/async-vault/src/lib.rs +++ b/contracts/async-vault/src/lib.rs @@ -24,7 +24,7 @@ use stellar_contract_utils::pausable::{self as pausable, Pausable}; use stellar_contract_utils::upgradeable; use stellar_macros::{only_admin, only_role, when_not_paused}; use stellar_tokens::fungible::FungibleTokenClient; -use timelock::{Clock, ClockSource, Refusal, TimelockHost, DAY_DURATION}; +use strata_timelock::{Clock, ClockSource, Refusal, TimelockHost, DAY_DURATION}; use keys::DataKey; use roles::{GUARDIAN_ROLE, MANAGER_ROLE, TREASURY_ROLE}; @@ -297,7 +297,7 @@ impl AsyncVault { } pub fn setup_open(e: &Env) -> bool { - timelock::setup::is_open(e) + strata_timelock::setup::is_open(e) } /// Whether setup is open, the wiring `finish_setup` checks except the share @@ -345,8 +345,8 @@ impl AsyncVault { treasury::deploy(e, assets) } - pub fn fund(e: &Env, from: Address, assets: i128) -> i128 { - treasury::fund(e, &from, assets) + pub fn fund(e: &Env, caller: Address, assets: i128) -> i128 { + treasury::fund(e, &caller, assets) } pub fn notice(e: &Env) -> u64 { @@ -366,53 +366,45 @@ impl AsyncVault { settlement::get(e, epoch_id) } - pub fn get_deposit_request( - e: &Env, - epoch_id: u64, - controller: Address, - ) -> Option { - state::get_deposit_request(e, epoch_id, &controller) + pub fn get_deposit_request(e: &Env, epoch_id: u64, holder: Address) -> Option { + state::get_deposit_request(e, epoch_id, &holder) } #[when_not_paused] - pub fn request_deposit(e: &Env, from: Address, amount: i128) -> u64 { - deposit::request(e, &from, amount) + pub fn request_deposit(e: &Env, holder: Address, amount: i128) -> u64 { + deposit::request(e, &holder, amount) } - /// Returns the shares minted to the caller. Zero means none were: a deposit - /// too small for one share was refunded, or the caller is off the allowlist + /// Returns the shares minted to the holder. Zero means none were: a deposit + /// too small for one share was refunded, or the holder is off the allowlist /// and the shares joined an exit instead (`DepositConvertedToExit`). - pub fn claim_deposit(e: &Env, caller: Address, epoch_id: u64) -> i128 { - deposit::claim(e, &caller, epoch_id) + pub fn claim_deposit(e: &Env, holder: Address, epoch_id: u64) -> i128 { + deposit::claim(e, &holder, epoch_id) } /// Cancels an unpriced deposit. Never blocked by the pause. - pub fn cancel_deposit(e: &Env, from: Address, epoch_id: u64) -> i128 { - deposit::cancel(e, &from, epoch_id) + pub fn cancel_deposit(e: &Env, holder: Address, epoch_id: u64) -> i128 { + deposit::cancel(e, &holder, epoch_id) } /// Cancels an unpriced redemption. Outside a wind-down the shares come - /// back, even while the token is paused, unless the controller is delisted, + /// back, even while the token is paused, unless the holder is delisted, /// frozen or refused by the compliance rules; in one the escrow joins the - /// controller's entitlement, and a frozen controller is held. - pub fn cancel_redeem(e: &Env, from: Address, epoch_id: u64) -> i128 { - redeem::cancel(e, &from, epoch_id) + /// holder's entitlement, and a frozen holder is held. + pub fn cancel_redeem(e: &Env, holder: Address, epoch_id: u64) -> i128 { + redeem::cancel(e, &holder, epoch_id) } - pub fn request_redeem(e: &Env, from: Address, shares: i128) -> u64 { - redeem::request(e, &from, shares) + pub fn request_redeem(e: &Env, holder: Address, shares: i128) -> u64 { + redeem::request(e, &holder, shares) } - pub fn get_redeem_request( - e: &Env, - epoch_id: u64, - controller: Address, - ) -> Option { - state::get_redeem_request(e, epoch_id, &controller) + pub fn get_redeem_request(e: &Env, epoch_id: u64, holder: Address) -> Option { + state::get_redeem_request(e, epoch_id, &holder) } - pub fn claim_redeem(e: &Env, caller: Address, epoch_id: u64) -> i128 { - redeem::claim(e, &caller, epoch_id) + pub fn claim_redeem(e: &Env, holder: Address, epoch_id: u64) -> i128 { + redeem::claim(e, &holder, epoch_id) } #[only_role(caller, "manager")] @@ -471,14 +463,14 @@ impl Pausable for AsyncVault { #[only_role(caller, "guardian")] fn pause(e: &Env, caller: Address) { pausable::pause(e); - timelock::pause::start(e); + strata_timelock::pause::start(e); } #[only_admin] fn unpause(e: &Env, caller: Address) { caller.require_auth(); pausable::unpause(e); - timelock::pause::end(e); + strata_timelock::pause::end(e); } } diff --git a/contracts/async-vault/src/pricing.rs b/contracts/async-vault/src/pricing.rs index e9ca7849..bd08416a 100644 --- a/contracts/async-vault/src/pricing.rs +++ b/contracts/async-vault/src/pricing.rs @@ -1,6 +1,6 @@ -use bindings::OracleFeedClient; use soroban_sdk::Env; use stellar_contract_utils::math::{i128_fixed_point::checked_mul_div_floor, wad::WAD_SCALE}; +use strata_bindings::OracleFeedClient; use crate::error::VaultError; use crate::keys::DataKey; diff --git a/contracts/async-vault/src/redeem.rs b/contracts/async-vault/src/redeem.rs index bf4ec6fb..005638cc 100644 --- a/contracts/async-vault/src/redeem.rs +++ b/contracts/async-vault/src/redeem.rs @@ -15,15 +15,15 @@ use crate::state::{self, EpochStatus, RedeemRequest}; use crate::treasury; use crate::wind_down; -pub(crate) fn request(e: &Env, from: &Address, shares: i128) -> u64 { +pub(crate) fn request(e: &Env, holder: &Address, shares: i128) -> u64 { wind_down::refuse_if_active(e); - from.require_auth(); + holder.require_auth(); if shares <= 0 { panic_with_error!(e, VaultError::InvalidAmount); } - let standing = lock_policy::standing(e, from); + let standing = lock_policy::standing(e, holder); match lock_policy::decide(Exit::RequestRedeem, standing, wind_down::is_active(e)) { Decision::Proceed => {} Decision::Refuse | Decision::Hold | Decision::ConvertToExit | Decision::Surrender => { @@ -32,9 +32,9 @@ pub(crate) fn request(e: &Env, from: &Address, shares: i128) -> u64 { } // A request within the balance but short of the free shares needs frozen // ones; beyond the balance entirely, the token's own check applies instead. - if shares > lock_policy::free_shares(e, from) { + if shares > lock_policy::free_shares(e, holder) { let held = - FungibleTokenClient::new(e, &state::get_addr(e, &DataKey::ShareToken)).balance(from); + FungibleTokenClient::new(e, &state::get_addr(e, &DataKey::ShareToken)).balance(holder); if shares <= held { panic_with_error!(e, VaultError::Frozen); } @@ -42,7 +42,7 @@ pub(crate) fn request(e: &Env, from: &Address, shares: i128) -> u64 { let epoch_id = state::current_epoch(e); - if state::get_redeem_request(e, epoch_id, from).is_some() { + if state::get_redeem_request(e, epoch_id, holder).is_some() { panic_with_error!(e, VaultError::RequestOutstanding); } @@ -57,7 +57,7 @@ pub(crate) fn request(e: &Env, from: &Address, shares: i128) -> u64 { state::set_redeem_request( e, epoch_id, - from, + holder, &RedeemRequest { shares, claimed: false, @@ -68,10 +68,10 @@ pub(crate) fn request(e: &Env, from: &Address, shares: i128) -> u64 { let vault = e.current_contract_address(); let share_token = state::get_addr(e, &DataKey::ShareToken); - RWATokenClient::new(e, &share_token).forced_transfer(from, &vault, &shares, &vault); + RWATokenClient::new(e, &share_token).forced_transfer(holder, &vault, &shares, &vault); RedeemRequested { - controller: from.clone(), + holder: holder.clone(), epoch: epoch_id, shares, } @@ -80,8 +80,8 @@ pub(crate) fn request(e: &Env, from: &Address, shares: i128) -> u64 { epoch_id } -pub(crate) fn claim(e: &Env, caller: &Address, epoch_id: u64) -> i128 { - caller.require_auth(); +pub(crate) fn claim(e: &Env, holder: &Address, epoch_id: u64) -> i128 { + holder.require_auth(); let epoch = state::get_epoch(e, epoch_id) .unwrap_or_else(|| panic_with_error!(e, VaultError::EpochNotFound)); @@ -90,14 +90,14 @@ pub(crate) fn claim(e: &Env, caller: &Address, epoch_id: u64) -> i128 { panic_with_error!(e, VaultError::EpochNotFulfilled); } - let mut request = state::get_redeem_request(e, epoch_id, caller) + let mut request = state::get_redeem_request(e, epoch_id, holder) .unwrap_or_else(|| panic_with_error!(e, VaultError::RequestNotFound)); if request.claimed { panic_with_error!(e, VaultError::AlreadyClaimed); } - let standing = lock_policy::standing(e, caller); + let standing = lock_policy::standing(e, holder); match lock_policy::decide(Exit::ClaimRedeem, standing, wind_down::is_active(e)) { Decision::Proceed => {} Decision::Refuse | Decision::Hold | Decision::ConvertToExit | Decision::Surrender => { @@ -115,7 +115,7 @@ pub(crate) fn claim(e: &Env, caller: &Address, epoch_id: u64) -> i128 { // In a wind-down they are burned below instead: its snapshot left them out. if assets == 0 && !wind_down::is_active(e) { - return_shares(e, caller, epoch_id, request.shares); + return_shares(e, holder, epoch_id, request.shares); settlement::on_redeem_claimed(e, epoch_id, 0); return 0; } @@ -130,11 +130,11 @@ pub(crate) fn claim(e: &Env, caller: &Address, epoch_id: u64) -> i128 { let asset = state::get_addr(e, &DataKey::Asset); request.claimed = true; - state::set_redeem_request(e, epoch_id, caller, &request); + state::set_redeem_request(e, epoch_id, holder, &request); state::set_committed(e, state::reduce(e, state::committed(e), assets)); if assets > 0 { - TokenClient::new(e, &asset).transfer(&vault, caller, &assets); + TokenClient::new(e, &asset).transfer(&vault, holder, &assets); } RWATokenClient::new(e, &state::get_addr(e, &DataKey::ShareToken)).burn( &vault, @@ -143,7 +143,7 @@ pub(crate) fn claim(e: &Env, caller: &Address, epoch_id: u64) -> i128 { ); RedeemClaimed { - controller: caller.clone(), + holder: holder.clone(), epoch: epoch_id, shares: request.shares, assets, @@ -155,7 +155,7 @@ pub(crate) fn claim(e: &Env, caller: &Address, epoch_id: u64) -> i128 { } /// Queues shares the vault already holds, adding to any redemption open in the epoch. -pub(crate) fn queue_escrowed(e: &Env, controller: &Address, shares: i128) -> u64 { +pub(crate) fn queue_escrowed(e: &Env, holder: &Address, shares: i128) -> u64 { let epoch_id = state::current_epoch(e); let mut epoch = state::get_epoch(e, epoch_id) .unwrap_or_else(|| panic_with_error!(e, VaultError::EpochNotFound)); @@ -164,7 +164,7 @@ pub(crate) fn queue_escrowed(e: &Env, controller: &Address, shares: i128) -> u64 .checked_add(shares) .unwrap_or_else(|| panic_with_error!(e, VaultError::AmountTooLarge)); - let request = match state::get_redeem_request(e, epoch_id, controller) { + let request = match state::get_redeem_request(e, epoch_id, holder) { Some(mut open) => { open.shares = open .shares @@ -180,23 +180,23 @@ pub(crate) fn queue_escrowed(e: &Env, controller: &Address, shares: i128) -> u64 } } }; - state::set_redeem_request(e, epoch_id, controller, &request); + state::set_redeem_request(e, epoch_id, holder, &request); state::set_epoch(e, epoch_id, &epoch); epoch_id } -/// Moves escrowed shares back to the controller and clears the request. -fn return_shares(e: &Env, controller: &Address, epoch_id: u64, shares: i128) { +/// Moves escrowed shares back to the holder and clears the request. +fn return_shares(e: &Env, holder: &Address, epoch_id: u64, shares: i128) { let mut epoch = state::get_epoch(e, epoch_id) .unwrap_or_else(|| panic_with_error!(e, VaultError::EpochNotFound)); epoch.total_shares_redeeming = state::reduce(e, epoch.total_shares_redeeming, shares); state::set_epoch(e, epoch_id, &epoch); - state::remove_redeem_request(e, epoch_id, controller); + state::remove_redeem_request(e, epoch_id, holder); - deliver(e, controller, shares); + deliver(e, holder, shares); RedeemCancelled { - controller: controller.clone(), + holder: holder.clone(), epoch: epoch_id, shares, } @@ -204,21 +204,20 @@ fn return_shares(e: &Env, controller: &Address, epoch_id: u64, shares: i128) { } /// Checks the receiver, then moves as manager, so the vault's listing and token pause never apply. -fn deliver(e: &Env, controller: &Address, shares: i128) { +fn deliver(e: &Env, holder: &Address, shares: i128) { let vault = e.current_contract_address(); let token = state::get_addr(e, &DataKey::ShareToken); let share = RWATokenClient::new(e, &token); - if lock_policy::standing(e, controller) != Standing::Listed - || !RulesClient::new(e, &share.compliance()) - .can_transfer(&vault, controller, &shares, &token) + if lock_policy::standing(e, holder) != Standing::Listed + || !RulesClient::new(e, &share.compliance()).can_transfer(&vault, holder, &shares, &token) { panic_with_error!(e, VaultError::HolderNotEligible); } - share.forced_transfer(&vault, controller, &shares, &vault); + share.forced_transfer(&vault, holder, &shares, &vault); } -pub(crate) fn cancel(e: &Env, from: &Address, epoch_id: u64) -> i128 { - from.require_auth(); +pub(crate) fn cancel(e: &Env, holder: &Address, epoch_id: u64) -> i128 { + holder.require_auth(); let epoch = state::get_epoch(e, epoch_id) .unwrap_or_else(|| panic_with_error!(e, VaultError::EpochNotFound)); @@ -237,13 +236,13 @@ pub(crate) fn cancel(e: &Env, from: &Address, epoch_id: u64) -> i128 { panic_with_error!(e, VaultError::PriceAvailable); } - let request = state::get_redeem_request(e, epoch_id, from) + let request = state::get_redeem_request(e, epoch_id, holder) .unwrap_or_else(|| panic_with_error!(e, VaultError::RequestNotFound)); - let standing = lock_policy::standing(e, from); + let standing = lock_policy::standing(e, holder); match lock_policy::decide(Exit::CancelRedeem, standing, winding_down) { - Decision::Proceed => return_shares(e, from, epoch_id, request.shares), - Decision::Surrender => surrender_escrow(e, from, epoch_id, request.shares), + Decision::Proceed => return_shares(e, holder, epoch_id, request.shares), + Decision::Surrender => surrender_escrow(e, holder, epoch_id, request.shares), Decision::Refuse | Decision::Hold | Decision::ConvertToExit => { panic_with_error!(e, lock_policy::refused_with(standing)) } @@ -253,19 +252,19 @@ pub(crate) fn cancel(e: &Env, from: &Address, epoch_id: u64) -> i128 { } /// Burns the escrow into the entitlement, so no token pause or allowlist place can hold it. -fn surrender_escrow(e: &Env, controller: &Address, epoch_id: u64, shares: i128) { +fn surrender_escrow(e: &Env, holder: &Address, epoch_id: u64, shares: i128) { let mut epoch = state::get_epoch(e, epoch_id) .unwrap_or_else(|| panic_with_error!(e, VaultError::EpochNotFound)); epoch.total_shares_redeeming = state::reduce(e, epoch.total_shares_redeeming, shares); state::set_epoch(e, epoch_id, &epoch); - state::remove_redeem_request(e, epoch_id, controller); + state::remove_redeem_request(e, epoch_id, holder); let vault = e.current_contract_address(); RWATokenClient::new(e, &state::get_addr(e, &DataKey::ShareToken)).burn(&vault, &shares, &vault); - wind_down::credit(e, controller, shares); + wind_down::credit(e, holder, shares); RedemptionSurrendered { - controller: controller.clone(), + holder: holder.clone(), epoch: epoch_id, shares, } diff --git a/contracts/async-vault/src/settlement.rs b/contracts/async-vault/src/settlement.rs index 6baeb408..24ab22cb 100644 --- a/contracts/async-vault/src/settlement.rs +++ b/contracts/async-vault/src/settlement.rs @@ -22,7 +22,7 @@ pub struct EpochSettlement { } pub(crate) fn get(e: &Env, epoch: u64) -> Option { - storage::get_persistent(e, &DataKey::Settlement(epoch)) + strata_storage::get_persistent(e, &DataKey::Settlement(epoch)) } fn load(e: &Env, epoch: u64) -> EpochSettlement { @@ -30,7 +30,7 @@ fn load(e: &Env, epoch: u64) -> EpochSettlement { } fn save(e: &Env, epoch: u64, s: &EpochSettlement) { - storage::set_persistent(e, &DataKey::Settlement(epoch), s); + strata_storage::set_persistent(e, &DataKey::Settlement(epoch), s); } fn close_one(e: &Env, open: u32) -> u32 { diff --git a/contracts/async-vault/src/state.rs b/contracts/async-vault/src/state.rs index 64a1419f..f10fd294 100644 --- a/contracts/async-vault/src/state.rs +++ b/contracts/async-vault/src/state.rs @@ -52,141 +52,123 @@ pub struct RedeemRequest { } pub(crate) fn set_addr(e: &Env, key: &DataKey, addr: &Address) { - storage::set_instance(e, key, addr); + strata_storage::set_instance(e, key, addr); } pub(crate) fn get_addr(e: &Env, key: &DataKey) -> Address { - storage::get_instance(e, key) + strata_storage::get_instance(e, key) .unwrap_or_else(|| panic_with_error!(e, VaultError::NotInitialized)) } pub(crate) fn get_addr_opt(e: &Env, key: &DataKey) -> Option
{ - storage::get_instance(e, key) + strata_storage::get_instance(e, key) } pub(crate) fn set_net_deployed(e: &Env, assets: i128) { - storage::set_instance(e, &DataKey::NetDeployed, &assets); + strata_storage::set_instance(e, &DataKey::NetDeployed, &assets); } pub(crate) fn net_deployed(e: &Env) -> i128 { - storage::get_instance(e, &DataKey::NetDeployed).unwrap_or(0) + strata_storage::get_instance(e, &DataKey::NetDeployed).unwrap_or(0) } pub(crate) fn set_current_epoch(e: &Env, id: u64) { - storage::set_instance(e, &DataKey::CurrentEpoch, &id); + strata_storage::set_instance(e, &DataKey::CurrentEpoch, &id); } pub(crate) fn current_epoch(e: &Env) -> u64 { - storage::get_instance(e, &DataKey::CurrentEpoch) + strata_storage::get_instance(e, &DataKey::CurrentEpoch) .unwrap_or_else(|| panic_with_error!(e, VaultError::NotInitialized)) } pub(crate) fn set_notice(e: &Env, secs: u64) { - storage::set_instance(e, &DataKey::Notice, &secs); + strata_storage::set_instance(e, &DataKey::Notice, &secs); } /// Seconds an epoch waits after closing before it can be priced. Absent means /// none, so a vault deployed without one behaves as it always did. pub(crate) fn notice(e: &Env) -> u64 { - storage::get_instance(e, &DataKey::Notice).unwrap_or(0) + strata_storage::get_instance(e, &DataKey::Notice).unwrap_or(0) } pub(crate) fn set_epoch(e: &Env, id: u64, epoch: &EpochInfo) { - storage::set_persistent(e, &DataKey::Epoch(id), epoch); + strata_storage::set_persistent(e, &DataKey::Epoch(id), epoch); } pub(crate) fn get_epoch(e: &Env, id: u64) -> Option { - storage::get_persistent(e, &DataKey::Epoch(id)) + strata_storage::get_persistent(e, &DataKey::Epoch(id)) } -pub(crate) fn set_deposit_request( - e: &Env, - epoch: u64, - controller: &Address, - request: &DepositRequest, -) { - storage::set_persistent(e, &DataKey::UserDeposit(epoch, controller.clone()), request); +pub(crate) fn set_deposit_request(e: &Env, epoch: u64, holder: &Address, request: &DepositRequest) { + strata_storage::set_persistent(e, &DataKey::UserDeposit(epoch, holder.clone()), request); } -pub(crate) fn get_deposit_request( - e: &Env, - epoch: u64, - controller: &Address, -) -> Option { - storage::get_persistent(e, &DataKey::UserDeposit(epoch, controller.clone())) +pub(crate) fn get_deposit_request(e: &Env, epoch: u64, holder: &Address) -> Option { + strata_storage::get_persistent(e, &DataKey::UserDeposit(epoch, holder.clone())) } -pub(crate) fn set_redeem_request( - e: &Env, - epoch: u64, - controller: &Address, - request: &RedeemRequest, -) { - storage::set_persistent(e, &DataKey::UserRedeem(epoch, controller.clone()), request); +pub(crate) fn set_redeem_request(e: &Env, epoch: u64, holder: &Address, request: &RedeemRequest) { + strata_storage::set_persistent(e, &DataKey::UserRedeem(epoch, holder.clone()), request); } -pub(crate) fn get_redeem_request( - e: &Env, - epoch: u64, - controller: &Address, -) -> Option { - storage::get_persistent(e, &DataKey::UserRedeem(epoch, controller.clone())) +pub(crate) fn get_redeem_request(e: &Env, epoch: u64, holder: &Address) -> Option { + strata_storage::get_persistent(e, &DataKey::UserRedeem(epoch, holder.clone())) } /// Cash owed to holders whose exit is priced but not yet claimed. pub(crate) fn set_committed(e: &Env, assets: i128) { - storage::set_instance(e, &DataKey::Committed, &assets); + strata_storage::set_instance(e, &DataKey::Committed, &assets); } pub(crate) fn committed(e: &Env) -> i128 { - storage::get_instance(e, &DataKey::Committed).unwrap_or(0) + strata_storage::get_instance(e, &DataKey::Committed).unwrap_or(0) } /// Deposit escrow across every unpriced epoch, still cancellable by the /// investor. Pricing an epoch moves its share of this into the reserve. pub(crate) fn set_cancellable_escrow(e: &Env, assets: i128) { - storage::set_instance(e, &DataKey::CancellableEscrow, &assets); + strata_storage::set_instance(e, &DataKey::CancellableEscrow, &assets); } pub(crate) fn cancellable_escrow(e: &Env) -> i128 { - storage::get_instance(e, &DataKey::CancellableEscrow).unwrap_or(0) + strata_storage::get_instance(e, &DataKey::CancellableEscrow).unwrap_or(0) } -pub(crate) fn remove_deposit_request(e: &Env, epoch: u64, controller: &Address) { +pub(crate) fn remove_deposit_request(e: &Env, epoch: u64, holder: &Address) { e.storage() .persistent() - .remove(&DataKey::UserDeposit(epoch, controller.clone())); + .remove(&DataKey::UserDeposit(epoch, holder.clone())); } -pub(crate) fn remove_redeem_request(e: &Env, epoch: u64, controller: &Address) { +pub(crate) fn remove_redeem_request(e: &Env, epoch: u64, holder: &Address) { e.storage() .persistent() - .remove(&DataKey::UserRedeem(epoch, controller.clone())); + .remove(&DataKey::UserRedeem(epoch, holder.clone())); } pub(crate) fn set_pending_mint_shares(e: &Env, shares: i128) { - storage::set_instance(e, &DataKey::PendingMintShares, &shares); + strata_storage::set_instance(e, &DataKey::PendingMintShares, &shares); } pub(crate) fn pending_mint_shares(e: &Env) -> i128 { - storage::get_instance(e, &DataKey::PendingMintShares).unwrap_or(0) + strata_storage::get_instance(e, &DataKey::PendingMintShares).unwrap_or(0) } pub(crate) fn set_pending_burn_shares(e: &Env, shares: i128) { - storage::set_instance(e, &DataKey::PendingBurnShares, &shares); + strata_storage::set_instance(e, &DataKey::PendingBurnShares, &shares); } pub(crate) fn pending_burn_shares(e: &Env) -> i128 { - storage::get_instance(e, &DataKey::PendingBurnShares).unwrap_or(0) + strata_storage::get_instance(e, &DataKey::PendingBurnShares).unwrap_or(0) } pub(crate) fn deposit_cap(e: &Env) -> Option { - storage::get_instance(e, &DataKey::DepositCap) + strata_storage::get_instance(e, &DataKey::DepositCap) } pub(crate) fn set_deposit_cap(e: &Env, cap: Option) { match cap { - Some(val) => storage::set_instance(e, &DataKey::DepositCap, &val), + Some(val) => strata_storage::set_instance(e, &DataKey::DepositCap, &val), None => e.storage().instance().remove(&DataKey::DepositCap), } } diff --git a/contracts/async-vault/src/test/delisted.rs b/contracts/async-vault/src/test/delisted.rs index bb7430a2..cead24d1 100644 --- a/contracts/async-vault/src/test/delisted.rs +++ b/contracts/async-vault/src/test/delisted.rs @@ -25,7 +25,7 @@ fn a_delisted_holders_priced_deposit_joins_their_redemption_in_the_open_epoch() f.e.events().all().filter_by_contract(&f.vault.address), [ DepositConvertedToExit { - controller: investor.clone(), + holder: investor.clone(), epoch, into_epoch: open, shares: 200, @@ -116,7 +116,7 @@ fn in_a_wind_down_the_shares_are_credited_before_the_first_round() { f.e.events().all().filter_by_contract(&f.vault.address), [ DepositConvertedToExit { - controller: investor.clone(), + holder: investor.clone(), epoch, into_epoch: 0, shares: 200, @@ -204,7 +204,7 @@ fn a_conversion_caught_by_a_wind_down_is_paid_from_the_rounds() { assert_eq!( f.e.events().all().filter_by_contract(&f.vault.address), [RedemptionSurrendered { - controller: investor.clone(), + holder: investor.clone(), epoch: open, shares: 200, } diff --git a/contracts/async-vault/src/test/events.rs b/contracts/async-vault/src/test/events.rs index 39fdb963..8d2988a7 100644 --- a/contracts/async-vault/src/test/events.rs +++ b/contracts/async-vault/src/test/events.rs @@ -13,7 +13,7 @@ fn requesting_a_deposit_publishes_the_request() { assert_eq!( f.e.events().all().filter_by_contract(&f.vault.address), [DepositRequested { - controller: user, + holder: user, epoch: 1, amount: 400, } @@ -74,7 +74,7 @@ fn claiming_the_last_deposit_publishes_the_shares_paid_and_the_settlement() { f.e.events().all().filter_by_contract(&f.vault.address), [ DepositClaimed { - controller: user, + holder: user, epoch: 1, amount: 400, shares: 200, diff --git a/contracts/async-vault/src/test/mod.rs b/contracts/async-vault/src/test/mod.rs index b95c7a16..7b719b32 100644 --- a/contracts/async-vault/src/test/mod.rs +++ b/contracts/async-vault/src/test/mod.rs @@ -43,7 +43,7 @@ pub(crate) use share_token::{ShareToken, ShareTokenClient}; pub(crate) use stellar_contract_utils::math::wad::WAD_SCALE; use stellar_tokens::rwa::compliance::ComplianceHook; -pub(crate) use timelock::DAY_DURATION; +pub(crate) use strata_timelock::DAY_DURATION; pub(crate) use crate::{ AsyncVault, AsyncVaultClient, DirectUnitPricing, EpochStatus, PricingScheme, VaultChange, diff --git a/contracts/async-vault/src/test/multi_epoch.rs b/contracts/async-vault/src/test/multi_epoch.rs index aab0a04e..0e19b678 100644 --- a/contracts/async-vault/src/test/multi_epoch.rs +++ b/contracts/async-vault/src/test/multi_epoch.rs @@ -78,7 +78,7 @@ fn the_rounding_residue_stays_in_the_reserve_once_the_epoch_settles() { assert_eq!(f.vault.free_reserve(), held - 462); } -/// One controller may hold a live redemption in two epochs at once, because the +/// One holder may hold a live redemption in two epochs at once, because the /// outstanding-request check is keyed per epoch. Claims are taken newest first /// here, which today is allowed. #[test] diff --git a/contracts/async-vault/src/test/oracle_swap.rs b/contracts/async-vault/src/test/oracle_swap.rs index ef5966f7..a383343c 100644 --- a/contracts/async-vault/src/test/oracle_swap.rs +++ b/contracts/async-vault/src/test/oracle_swap.rs @@ -12,13 +12,13 @@ impl FeedStub { e.storage().instance().set(&symbol_short!("vault"), &vault); } - pub fn state(_e: Env) -> bindings::OracleState { - bindings::OracleState::Valid + pub fn state(_e: Env) -> strata_bindings::OracleState { + strata_bindings::OracleState::Valid } } #[contractimpl(contracttrait)] -impl bindings::VaultBinding for FeedStub { +impl strata_bindings::VaultBinding for FeedStub { fn vault(e: &Env) -> Option
{ e.storage() .instance() diff --git a/contracts/async-vault/src/test/settlement.rs b/contracts/async-vault/src/test/settlement.rs index aded2f31..9976020f 100644 --- a/contracts/async-vault/src/test/settlement.rs +++ b/contracts/async-vault/src/test/settlement.rs @@ -49,7 +49,7 @@ fn the_last_redemption_claim_clears_the_cash_remainder_and_says_so_once() { f.e.events().all().filter_by_contract(&f.vault.address), [ RedeemClaimed { - controller: b, + holder: b, epoch, shares: 103, assets: 154, diff --git a/contracts/async-vault/src/test/ttl.rs b/contracts/async-vault/src/test/ttl.rs index 06203925..9bc2ab65 100644 --- a/contracts/async-vault/src/test/ttl.rs +++ b/contracts/async-vault/src/test/ttl.rs @@ -1,5 +1,5 @@ use soroban_sdk::testutils::storage::{Instance as _, Persistent as _}; -use storage::{INSTANCE_EXTEND_AMOUNT, PERSISTENT_EXTEND_AMOUNT}; +use strata_storage::{INSTANCE_EXTEND_AMOUNT, PERSISTENT_EXTEND_AMOUNT}; use super::*; use crate::keys::DataKey; diff --git a/contracts/async-vault/src/test/upgrade.rs b/contracts/async-vault/src/test/upgrade.rs index dd01ca0c..5c4ed548 100644 --- a/contracts/async-vault/src/test/upgrade.rs +++ b/contracts/async-vault/src/test/upgrade.rs @@ -413,11 +413,11 @@ fn the_vault_reports_its_clock_to_the_token_and_the_oracle() { f.vault.pause(&f.guardian); f.advance(DAY_DURATION); - let clock = timelock::ClockSourceClient::new(&f.e, &f.vault.address).timelock_clock(); + let clock = strata_timelock::ClockSourceClient::new(&f.e, &f.vault.address).timelock_clock(); assert_eq!( clock, - timelock::Clock { + strata_timelock::Clock { delay: MIN_UPGRADE_DELAY, exit_window: 0, paused_secs: DAY_DURATION, diff --git a/contracts/async-vault/src/treasury.rs b/contracts/async-vault/src/treasury.rs index 4958a1fe..e8d0c36c 100644 --- a/contracts/async-vault/src/treasury.rs +++ b/contracts/async-vault/src/treasury.rs @@ -73,8 +73,8 @@ pub(crate) fn deploy(e: &Env, assets: i128) -> i128 { net_deployed } -pub(crate) fn fund(e: &Env, from: &Address, assets: i128) -> i128 { - from.require_auth(); +pub(crate) fn fund(e: &Env, caller: &Address, assets: i128) -> i128 { + caller.require_auth(); if assets <= 0 { panic_with_error!(e, VaultError::InvalidAmount); @@ -86,10 +86,10 @@ pub(crate) fn fund(e: &Env, from: &Address, assets: i128) -> i128 { state::set_net_deployed(e, net_deployed); let asset = state::get_addr(e, &DataKey::Asset); - TokenClient::new(e, &asset).transfer(from, e.current_contract_address(), &assets); + TokenClient::new(e, &asset).transfer(caller, e.current_contract_address(), &assets); Funded { - from: from.clone(), + caller: caller.clone(), assets, net_deployed, } diff --git a/contracts/async-vault/src/upgrade.rs b/contracts/async-vault/src/upgrade.rs index ff532e71..28000229 100644 --- a/contracts/async-vault/src/upgrade.rs +++ b/contracts/async-vault/src/upgrade.rs @@ -6,17 +6,17 @@ use crate::state; /// Seven days. Short enough to fix a defect in a week, long enough that an /// investor who dislikes the change has time to act on it. -pub const MIN_UPGRADE_DELAY: u64 = timelock::MIN_DELAY; +pub const MIN_UPGRADE_DELAY: u64 = strata_timelock::MIN_DELAY; /// Ninety days. Prevents setting a delay that effectively brick-walls the vault /// against future upgrades. -pub const MAX_UPGRADE_DELAY: u64 = timelock::MAX_DELAY; +pub const MAX_UPGRADE_DELAY: u64 = strata_timelock::MAX_DELAY; pub(crate) fn delay(e: &Env) -> u64 { - storage::get_instance(e, &DataKey::UpgradeDelay).unwrap_or(MIN_UPGRADE_DELAY) + strata_storage::get_instance(e, &DataKey::UpgradeDelay).unwrap_or(MIN_UPGRADE_DELAY) } pub(crate) fn set_delay(e: &Env, secs: u64) { - storage::set_instance(e, &DataKey::UpgradeDelay, &secs); + strata_storage::set_instance(e, &DataKey::UpgradeDelay, &secs); } /// An exit takes a full notice, so a delay shorter than one is not a window. diff --git a/contracts/async-vault/src/wind_down.rs b/contracts/async-vault/src/wind_down.rs index 651ab517..6667abe9 100644 --- a/contracts/async-vault/src/wind_down.rs +++ b/contracts/async-vault/src/wind_down.rs @@ -14,12 +14,12 @@ use crate::treasury; /// Ninety days. Long enough for a real announcement period, short enough that /// governance cannot set a delay the vault would never outlive. -pub const MAX_WIND_DOWN_DELAY: u64 = 90 * timelock::DAY_DURATION; +pub const MAX_WIND_DOWN_DELAY: u64 = 90 * strata_timelock::DAY_DURATION; /// Seven days: holders always get a week's warning before a wind-down starts. /// It is also the delay when none was set. #[cfg(not(feature = "short_wind_down"))] -pub const MIN_WIND_DOWN_DELAY: u64 = 7 * timelock::DAY_DURATION; +pub const MIN_WIND_DOWN_DELAY: u64 = 7 * strata_timelock::DAY_DURATION; /// The local harness build activates a wind-down at once. #[cfg(feature = "short_wind_down")] pub const MIN_WIND_DOWN_DELAY: u64 = 0; @@ -50,7 +50,7 @@ pub struct WindDownPosition { } pub(crate) fn delay(e: &Env) -> u64 { - storage::get_instance(e, &DataKey::WindDownDelay).unwrap_or(MIN_WIND_DOWN_DELAY) + strata_storage::get_instance(e, &DataKey::WindDownDelay).unwrap_or(MIN_WIND_DOWN_DELAY) } #[cfg_attr(feature = "short_wind_down", allow(clippy::absurd_extreme_comparisons))] @@ -65,12 +65,12 @@ pub(crate) fn check_delay(e: &Env, secs: u64) { pub(crate) fn set_delay(e: &Env, secs: u64) { check_delay(e, secs); - storage::set_instance(e, &DataKey::WindDownDelay, &secs); + strata_storage::set_instance(e, &DataKey::WindDownDelay, &secs); WindDownDelaySet { secs }.publish(e); } pub(crate) fn info(e: &Env) -> Option { - storage::get_instance(e, &DataKey::WindDown) + strata_storage::get_instance(e, &DataKey::WindDown) } pub(crate) fn is_active(e: &Env) -> bool { @@ -108,7 +108,7 @@ pub(crate) fn propose(e: &Env) { } let active_at = e.ledger().timestamp().saturating_add(delay(e)); - storage::set_instance( + strata_storage::set_instance( e, &DataKey::WindDown, &WindDownInfo { @@ -149,25 +149,25 @@ pub(crate) fn activate(e: &Env) { } wd.status = WindDownStatus::Active; - storage::set_instance(e, &DataKey::WindDown, &wd); + strata_storage::set_instance(e, &DataKey::WindDown, &wd); WindDownActivated {}.publish(e); } pub(crate) fn owed(e: &Env) -> i128 { - storage::get_instance(e, &DataKey::WindDownOwed).unwrap_or(0) + strata_storage::get_instance(e, &DataKey::WindDownOwed).unwrap_or(0) } pub(crate) fn acc(e: &Env) -> i128 { - storage::get_instance(e, &DataKey::WindDownAcc).unwrap_or(0) + strata_storage::get_instance(e, &DataKey::WindDownAcc).unwrap_or(0) } pub(crate) fn supply_snapshot(e: &Env) -> i128 { - storage::get_instance(e, &DataKey::WindDownSupply).unwrap_or(0) + strata_storage::get_instance(e, &DataKey::WindDownSupply).unwrap_or(0) } fn credited_before_snapshot(e: &Env) -> i128 { - storage::get_instance(e, &DataKey::WindDownCredited).unwrap_or(0) + strata_storage::get_instance(e, &DataKey::WindDownCredited).unwrap_or(0) } /// Shares that exist or are owed: circulating, escrowed against an unpriced @@ -190,13 +190,13 @@ pub(crate) fn credit(e: &Env, holder: &Address, shares: i128) { .entitlement .checked_add(shares) .unwrap_or_else(|| panic_with_error!(e, VaultError::AmountTooLarge)); - storage::set_persistent(e, &DataKey::WindDownPosition(holder.clone()), &p); + strata_storage::set_persistent(e, &DataKey::WindDownPosition(holder.clone()), &p); if info(e).map(|wd| wd.round).unwrap_or(0) == 0 { let credited = credited_before_snapshot(e) .checked_add(shares) .unwrap_or_else(|| panic_with_error!(e, VaultError::AmountTooLarge)); - storage::set_instance(e, &DataKey::WindDownCredited, &credited); + strata_storage::set_instance(e, &DataKey::WindDownCredited, &credited); } } @@ -208,7 +208,7 @@ pub(crate) fn finalize_round(e: &Env) -> i128 { let snapshot = if wd.round == 0 { let s = snapshot_supply(e); - storage::set_instance(e, &DataKey::WindDownSupply, &s); + strata_storage::set_instance(e, &DataKey::WindDownSupply, &s); s } else { supply_snapshot(e) @@ -242,11 +242,11 @@ pub(crate) fn finalize_round(e: &Env) -> i128 { let owed_total = owed(e) .checked_add(credited) .unwrap_or_else(|| panic_with_error!(e, VaultError::AmountTooLarge)); - storage::set_instance(e, &DataKey::WindDownAcc, &acc_per_share); - storage::set_instance(e, &DataKey::WindDownOwed, &owed_total); + strata_storage::set_instance(e, &DataKey::WindDownAcc, &acc_per_share); + strata_storage::set_instance(e, &DataKey::WindDownOwed, &owed_total); wd.round += 1; - storage::set_instance(e, &DataKey::WindDown, &wd); + strata_storage::set_instance(e, &DataKey::WindDown, &wd); WindDownRoundFinalized { round: wd.round, @@ -260,7 +260,7 @@ pub(crate) fn finalize_round(e: &Env) -> i128 { } pub(crate) fn position(e: &Env, holder: &Address) -> WindDownPosition { - storage::get_persistent(e, &DataKey::WindDownPosition(holder.clone())).unwrap_or( + strata_storage::get_persistent(e, &DataKey::WindDownPosition(holder.clone())).unwrap_or( WindDownPosition { entitlement: 0, paid: 0, @@ -324,14 +324,14 @@ pub(crate) fn claim(e: &Env, holder: &Address) -> i128 { } p.paid = earned; - storage::set_persistent(e, &DataKey::WindDownPosition(holder.clone()), &p); + strata_storage::set_persistent(e, &DataKey::WindDownPosition(holder.clone()), &p); if surrendered > 0 { RWATokenClient::new(e, &share_token).burn(holder, &surrendered, &vault); } if assets > 0 { - storage::set_instance( + strata_storage::set_instance( e, &DataKey::WindDownOwed, &state::reduce(e, owed(e), assets), diff --git a/contracts/compliance/Cargo.toml b/contracts/compliance/Cargo.toml index 6ebfdf51..aa79d1b2 100644 --- a/contracts/compliance/Cargo.toml +++ b/contracts/compliance/Cargo.toml @@ -14,9 +14,9 @@ soroban-sdk = { workspace = true } stellar-access = { workspace = true } stellar-macros = { workspace = true } stellar-tokens = { workspace = true } -storage = { path = "../../crates/storage" } -timelock = { path = "../../crates/timelock" } -bindings = { path = "../../crates/bindings" } +strata-storage = { path = "../../crates/strata-storage" } +strata-timelock = { path = "../../crates/strata-timelock" } +strata-bindings = { path = "../../crates/strata-bindings" } [dev-dependencies] soroban-sdk = { workspace = true, features = ["testutils"] } diff --git a/contracts/compliance/src/governance.rs b/contracts/compliance/src/governance.rs index ee70a2a1..657f157c 100644 --- a/contracts/compliance/src/governance.rs +++ b/contracts/compliance/src/governance.rs @@ -4,7 +4,7 @@ use stellar_access::{ role_transfer, }; use stellar_tokens::rwa::{compliance, compliance::ComplianceHook, utils::token_binder}; -use timelock::{queue, setup, Clock, Refusal, TimelockHost}; +use strata_timelock::{queue, setup, Clock, Refusal, TimelockHost}; use crate::{ ChangeApplied, ChangeCancelled, ChangeProposed, Compliance, ComplianceContractError, @@ -37,7 +37,7 @@ pub struct CompliancePendingChange { /// The compliance contract keeps no clock of its own: it reads the vault's. impl TimelockHost for Compliance { fn clock(e: &Env) -> Result { - timelock::clock::from_source(e) + strata_timelock::clock::from_source(e) } } diff --git a/contracts/compliance/src/lib.rs b/contracts/compliance/src/lib.rs index f738634f..8340e0ee 100644 --- a/contracts/compliance/src/lib.rs +++ b/contracts/compliance/src/lib.rs @@ -12,10 +12,10 @@ contractmeta!( key = "stub", val = "testnet-only; prod = external compliance module" ); -use bindings::VaultBinding; use stellar_access::access_control; use stellar_macros::only_admin; use stellar_tokens::rwa::{compliance, compliance::ComplianceHook, utils::token_binder}; +use strata_bindings::VaultBinding; pub use governance::{ComplianceChange, CompliancePendingChange, ADMIN_ACCEPT_LEDGERS}; @@ -90,43 +90,43 @@ pub struct Compliance; impl Compliance { pub fn __constructor(e: &Env, admin: Address) { access_control::set_admin(e, &admin); - storage::bump_instance(e); + strata_storage::bump_instance(e); } /// Names the vault whose clock this contract's timelock reads. Once, and /// only during setup. #[only_admin] - pub fn bind_vault(e: &Env, vault: Address, _operator: Address) { - storage::bump_instance(e); + pub fn bind_vault(e: &Env, vault: Address, _caller: Address) { + strata_storage::bump_instance(e); governance::bind_vault(e, &vault); } /// Ends the window in which module and binding changes are instant. It /// never reopens. #[only_admin] - pub fn finish_setup(e: &Env, _operator: Address) { - storage::bump_instance(e); + pub fn finish_setup(e: &Env, _caller: Address) { + strata_storage::bump_instance(e); governance::finish_setup(e); } /// Queues a change behind the vault's timelock and returns its id. #[only_admin] - pub fn propose_change(e: &Env, change: ComplianceChange, _operator: Address) -> u32 { - storage::bump_instance(e); + pub fn propose_change(e: &Env, change: ComplianceChange, _caller: Address) -> u32 { + strata_storage::bump_instance(e); governance::propose(e, change) } #[only_admin] - pub fn cancel_change(e: &Env, id: u32, _operator: Address) { - storage::bump_instance(e); + pub fn cancel_change(e: &Env, id: u32, _caller: Address) { + strata_storage::bump_instance(e); governance::cancel(e, id); } /// Lands a change whose delay has run. Refused while the vault is paused /// or winding down. #[only_admin] - pub fn apply_change(e: &Env, id: u32, _operator: Address) { - storage::bump_instance(e); + pub fn apply_change(e: &Env, id: u32, _caller: Address) { + strata_storage::bump_instance(e); governance::apply(e, id); } @@ -141,12 +141,12 @@ impl Compliance { if live_until_ledger != 0 { governance::require_setup_open(e); } - storage::bump_instance(e); + strata_storage::bump_instance(e); access_control::transfer_admin_role(e, &new_admin, live_until_ledger); } pub fn accept_admin_transfer(e: &Env) { - storage::bump_instance(e); + strata_storage::bump_instance(e); access_control::accept_admin_transfer(e); } @@ -158,11 +158,11 @@ impl Compliance { #[contractimpl(contracttrait)] impl VaultBinding for Compliance { fn vault(e: &Env) -> Option
{ - timelock::setup::source(e) + strata_timelock::setup::source(e) } fn setup_open(e: &Env) -> bool { - timelock::setup::is_open(e) + strata_timelock::setup::is_open(e) } } @@ -173,14 +173,14 @@ impl token_binder::TokenBinder for Compliance { #[only_admin] fn bind_token(e: &Env, token: Address, _operator: Address) { governance::require_setup_open(e); - storage::bump_instance(e); + strata_storage::bump_instance(e); token_binder::bind_token(e, &token); } #[only_admin] fn unbind_token(e: &Env, token: Address, _operator: Address) { governance::require_setup_open(e); - storage::bump_instance(e); + strata_storage::bump_instance(e); token_binder::unbind_token(e, &token); } } @@ -194,39 +194,39 @@ impl compliance::Compliance for Compliance { #[only_admin] fn add_module_to(e: &Env, hook: ComplianceHook, module: Address, _operator: Address) { governance::require_setup_open(e); - storage::bump_instance(e); + strata_storage::bump_instance(e); compliance::storage::add_module_to(e, hook, module); } #[only_admin] fn remove_module_from(e: &Env, hook: ComplianceHook, module: Address, _operator: Address) { governance::require_setup_open(e); - storage::bump_instance(e); + strata_storage::bump_instance(e); compliance::storage::remove_module_from(e, hook, module); } fn transferred(e: &Env, from: Address, to: Address, amount: i128, token: Address) { - storage::bump_instance(e); + strata_storage::bump_instance(e); compliance::storage::transferred(e, from, to, amount, token); } fn created(e: &Env, to: Address, amount: i128, token: Address) { - storage::bump_instance(e); + strata_storage::bump_instance(e); compliance::storage::created(e, to, amount, token); } fn destroyed(e: &Env, from: Address, amount: i128, token: Address) { - storage::bump_instance(e); + strata_storage::bump_instance(e); compliance::storage::destroyed(e, from, amount, token); } fn can_transfer(e: &Env, from: Address, to: Address, amount: i128, token: Address) -> bool { - storage::bump_instance(e); + strata_storage::bump_instance(e); compliance::storage::can_transfer(e, from, to, amount, token) } fn can_create(e: &Env, to: Address, amount: i128, token: Address) -> bool { - storage::bump_instance(e); + strata_storage::bump_instance(e); compliance::storage::can_create(e, to, amount, token) } } diff --git a/contracts/compliance/src/test.rs b/contracts/compliance/src/test.rs index 14200161..07de0b74 100644 --- a/contracts/compliance/src/test.rs +++ b/contracts/compliance/src/test.rs @@ -4,7 +4,7 @@ use soroban_sdk::{ testutils::{storage::Instance as _, Address as _, Ledger as _}, Address, Env, }; -use storage::INSTANCE_EXTEND_AMOUNT; +use strata_storage::INSTANCE_EXTEND_AMOUNT; use crate::{Compliance, ComplianceClient}; diff --git a/contracts/compliance/src/test/changes.rs b/contracts/compliance/src/test/changes.rs index 31b6e9d1..89734b4f 100644 --- a/contracts/compliance/src/test/changes.rs +++ b/contracts/compliance/src/test/changes.rs @@ -5,8 +5,8 @@ use soroban_sdk::{ }; use stellar_access::role_transfer::RoleTransferError; use stellar_tokens::rwa::compliance::ComplianceHook; -use storage::INSTANCE_EXTEND_AMOUNT; -use timelock::{Clock, DAY_DURATION, MIN_DELAY}; +use strata_storage::INSTANCE_EXTEND_AMOUNT; +use strata_timelock::{Clock, DAY_DURATION, MIN_DELAY}; use super::{age, instance_ttl}; use crate::{ @@ -33,7 +33,7 @@ impl ClockStub { } #[contractimpl(contracttrait)] -impl timelock::ClockSource for ClockStub { +impl strata_timelock::ClockSource for ClockStub { fn timelock_clock(e: &Env) -> Clock { let reads = Self::reads(e.clone()) + 1; e.storage().instance().set(&symbol_short!("reads"), &reads); @@ -186,13 +186,13 @@ fn an_address_that_reports_no_clock_cannot_be_bound() { assert_eq!(f.rules.vault(), None); } -fn implements_the_vault_view() {} +fn implements_the_vault_view() {} #[test] fn the_contract_answers_the_view_the_vault_reads() { implements_the_vault_view::(); let f = unbound(); - let view = bindings::VaultBindingClient::new(&f.e, &f.rules.address); + let view = strata_bindings::VaultBindingClient::new(&f.e, &f.rules.address); assert_eq!(view.vault(), None); assert!(view.setup_open()); diff --git a/contracts/identity-verifier/Cargo.toml b/contracts/identity-verifier/Cargo.toml index 86550d5b..7eb6f83c 100644 --- a/contracts/identity-verifier/Cargo.toml +++ b/contracts/identity-verifier/Cargo.toml @@ -14,7 +14,7 @@ soroban-sdk = { workspace = true } stellar-access = { workspace = true } stellar-macros = { workspace = true } stellar-tokens = { workspace = true } -storage = { path = "../../crates/storage" } +strata-storage = { path = "../../crates/strata-storage" } [dev-dependencies] soroban-sdk = { workspace = true, features = ["testutils"] } diff --git a/contracts/identity-verifier/src/lib.rs b/contracts/identity-verifier/src/lib.rs index 4206c639..83a677c1 100644 --- a/contracts/identity-verifier/src/lib.rs +++ b/contracts/identity-verifier/src/lib.rs @@ -26,20 +26,20 @@ pub struct IdentityVerifier; impl IdentityVerifier { pub fn __constructor(e: &Env, admin: Address) { access_control::set_admin(e, &admin); - storage::bump_instance(e); + strata_storage::bump_instance(e); } /// Adds or removes an account from the allowlist. #[only_admin] pub fn allow(e: &Env, account: Address, allowed: bool, caller: Address) { caller.require_auth(); - storage::bump_instance(e); - storage::set_persistent(e, &DataKey::Allowed(account), &allowed); + strata_storage::bump_instance(e); + strata_storage::set_persistent(e, &DataKey::Allowed(account), &allowed); } pub fn is_allowed(e: &Env, account: Address) -> bool { - storage::bump_instance(e); - storage::get_persistent(e, &DataKey::Allowed(account)).unwrap_or(false) + strata_storage::bump_instance(e); + strata_storage::get_persistent(e, &DataKey::Allowed(account)).unwrap_or(false) } } @@ -48,9 +48,9 @@ impl identity_verification::IdentityVerifier for IdentityVerifier { /// The share token calls this on every mint and transfer, so it is what /// keeps the instance alive. fn verify_identity(e: &Env, account: &Address) { - storage::bump_instance(e); + strata_storage::bump_instance(e); let allowed: bool = - storage::get_persistent(e, &DataKey::Allowed(account.clone())).unwrap_or(false); + strata_storage::get_persistent(e, &DataKey::Allowed(account.clone())).unwrap_or(false); if !allowed { panic_with_error!(e, RWAError::IdentityVerificationFailed); } diff --git a/contracts/identity-verifier/src/test.rs b/contracts/identity-verifier/src/test.rs index 221bf836..7b9190a1 100644 --- a/contracts/identity-verifier/src/test.rs +++ b/contracts/identity-verifier/src/test.rs @@ -3,7 +3,7 @@ use soroban_sdk::{ testutils::{storage::Instance as _, storage::Persistent as _, Address as _, Ledger as _}, Address, Env, }; -use storage::{INSTANCE_EXTEND_AMOUNT, PERSISTENT_EXTEND_AMOUNT}; +use strata_storage::{INSTANCE_EXTEND_AMOUNT, PERSISTENT_EXTEND_AMOUNT}; const DAY_IN_LEDGERS: u32 = 17_280; use stellar_tokens::rwa::identity_verification::IdentityVerifierClient as IdClient; diff --git a/contracts/nav-oracle/Cargo.toml b/contracts/nav-oracle/Cargo.toml index 5061ab97..3835900c 100644 --- a/contracts/nav-oracle/Cargo.toml +++ b/contracts/nav-oracle/Cargo.toml @@ -11,11 +11,11 @@ doctest = false [dependencies] soroban-sdk = { workspace = true } -storage = { path = "../../crates/storage" } +strata-storage = { path = "../../crates/strata-storage" } stellar-access = { workspace = true } stellar-macros = { workspace = true } -bindings = { path = "../../crates/bindings" } -timelock = { path = "../../crates/timelock" } +strata-bindings = { path = "../../crates/strata-bindings" } +strata-timelock = { path = "../../crates/strata-timelock" } stellar-contract-utils = { workspace = true } [dev-dependencies] diff --git a/contracts/nav-oracle/src/governance.rs b/contracts/nav-oracle/src/governance.rs index 553af765..aafa501d 100644 --- a/contracts/nav-oracle/src/governance.rs +++ b/contracts/nav-oracle/src/governance.rs @@ -4,7 +4,7 @@ use stellar_access::{ role_transfer, }; use stellar_contract_utils::upgradeable; -use timelock::{queue, setup, Clock, Refusal, TimelockHost}; +use strata_timelock::{queue, setup, Clock, Refusal, TimelockHost}; use crate::state::OracleConfig; use crate::{ @@ -51,7 +51,7 @@ pub fn shortens_freshness_only(current: &OracleConfig, next: &OracleConfig) -> b /// The oracle keeps no clock of its own: it reads the vault's. impl TimelockHost for NavOracleContract { fn clock(e: &Env) -> Result { - timelock::clock::from_source(e) + strata_timelock::clock::from_source(e) } } diff --git a/contracts/nav-oracle/src/lib.rs b/contracts/nav-oracle/src/lib.rs index cbe84820..12eecb99 100644 --- a/contracts/nav-oracle/src/lib.rs +++ b/contracts/nav-oracle/src/lib.rs @@ -3,7 +3,6 @@ mod governance; mod state; -use bindings::{OracleFeed, VaultBinding}; use soroban_sdk::{ contract, contracterror, contractevent, contractimpl, panic_with_error, Address, Env, Symbol, Vec, @@ -11,6 +10,7 @@ use soroban_sdk::{ use stellar_access::access_control::{self, AccessControl}; use stellar_contract_utils::upgradeable; use stellar_macros::{only_admin, only_role}; +use strata_bindings::{OracleFeed, VaultBinding}; use state::{get_config, get_latest, nav_paused, ATTESTER_ROLE, GUARDIAN_ROLE}; @@ -25,7 +25,7 @@ const WAD: i128 = 1_000_000_000_000_000_000; /// One report per ledger at most. A deployment sets a longer cooldown. pub const MIN_COOLDOWN_SECS: u64 = 1; /// Thirty-five days: a monthly valuation with slack, and no longer. -pub const MAX_FRESHNESS_SECS: u64 = 35 * timelock::DAY_DURATION; +pub const MAX_FRESHNESS_SECS: u64 = 35 * strata_timelock::DAY_DURATION; /// A million units of the asset per share. Far above any real price, and far /// below where the deviation arithmetic could overflow. pub const MAX_ANSWER: i128 = 1_000_000 * WAD; @@ -202,7 +202,7 @@ impl AccessControl for NavOracleContract { /// value. fn grant_role(e: &Env, account: Address, role: Symbol, caller: Address) { governance::require_setup_open(e); - storage::bump_instance(e); + strata_storage::bump_instance(e); access_control::grant_role(e, &account, &role, &caller); } @@ -212,7 +212,7 @@ impl AccessControl for NavOracleContract { if live_until_ledger != 0 { governance::require_setup_open(e); } - storage::bump_instance(e); + strata_storage::bump_instance(e); access_control::transfer_admin_role(e, &new_admin, live_until_ledger); } @@ -233,11 +233,11 @@ impl AccessControl for NavOracleContract { #[contractimpl(contracttrait)] impl VaultBinding for NavOracleContract { fn vault(e: &Env) -> Option
{ - timelock::setup::source(e) + strata_timelock::setup::source(e) } fn setup_open(e: &Env) -> bool { - timelock::setup::is_open(e) + strata_timelock::setup::is_open(e) } } @@ -269,7 +269,7 @@ impl NavOracleContract { #[only_admin] pub fn bind_vault(e: &Env, vault: Address, caller: Address) { caller.require_auth(); - storage::bump_instance(e); + strata_storage::bump_instance(e); governance::bind_vault(e, &vault); } @@ -277,7 +277,7 @@ impl NavOracleContract { #[only_admin] pub fn finish_setup(e: &Env, caller: Address) { caller.require_auth(); - storage::bump_instance(e); + strata_storage::bump_instance(e); governance::finish_setup(e); } @@ -285,14 +285,14 @@ impl NavOracleContract { #[only_admin] pub fn propose_change(e: &Env, change: OracleChange, caller: Address) -> u32 { caller.require_auth(); - storage::bump_instance(e); + strata_storage::bump_instance(e); governance::propose(e, change) } #[only_admin] pub fn cancel_change(e: &Env, id: u32, caller: Address) { caller.require_auth(); - storage::bump_instance(e); + strata_storage::bump_instance(e); governance::cancel(e, id); } @@ -301,7 +301,7 @@ impl NavOracleContract { #[only_admin] pub fn apply_change(e: &Env, id: u32, caller: Address) { caller.require_auth(); - storage::bump_instance(e); + strata_storage::bump_instance(e); governance::apply(e, id); } @@ -328,7 +328,7 @@ impl NavOracleContract { #[only_role(caller, "attester")] pub fn attest(e: &Env, report: NavReport, caller: Address) { - storage::bump_instance(e); + strata_storage::bump_instance(e); if nav_paused(e) { panic_with_error!(e, OracleError::NavPaused); } @@ -412,7 +412,8 @@ impl NavOracleContract { #[only_admin] pub fn set_config(e: &Env, config: OracleConfig) { validate_config(e, &config); - if !timelock::setup::is_open(e) && !shortens_freshness_only(&get_config(e), &config) { + if !strata_timelock::setup::is_open(e) && !shortens_freshness_only(&get_config(e), &config) + { panic_with_error!(e, OracleError::UseTimelock); } store_config(e, &config); @@ -455,7 +456,7 @@ impl NavOracleContract { #[only_admin] pub fn migrate(e: &Env, caller: Address) { caller.require_auth(); - storage::bump_instance(e); + strata_storage::bump_instance(e); if upgradeable::get_schema_version(e) >= 2 { panic_with_error!(e, ProbeError::AlreadyMigrated); } diff --git a/contracts/nav-oracle/src/state.rs b/contracts/nav-oracle/src/state.rs index 59af24e2..81786870 100644 --- a/contracts/nav-oracle/src/state.rs +++ b/contracts/nav-oracle/src/state.rs @@ -8,7 +8,7 @@ use soroban_sdk::{contracttype, symbol_short, Env, Symbol}; // Value untrusted; BOTH mint and redeem halt. // - `Paused`: the NAV is paused (issuer freeze). Value ignored entirely, both // directions, until the issuer clears the NAV pause. -pub use bindings::OracleState; +pub use strata_bindings::OracleState; pub const ATTESTER_ROLE: Symbol = symbol_short!("attester"); @@ -49,23 +49,23 @@ pub(crate) enum DataKey { } pub(crate) fn set_config(e: &Env, config: &OracleConfig) { - storage::set_instance(e, &DataKey::Config, config); + strata_storage::set_instance(e, &DataKey::Config, config); } pub(crate) fn get_config(e: &Env) -> OracleConfig { - storage::get_instance(e, &DataKey::Config).unwrap() + strata_storage::get_instance(e, &DataKey::Config).unwrap() } pub(crate) fn set_nav_paused(e: &Env, paused: bool) { - storage::set_instance(e, &DataKey::NavPaused, &paused); + strata_storage::set_instance(e, &DataKey::NavPaused, &paused); } pub(crate) fn nav_paused(e: &Env) -> bool { - storage::get_instance(e, &DataKey::NavPaused).unwrap_or(false) + strata_storage::get_instance(e, &DataKey::NavPaused).unwrap_or(false) } pub(crate) fn set_latest(e: &Env, report: &NavReport) { - storage::set_persistent(e, &DataKey::Latest, report); + strata_storage::set_persistent(e, &DataKey::Latest, report); } pub(crate) fn clear_latest(e: &Env) { @@ -73,5 +73,5 @@ pub(crate) fn clear_latest(e: &Env) { } pub(crate) fn get_latest(e: &Env) -> Option { - storage::get_persistent(e, &DataKey::Latest) + strata_storage::get_persistent(e, &DataKey::Latest) } diff --git a/contracts/nav-oracle/src/test.rs b/contracts/nav-oracle/src/test.rs index 3e9c0b99..bd435db8 100644 --- a/contracts/nav-oracle/src/test.rs +++ b/contracts/nav-oracle/src/test.rs @@ -181,14 +181,14 @@ fn the_latest_valuation_keeps_a_full_ttl_while_it_is_read() { let decayed = f.e.as_contract(&f.oracle.address, || { f.e.storage().persistent().get_ttl(&DataKey::Latest) }); - assert!(decayed < storage::PERSISTENT_EXTEND_AMOUNT); + assert!(decayed < strata_storage::PERSISTENT_EXTEND_AMOUNT); f.oracle.latest(); let restored = f.e.as_contract(&f.oracle.address, || { f.e.storage().persistent().get_ttl(&DataKey::Latest) }); - assert_eq!(restored, storage::PERSISTENT_EXTEND_AMOUNT); + assert_eq!(restored, strata_storage::PERSISTENT_EXTEND_AMOUNT); } #[test] @@ -651,7 +651,7 @@ fn the_bounds_themselves_are_accepted() { }); } -fn implements_the_feed() {} +fn implements_the_feed() {} #[test] fn the_oracle_answers_the_feed_interface_the_vault_reads() { @@ -660,7 +660,7 @@ fn the_oracle_answers_the_feed_interface_the_vault_reads() { f.oracle .attest(&report(&f.e, SCALE, 0, 1_000_000), &f.attester); - let feed = bindings::OracleFeedClient::new(&f.e, &f.oracle.address); + let feed = strata_bindings::OracleFeedClient::new(&f.e, &f.oracle.address); assert_eq!(feed.state(), OracleState::Valid); assert_eq!(feed.nav_per_share(), SCALE); diff --git a/contracts/nav-oracle/src/test_governance.rs b/contracts/nav-oracle/src/test_governance.rs index dd265fcd..842fa6ee 100644 --- a/contracts/nav-oracle/src/test_governance.rs +++ b/contracts/nav-oracle/src/test_governance.rs @@ -6,8 +6,8 @@ use soroban_sdk::{ Address, BytesN, Env, Event as _, IntoVal, }; use stellar_access::role_transfer::RoleTransferError; -use storage::INSTANCE_EXTEND_AMOUNT; -use timelock::{Clock, DAY_DURATION, MIN_DELAY}; +use strata_storage::INSTANCE_EXTEND_AMOUNT; +use strata_timelock::{Clock, DAY_DURATION, MIN_DELAY}; use crate::state::{ATTESTER_ROLE, GUARDIAN_ROLE}; use crate::{ @@ -38,7 +38,7 @@ impl ClockStub { } #[contractimpl(contracttrait)] -impl timelock::ClockSource for ClockStub { +impl strata_timelock::ClockSource for ClockStub { fn timelock_clock(e: &Env) -> Clock { let reads = Self::reads(e.clone()) + 1; e.storage().instance().set(&symbol_short!("reads"), &reads); @@ -60,7 +60,7 @@ struct Fixture<'a> { impl Fixture<'_> { fn clock(&self, f: impl FnOnce(&mut Clock)) { let mut clock = - timelock::ClockSourceClient::new(&self.e, &self.vault.address).timelock_clock(); + strata_timelock::ClockSourceClient::new(&self.e, &self.vault.address).timelock_clock(); f(&mut clock); self.vault.set(&clock); } @@ -416,8 +416,11 @@ fn a_config_outside_the_bounds_is_refused_when_it_applies() { // Skips the proposal's check, like a change proposed before a later // version narrowed the bounds. let id = f.e.as_contract(&f.oracle.address, || { - timelock::queue::propose::(&f.e, &OracleChange::Config(invalid)) - .unwrap() + strata_timelock::queue::propose::( + &f.e, + &OracleChange::Config(invalid), + ) + .unwrap() }); f.advance(MIN_DELAY); @@ -502,13 +505,13 @@ fn only_the_admin_binds_and_finishes() { assert!(f.oracle.setup_open()); } -fn implements_the_vault_view() {} +fn implements_the_vault_view() {} #[test] fn the_oracle_answers_the_view_the_vault_reads() { implements_the_vault_view::(); let f = setup(); - let view = bindings::VaultBindingClient::new(&f.e, &f.oracle.address); + let view = strata_bindings::VaultBindingClient::new(&f.e, &f.oracle.address); assert_eq!(view.vault(), None); assert!(view.setup_open()); diff --git a/contracts/share-token/Cargo.toml b/contracts/share-token/Cargo.toml index f26c96d0..d8588513 100644 --- a/contracts/share-token/Cargo.toml +++ b/contracts/share-token/Cargo.toml @@ -15,9 +15,9 @@ stellar-access = { workspace = true } stellar-macros = { workspace = true } stellar-tokens = { workspace = true } stellar-contract-utils = { workspace = true } -storage = { path = "../../crates/storage" } -timelock = { path = "../../crates/timelock" } -bindings = { path = "../../crates/bindings" } +strata-storage = { path = "../../crates/strata-storage" } +strata-timelock = { path = "../../crates/strata-timelock" } +strata-bindings = { path = "../../crates/strata-bindings" } [dev-dependencies] soroban-sdk = { workspace = true, features = ["testutils"] } diff --git a/contracts/share-token/src/contract.rs b/contracts/share-token/src/contract.rs index acfbe495..bbff2999 100644 --- a/contracts/share-token/src/contract.rs +++ b/contracts/share-token/src/contract.rs @@ -3,7 +3,6 @@ use crate::event::LegalTransfer; use crate::governance::{self, TokenChange, TokenPendingChange}; use crate::roles::COMPLIANCE_ROLE; -use bindings::VaultBinding; use soroban_sdk::{ contract, contractimpl, panic_with_error, Address, Env, MuxedAddress, String, Symbol, Vec, }; @@ -15,20 +14,21 @@ use stellar_tokens::{ fungible::{Base, ContractOverrides, FungibleToken}, rwa::{identity_verification::IdentityVerifierClient, RWAToken, RWA}, }; +use strata_bindings::VaultBinding; /// Longest order reference a legal transfer accepts, in bytes. pub const MAX_REFERENCE_LEN: u32 = 128; -/// The co-signing admin must be a different account from the operator. -fn require_admin_co_sign(e: &Env, operator: &Address) { +/// The co-signing admin must be a different account from the caller. +fn require_admin_co_sign(e: &Env, caller: &Address) { let admin = access_control::enforce_admin_auth(e); - if admin == *operator { + if admin == *caller { panic_with_error!(e, ShareTokenError::CoSignRequired); } } fn refuse_vault_address(e: &Env, account: &Address) { - if timelock::setup::source(e).as_ref() == Some(account) { + if strata_timelock::setup::source(e).as_ref() == Some(account) { panic_with_error!(e, ShareTokenError::VaultAddress); } } @@ -58,7 +58,7 @@ impl ShareToken { RWA::set_compliance(e, &compliance); RWA::set_identity_verifier(e, &identity_verifier); upgradeable::set_schema_version(e, 1); - storage::bump_instance(e); + strata_storage::bump_instance(e); } pub fn schema_version(e: &Env) -> u32 { @@ -70,7 +70,7 @@ impl ShareToken { #[only_admin] pub fn bind_vault(e: &Env, vault: Address, caller: Address) { caller.require_auth(); - storage::bump_instance(e); + strata_storage::bump_instance(e); governance::bind_vault(e, &vault); } @@ -78,7 +78,7 @@ impl ShareToken { #[only_admin] pub fn finish_setup(e: &Env, caller: Address) { caller.require_auth(); - storage::bump_instance(e); + strata_storage::bump_instance(e); governance::finish_setup(e); } @@ -86,14 +86,14 @@ impl ShareToken { #[only_admin] pub fn propose_change(e: &Env, change: TokenChange, caller: Address) -> u32 { caller.require_auth(); - storage::bump_instance(e); + strata_storage::bump_instance(e); governance::propose(e, change) } #[only_admin] pub fn cancel_change(e: &Env, id: u32, caller: Address) { caller.require_auth(); - storage::bump_instance(e); + strata_storage::bump_instance(e); governance::cancel(e, id); } @@ -102,7 +102,7 @@ impl ShareToken { #[only_admin] pub fn apply_change(e: &Env, id: u32, caller: Address) { caller.require_auth(); - storage::bump_instance(e); + strata_storage::bump_instance(e); governance::apply(e, id); } @@ -113,17 +113,17 @@ impl ShareToken { /// Moves shares under a legal order, at once. The compliance authority /// asks and the admin co-signs the same call. Never out of or into the /// vault's address, and only to an account the identity verifier accepts. - #[only_role(operator, "compliance")] + #[only_role(caller, "compliance")] pub fn legal_transfer( e: &Env, from: Address, to: Address, amount: i128, reference: String, - operator: Address, + caller: Address, ) { - storage::bump_instance(e); - require_admin_co_sign(e, &operator); + strata_storage::bump_instance(e); + require_admin_co_sign(e, &caller); if reference.is_empty() || reference.len() > MAX_REFERENCE_LEN { panic_with_error!(e, ShareTokenError::InvalidReference); } @@ -144,11 +144,11 @@ impl ShareToken { #[contractimpl(contracttrait)] impl VaultBinding for ShareToken { fn vault(e: &Env) -> Option
{ - timelock::setup::source(e) + strata_timelock::setup::source(e) } fn setup_open(e: &Env) -> bool { - timelock::setup::is_open(e) + strata_timelock::setup::is_open(e) } } @@ -157,13 +157,13 @@ impl Pausable for ShareToken { #[only_admin] fn pause(e: &Env, caller: Address) { caller.require_auth(); - storage::bump_instance(e); + strata_storage::bump_instance(e); pausable::pause(e); } #[only_admin] fn unpause(e: &Env, caller: Address) { caller.require_auth(); - storage::bump_instance(e); + strata_storage::bump_instance(e); pausable::unpause(e); } } @@ -175,17 +175,17 @@ impl FungibleToken for ShareToken { type ContractType = RWA; fn transfer(e: &Env, from: Address, to: MuxedAddress, amount: i128) { - storage::bump_instance(e); + strata_storage::bump_instance(e); ::transfer(e, &from, &to, amount); } fn transfer_from(e: &Env, spender: Address, from: Address, to: Address, amount: i128) { - storage::bump_instance(e); + strata_storage::bump_instance(e); ::transfer_from(e, &spender, &from, &to, amount); } fn approve(e: &Env, owner: Address, spender: Address, amount: i128, live_until_ledger: u32) { - storage::bump_instance(e); + strata_storage::bump_instance(e); ::approve( e, &owner, @@ -201,7 +201,7 @@ impl AccessControl for ShareToken { /// Instant during setup; after it, propose the grant through the timelock. fn grant_role(e: &Env, account: Address, role: Symbol, caller: Address) { governance::require_setup_open(e); - storage::bump_instance(e); + strata_storage::bump_instance(e); access_control::grant_role(e, &account, &role, &caller); } @@ -209,7 +209,7 @@ impl AccessControl for ShareToken { /// because the vault needs it to pay claims. fn revoke_role(e: &Env, account: Address, role: Symbol, caller: Address) { governance::require_revoke_allowed(e, &role); - storage::bump_instance(e); + strata_storage::bump_instance(e); access_control::revoke_role(e, &account, &role, &caller); } @@ -219,7 +219,7 @@ impl AccessControl for ShareToken { if live_until_ledger != 0 { governance::require_setup_open(e); } - storage::bump_instance(e); + strata_storage::bump_instance(e); access_control::transfer_admin_role(e, &new_admin, live_until_ledger); } @@ -241,19 +241,19 @@ impl AccessControl for ShareToken { impl RWAToken for ShareToken { #[only_role(operator, "manager")] fn mint(e: &Env, to: Address, amount: i128, operator: Address) { - storage::bump_instance(e); + strata_storage::bump_instance(e); RWA::mint(e, &to, amount); } #[only_role(operator, "manager")] fn burn(e: &Env, user_address: Address, amount: i128, operator: Address) { - storage::bump_instance(e); + strata_storage::bump_instance(e); RWA::burn(e, &user_address, amount); } /// The vault escrows redemptions through it. Compliance moves shares with /// `legal_transfer` instead. #[only_role(operator, "manager")] fn forced_transfer(e: &Env, from: Address, to: Address, amount: i128, operator: Address) { - storage::bump_instance(e); + strata_storage::bump_instance(e); RWA::forced_transfer(e, &from, &to, amount); } /// Moves a whole balance to the account the identity verifier names, so @@ -266,7 +266,7 @@ impl RWAToken for ShareToken { new_account: Address, operator: Address, ) -> bool { - storage::bump_instance(e); + strata_storage::bump_instance(e); require_admin_co_sign(e, &operator); refuse_vault_address(e, &old_account); refuse_vault_address(e, &new_account); @@ -274,7 +274,7 @@ impl RWAToken for ShareToken { } #[only_role(operator, "compliance")] fn set_address_frozen(e: &Env, user_address: Address, freeze: bool, operator: Address) { - storage::bump_instance(e); + strata_storage::bump_instance(e); if freeze { refuse_vault_address(e, &user_address); } @@ -282,27 +282,27 @@ impl RWAToken for ShareToken { } #[only_role(operator, "compliance")] fn freeze_partial_tokens(e: &Env, user_address: Address, amount: i128, operator: Address) { - storage::bump_instance(e); + strata_storage::bump_instance(e); refuse_vault_address(e, &user_address); RWA::freeze_partial_tokens(e, &user_address, amount); } #[only_role(operator, "compliance")] fn unfreeze_partial_tokens(e: &Env, user_address: Address, amount: i128, operator: Address) { - storage::bump_instance(e); + strata_storage::bump_instance(e); RWA::unfreeze_partial_tokens(e, &user_address, amount); } #[only_admin] fn set_compliance(e: &Env, compliance: Address, operator: Address) { operator.require_auth(); governance::require_setup_open(e); - storage::bump_instance(e); + strata_storage::bump_instance(e); RWA::set_compliance(e, &compliance); } #[only_admin] fn set_identity_verifier(e: &Env, identity_verifier: Address, operator: Address) { operator.require_auth(); governance::require_setup_open(e); - storage::bump_instance(e); + strata_storage::bump_instance(e); RWA::set_identity_verifier(e, &identity_verifier); } } @@ -322,7 +322,7 @@ impl ShareToken { #[only_admin] pub fn migrate(e: &Env, caller: Address) { caller.require_auth(); - storage::bump_instance(e); + strata_storage::bump_instance(e); if upgradeable::get_schema_version(e) >= 2 { panic_with_error!(e, ProbeError::AlreadyMigrated); } diff --git a/contracts/share-token/src/governance.rs b/contracts/share-token/src/governance.rs index b22f960b..0407a211 100644 --- a/contracts/share-token/src/governance.rs +++ b/contracts/share-token/src/governance.rs @@ -5,7 +5,7 @@ use stellar_access::{ }; use stellar_contract_utils::upgradeable; use stellar_tokens::rwa::RWA; -use timelock::{queue, setup, Clock, Refusal, TimelockHost}; +use strata_timelock::{queue, setup, Clock, Refusal, TimelockHost}; use crate::error::ShareTokenError; use crate::event::{ChangeApplied, ChangeCancelled, ChangeProposed, SetupFinished, VaultBound}; @@ -39,7 +39,7 @@ pub struct TokenPendingChange { /// The token keeps no clock of its own: it reads the vault's. impl TimelockHost for ShareToken { fn clock(e: &Env) -> Result { - timelock::clock::from_source(e) + strata_timelock::clock::from_source(e) } } diff --git a/contracts/share-token/src/test.rs b/contracts/share-token/src/test.rs index 7d7b7c63..7a4a67b0 100644 --- a/contracts/share-token/src/test.rs +++ b/contracts/share-token/src/test.rs @@ -5,7 +5,7 @@ use soroban_sdk::{ }; use stellar_access::access_control::{self as access_control, AccessControlError}; use stellar_tokens::rwa::RWAError; -use storage::INSTANCE_EXTEND_AMOUNT; +use strata_storage::INSTANCE_EXTEND_AMOUNT; use crate::{ShareToken, ShareTokenClient}; use compliance::{Compliance, ComplianceClient}; @@ -541,7 +541,7 @@ mod legal_transfer { Event as _, }; use stellar_tokens::rwa::identity_verification; - use timelock::{Clock, MIN_DELAY}; + use strata_timelock::{Clock, MIN_DELAY}; use super::*; use crate::{LegalTransfer, ShareTokenError, MAX_REFERENCE_LEN}; @@ -552,7 +552,7 @@ mod legal_transfer { struct VaultStub; #[contractimpl(contracttrait)] - impl timelock::ClockSource for VaultStub { + impl strata_timelock::ClockSource for VaultStub { fn timelock_clock(_e: &Env) -> Clock { Clock { delay: MIN_DELAY, diff --git a/contracts/share-token/src/test_governance.rs b/contracts/share-token/src/test_governance.rs index f394bba3..b0510654 100644 --- a/contracts/share-token/src/test_governance.rs +++ b/contracts/share-token/src/test_governance.rs @@ -6,8 +6,8 @@ use soroban_sdk::{ Address, BytesN, Env, Event as _, IntoVal, String, Symbol, }; use stellar_access::role_transfer::RoleTransferError; -use storage::INSTANCE_EXTEND_AMOUNT; -use timelock::{Clock, DAY_DURATION, MIN_DELAY}; +use strata_storage::INSTANCE_EXTEND_AMOUNT; +use strata_timelock::{Clock, DAY_DURATION, MIN_DELAY}; use crate::{ ChangeApplied, ChangeProposed, ShareToken, ShareTokenClient, ShareTokenError, TokenChange, @@ -35,7 +35,7 @@ impl ClockStub { } #[contractimpl(contracttrait)] -impl timelock::ClockSource for ClockStub { +impl strata_timelock::ClockSource for ClockStub { fn timelock_clock(e: &Env) -> Clock { let reads = Self::reads(e.clone()) + 1; e.storage().instance().set(&symbol_short!("reads"), &reads); @@ -53,7 +53,7 @@ struct Fixture<'a> { impl Fixture<'_> { fn clock(&self, f: impl FnOnce(&mut Clock)) { let mut clock = - timelock::ClockSourceClient::new(&self.e, &self.vault.address).timelock_clock(); + strata_timelock::ClockSourceClient::new(&self.e, &self.vault.address).timelock_clock(); f(&mut clock); self.vault.set(&clock); } @@ -231,13 +231,13 @@ fn only_the_admin_binds_and_finishes() { assert!(f.token.setup_open()); } -fn implements_the_vault_view() {} +fn implements_the_vault_view() {} #[test] fn the_token_answers_the_view_the_vault_reads() { implements_the_vault_view::(); let f = setup(); - let view = bindings::VaultBindingClient::new(&f.e, &f.token.address); + let view = strata_bindings::VaultBindingClient::new(&f.e, &f.token.address); assert_eq!(view.vault(), None); assert!(view.setup_open()); diff --git a/crates/bindings/Cargo.toml b/crates/strata-bindings/Cargo.toml similarity index 83% rename from crates/bindings/Cargo.toml rename to crates/strata-bindings/Cargo.toml index a7fb4a54..119a584c 100644 --- a/crates/bindings/Cargo.toml +++ b/crates/strata-bindings/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "bindings" +name = "strata-bindings" version = "0.1.0" edition.workspace = true license.workspace = true diff --git a/crates/bindings/src/lib.rs b/crates/strata-bindings/src/lib.rs similarity index 100% rename from crates/bindings/src/lib.rs rename to crates/strata-bindings/src/lib.rs diff --git a/crates/storage/Cargo.toml b/crates/strata-storage/Cargo.toml similarity index 89% rename from crates/storage/Cargo.toml rename to crates/strata-storage/Cargo.toml index 853bfd7e..3e577a0b 100644 --- a/crates/storage/Cargo.toml +++ b/crates/strata-storage/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "storage" +name = "strata-storage" version = "0.1.0" edition.workspace = true license.workspace = true diff --git a/crates/storage/src/lib.rs b/crates/strata-storage/src/lib.rs similarity index 100% rename from crates/storage/src/lib.rs rename to crates/strata-storage/src/lib.rs diff --git a/crates/storage/src/test.rs b/crates/strata-storage/src/test.rs similarity index 100% rename from crates/storage/src/test.rs rename to crates/strata-storage/src/test.rs diff --git a/crates/timelock/Cargo.toml b/crates/strata-timelock/Cargo.toml similarity index 73% rename from crates/timelock/Cargo.toml rename to crates/strata-timelock/Cargo.toml index ada90bb8..68733c88 100644 --- a/crates/timelock/Cargo.toml +++ b/crates/strata-timelock/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "timelock" +name = "strata-timelock" version = "0.1.0" edition = { workspace = true } license = { workspace = true } @@ -10,8 +10,8 @@ doctest = false [dependencies] soroban-sdk = { workspace = true } -storage = { path = "../storage" } +strata-storage = { path = "../strata-storage" } [dev-dependencies] soroban-sdk = { workspace = true, features = ["testutils"] } -proptest = "1" +proptest = "=1.11.0" diff --git a/crates/timelock/src/clock.rs b/crates/strata-timelock/src/clock.rs similarity index 100% rename from crates/timelock/src/clock.rs rename to crates/strata-timelock/src/clock.rs diff --git a/crates/timelock/src/delay.rs b/crates/strata-timelock/src/delay.rs similarity index 100% rename from crates/timelock/src/delay.rs rename to crates/strata-timelock/src/delay.rs diff --git a/crates/timelock/src/keys.rs b/crates/strata-timelock/src/keys.rs similarity index 100% rename from crates/timelock/src/keys.rs rename to crates/strata-timelock/src/keys.rs diff --git a/crates/timelock/src/lib.rs b/crates/strata-timelock/src/lib.rs similarity index 100% rename from crates/timelock/src/lib.rs rename to crates/strata-timelock/src/lib.rs diff --git a/crates/timelock/src/pause.rs b/crates/strata-timelock/src/pause.rs similarity index 62% rename from crates/timelock/src/pause.rs rename to crates/strata-timelock/src/pause.rs index 51f1b363..d9cdc856 100644 --- a/crates/timelock/src/pause.rs +++ b/crates/strata-timelock/src/pause.rs @@ -7,29 +7,29 @@ use crate::keys::Key; /// A second start while paused changes nothing. pub fn start(e: &Env) { - let since: Option = storage::get_instance(e, &Key::TimelockPausedSince); + let since: Option = strata_storage::get_instance(e, &Key::TimelockPausedSince); if since.is_none() { - storage::set_instance(e, &Key::TimelockPausedSince, &e.ledger().timestamp()); + strata_storage::set_instance(e, &Key::TimelockPausedSince, &e.ledger().timestamp()); } } /// Banks the time since `start`. An end with no pause running changes nothing. pub fn end(e: &Env) { - let since: Option = storage::get_instance(e, &Key::TimelockPausedSince); + let since: Option = strata_storage::get_instance(e, &Key::TimelockPausedSince); if let Some(since) = since { let total = closed_secs(e).saturating_add(e.ledger().timestamp().saturating_sub(since)); - storage::set_instance(e, &Key::TimelockPausedSecs, &total); + strata_storage::set_instance(e, &Key::TimelockPausedSecs, &total); e.storage().instance().remove(&Key::TimelockPausedSince); } } /// Seconds spent paused so far, a running pause included. pub fn paused_secs(e: &Env) -> u64 { - let since: Option = storage::get_instance(e, &Key::TimelockPausedSince); + let since: Option = strata_storage::get_instance(e, &Key::TimelockPausedSince); let running = since.map_or(0, |s| e.ledger().timestamp().saturating_sub(s)); closed_secs(e).saturating_add(running) } fn closed_secs(e: &Env) -> u64 { - storage::get_instance(e, &Key::TimelockPausedSecs).unwrap_or(0) + strata_storage::get_instance(e, &Key::TimelockPausedSecs).unwrap_or(0) } diff --git a/crates/timelock/src/queue.rs b/crates/strata-timelock/src/queue.rs similarity index 78% rename from crates/timelock/src/queue.rs rename to crates/strata-timelock/src/queue.rs index d1adc354..31f39d84 100644 --- a/crates/timelock/src/queue.rs +++ b/crates/strata-timelock/src/queue.rs @@ -13,7 +13,7 @@ pub const MAX_PENDING: u32 = 16; /// The ids of the changes waiting, oldest first. pub fn open_ids(e: &Env) -> Vec { - storage::get_instance(e, &Key::TimelockOpen).unwrap_or_else(|| Vec::new(e)) + strata_storage::get_instance(e, &Key::TimelockOpen).unwrap_or_else(|| Vec::new(e)) } pub fn pending(e: &Env, id: u32) -> Option<(C, Schedule)> @@ -21,7 +21,7 @@ where C: TryFromVal, >::Error: Debug, { - storage::get_persistent(e, &Key::TimelockPending(id)) + strata_storage::get_persistent(e, &Key::TimelockPending(id)) } /// Queues a change behind the host's current delay and returns its id. @@ -41,16 +41,16 @@ where if open.len() >= MAX_PENDING { return Err(Refusal::TooMany); } - let id: u32 = storage::get_instance(e, &Key::TimelockNextId).unwrap_or(1); + let id: u32 = strata_storage::get_instance(e, &Key::TimelockNextId).unwrap_or(1); let schedule = Schedule { proposed_at: e.ledger().timestamp(), delay: clock.delay, paused_secs: clock.paused_secs, }; - storage::set_persistent(e, &Key::TimelockPending(id), &(change.clone(), schedule)); + strata_storage::set_persistent(e, &Key::TimelockPending(id), &(change.clone(), schedule)); open.push_back(id); - storage::set_instance(e, &Key::TimelockOpen, &open); - storage::set_instance(e, &Key::TimelockNextId, &(id + 1)); + strata_storage::set_instance(e, &Key::TimelockOpen, &open); + strata_storage::set_instance(e, &Key::TimelockNextId, &(id + 1)); Ok(id) } @@ -74,7 +74,7 @@ fn remove(e: &Env, id: u32) -> Result<(), Refusal> { let mut open = open_ids(e); let index = open.first_index_of(id).ok_or(Refusal::NotFound)?; open.remove(index); - storage::set_instance(e, &Key::TimelockOpen, &open); + strata_storage::set_instance(e, &Key::TimelockOpen, &open); e.storage().persistent().remove(&Key::TimelockPending(id)); Ok(()) } diff --git a/crates/timelock/src/refusal.rs b/crates/strata-timelock/src/refusal.rs similarity index 100% rename from crates/timelock/src/refusal.rs rename to crates/strata-timelock/src/refusal.rs diff --git a/crates/timelock/src/schedule.rs b/crates/strata-timelock/src/schedule.rs similarity index 100% rename from crates/timelock/src/schedule.rs rename to crates/strata-timelock/src/schedule.rs diff --git a/crates/timelock/src/setup.rs b/crates/strata-timelock/src/setup.rs similarity index 78% rename from crates/timelock/src/setup.rs rename to crates/strata-timelock/src/setup.rs index 195ad642..6d165eb2 100644 --- a/crates/timelock/src/setup.rs +++ b/crates/strata-timelock/src/setup.rs @@ -8,12 +8,12 @@ use crate::keys::Key; use crate::refusal::Refusal; pub fn is_open(e: &Env) -> bool { - !storage::get_instance::<_, bool>(e, &Key::TimelockSetupDone).unwrap_or(false) + !strata_storage::get_instance::<_, bool>(e, &Key::TimelockSetupDone).unwrap_or(false) } /// The clock source this contract is bound to. pub fn source(e: &Env) -> Option
{ - storage::get_instance(e, &Key::TimelockSource) + strata_storage::get_instance(e, &Key::TimelockSource) } /// Binds this contract to a clock source, once and only during setup. @@ -26,7 +26,7 @@ pub fn bind_source(e: &Env, source: &Address) -> Result<(), Refusal> { } // An address that does not answer as a clock source fails the call here. ClockSourceClient::new(e, source).timelock_clock(); - storage::set_instance(e, &Key::TimelockSource, source); + strata_storage::set_instance(e, &Key::TimelockSource, source); Ok(()) } @@ -36,6 +36,6 @@ pub fn finish(e: &Env) -> Result<(), Refusal> { return Err(Refusal::SetupClosed); } H::clock(e)?; - storage::set_instance(e, &Key::TimelockSetupDone, &true); + strata_storage::set_instance(e, &Key::TimelockSetupDone, &true); Ok(()) } diff --git a/crates/timelock/src/test/clock.rs b/crates/strata-timelock/src/test/clock.rs similarity index 100% rename from crates/timelock/src/test/clock.rs rename to crates/strata-timelock/src/test/clock.rs diff --git a/crates/timelock/src/test/delay.rs b/crates/strata-timelock/src/test/delay.rs similarity index 100% rename from crates/timelock/src/test/delay.rs rename to crates/strata-timelock/src/test/delay.rs diff --git a/crates/timelock/src/test/mod.rs b/crates/strata-timelock/src/test/mod.rs similarity index 100% rename from crates/timelock/src/test/mod.rs rename to crates/strata-timelock/src/test/mod.rs diff --git a/crates/timelock/src/test/pause.rs b/crates/strata-timelock/src/test/pause.rs similarity index 100% rename from crates/timelock/src/test/pause.rs rename to crates/strata-timelock/src/test/pause.rs diff --git a/crates/timelock/src/test/queue.rs b/crates/strata-timelock/src/test/queue.rs similarity index 100% rename from crates/timelock/src/test/queue.rs rename to crates/strata-timelock/src/test/queue.rs diff --git a/crates/timelock/src/test/schedule.rs b/crates/strata-timelock/src/test/schedule.rs similarity index 100% rename from crates/timelock/src/test/schedule.rs rename to crates/strata-timelock/src/test/schedule.rs diff --git a/crates/timelock/src/test/setup.rs b/crates/strata-timelock/src/test/setup.rs similarity index 100% rename from crates/timelock/src/test/setup.rs rename to crates/strata-timelock/src/test/setup.rs diff --git a/docs/AUTHORIZATION_MATRIX.md b/docs/AUTHORIZATION_MATRIX.md index e1de3f84..8b97446f 100644 --- a/docs/AUTHORIZATION_MATRIX.md +++ b/docs/AUTHORIZATION_MATRIX.md @@ -7,7 +7,7 @@ This document provides the complete authorization and role model specification f ## 1. Architectural Principles 1. **Direct Role Membership Checks**: Role membership is checked directly via `has_role(e, &caller, &role)` or `stellar_macros::only_role` / `only_admin`, never by enumerating members. -2. **Strict Caller Authentication**: No entrypoint takes a caller argument that it does not authenticate. Every function accepting a `caller: Address` executes `caller.require_auth()`. The testnet compliance stub is the exception: its admin calls check the stored admin's signature and ignore their `operator` argument (§5.5). +2. **Strict Caller Authentication**: No entrypoint takes a caller argument that it does not authenticate. Every function accepting a `caller: Address` executes `caller.require_auth()`. The testnet compliance stub is the exception: its admin calls check the stored admin's signature and ignore their `caller` or `operator` argument (§5.5). 3. **Exit guarantee, with one hold**: A covered redemption claim pays out cash regardless of pause, staleness or allowlist status. A delisted investor exits through the cash path and cannot re-enter share circulation. A frozen investor is held: the vault refuses their requests, cancellations and claims, and pays them only after the freeze lifts. 4. **Five Separate Authorities**: - **Governance (Admin)**: Root authority; upgrades, timelock parameters, custodian settings, unpause, oracle parameters, emergency NAV pause reset. As the share token's admin, it co-signs compliance's `legal_transfer` and `recover_balance`. @@ -37,7 +37,7 @@ In `scripts/harness/deploy.ts` and automated deployment scripts, the separation - The share token role is granted to the deployed vault contract: `token.grant_role({ account: asyncVault.address, role: "manager", caller: governance })`. - After granting, the script checks `token.has_role({ account: vault, role: "manager" })` and stops if the vault does not hold the role. That no other account holds it is checked by the vault's `finish_setup` below. - It then calls `token.bind_vault({ vault, caller: governance })` and `token.finish_setup({ caller: governance })`, and stops if `token.setup_open()` still reads true. From then on the token's admin changes wait for the vault's timelock (§5.2). -- It calls `compliance.bind_token({ token, operator: governance })`, then `compliance.bind_vault({ vault, operator: governance })` and `compliance.finish_setup({ operator: governance })`, and stops if `compliance.setup_open()` still reads true. From then on the compliance contract's module, token-binding and admin changes wait for the vault's timelock (§5.5). +- It calls `compliance.bind_token({ token, operator: governance })`, then `compliance.bind_vault({ vault, caller: governance })` and `compliance.finish_setup({ caller: governance })`, and stops if `compliance.setup_open()` still reads true. From then on the compliance contract's module, token-binding and admin changes wait for the vault's timelock (§5.5). - It calls `oracle.bind_vault({ vault, caller: governance })` and `oracle.finish_setup({ caller: governance })`, and stops if `oracle.setup_open()` still reads true. From then on the oracle's admin changes wait for the vault's timelock (§5.3). - Last, it calls `vault.set_custodian({ custodian, caller: governance })` and `vault.finish_setup({ caller: governance })`, and stops if `vault.setup_open()` still reads true. `finish_setup` refuses with `SetupIncomplete` unless the token and the oracle report this vault with their setup closed, the vault is the token's only `manager`, the token has no shares outstanding, the vault passes the identity check, its address is not frozen on the token and the token is bound to its compliance contract. A compliance contract that answers `vault()` and `setup_open()` must report this vault with its setup closed; one that does not answer them is an external module and is accepted. From then on the vault takes deposits and its admin changes wait for its timelock (§5.1). @@ -59,17 +59,17 @@ The following entrypoints are open to any caller on purpose: | Contract | Entrypoint | Authorization | Rationale | |---|---|---|---| -| **`AsyncVault`** | `fund` | `from.require_auth()` | Anyone (treasury, LP, custodian, donor) may fund the vault with settlement assets. `from.require_auth()` ensures that the sender explicitly approves transferring their own assets into the vault. Receiving capital is non-privileged and always safe. | +| **`AsyncVault`** | `fund` | `caller.require_auth()` | Anyone (treasury, LP, custodian, donor) may fund the vault with settlement assets. `caller.require_auth()` ensures that the sender explicitly approves transferring their own assets into the vault. Receiving capital is non-privileged and always safe. | | **`AsyncVault`** | `fulfill_epoch` | Open (Any caller) | Pricing is deterministic once an epoch is closed and the oracle publishes a valid attestation. Permissionless fulfillment prevents a malicious operator from stalling pricing or censoring settlements. Guarded by `#[when_not_paused]` and oracle validity. | | **`AsyncVault`** | `activate_wind_down` | Open (Any caller) | Once governance announces wind-down and the timelock expires, anyone (including investors) can trigger activation. This ensures an operator cannot propose wind-down to block deposits and then stall activation indefinitely. | | **`AsyncVault`** | `finalize_wind_down_round` | Open (Any caller) | Distributing returned capital pro-rata across the supply snapshot is deterministic. Making finalisation open ensures investors or keepers can trigger payout rounds immediately when funds arrive. | | **`AsyncVault`** | `claim_wind_down` | `holder.require_auth()` | The holder signs their own claim; a keeper cannot claim for them. The payout goes to `holder`. | -| **`AsyncVault`** | `request_deposit` | `from.require_auth()` | Investor entrypoint. Deposits escrowed cash; guarded by `when_not_paused`, not winding down, and refused with `SetupOpen` until the vault's setup finishes, so every deposit is covered by the timelock. | -| **`AsyncVault`** | `claim_deposit` | `caller.require_auth()` | Investor entrypoint. Mints the shares once the epoch is priced; the token checks the receiver on `mint`. A delisted holder's shares become an exit instead (`DepositConvertedToExit`): they join the holder's redemption in the open epoch, or the holder's wind-down entitlement during a wind-down. A frozen holder's claim is held (`Frozen`). | -| **`AsyncVault`** | `cancel_deposit` | `from.require_auth()` | Investor exit while the epoch cannot be priced. Refunds the whole escrowed deposit in one step, delisted or not. A frozen holder is held (`Frozen`). | -| **`AsyncVault`** | `request_redeem` | `from.require_auth()` | Investor exit. Always open, even during pause. | -| **`AsyncVault`** | `claim_redeem` | `caller.require_auth()` | Investor exit. Pays settlement asset once covered. | -| **`AsyncVault`** | `cancel_redeem` | `from.require_auth()` | Investor exit while the epoch cannot be priced. Outside a wind-down the vault checks the controller itself and returns the escrowed shares with the manager `forced_transfer`; it refuses a delisted controller (`NotAllowlisted`) and a move the compliance rules refuse (`HolderNotEligible`). In an active wind-down the escrow is surrendered into the controller's wind-down entitlement instead (`RedemptionSurrendered`). A frozen controller is held (`Frozen`) either way. | +| **`AsyncVault`** | `request_deposit` | `holder.require_auth()` | Investor entrypoint. Deposits escrowed cash; guarded by `when_not_paused`, not winding down, and refused with `SetupOpen` until the vault's setup finishes, so every deposit is covered by the timelock. | +| **`AsyncVault`** | `claim_deposit` | `holder.require_auth()` | Investor entrypoint. Mints the shares once the epoch is priced; the token checks the receiver on `mint`. A delisted holder's shares become an exit instead (`DepositConvertedToExit`): they join the holder's redemption in the open epoch, or the holder's wind-down entitlement during a wind-down. A frozen holder's claim is held (`Frozen`). | +| **`AsyncVault`** | `cancel_deposit` | `holder.require_auth()` | Investor exit while the epoch cannot be priced. Refunds the whole escrowed deposit in one step, delisted or not. A frozen holder is held (`Frozen`). | +| **`AsyncVault`** | `request_redeem` | `holder.require_auth()` | Investor exit. Always open, even during pause. | +| **`AsyncVault`** | `claim_redeem` | `holder.require_auth()` | Investor exit. Pays settlement asset once covered. | +| **`AsyncVault`** | `cancel_redeem` | `holder.require_auth()` | Investor exit while the epoch cannot be priced. Outside a wind-down the vault checks the holder itself and returns the escrowed shares with the manager `forced_transfer`; it refuses a delisted holder (`NotAllowlisted`) and a move the compliance rules refuse (`HolderNotEligible`). In an active wind-down the escrow is surrendered into the holder's wind-down entitlement instead (`RedemptionSurrendered`). A frozen holder is held (`Frozen`) either way. | --- @@ -119,7 +119,7 @@ Tests: [`contracts/share-token/src/test.rs`](../contracts/share-token/src/test.r | `mint` | `#[only_role(operator, "manager")]` | Receiver allowlisted | `test::unauthorized_caller_cannot_mint`, `test::the_compliance_authority_cannot_mint_burn_or_swap_the_rules` | | `burn` | `#[only_role(operator, "manager")]` | Balance sufficient | `test::unauthorized_caller_cannot_burn` | | `forced_transfer` | `#[only_role(operator, "manager")]` | Balance sufficient | `test::unauthorized_caller_cannot_forced_transfer`, `test::legal_transfer::compliance_no_longer_forces_transfers` | -| `legal_transfer` | `#[only_role(operator, "compliance")]` and the admin's `require_auth()` in the same call, from two different accounts (`CoSignRequired`) | Reference of 1–128 bytes (`InvalidReference`); never from or to the vault's address (`VaultAddress`); destination passes `verify_identity`; balance sufficient | `test::legal_transfer::*` | +| `legal_transfer` | `#[only_role(caller, "compliance")]` and the admin's `require_auth()` in the same call, from two different accounts (`CoSignRequired`) | Reference of 1–128 bytes (`InvalidReference`); never from or to the vault's address (`VaultAddress`); destination passes `verify_identity`; balance sufficient | `test::legal_transfer::*` | | `set_address_frozen` | `#[only_role(operator, "compliance")]` | Never freezes the bound vault's address (`VaultAddress`); unfreezing it stays possible | `test::unauthorized_caller_cannot_freeze_or_recover`, `test::the_manager_escrows_but_does_not_intervene`, `test::legal_transfer::the_vault_address_cannot_be_frozen` | | `freeze_partial_tokens` | `#[only_role(operator, "compliance")]` | Balance sufficient; never the bound vault's address (`VaultAddress`) | `test::unauthorized_caller_cannot_freeze_or_recover`, `test::the_manager_escrows_but_does_not_intervene`, `test::legal_transfer::the_vault_address_cannot_be_frozen` | | `unfreeze_partial_tokens`| `#[only_role(operator, "compliance")]` | Frozen amount sufficient | `test::unauthorized_caller_cannot_freeze_or_recover`, `test::the_manager_escrows_but_does_not_intervene` | @@ -180,7 +180,7 @@ Tests: [`contracts/identity-verifier/src/test.rs`](../contracts/identity-verifie ### 5.5 Compliance (`contracts/compliance`) -A testnet-only stub. Its admin is governance. Module changes, token bindings and admin hand-overs are instant during setup and wait for the vault's timelock after `finish_setup`. Each admin call checks the stored admin's signature; its `operator` argument is not checked. +A testnet-only stub. Its admin is governance. Module changes, token bindings and admin hand-overs are instant during setup and wait for the vault's timelock after `finish_setup`. Each admin call checks the stored admin's signature; its `caller` or `operator` argument is not checked. Tests: [`contracts/compliance/src/test/changes.rs`](../contracts/compliance/src/test/changes.rs). diff --git a/docs/COMPLIANCE_MATRIX.md b/docs/COMPLIANCE_MATRIX.md index 3038a909..9e01af2b 100644 --- a/docs/COMPLIANCE_MATRIX.md +++ b/docs/COMPLIANCE_MATRIX.md @@ -32,10 +32,10 @@ Every investor account in the vault ecosystem exists in one of three primary com | Operation | Allowlisted | De-listed (Non-Frozen) | Frozen (Address or Partial) | Contract Mechanics & Regulatory Rationale | |---|---|---|---|---| | **`request_deposit`** | ✅ Allowed | ❌ **Refused** | ❌ **Refused** | Before any cash moves, the vault asks the share token whether the address is frozen and then asks the token's identity verifier. It refuses a frozen address (`Frozen`) and an address the token would not let hold shares (`NotAllowlisted`). A partial freeze is not checked. | -| **`cancel_deposit`** | ✅ Allowed | ✅ **Allowed** | ❌ **Refused** (`Frozen`); the escrow stays | **Escape Hatch**: Calls `refund_deposit`. Escrowed settlement asset (USDC) is refunded directly to `controller`. The vault checks `is_frozen` on the share token before it refunds anything; once past that, cash refunds bypass share token compliance checks. A partial freeze is not checked. | +| **`cancel_deposit`** | ✅ Allowed | ✅ **Allowed** | ❌ **Refused** (`Frozen`); the escrow stays | **Escape Hatch**: Calls `refund_deposit`. Escrowed settlement asset (USDC) is refunded directly to `holder`. The vault checks `is_frozen` on the share token before it refunds anything; once past that, cash refunds bypass share token compliance checks. A partial freeze is not checked. | | **`claim_deposit`** | ✅ Allowed | ↪ **Moved to the exit queue** | ⛔ **Held** | A receiver off the allowlist gets no shares: the vault mints them into its own escrow and adds them to the holder's redemption in the open epoch (`DepositConvertedToExit`), paid in cash at that epoch's price. In a wind-down they join the holder's wind-down entitlement. A compliance module that refuses the mint (`can_create`) still blocks the claim. A receiver whose address is frozen, listed or not, is refused with `Frozen` (6071) and keeps the priced deposit until the freeze is lifted. A partial freeze is not checked. | | **`request_redeem`** | ✅ Allowed | ✅ **Allowed** | ❌ **Refused** (`Frozen`); a partial freeze caps the request to the free balance | **Exit Right**: the vault checks `is_frozen` and `get_frozen_tokens` on the share token before it escrows anything with `RWATokenClient::forced_transfer`. A de-listed, non-frozen holder can still start an exit; a frozen one cannot, and a partial freeze caps the request to the shares `get_frozen_tokens` leaves free. | -| **`cancel_redeem`** | ✅ Allowed | ❌ **Refused**; ↪ **surrendered** in a wind-down | ⛔ **Held**; a partial freeze does not stop it: the shares return and stay partly frozen | Outside a wind-down the vault refuses a de-listed controller (`NotAllowlisted`) and holds one whose address is frozen (`Frozen`). For anyone else it checks the receiver itself (passes the identity check, not frozen, accepted by the compliance rules' `can_transfer`) and returns the shares with the manager `forced_transfer`; a move the rules refuse gets `HolderNotEligible`. Neither the vault's own allowlist place nor the share-token pause stops a return. A refused controller stays in the queue and exits in cash once the epoch is priced. In an active wind-down no shares come back: the escrow is burned into the controller's wind-down entitlement (`RedemptionSurrendered`), allowlisted or not. A frozen controller's escrow stays until the freeze is lifted. | +| **`cancel_redeem`** | ✅ Allowed | ❌ **Refused**; ↪ **surrendered** in a wind-down | ⛔ **Held**; a partial freeze does not stop it: the shares return and stay partly frozen | Outside a wind-down the vault refuses a de-listed holder (`NotAllowlisted`) and holds one whose address is frozen (`Frozen`). For anyone else it checks the receiver itself (passes the identity check, not frozen, accepted by the compliance rules' `can_transfer`) and returns the shares with the manager `forced_transfer`; a move the rules refuse gets `HolderNotEligible`. Neither the vault's own allowlist place nor the share-token pause stops a return. A refused holder stays in the queue and exits in cash once the epoch is priced. In an active wind-down no shares come back: the escrow is burned into the holder's wind-down entitlement (`RedemptionSurrendered`), allowlisted or not. A frozen holder's escrow stays until the freeze is lifted. | | **`claim_redeem`** | ✅ Allowed | ✅ **Allowed** | ❌ **Refused** (`Frozen`); the cash stays in `committed` | **Exit-Only Cash Path**: the settlement asset pays out from the liquid reserve and the escrowed shares are burned from the vault. The cash claim checks `is_frozen` on the share token first; it does not check the allowlist. A partial freeze is not checked. Outside a wind-down, a claim that pays nothing returns the escrowed shares instead, under the same checks as `cancel_redeem`: a de-listed holder's is refused with `HolderNotEligible` and the request stays. In a wind-down such a claim burns the escrowed shares. | | **`claim_wind_down`** | ✅ Allowed | ✅ **Allowed** | ❌ **Refused** (`Frozen`); the slice stays in `wind_down_owed` | **Wind-Down Payout**: pays the holder's pro-rata entitlement and burns the free shares it surrenders. The vault checks `is_frozen` and `get_frozen_tokens` on the share token first; a partial freeze surrenders only the free shares — the rest keeps earning every round and is paid once unfrozen. | | **`transfer`** (Secondary) | ✅ Allowed | ❌ **Refused** | ❌ **Refused** | Standard peer-to-peer share transfer via SEP-57. Requires both sender and receiver to be allowlisted and neither to be frozen. | @@ -57,7 +57,7 @@ A core guarantee of the Strata Vault Kit is that **a de-listed, non-frozen inves ### 3.2 Deposit Cancellation for De-Listed Investors If an investor deposits into an open epoch but is de-listed (or KYC expires) before the epoch is priced: -- The investor is permitted to call `cancel_deposit(investor, epoch_id)`. +- The investor is permitted to call `cancel_deposit(holder, epoch_id)`. - `refund_deposit` transfers the escrowed USDC back to the investor. - Because no shares were minted, returning cash restores the investor's funds without violating share token compliance rules. @@ -77,9 +77,9 @@ An address that was never allowlisted cannot reach this state: `request_deposit` ### 3.4 Redemption Cancellation Refusal When an investor requests redemption, their shares are transferred to the vault. If the investor becomes de-listed or frozen before pricing: -- The vault checks the controller itself before it returns escrowed shares. A de-listed controller is refused (`NotAllowlisted`), and so is a move the compliance rules refuse (`HolderNotEligible`); either way the investor stays in the queue and exits in cash once the epoch is priced. A frozen controller is held (`Frozen`) until the freeze is lifted. +- The vault checks the holder itself before it returns escrowed shares. A de-listed holder is refused (`NotAllowlisted`), and so is a move the compliance rules refuse (`HolderNotEligible`); either way the investor stays in the queue and exits in cash once the epoch is priced. A frozen holder is held (`Frozen`) until the freeze is lifted. - The vault moves the shares as the token's `manager` (`forced_transfer`), so removing the vault's own address from the allowlist does not stop returns. A share-token pause does not stop them either: it stops trading between holders, not the vault's escrow. A partial freeze does not stop them: the shares return and stay partly frozen. -- During a wind-down, every cancel burns the escrowed shares into the controller's wind-down entitlement (`RedemptionSurrendered`), listed or not, and they collect pro rata with `claim_wind_down`. A frozen controller's escrow stays until the freeze is lifted. +- During a wind-down, every cancel burns the escrowed shares into the holder's wind-down entitlement (`RedemptionSurrendered`), listed or not, and they collect pro rata with `claim_wind_down`. A frozen holder's escrow stays until the freeze is lifted. --- diff --git a/docs/TESTING.md b/docs/TESTING.md index ff1ebe4d..334d7d35 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -36,7 +36,7 @@ suite under it: tests that subtract from the minimum assume seven days. ## Storage and TTL The contracts read and write through the shared helpers in -[`crates/storage`](../crates/storage/src/lib.rs). A write extends the entry to +[`crates/strata-storage`](../crates/strata-storage/src/lib.rs). A write extends the entry to 30 days; a read that finds the entry extends it again once it has fallen more than a day short. The instance follows the same rule. The [TTL policy](./kit-spec.md#5-storage-lifetime-ttl-policy) says what each @@ -44,9 +44,9 @@ contract extends, who pays, and how to restore an archived one. | Behaviour | Tests | Key test names | | --- | --- | --- | -| Persistent entries round-trip and get the full TTL on write | [`crates/storage/src/test.rs`](../crates/storage/src/test.rs) | `persistent_round_trips`, `writing_sets_the_full_ttl` | -| A read restores a decayed entry; a missing entry is not created | [`crates/storage/src/test.rs`](../crates/storage/src/test.rs) | `reading_a_decayed_entry_restores_the_full_ttl`, `reading_a_missing_entry_creates_nothing` | -| Instance writes and reads extend the contract instance | [`crates/storage/src/test.rs`](../crates/storage/src/test.rs) | `instance_writes_bump_the_instance`, `reading_the_instance_restores_its_ttl` | +| Persistent entries round-trip and get the full TTL on write | [`crates/strata-storage/src/test.rs`](../crates/strata-storage/src/test.rs) | `persistent_round_trips`, `writing_sets_the_full_ttl` | +| A read restores a decayed entry; a missing entry is not created | [`crates/strata-storage/src/test.rs`](../crates/strata-storage/src/test.rs) | `reading_a_decayed_entry_restores_the_full_ttl`, `reading_a_missing_entry_creates_nothing` | +| Instance writes and reads extend the contract instance | [`crates/strata-storage/src/test.rs`](../crates/strata-storage/src/test.rs) | `instance_writes_bump_the_instance`, `reading_the_instance_restores_its_ttl` | | The share token, identity verifier and compliance contract extend their own instance on every call that does work | [`contracts/share-token/src/test.rs`](../contracts/share-token/src/test.rs), [`contracts/share-token/src/test_governance.rs`](../contracts/share-token/src/test_governance.rs), [`contracts/identity-verifier/src/test.rs`](../contracts/identity-verifier/src/test.rs), [`contracts/compliance/src/test.rs`](../contracts/compliance/src/test.rs), [`contracts/compliance/src/test/changes.rs`](../contracts/compliance/src/test/changes.rs) | `a_transfer_restores_the_instance_ttl`, `the_vaults_mint_burn_and_escrow_restore_the_instance_ttl`, `every_admin_call_restores_the_instance_ttl`, `allowing_and_checking_restore_the_instance_ttl`, `the_hooks_a_token_calls_after_moving_shares_restore_the_instance_ttl` | | Vault requests and epochs are written with the full TTL | [`contracts/async-vault/src/test/ttl.rs`](../contracts/async-vault/src/test/ttl.rs) | `a_request_and_its_epoch_are_written_with_the_full_ttl`, `a_redemption_request_is_written_with_the_full_ttl` | | Reading a vault request, epoch or the instance restores its TTL | [`contracts/async-vault/src/test/ttl.rs`](../contracts/async-vault/src/test/ttl.rs) | `reading_a_decayed_request_restores_its_full_ttl`, `reading_a_decayed_epoch_restores_its_full_ttl`, `any_vault_read_restores_the_instance_ttl` | diff --git a/docs/kit-spec.md b/docs/kit-spec.md index f1f91aeb..7a974229 100644 --- a/docs/kit-spec.md +++ b/docs/kit-spec.md @@ -53,7 +53,7 @@ The contracts enforce a set of mathematical and state-machine invariants across - `request_deposit` is refused by `#[when_not_paused]` (`PausableError::EnforcedPause`). - `request_redeem` succeeds and locks shares into escrow. - `claim_redeem` and `claim_deposit` remain open. - - `cancel_redeem` remains open. Outside a wind-down, a delisted controller, or one the compliance rules refuse, exits through the cash claim instead; in a wind-down the escrow is surrendered. A frozen controller is held (Invariant 6). + - `cancel_redeem` remains open. Outside a wind-down, a delisted holder, or one the compliance rules refuse, exits through the cash claim instead; in a wind-down the escrow is surrendered. A frozen holder is held (Invariant 6). - `cancel_deposit` and `cancel_redeem` stay open for a sealed epoch while the vault is paused, because a paused vault cannot price it (Invariant 6). ### Invariant 6: Single-Step Atomic Cancellation @@ -65,7 +65,7 @@ The contracts enforce a set of mathematical and state-machine invariants across - While an epoch is `Open`, or `Pending` with no readable price or with the vault paused, `cancel_deposit` and `cancel_redeem` return 100% of the escrow. - A readable price refuses cancellation with `PriceAvailable`, even before `priceable_at`; once that report expires, cancellation works again (`cancellation_reopens_when_the_price_that_closed_it_expires`). - Pausing the vault reopens cancellation; unpausing closes it again while the price is still readable (`a_paused_vault_reopens_cancellation_of_an_epoch_it_could_price`). - - Outside a wind-down, `cancel_redeem` refuses a delisted controller (`NotAllowlisted`) and a move the compliance rules refuse (`HolderNotEligible`); the request stays pending and the cash claim remains open. It holds a frozen controller (`Frozen`): the request stays pending, and its cash claim is held too until the freeze is lifted. Neither the share token's transfer pause nor the vault's own allowlist place stops a cancellation (`a_token_pause_stops_trading_but_not_a_share_return`, `a_delisted_vault_still_returns_escrowed_shares`). + - Outside a wind-down, `cancel_redeem` refuses a delisted holder (`NotAllowlisted`) and a move the compliance rules refuse (`HolderNotEligible`); the request stays pending and the cash claim remains open. It holds a frozen holder (`Frozen`): the request stays pending, and its cash claim is held too until the freeze is lifted. Neither the share token's transfer pause nor the vault's own allowlist place stops a cancellation (`a_token_pause_stops_trading_but_not_a_share_return`, `a_delisted_vault_still_returns_escrowed_shares`). - In an active wind-down, `cancel_redeem` never returns shares: the escrow joins the holder's wind-down entitlement (`RedemptionSurrendered`), whether the holder is allowlisted or not. A frozen holder's escrow stays until the freeze is lifted. - Once fulfilled, cancellation is refused with `AlreadyPriced`. - **Operating note**: keep `freshness_duration` longer than the notice, so a normal epoch's price does not lapse and reopen the window on its own. @@ -211,12 +211,12 @@ Strata distributes control across five native Stellar multisig authorities and p | Contract | Entrypoint | Calling Authority | Access Control Guard | Pausable? | |---|---|---|---|---| -| **`AsyncVault`** | `request_deposit` | Investor (Caller) | `from.require_auth()`; refused until setup finishes (`SetupOpen`) | Yes (halted by pause) | -| **`AsyncVault`** | `cancel_deposit` | Investor (Caller) | `from.require_auth()` | No (open during pause) | -| **`AsyncVault`** | `claim_deposit` | Investor (Caller) | `caller.require_auth()` | No (open during pause) | -| **`AsyncVault`** | `request_redeem` | Investor (Caller) | `from.require_auth()` | No (always open) | -| **`AsyncVault`** | `cancel_redeem` | Investor (Caller) | `from.require_auth()` | No (open during pause) | -| **`AsyncVault`** | `claim_redeem` | Investor (Caller) | `caller.require_auth()` | No (always open) | +| **`AsyncVault`** | `request_deposit` | Investor (Caller) | `holder.require_auth()`; refused until setup finishes (`SetupOpen`) | Yes (halted by pause) | +| **`AsyncVault`** | `cancel_deposit` | Investor (Caller) | `holder.require_auth()` | No (open during pause) | +| **`AsyncVault`** | `claim_deposit` | Investor (Caller) | `holder.require_auth()` | No (open during pause) | +| **`AsyncVault`** | `request_redeem` | Investor (Caller) | `holder.require_auth()` | No (always open) | +| **`AsyncVault`** | `cancel_redeem` | Investor (Caller) | `holder.require_auth()` | No (open during pause) | +| **`AsyncVault`** | `claim_redeem` | Investor (Caller) | `holder.require_auth()` | No (always open) | | **`AsyncVault`** | `close_epoch` | Manager | `#[only_role(caller, "manager")]` | No | | **`AsyncVault`** | `fulfill_epoch` | Permissionless | None | Yes (halted by pause) | | **`AsyncVault`** | `pause` | Guardian | `#[only_role(caller, "guardian")]` | N/A | @@ -231,7 +231,7 @@ Strata distributes control across five native Stellar multisig authorities and p | **`AsyncVault`** | `cancel_wind_down_proposal` | Governance | `#[only_admin]` | No | | **`AsyncVault`** | `activate_wind_down` | Permissionless | None (requires timelock elapsed) | No | | **`AsyncVault`** | `finalize_wind_down_round` | Permissionless | None | No | -| **`AsyncVault`** | `claim_wind_down` | Shareholder (Caller) | `caller.require_auth()` | No | +| **`AsyncVault`** | `claim_wind_down` | Shareholder (Caller) | `holder.require_auth()` | No | | **`AsyncVault`** | `deploy_to_custodian` | Treasury | `#[only_role(caller, "treasury")]` | No (open during pause) | | **`AsyncVault`** | `fund` | Treasury | None (caller transfers funds) | No | | **`AsyncVault`** | `propose_change` | Governance | `#[only_admin]`; after setup | No | @@ -261,7 +261,7 @@ Strata distributes control across five native Stellar multisig authorities and p | **`ShareToken`** | `mint` | Manager (Vault) | `#[only_role(operator, "manager")]` | Bypasses token pause | | **`ShareToken`** | `burn` | Manager (Vault) | `#[only_role(operator, "manager")]` | Bypasses token pause | | **`ShareToken`** | `forced_transfer` | Manager (Vault) | `#[only_role(operator, "manager")]` | Bypasses token pause | -| **`ShareToken`** | `legal_transfer` | Compliance, co-signed by Governance (token admin) | `#[only_role(operator, "compliance")]` and the admin's `require_auth()` in the same call, from two different accounts; never from or to the vault's address; the destination passes `verify_identity`; a reference of 1–128 bytes | Bypasses token pause | +| **`ShareToken`** | `legal_transfer` | Compliance, co-signed by Governance (token admin) | `#[only_role(caller, "compliance")]` and the admin's `require_auth()` in the same call, from two different accounts; never from or to the vault's address; the destination passes `verify_identity`; a reference of 1–128 bytes | Bypasses token pause | | **`ShareToken`** | `set_address_frozen`, `freeze_partial_tokens`, `unfreeze_partial_tokens` | Compliance | `#[only_role(operator, "compliance")]`; never freezes the vault's address once the token knows its vault | No | | **`ShareToken`** | `recover_balance` | Compliance, co-signed by Governance (token admin) | `#[only_role(operator, "compliance")]` and the admin's `require_auth()` in the same call, from two different accounts; never from or to the vault's address | No | | **`ShareToken`** | `set_compliance`, `set_identity_verifier` | Governance | `#[only_admin]`; instant during setup, then `propose_change` | No | @@ -321,7 +321,7 @@ instance is archived refuses every call. | `identity_verifier` | Its instance and each allowlist entry | `allow`, `is_allowed`, `verify_identity` | Same | | `compliance` | Its instance, and each pending governance change it reads or writes | Every hook the token calls (`can_create`, `can_transfer`, `created`, `transferred`, `destroyed`) and its admin calls | Same | -The lengths are the constants in `crates/storage` +The lengths are the constants in `crates/strata-storage` (`PERSISTENT_EXTEND_AMOUNT`, `INSTANCE_EXTEND_AMOUNT`, and thresholds one day shorter). They are fixed in code: a governance setting would add an admin power for little gain. @@ -329,7 +329,7 @@ for little gain. **Pending governance changes.** A change waiting in the timelock of `async_vault`, `share_token`, `nav_oracle` or `compliance` lives as a persistent entry, extended to 30 days when it is proposed. The wait itself can run up to 90 days -(`timelock::MAX_DELAY`), so a change nobody touches can archive before it is +(`strata_timelock::MAX_DELAY`), so a change nobody touches can archive before it is due. Nothing is lost: the call that applies it restores the archived entry itself and pays the restore fee, the same as any other archived entry. `npm run extend` also keeps it alive: it reads each contract's open ids from @@ -401,7 +401,7 @@ implementation against what the vault calls. | Interface | Defined in | Implemented here by | What the vault calls | |---|---|---|---| -| `OracleFeed` | `crates/bindings` | `nav-oracle` | `state`, `nav_per_share`, `attested_at`, `freshness_duration` | +| `OracleFeed` | `crates/strata-bindings` | `nav-oracle` | `state`, `nav_per_share`, `attested_at`, `freshness_duration` | | `FungibleToken` | OpenZeppelin `stellar-tokens` 0.7.2 | `share-token` | `transfer`, `balance`, `total_supply` | | `RWAToken` | OpenZeppelin `stellar-tokens` 0.7.2 | `share-token` | `mint`, `burn`, `forced_transfer` | | `IdentityVerifier` | OpenZeppelin `stellar-tokens` 0.7.2 | `identity-verifier` (testnet stub) | `verify_identity`, called by the share token | @@ -412,8 +412,8 @@ be the ledger time the report was accepted, not a time the reporter supplies. Two more traits connect the share token, the oracle and the kit's compliance contract to the vault's timelock. The vault implements `ClockSource` -(`crates/timelock`), and the three read its `timelock_clock` to time their own -changes. Each implements `VaultBinding` (`crates/bindings`): `vault` names the +(`crates/strata-timelock`), and the three read its `timelock_clock` to time their own +changes. Each implements `VaultBinding` (`crates/strata-bindings`): `vault` names the vault it is bound to, and `setup_open` says whether its admin changes are still instant. diff --git a/docs/strata-product-and-architecture.md b/docs/strata-product-and-architecture.md index f1b7f4e4..abcbc3cb 100644 --- a/docs/strata-product-and-architecture.md +++ b/docs/strata-product-and-architecture.md @@ -313,14 +313,14 @@ batch boundary, though not the price it receives. #### Subscription -- Request: moves the deposit asset into escrow; at most one active request per controller. The vault first asks the share token whether the address is frozen and asks the token's identity verifier, and refuses a frozen address or one that could not receive the shares, before any cash moves; the token checks again when it mints at claim time. +- Request: moves the deposit asset into escrow; at most one active request per holder. The vault first asks the share token whether the address is frozen and asks the token's identity verifier, and refuses a frozen address or one that could not receive the shares, before any cash moves; the token checks again when it mints at claim time. - Pricing: the escrow leaves the cancellable bucket, the share quantity is set at the epoch's price, and the economic supply liability is recorded. To avoid unbounded loops that exceed Soroban transaction resource limits, physical share minting occurs lazily when each investor executes `claim_deposit`. - Cancellation: atomic, and returns the escrowed asset in full. Open while the epoch is open. Once sealed, a readable price closes it, even while the notice still runs: an investor who can see the price cannot decline it. If the vault can no longer price the epoch — the report expires, the NAV is paused, or the vault is paused — it opens again, so escrow is never stuck behind a stalled feed. The epoch is then priced at the latest valid report at fulfil time. Cancellation also remains open during an active wind-down, allowing pending depositors to recover their escrowed assets when pricing is closed. A frozen depositor's cancellation is held until the freeze is lifted, in a wind-down too. - Share claim: verifies the receiver and delivers the shares via `mint`. A receiver off the allowlist gets no shares: they join the holder's redemption in the open epoch and are paid in cash at that epoch's price; in a wind-down they join the holder's wind-down entitlement. A frozen receiver is held until the freeze is lifted. No nominal refund exists after pricing. #### Redemption -- Request: moves shares into escrow; at most one active request per controller per epoch. A frozen holder's request is held until the freeze is lifted; a partial freeze caps it to the free shares. +- Request: moves shares into escrow; at most one active request per holder per epoch. A frozen holder's request is held until the freeze is lifted; a partial freeze caps it to the free shares. - Cancellation: atomic, and returns the escrowed shares under the same rules as subscription cancellation; stays open during an active wind-down, where the escrow joins the holder's wind-down entitlement instead of coming back as shares. A frozen holder's cancellation is held until the freeze is lifted. - Pricing: the fixed cash liability enters committed at the epoch's price. Escrowed shares remain in the vault contract and are burned lazily as each claimant executes `claim_redeem`, preserving constant-bounded resource execution per transaction. Priced claims are never re-priced. - Coverage: a priced claim is payable when the liquid reserve covers that claim's own amount, in any order. An earlier unpaid claim never blocks a later one that is already covered. The gap between committed and liquid reserve is the uncovered amount treasury must top up. diff --git a/scripts/harness/deploy.ts b/scripts/harness/deploy.ts index 85f8c8e3..89154ba1 100644 --- a/scripts/harness/deploy.ts +++ b/scripts/harness/deploy.ts @@ -205,8 +205,8 @@ async function grantAndVerify( // Binding the token is instant only during setup, so the compliance // contract's setup closes after it. - await (await compliance.bind_vault({ vault, operator: caller })).signAndSend() - await (await compliance.finish_setup({ operator: caller })).signAndSend() + await (await compliance.bind_vault({ vault, caller })).signAndSend() + await (await compliance.finish_setup({ caller })).signAndSend() if ((await compliance.setup_open()).result) throw new Error("the compliance contract's setup did not finish") console.log("compliance bound to the vault, setup finished") diff --git a/scripts/harness/extend.ts b/scripts/harness/extend.ts index 289d102d..4194c800 100644 --- a/scripts/harness/extend.ts +++ b/scripts/harness/extend.ts @@ -41,7 +41,7 @@ const CONTRACTS_WITH_QUEUES = new Set([ /** * The timelock's own storage key for a change still waiting: * `Key::TimelockPending(id)`, a contracttype variant whose ScVal is a vec of - * its symbol and the id (`crates/timelock/src/keys.rs`, `queue.rs`). + * its symbol and the id (`crates/strata-timelock/src/keys.rs`, `queue.rs`). */ function pendingChangeKey(id: number): string { return xdr.ScVal.scvVec([ diff --git a/scripts/harness/guards.test.ts b/scripts/harness/guards.test.ts index a4c4ffa4..5cdc3ab9 100644 --- a/scripts/harness/guards.test.ts +++ b/scripts/harness/guards.test.ts @@ -303,7 +303,7 @@ describe("the oracle after setup", () => { }) describe("the compliance contract after setup", () => { - const operator = () => governance.publicKey() + const caller = () => governance.publicKey() const canCreate = { tag: "CanCreate", values: undefined } as const it("governance cannot add a module at once", async () => { @@ -312,7 +312,7 @@ describe("the compliance contract after setup", () => { await rules.add_module_to({ hook: canCreate, module: outsider.publicKey(), - operator: operator(), + operator: caller(), }), rules, "UseTimelock", @@ -324,15 +324,15 @@ describe("the compliance contract after setup", () => { const id = await send( rules.propose_change({ change: { tag: "AddModule", values: [canCreate, outsider.publicKey()] }, - operator: operator(), + caller: caller(), }), ) expectRefused( - await rules.apply_change({ id, operator: operator() }), + await rules.apply_change({ id, caller: caller() }), rules, "ChangeNotReady", ) - await send(rules.cancel_change({ id, operator: operator() })) + await send(rules.cancel_change({ id, caller: caller() })) expect((await rules.pending_changes()).result).toEqual([]) }) }) @@ -375,7 +375,7 @@ describe("invalid amounts and duplicate requests in an epoch", () => { it("rejects zero deposit amount", async () => { const v = vault(c, first) expectRefused( - await v.request_deposit({ from: first.publicKey(), amount: 0n }), + await v.request_deposit({ holder: first.publicKey(), amount: 0n }), v, "InvalidAmount", ) @@ -384,7 +384,7 @@ describe("invalid amounts and duplicate requests in an epoch", () => { it("rejects zero redeem shares", async () => { const v = vault(c, first) expectRefused( - await v.request_redeem({ from: first.publicKey(), shares: 0n }), + await v.request_redeem({ holder: first.publicKey(), shares: 0n }), v, "InvalidAmount", ) @@ -393,14 +393,14 @@ describe("invalid amounts and duplicate requests in an epoch", () => { it("rejects a second request from the same account in one epoch", async () => { const v = vault(c, first) const epoch = await send( - v.request_deposit({ from: first.publicKey(), amount: usdc(10) }), + v.request_deposit({ holder: first.publicKey(), amount: usdc(10) }), ) expectRefused( - await v.request_deposit({ from: first.publicKey(), amount: usdc(10) }), + await v.request_deposit({ holder: first.publicKey(), amount: usdc(10) }), v, "RequestOutstanding", ) - await send(v.cancel_deposit({ from: first.publicKey(), epoch_id: epoch })) + await send(v.cancel_deposit({ holder: first.publicKey(), epoch_id: epoch })) }) }) @@ -409,7 +409,7 @@ describe("who may subscribe", () => { const v = vault(c, outsider) expectRefused( await v.request_deposit({ - from: outsider.publicKey(), + holder: outsider.publicKey(), amount: usdc(10), }), v, @@ -427,14 +427,14 @@ describe("two epochs pending at once", () => { it("both read Pending", async () => { epochA = await send( vault(c, first).request_deposit({ - from: first.publicKey(), + holder: first.publicKey(), amount: usdc(100), }), ) expect(await closeEpoch()).toBe(epochA) epochB = await send( vault(c, second).request_deposit({ - from: second.publicKey(), + holder: second.publicKey(), amount: usdc(100), }), ) @@ -443,7 +443,7 @@ describe("two epochs pending at once", () => { expect((await epochOf(epochB))?.status.tag).toBe("Pending") expectRefused( await vault(c, first).claim_deposit({ - caller: first.publicKey(), + holder: first.publicKey(), epoch_id: epochA, }), vault(c, first), @@ -464,7 +464,7 @@ describe("two epochs pending at once", () => { expect( await send( vault(c, first).claim_deposit({ - caller: first.publicKey(), + holder: first.publicKey(), epoch_id: epochA, }), ), @@ -472,14 +472,14 @@ describe("two epochs pending at once", () => { expect( await send( vault(c, second).claim_deposit({ - caller: second.publicKey(), + holder: second.publicKey(), epoch_id: epochB, }), ), ).toBe(sharesFor(usdc(100), secondPrice)) expectRefused( await vault(c, first).claim_deposit({ - caller: first.publicKey(), + holder: first.publicKey(), epoch_id: epochA, }), vault(c, first), @@ -487,7 +487,7 @@ describe("two epochs pending at once", () => { ) expectRefused( await vault(c, outsider).claim_deposit({ - caller: outsider.publicKey(), + holder: outsider.publicKey(), epoch_id: epochA, }), vault(c, outsider), @@ -506,13 +506,13 @@ describe("a small covered claim pays while a larger one waits", () => { large = await shares(second.publicKey()) await send( vault(c, first).request_redeem({ - from: first.publicKey(), + holder: first.publicKey(), shares: small, }), ) epoch = await send( vault(c, second).request_redeem({ - from: second.publicKey(), + holder: second.publicKey(), shares: large, }), ) @@ -534,7 +534,7 @@ describe("a small covered claim pays while a larger one waits", () => { const before = await usdcBalance(c, first.publicKey()) const paid = await send( vault(c, first).claim_redeem({ - caller: first.publicKey(), + holder: first.publicKey(), epoch_id: epoch, }), ) @@ -545,7 +545,7 @@ describe("a small covered claim pays while a larger one waits", () => { it("the large claim waits", async () => { const v = vault(c, second) expectRefused( - await v.claim_redeem({ caller: second.publicKey(), epoch_id: epoch }), + await v.claim_redeem({ holder: second.publicKey(), epoch_id: epoch }), v, "ClaimNotCovered", ) @@ -555,13 +555,13 @@ describe("a small covered claim pays while a larger one waits", () => { it("pays once the capital returns", async () => { await send( vault(c, custodian).fund({ - from: custodian.publicKey(), + caller: custodian.publicKey(), assets: usdc(150), }), ) const paid = await send( vault(c, second).claim_redeem({ - caller: second.publicKey(), + holder: second.publicKey(), epoch_id: epoch, }), ) @@ -569,7 +569,7 @@ describe("a small covered claim pays while a larger one waits", () => { expect((await reader.uncovered()).result).toBe(0n) expectRefused( await vault(c, second).claim_redeem({ - caller: second.publicKey(), + holder: second.publicKey(), epoch_id: epoch, }), vault(c, second), @@ -584,7 +584,10 @@ describe("a deposit too small to mint a share", () => { it("is refunded at claim", async () => { const before = await usdcBalance(c, third.publicKey()) epoch = await send( - vault(c, third).request_deposit({ from: third.publicKey(), amount: 1n }), + vault(c, third).request_deposit({ + holder: third.publicKey(), + amount: 1n, + }), ) expect(await closeEpoch()).toBe(epoch) await attest(c, secondPrice) @@ -592,7 +595,7 @@ describe("a deposit too small to mint a share", () => { expect( await send( vault(c, third).claim_deposit({ - caller: third.publicKey(), + holder: third.publicKey(), epoch_id: epoch, }), ), @@ -603,14 +606,14 @@ describe("a deposit too small to mint a share", () => { }) describe("a de-listed holder", () => { - const from = first.publicKey() + const holder = first.publicKey() let epoch = 0n let redeeming = 0n it("cannot cancel a redemption back to shares", async () => { redeeming = sharesFor(usdc(10), secondPrice) epoch = await send( - vault(c, first).request_redeem({ from, shares: redeeming }), + vault(c, first).request_redeem({ holder, shares: redeeming }), ) await send( identityVerifier(c, compliance).allow({ @@ -620,7 +623,7 @@ describe("a de-listed holder", () => { }), ) expectRefused( - await vault(c, first).cancel_redeem({ from, epoch_id: epoch }), + await vault(c, first).cancel_redeem({ holder, epoch_id: epoch }), vault(c, first), "NotAllowlisted", ) @@ -633,7 +636,7 @@ describe("a de-listed holder", () => { const before = await usdcBalance(c, first.publicKey()) const paid = await send( vault(c, first).claim_redeem({ - caller: first.publicKey(), + holder: first.publicKey(), epoch_id: epoch, }), ) @@ -667,13 +670,13 @@ describe("a sealed epoch while the feed is down", () => { redeeming = await shares(first.publicKey()) await send( vault(c, first).request_deposit({ - from: first.publicKey(), + holder: first.publicKey(), amount: usdc(100), }), ) epoch = await send( vault(c, first).request_redeem({ - from: first.publicKey(), + holder: first.publicKey(), shares: redeeming, }), ) @@ -681,12 +684,12 @@ describe("a sealed epoch while the feed is down", () => { await attest(c, secondPrice) const v = vault(c, first) expectRefused( - await v.cancel_deposit({ from: first.publicKey(), epoch_id: epoch }), + await v.cancel_deposit({ holder: first.publicKey(), epoch_id: epoch }), v, "PriceAvailable", ) expectRefused( - await v.cancel_redeem({ from: first.publicKey(), epoch_id: epoch }), + await v.cancel_redeem({ holder: first.publicKey(), epoch_id: epoch }), v, "PriceAvailable", ) @@ -707,7 +710,7 @@ describe("a sealed epoch while the feed is down", () => { expect( await send( vault(c, first).cancel_deposit({ - from: first.publicKey(), + holder: first.publicKey(), epoch_id: epoch, }), ), @@ -719,7 +722,7 @@ describe("a sealed epoch while the feed is down", () => { expect( await send( vault(c, first).cancel_redeem({ - from: first.publicKey(), + holder: first.publicKey(), epoch_id: epoch, }), ), @@ -730,7 +733,7 @@ describe("a sealed epoch while the feed is down", () => { it("a second cancel finds no request", async () => { expectRefused( await vault(c, first).cancel_redeem({ - from: first.publicKey(), + holder: first.publicKey(), epoch_id: epoch, }), reader, @@ -762,7 +765,7 @@ describe("the notice period and valuation age rules", () => { it("records priceable_at at close and holds it through the notice", async () => { await send( vault(c, first).request_deposit({ - from: first.publicKey(), + holder: first.publicKey(), amount: usdc(100), }), ) @@ -801,7 +804,7 @@ describe("the notice period and valuation age rules", () => { it("refuses cancellation while the notice is running once price is readable", async () => { await send( vault(c, first).request_deposit({ - from: first.publicKey(), + holder: first.publicKey(), amount: usdc(100), }), ) @@ -809,7 +812,7 @@ describe("the notice period and valuation age rules", () => { await attest(c, secondPrice) const v = vault(c, first) expectRefused( - await v.cancel_deposit({ from: first.publicKey(), epoch_id: epochB }), + await v.cancel_deposit({ holder: first.publicKey(), epoch_id: epochB }), v, "PriceAvailable", ) diff --git a/scripts/harness/happy-path.test.ts b/scripts/harness/happy-path.test.ts index 415ae3d2..164cffc7 100644 --- a/scripts/harness/happy-path.test.ts +++ b/scripts/harness/happy-path.test.ts @@ -94,7 +94,7 @@ describe("subscription", () => { for (const holder of [first, second]) { const epoch = await send( vault(c, holder).request_deposit({ - from: holder.publicKey(), + holder: holder.publicKey(), amount: each(), }), ) @@ -102,7 +102,7 @@ describe("subscription", () => { const request = ( await reader.get_deposit_request({ epoch_id: subscriptionEpoch, - controller: holder.publicKey(), + holder: holder.publicKey(), }) ).result expect(request?.amount).toBe(each()) @@ -120,7 +120,7 @@ describe("subscription", () => { const v = vault(c, first) expectRefused( await v.cancel_deposit({ - from: first.publicKey(), + holder: first.publicKey(), epoch_id: subscriptionEpoch + 1000n, }), v, @@ -132,12 +132,12 @@ describe("subscription", () => { const before = await usdcBalance(c, canceller.publicKey()) const v = vault(c, canceller) await send( - v.request_deposit({ from: canceller.publicKey(), amount: each() }), + v.request_deposit({ holder: canceller.publicKey(), amount: each() }), ) expect(await usdcBalance(c, canceller.publicKey())).toBe(before - each()) const returned = await send( v.cancel_deposit({ - from: canceller.publicKey(), + holder: canceller.publicKey(), epoch_id: subscriptionEpoch, }), ) @@ -201,7 +201,7 @@ describe("pricing", () => { const before = await shares(holder.publicKey()) const got = await send( vault(c, holder).claim_deposit({ - caller: holder.publicKey(), + holder: holder.publicKey(), epoch_id: subscriptionEpoch, }), ) @@ -216,7 +216,7 @@ describe("pricing", () => { const v = vault(c, first) expectRefused( await v.claim_deposit({ - caller: first.publicKey(), + holder: first.publicKey(), epoch_id: subscriptionEpoch, }), v, @@ -267,7 +267,7 @@ describe("redemption", () => { const held = await shares(holder.publicKey()) const epoch = await send( vault(c, holder).request_redeem({ - from: holder.publicKey(), + holder: holder.publicKey(), shares: held, }), ) @@ -282,7 +282,7 @@ describe("redemption", () => { it("refuses a second exit request in one epoch", async () => { const v = vault(c, first) expectRefused( - await v.request_redeem({ from: first.publicKey(), shares: 1n }), + await v.request_redeem({ holder: first.publicKey(), shares: 1n }), v, "RequestOutstanding", ) @@ -292,7 +292,7 @@ describe("redemption", () => { const v = vault(c, first) expectRefused( await v.claim_redeem({ - caller: first.publicKey(), + holder: first.publicKey(), epoch_id: redemptionEpoch, }), v, @@ -339,7 +339,7 @@ describe("treasury", () => { const v = vault(c, first) expectRefused( await v.claim_redeem({ - caller: first.publicKey(), + holder: first.publicKey(), epoch_id: redemptionEpoch, }), v, @@ -362,7 +362,7 @@ describe("treasury", () => { it("fund returns the capital and the refused claims then pay", async () => { await send( vault(c, custodian).fund({ - from: custodian.publicKey(), + caller: custodian.publicKey(), assets: deployed(), }), ) @@ -371,7 +371,7 @@ describe("treasury", () => { const before = await usdcBalance(c, holder.publicKey()) const paid = await send( vault(c, holder).claim_redeem({ - caller: holder.publicKey(), + holder: holder.publicKey(), epoch_id: redemptionEpoch, }), ) diff --git a/scripts/harness/seed.ts b/scripts/harness/seed.ts index 4bb16227..c8285936 100644 --- a/scripts/harness/seed.ts +++ b/scripts/harness/seed.ts @@ -185,13 +185,13 @@ async function main(): Promise { ) await send( vault(c, holder1).request_deposit({ - from: holder1.publicKey(), + holder: holder1.publicKey(), amount: usdc(5_000), }), ) await send( vault(c, holder2).request_deposit({ - from: holder2.publicKey(), + holder: holder2.publicKey(), amount: usdc(3_000), }), ) @@ -213,13 +213,13 @@ async function main(): Promise { console.log(" + claiming shares for holder-1 and holder-2...") await send( vault(c, holder1).claim_deposit({ - caller: holder1.publicKey(), + holder: holder1.publicKey(), epoch_id: 1n, }), ) await send( vault(c, holder2).claim_deposit({ - caller: holder2.publicKey(), + holder: holder2.publicKey(), epoch_id: 1n, }), ) @@ -266,14 +266,14 @@ async function main(): Promise { const h1RedeemReq = ( await reader.get_redeem_request({ epoch_id: currentEpoch, - controller: holder1.publicKey(), + holder: holder1.publicKey(), }) ).result if (!h1RedeemReq && h1Shares >= usdc(1_000)) { console.log(" + holder-1 requesting redemption of 1,000 shares...") await send( vault(c, holder1).request_redeem({ - from: holder1.publicKey(), + holder: holder1.publicKey(), shares: usdc(1_000), }), ) @@ -287,14 +287,14 @@ async function main(): Promise { const h3DepositReq = ( await reader.get_deposit_request({ epoch_id: currentEpoch, - controller: holder3.publicKey(), + holder: holder3.publicKey(), }) ).result if (!h3DepositReq) { console.log(" + holder-3 requesting deposit of 2,500 USDC...") await send( vault(c, holder3).request_deposit({ - from: holder3.publicKey(), + holder: holder3.publicKey(), amount: usdc(2_500), }), ) @@ -371,13 +371,13 @@ async function main(): Promise { const depReq = ( await reader.get_deposit_request({ epoch_id: finalEpoch, - controller: key.publicKey(), + holder: key.publicKey(), }) ).result const redReq = ( await reader.get_redeem_request({ epoch_id: finalEpoch, - controller: key.publicKey(), + holder: key.publicKey(), }) ).result if (depReq) { diff --git a/scripts/harness/wind-down.test.ts b/scripts/harness/wind-down.test.ts index 8bb27fea..7dd0eccb 100644 --- a/scripts/harness/wind-down.test.ts +++ b/scripts/harness/wind-down.test.ts @@ -84,7 +84,7 @@ describe("a wind-down on a throwaway vault", () => { await call( "request_deposit", "holder-1", - { from: holder(), amount: usdc(200) }, + { holder: holder(), amount: usdc(200) }, true, ), ).toBe("accepted") @@ -104,7 +104,7 @@ describe("a wind-down on a throwaway vault", () => { await call( "claim_deposit", "holder-1", - { caller: holder(), epoch_id: 1n }, + { holder: holder(), epoch_id: 1n }, true, ), ).toBe("accepted") @@ -115,7 +115,7 @@ describe("a wind-down on a throwaway vault", () => { await call( "request_redeem", "holder-1", - { from: holder(), shares: usdc(10) }, + { holder: holder(), shares: usdc(10) }, true, ), ).toBe("accepted") @@ -204,8 +204,8 @@ describe("a wind-down on a throwaway vault", () => { ["wind_down_claimable", { holder: holder() }], ["get_epoch", { epoch_id: 1n }], ["get_epoch_settlement", { epoch_id: 1n }], - ["get_deposit_request", { epoch_id: 1n, controller: holder() }], - ["get_redeem_request", { epoch_id: 2n, controller: holder() }], + ["get_deposit_request", { epoch_id: 1n, holder: holder() }], + ["get_redeem_request", { epoch_id: 2n, holder: holder() }], ["has_role", { account: gov, role: "manager" }], ["get_role_member_count", { role: "manager" }], ["get_role_member", { role: "manager", index: 0 }], @@ -264,23 +264,23 @@ describe("a wind-down on a throwaway vault", () => { { caller: c.keys.treasury.publicKey(), assets: 1n }, "refused: WindDownActive", ], - ["fund", "holder-1", { from: holder(), assets: 1n }, "accepted"], + ["fund", "holder-1", { caller: holder(), assets: 1n }, "accepted"], [ "cancel_deposit", "holder-1", - { from: holder(), epoch_id: 1n }, + { holder: holder(), epoch_id: 1n }, "refused: AlreadyPriced", ], [ "cancel_redeem", "holder-1", - { from: holder(), epoch_id: 2n }, + { holder: holder(), epoch_id: 2n }, "accepted", ], [ "claim_redeem", "holder-1", - { caller: holder(), epoch_id: 2n }, + { holder: holder(), epoch_id: 2n }, "refused: EpochNotFulfilled", ], [