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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 35 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,15 +114,48 @@ 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.**
- Issue-first: every PR closes an issue (`Closes #N`). Branches are
`feat/<issue>-<desc>`, `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.
Expand Down
60 changes: 30 additions & 30 deletions Cargo.lock

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

8 changes: 4 additions & 4 deletions contracts/async-vault/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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 = []
Expand Down
56 changes: 28 additions & 28 deletions contracts/async-vault/src/deposit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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);
}

Expand All @@ -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,
Expand All @@ -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,
}
Expand All @@ -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));
Expand All @@ -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 => {}
Expand All @@ -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,
Expand Down Expand Up @@ -178,7 +178,7 @@ fn convert_to_exit(
};

DepositConvertedToExit {
controller: holder.clone(),
holder: holder.clone(),
epoch: epoch_id,
into_epoch,
shares,
Expand All @@ -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));
Expand All @@ -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 => {
Expand All @@ -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
}
Loading
Loading