diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5f7a7ab7..01a6db37 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,10 +2,15 @@ name: Build Project and Run Tests on: push: - branches: ["main"] + branches: ["main", "feedback"] pull_request: types: [opened, synchronize, reopened, labeled, unlabeled] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + # A cancelled push run would leave a failed build check on that commit. + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + env: CARGO_TERM_COLOR: always PKG_CONFIG_PATH: /usr/lib/pkgconfig @@ -15,39 +20,127 @@ permissions: contents: read jobs: - build: + changes: runs-on: ubuntu-latest + outputs: + code: ${{ steps.filter.outputs.code }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false + # The test merge commit and its parents, to diff a pull request against its base. + fetch-depth: 2 + - name: Check whether anything besides docs changed + id: filter + run: | + code=true + if [ "$GITHUB_EVENT_NAME" = pull_request ]; then + CHANGED=$(git diff --no-renames --name-only HEAD^1 HEAD) + printf '%s\n' "$CHANGED" + code=false + while IFS= read -r path; do + case "$path" in + "") ;; + # The harness hashes the sample valuation. + docs/evidence/*) code=true ;; + *.md) ;; + *) code=true ;; + esac + done <<< "$CHANGED" + fi + echo "code=$code" | tee -a "$GITHUB_OUTPUT" + # Runs for docs-only changes too: the js tests read the README. + contracts: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3 + with: + cache-bin: "false" + - name: Install the stellar CLI + run: scripts/ci/install-tools.sh stellar + - name: Build the contracts + run: stellar contract build + # A build path in the Wasm would tie its hash to the checkout location. + - name: Print the Wasm hashes and check no build path is embedded + run: | + sha256sum target/wasm32v1-none/release/*.wasm + if grep -l -a -E '/home/runner|/Users/|\.cargo/registry' target/wasm32v1-none/release/*.wasm; then + echo "These Wasm files embed a build path, so their hashes depend on where the repository is checked out." + exit 1 + fi - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 22 - run: npm i -g npm@11.7.0 - - run: npm --version + - run: npm ci + - name: Generate the TypeScript clients from the Wasm + run: npm run build:clients + # One path, so the artifact's root is the clients folder itself. + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: clients + path: app-lib/clients/ + if-no-files-found: error + retention-days: 7 - # ---- Fast checks first ---- - # Fail in seconds on lint / format before the multi-minute CLI build + - # Docker network below, so trivial fixes don't pay the full cost. + js: + needs: contracts + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 22 + - run: npm i -g npm@11.7.0 + - run: npm --version - run: npm ci + - name: Download the generated clients + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: clients + path: app-lib/clients - run: npm run lint - run: npm run format:check + - run: npm run typecheck - run: npm run test - # ---- Slow path: CLI toolchain, local network, contract codegen ---- - - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - # NB: ~/.cargo/bin is deliberately NOT cached here. Keying the - # stellar-scaffold binary on this repo's Cargo.lock would restore a - # binary built from an unrelated CLI revision. - path: | - ~/.cargo/registry/index/ - ~/.cargo/registry/cache/ - ~/.cargo/git/db/ - target/ - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + secret-scan: + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + # Every commit of the pull request, so each one is scanned. + fetch-depth: 0 + - name: Install gitleaks + run: scripts/ci/install-tools.sh gitleaks + # Per commit, so a key added and later removed in the same pull request still fails. + - name: Scan the pull request's commits for secrets + env: + BASE: ${{ github.event.pull_request.base.sha }} + HEAD: ${{ github.event.pull_request.head.sha }} + run: gitleaks git --redact --no-banner --log-opts="$BASE..$HEAD" . + + rust-tests: + needs: changes + if: needs.changes.outputs.code == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3 + with: + cache-bin: "false" + workspaces: | + . -> target # Before the CLI install so a broken contract test fails without # waiting on it. rustup picks the toolchain from rust-toolchain.toml. - name: Run the contract unit tests @@ -55,32 +148,117 @@ jobs: - run: sudo apt-get update && sudo apt-get install -y libudev-dev libdbus-1-dev pkg-config - # binstall fetches a prebuilt stellar-scaffold-cli instead of compiling it - - name: Install cargo-binstall - uses: cargo-bins/cargo-binstall@e00d2c94cc0067b77737821097a62d91c0301baa # v1.21.1 - - name: Install stellar-scaffold - run: | - if ! command -v stellar-scaffold &> /dev/null; then - echo "stellar-scaffold not found, installing..." - # --locked so the source-build fallback uses the CLI's published - # Cargo.lock instead of resolving fresh soroban deps whose MSRV - # may outrun the pinned toolchain. - cargo binstall --locked stellar-scaffold-cli@0.0.27 - else - echo "stellar-scaffold already installed. Clear cache to force reinstall." - fi - - name: Install stellar CLI + - name: Install the stellar CLI + run: scripts/ci/install-tools.sh stellar + # The upgrade tests run built Wasm, so they need both contracts and + # their probe builds first (docs/TESTING.md). + - name: Run the Wasm upgrade tests run: | - if ! command -v stellar &> /dev/null; then - cargo binstall --locked stellar-cli@27.0.0 - fi + # Setting pipefail so a failing test run is not hidden by `tee` + set -o pipefail + stellar contract build --package share-token + stellar contract build --package nav-oracle + CARGO_TARGET_DIR=target/upgrade-probe stellar contract build --package share-token --features upgrade_probe + CARGO_TARGET_DIR=target/upgrade-probe stellar contract build --package nav-oracle --features upgrade_probe + CARGO_TARGET_DIR=target/upgrade-probe stellar contract build --package async-vault --features upgrade_probe + cargo test --locked -p async-vault -p nav-oracle --features upgrade_wasm with_wasm 2>&1 | tee upgrade-tests.log + scripts/ci/check-tests-ran.sh upgrade-tests.log test::upgrade::with_wasm test::governance::with_wasm + + network: + needs: changes + if: needs.changes.outputs.code == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + # In the background, so the steps up to "Wait for the local network" run while it boots. - name: Start Stellar local network - # No tagged releases upstream, so this pins main as read on 2026-08-14 - uses: stellar/quickstart@864f91d804c0061bd11ef8872d3e318581febb55 + env: + # Pinned by digest: "latest" moves with every protocol build. + IMAGE: docker.io/stellar/quickstart:v687-b1580.1-latest@sha256:7711899e9942c60316ebacf8ab901be15bed7a5a31705d847b55375ff3bc8356 + # The image's own default can be a protocol its RPC cannot simulate. + PROTOCOL_VERSION: "30" + # The exit file lets the wait step fail at once when the pull or start fails. + run: | + ( + trap '' HUP + status=0 + docker run -d --name stellar -p 8000:8000 \ + -e ENABLE_LOGS=true -e ENABLE=core,rpc -e NETWORK=local \ + -e PROTOCOL_VERSION="$PROTOCOL_VERSION" "$IMAGE" || status=$? + echo "$status" > "$RUNNER_TEMP/quickstart.exit" + ) > "$RUNNER_TEMP/quickstart.log" 2>&1 & + + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 22 + - run: npm i -g npm@11.7.0 + - run: npm --version + - run: npm ci + - uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3 with: - enable: "core,rpc" - # Pinned image: "latest" tracks new protocol builds. - tag: v678-b1525.1-latest + cache-bin: "false" + - run: + sudo apt-get update && sudo apt-get install -y libudev-dev + libdbus-1-dev pkg-config + - name: Install the stellar CLI and stellar-scaffold + run: scripts/ci/install-tools.sh stellar stellar-scaffold + # The same cargo build Scaffold runs, so its build below reuses the Wasm. + - name: Build the contracts + run: stellar contract build + - name: Build the vault without the wind-down minimum + run: + CARGO_TARGET_DIR=target/short-wind-down stellar contract build + --package async-vault --features short_wind_down + - name: Read the Playwright version + id: playwright + run: | + VERSION=$(jq -r '.packages["node_modules/playwright-core"].version' package-lock.json) + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + - name: Cache the Playwright browser + id: playwright-cache + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/.cache/ms-playwright + key: + ${{ runner.os }}-playwright-${{ steps.playwright.outputs.version }} + - name: Install Playwright browser + if: steps.playwright-cache.outputs.cache-hit != 'true' + run: npx playwright install --with-deps chromium + working-directory: e2e + - name: Install Playwright's system dependencies + if: steps.playwright-cache.outputs.cache-hit == 'true' + run: npx playwright install-deps chromium + working-directory: e2e + + - name: Wait for the local network + timeout-minutes: 12 + run: | + fail() { + echo "$1" + cat "$RUNNER_TEMP/quickstart.log" + docker logs stellar || true + exit 1 + } + while [ "$SECONDS" -lt 600 ]; do + EXIT=$(cat "$RUNNER_TEMP/quickstart.exit" 2>/dev/null || true) + if [ -n "$EXIT" ] && [ "$EXIT" != 0 ]; then + fail "docker run failed with exit code $EXIT." + fi + case "$(docker inspect -f '{{.State.Status}}' stellar 2>/dev/null || true)" in + exited | dead) fail "The local network container stopped." ;; + esac + if curl --no-progress-meter --max-time 5 -X POST -H 'Content-Type: application/json' \ + -d '{"jsonrpc": "2.0", "id": 1, "method": "getHealth"}' \ + http://localhost:8000/rpc 2>/dev/null | grep -q healthy && + curl --no-progress-meter --max-time 5 http://localhost:8000/friendbot 2>/dev/null | + grep -q '"invalid_field": "addr"'; then + exit 0 + fi + sleep 2 + done + fail "The local network did not start in 10 minutes." - name: Add master account as default identity run: | mkdir -p .config/stellar/identity @@ -92,6 +270,8 @@ jobs: set -o pipefail STELLAR_SCAFFOLD_ENV=development stellar-scaffold build \ --build-clients 2>&1 | tee build_clients.log + # Build the clients against the workspace @stellar/stellar-sdk, not the scaffold's copy. + rm -rf app-lib/clients/*/node_modules - name: Check client generation summary run: | # Extract the number after "Failed: " @@ -127,10 +307,23 @@ jobs: exit 1 fi echo "Generated $ACTUAL client(s)." - - run: npm run build --workspaces --if-present - - name: Install Playwright browser - run: npx playwright install --with-deps chromium - working-directory: e2e + - name: + Deploy the native asset contract and register it as the alias asset + # The console is written against a Stellar Asset Contract, so the + # development build needs one. Its id is deterministic, so a rerun only + # re-registers the alias. + run: | + RPC=http://localhost:8000/rpc + PASSPHRASE="Standalone Network ; February 2017" + ID=$(stellar contract id asset --asset native --rpc-url "$RPC" --network-passphrase "$PASSPHRASE") + if ! stellar contract invoke --id "$ID" --source me --rpc-url "$RPC" --network-passphrase "$PASSPHRASE" -- name > /dev/null 2>&1; then + stellar contract asset deploy --asset native --source me --rpc-url "$RPC" --network-passphrase "$PASSPHRASE" + fi + stellar contract alias add asset --id "$ID" --rpc-url "$RPC" --network-passphrase "$PASSPHRASE" + - name: Build the workspaces against the contracts deployed above + run: npm run build --workspaces --if-present + env: + ADDRESSES_ENV: development - name: Run smoke tests run: npx playwright test working-directory: e2e @@ -138,8 +331,6 @@ jobs: run: npm run deploy -w scripts/harness env: HARNESS_NETWORK: local - - name: Typecheck the harness workspace - run: npm run typecheck -w scripts/harness - name: Run the vault happy path on the local network run: npm run test:happy-path -w scripts/harness env: @@ -148,10 +339,64 @@ jobs: run: npm run test:guards -w scripts/harness env: HARNESS_NETWORK: local + - name: Run a wind-down on a throwaway vault on the local network + run: npm run test:wind-down -w scripts/harness + env: + HARNESS_NETWORK: local - name: Keep the harness report when it fails if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: harness-runs path: scripts/harness/runs/ if-no-files-found: ignore + + rust-checks: + needs: changes + if: needs.changes.outputs.code == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3 + with: + cache-bin: "false" + # rustup reads rust-toolchain.toml, which includes clippy and rustfmt. + - name: Check formatting + run: cargo fmt --all -- --check + - name: Lint with warnings as errors + run: cargo clippy --locked --workspace --all-targets -- -D warnings + - name: Check licences, advisories, bans and sources + uses: EmbarkStudios/cargo-deny-action@3c6349835b2b7b196a839186cb8b78e02f7b5f25 # v2.1.1 + with: + # Replaces the action's default --all-features, so CI runs the + # same command as docs/TESTING.md. + arguments: --locked + command: check + + # The check branch protection requires. Rust and network skip only for docs; the secret scan runs on pull requests. + build: + if: always() + needs: + [changes, contracts, js, secret-scan, rust-tests, rust-checks, network] + runs-on: ubuntu-latest + steps: + - name: Check the results + env: + CODE: ${{ needs.changes.outputs.code }} + CHANGES: ${{ needs.changes.result }} + CONTRACTS: ${{ needs.contracts.result }} + JS: ${{ needs.js.result }} + EVENT: ${{ github.event_name }} + SECRET_SCAN: ${{ needs.secret-scan.result }} + RUST_TESTS: ${{ needs.rust-tests.result }} + RUST_CHECKS: ${{ needs.rust-checks.result }} + NETWORK: ${{ needs.network.result }} + run: | + echo "changes=$CHANGES contracts=$CONTRACTS js=$JS event=$EVENT secret-scan=$SECRET_SCAN rust-tests=$RUST_TESTS rust-checks=$RUST_CHECKS network=$NETWORK code=$CODE" + [ "$CHANGES" = success ] && [ "$CONTRACTS" = success ] && [ "$JS" = success ] || exit 1 + if [ "$EVENT" = pull_request ]; then [ "$SECRET_SCAN" = success ] || exit 1; fi + if [ "$CODE" != false ]; then + [ "$RUST_TESTS" = success ] && [ "$RUST_CHECKS" = success ] && [ "$NETWORK" = success ] || exit 1 + fi diff --git a/.gitleaksignore b/.gitleaksignore new file mode 100644 index 00000000..1100f376 --- /dev/null +++ b/.gitleaksignore @@ -0,0 +1,2 @@ +# A captured RPC fetch response used as a test fixture, not a secret +scripts/harness/lib/ttl.test.ts:generic-api-key:22 diff --git a/AGENTS.md b/AGENTS.md index 7ad91313..32389266 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,18 +20,23 @@ result. Three states per side, none skipped: **pending, priced, claimed**. What the contracts enforce: -1. **Entry is gated** by a post-KYC allowlist, checked on the receiver of a - subscription (enforced on share claim in contracts today; request-time - check is targeted for a future milestone) and on every share transfer. +1. **Only allowlisted addresses enter.** The post-KYC allowlist is checked + when a subscription is requested, again when its shares are minted, and on + every share transfer. 2. **A covered claim always pays.** Once priced and covered, a cash claim cannot be blocked by a pause, a stale valuation, or the investor losing their - allowlist place. Priced claims are never re-priced and never identity-gated; - a delisted, non-frozen investor leaves through the exit-only cash path. -3. **Cancellation is atomic and single-step**, open only until the attestation - that prices the request is accepted. There is no instant exit. - -Five authorities, each a native Stellar multisig: governance, compliance, -attestation, treasury, guardian. Testnet only. Not audited. + allowlist place. Priced claims are never re-priced, and a cash payout never + needs the identity check; a delisted, non-frozen investor leaves through + the exit-only cash path, including a deposit that was priced before the + delisting. A compliance freeze holds the claim until it is lifted. +3. **Cancellation is atomic and single-step**, open only while the vault cannot + price the request: the attestation that prices it closes cancellation, and + cancellation reopens if that report expires, the NAV is paused or the vault + is paused. There is no instant exit. + +Five authorities, each meant to be a native Stellar multisig: governance, +compliance, attestation, treasury, guardian. The deploy script gives each one +key for now. Testnet only. Not audited. ## Reference base @@ -46,8 +51,13 @@ does not carry over. Read it for Soroban and OZ mechanics only. enable an experimental `soroban-sdk` feature (`spec_shaking_v2`) that only works through the CLI wrapper. The devshell pins the Stellar CLI it expects. - **Tests:** `cargo test` from the repo root runs every workspace member against - the in-memory `Env`. There is no unit-test runner for `app/` or `app-lib/`; - `e2e/` runs Playwright separately. CI runs `cargo test --locked` on every pull + the in-memory `Env`. `npm run test` runs vitest over `app/`, `app-lib/` and + `scripts/`, and `npm run typecheck` checks `app/`, `app-lib/` and the + harness. Both use the generated contract clients, so run + `stellar contract build && npm run build:clients` first. `e2e/` runs + Playwright separately. CI runs `cargo test --locked` on every pull request + except one that changes only Markdown files outside `docs/evidence/`: that + one skips the Rust and local-network jobs. The secret scan runs on every pull request. - **Toolchain:** `nix develop` provides it, or rustup honours `rust-toolchain.toml`. @@ -80,19 +90,35 @@ apply as the corresponding code lands here. `scripts/generate-clients.mjs`, which reads the contract ids and RPC URL of `ADDRESSES_ENV` (default `staging`) from `environments.toml` and writes `app-lib/clients//` from each contract's on-chain spec, with Node - alone. Those generated types are the only contract types; `app-lib/contracts.ts` + alone. `development` declares no ids: there the script resolves each + `[development.contracts.]` block plus an alias named `asset` through + `stellar contract alias show`, so CI builds the app against the contracts + the branch just deployed. The scaffold registers the block aliases when it + deploys; the asset is a Stellar Asset Contract a developer registers once with + `stellar contract asset deploy --asset native --alias asset --source me` + against the local network. Those generated types are the only contract types; `app-lib/contracts.ts` re-exports them and builds each runtime client with `new Client(...)` from its generated `src/index`, whose embedded spec means no page fetches a contract's instance or Wasm. Vite dedupes `@stellar/stellar-sdk` so a client's nested copy - never reaches the bundle, and vitest aliases the generated modules to - `vitest.clients.ts`, since tests run without them. + never reaches the bundle. A fresh clone builds `app` with no cargo, no stellar CLI and no local network, but needs RPC access to the selected network at build time. A local scaffold build writes the same directory from the WASM. -- When a tool must drive `cargo` directly rather than the CLI, set - `SOROBAN_SDK_BUILD_SYSTEM_SUPPORTS_SPEC_SHAKING_V2=1`. The build script fails - without it and its error names neither the variable nor this way out, so - coverage runs and size checks look impossible until you read the SDK source. +- The clients come from one generator, the SDK's `BindingGenerator`, which + types an empty `Option` as `null`. `npm run build:clients` and the harness + deploy write each client's `src/` from the local Wasm, with no network, and + compile it to the `dist/` the harness imports. A workspace build + (`npm run build --workspaces`) recompiles `dist/` from `src/`. The scaffold's + `--build-clients` writes the stellar CLI's bindings instead, which type an + empty `Option` as `undefined`, so run `npm run build:clients` again after it + before type-checking. +- When `cargo` itself builds a contract for the Wasm target, rather than + `stellar contract build` (a size check with + `cargo build --release --target wasm32v1-none`, for example), set + `SOROBAN_SDK_BUILD_SYSTEM_SUPPORTS_SPEC_SHAKING_V2=1`. soroban-sdk's build + script stops that build without it, and its error names neither the variable + nor this way out. Host builds such as `cargo test` and `cargo clippy` never + need it. - Change a contract's `__constructor` and you must change its `constructor_args` in `environments.toml` in the same commit. Otherwise the scaffold build still exits 0 and CI fails minutes later on a client count, naming neither the @@ -111,6 +137,45 @@ 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. Each contract + declares its error enum through `strata_bindings::error_enum!`, which fails + the build if a code leaves the contract's block (`docs/kit-spec.md`, section + 7). +- **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, which keeps + 1,024 bytes of each doc and drops the rest without a warning. + `every_contract_doc_fits_the_spec` (`crates/strata-bindings/tests/spec_docs.rs`) + fails when one is longer. +- **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.** @@ -118,8 +183,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 2eda79d5..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", @@ -194,7 +193,10 @@ dependencies = [ "stellar-access", "stellar-contract-utils", "stellar-macros", - "storage", + "stellar-tokens", + "strata-bindings", + "strata-storage", + "strata-timelock", ] [[package]] @@ -221,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" @@ -353,6 +348,9 @@ dependencies = [ "stellar-access", "stellar-macros", "stellar-tokens", + "strata-bindings", + "strata-storage", + "strata-timelock", ] [[package]] @@ -1031,7 +1029,7 @@ dependencies = [ "stellar-access", "stellar-macros", "stellar-tokens", - "storage", + "strata-storage", ] [[package]] @@ -1208,11 +1206,13 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" name = "nav-oracle" version = "0.1.0" dependencies = [ - "bindings", "soroban-sdk", "stellar-access", + "stellar-contract-utils", "stellar-macros", - "storage", + "strata-bindings", + "strata-storage", + "strata-timelock", ] [[package]] @@ -1711,6 +1711,9 @@ dependencies = [ "stellar-contract-utils", "stellar-macros", "stellar-tokens", + "strata-bindings", + "strata-storage", + "strata-timelock", ] [[package]] @@ -2052,12 +2055,28 @@ 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]] name = "strsim" version = "0.11.1" diff --git a/Cargo.toml b/Cargo.toml index 72abd44b..600a7b0c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,6 @@ [workspace] resolver = "2" members = ["crates/*", "contracts/*"] -exclude = ["fixtures/upgrade-target"] [workspace.package] edition = "2021" diff --git a/README.md b/README.md index b3770010..9e40b0e2 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,16 @@ Two rules the contracts enforce: 1. **Only approved addresses can enter** — an allowlist maintained post-KYC. 2. **A priced claim always pays** — neither a pause, a delisting nor a stale - valuation can block an already-priced, funded cash claim. + valuation can block an already-priced, funded cash claim. A compliance + freeze holds it until the freeze is lifted. + +A request can be cancelled until the vault can read its price. Once an epoch is +sealed, a valid report accepted after the seal closes cancellation, even while +the notice still runs; the epoch is priced once the notice ends. If that report +expires, or the NAV or the vault is paused, cancellation opens again. New +deposits are refused while the vault is paused, while the valuation is stale +or the NAV is paused, while an issuer clawback leaves the vault holding less +than its pending deposits, and once a wind-down is proposed. ## Documentation @@ -22,6 +31,14 @@ Two rules the contracts enforce: parameters, and complete role matrix. - [Testing](./docs/TESTING.md) — which tests cover storage and TTL, auth, ledger time, events and calls between contracts. +- [Threat model](./docs/THREAT_MODEL.md) — what can go wrong with the oracle, + the roles and the redemption paths, what stops it, what is left, and the + test for each control. +- [Monitoring plan](./docs/MONITORING_PLAN.md) — what an operator watches on + chain, where each signal comes from, and what to do when it fires. +- [Grant differences](./docs/GRANT_DIFFERENCES.md) — where the built kit + differs from the SCF grant proposal, what is not built yet, and the limits an + adopter should know. ## Status @@ -35,11 +52,11 @@ deploy from `environments.toml` → `[staging.contracts]`, which is the record. | Contract | Address | | --- | --- | -| `async_vault` | `CD5LYYXGOJ7R23P6D7YUN6DUJQYMI23KEUEOIFKXOSWOX3KHKTXRITED` | -| `nav_oracle` | `CBKVMUMIX7MXYHX5K34N5HYZOP6S2VGWF7MN27RIWBV3A3WD3X3L63CO` | -| `share_token` | `CDOISQTQRCYOTH6S3DFQ6UKJ2TEXLNO7NFIGOZS4RZL63IRCVXXCGP5N` | -| `identity_verifier` | `CCTCCY7AVFAIRGEWWJZT2UDMBHS3NGNKVUWLY43CHDNN75AQEV6P3EOY` | -| `compliance` | `CDEH6FJBK7NDFLALO3X3GDXZUREYHUSNC7ODPM64RBG3BBXW2USMW7OO` | +| `async_vault` | `CDJHQ2R5XUJGP6WUKCW4VBEVGWFAZ5TZLEONBITKITNXFDLWNNK7WVYO` | +| `nav_oracle` | `CCZ4X25GLMYRQ33PM4ZYDMGKAJJKPUG2ABUZ4NIGUSA7W4RUTBOEEDYI` | +| `share_token` | `CDOZT5VNIKSLQSFELF4PHVZRZA3GCJYJEHVDZTUB7O2QMBHBKKOYORQI` | +| `identity_verifier` | `CCGHNGPI66WGVSLLALQVCIRIOZKX73QW23UDZLZ3LMXLGEG7SLVUOULD` | +| `compliance` | `CCQI5NFRWBOO77LYSFY54D6PSU4TVWD5A3CFX2A5AWLEURVR35XISMKG` | | `asset` | `CCOHKZLQ5EAD2A3RLHYHH7W4K2TNLNELXUVLWZIZALH45WXHH5H3QV7E` | @@ -57,6 +74,128 @@ above. | Redeem (request) | `request_redeem` | [`d6c02ee0…7524`](https://stellar.expert/explorer/testnet/tx/d6c02ee0868235fd12c280ba8c4e17fd6d63f4a117e9c48a897185c72dc97524) | | Withdraw (claim cash) | `claim_redeem` | [`27a81778…db64`](https://stellar.expert/explorer/testnet/tx/27a8177877ecac5a4c87c1b4bed12e5f8664ca387f13fd9c05f8062ec68bdb64) | +## Redemption queue + +The notice-period queue on testnet, against the contract IDs above, run by the +harness scenario [`redemption-queue.test.ts`](./scripts/harness/redemption-queue.test.ts) +on 2026-11-02. Repeat it on a seeded deployment: + +```sh +HARNESS_NETWORK=testnet npm run seed # once: holders hold shares +HARNESS_NETWORK=testnet npm run test:queue -w scripts/harness # prints `tx