Skip to content
Merged
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
66 changes: 37 additions & 29 deletions .github/workflows/contracts-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,18 @@ name: Contracts CI
on:
push:
branches: [main, develop]
paths:
paths: &contracts-paths
- "contracts/**"
- "Cargo.toml"
- "**/Cargo.toml"
- "Cargo.lock"
- "sdk/tests/fixtures/oracle-parity-table.json"
- "scripts/generate-oracle-parity-table.cjs"
- "scripts/check-soroban-sdk-version.cjs"
- "scripts/check-wasm-size.cjs"
- ".github/workflows/contracts-ci.yml"
pull_request:
branches: [main, develop]
paths:
- "contracts/**"
- "Cargo.toml"
- "Cargo.lock"
- "sdk/tests/fixtures/oracle-parity-table.json"
- "scripts/generate-oracle-parity-table.cjs"
paths: *contracts-paths

env:
CARGO_TERM_COLOR: always
Expand Down Expand Up @@ -45,6 +42,9 @@ jobs:
with:
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}

- name: Check soroban-sdk version consistency
run: node scripts/check-soroban-sdk-version.cjs

- name: cargo fmt check
run: cargo fmt --all -- --check

Expand Down Expand Up @@ -79,6 +79,9 @@ jobs:
build-wasm:
name: Build WASM Artifacts
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0

Expand Down Expand Up @@ -110,40 +113,45 @@ jobs:
stellar contract build --manifest-path contracts/oracle-adapter/Cargo.toml
stellar contract build --manifest-path contracts/vault/Cargo.toml

- name: Check WASM size budgets
run: |
node -e '
const fs = require("fs");
const path = require("path");
const budgets = JSON.parse(fs.readFileSync("contracts/size-budget.json", "utf8"));
let failed = false;
for (const [name, budget] of Object.entries(budgets)) {
const file = path.join("target/wasm32v1-none/release", `${name}.wasm`);
if (fs.existsSync(file)) {
const size = fs.statSync(file).size;
console.log(`[SIZE] ${name}: ${size} bytes (budget: ${budget} bytes)`);
if (size > budget) {
console.error(`[ERROR] ${name} exceeds budget (${size} > ${budget})`);
failed = true;
}
}
}
if (failed) process.exit(1);
'

# Published with the artifacts, so the hash of a deployed contract's wasm
# can be checked against the commit that built it.
- name: Record wasm checksums
working-directory: target/wasm32v1-none/release
run: sha256sum *.wasm | tee SHA256SUMS

- name: Extract base size budget for delta calculation
if: github.event_name == 'pull_request'
run: |
git fetch origin "${{ github.base_ref }}" --depth=1
if git cat-file -e FETCH_HEAD:contracts/size-budget.json 2>/dev/null; then
git show FETCH_HEAD:contracts/size-budget.json > /tmp/base-size-budget.json
fi

- name: Check contract WASM sizes against budget
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
BASE_ARG=""
if [ -f /tmp/base-size-budget.json ]; then
BASE_ARG="--base /tmp/base-size-budget.json"
fi
node scripts/check-wasm-size.cjs \
--budget contracts/size-budget.json \
--wasm-dir target/wasm32v1-none/release \
--output-markdown target/wasm32v1-none/release/wasm-size-report.md \
--output-json target/wasm32v1-none/release/wasm-sizes.json \
--post-pr-comment \
$BASE_ARG

- name: Upload WASM artifacts
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf6581a12fe42986043a # v4.4.3
with:
name: contract-wasm
path: |
target/wasm32v1-none/release/*.wasm
target/wasm32v1-none/release/SHA256SUMS
target/wasm32v1-none/release/wasm-size-report.md
target/wasm32v1-none/release/wasm-sizes.json
retention-days: 7

reproducible-wasm:
Expand Down
23 changes: 14 additions & 9 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ on:
- "contracts/**/*.rs"
- "**/Cargo.toml"
- "Cargo.lock"
- "deny.toml"
- "sdk/package.json"
- "sdk/package-lock.json"
- ".github/workflows/security.yml"
Expand Down Expand Up @@ -39,6 +40,19 @@ jobs:
- name: Run audit
run: cargo audit

cargo-deny:
name: Cargo Deny (licenses, bans, and sources)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0

# Pinned and --locked, matching cargo-audit above.
- name: Install cargo-deny
run: cargo install cargo-deny --locked --version 0.20.2

- name: Check licenses, bans, and sources
run: cargo deny check

npm-audit:
name: npm Audit (SDK runtime dependencies)
runs-on: ubuntu-latest
Expand All @@ -62,12 +76,3 @@ jobs:
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/dependency-review-action@72eb423297a7cc29376c3e0026574f1a203f9052 # v4.5.0

cargo-deny:
name: Cargo Deny (Licenses & Bans)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: EmbarkStudios/cargo-deny-action@v2
with:
command: check licenses bans sources
7 changes: 4 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- `deny.toml` and a `cargo-deny` job in `.github/workflows/security.yml` (#91), enforcing permissive dependency licenses (`Apache-2.0`, `Apache-2.0 WITH LLVM-exception`, `MIT`, `BSD-2-Clause`, `BSD-3-Clause`, `Unicode-3.0`), rejecting copyleft/GPL dependencies, and denying unknown registries and git sources. Findings, assumptions, and residual risks are documented in `docs/SECURITY.md`.
- `contracts/size-budget.json` (#94) and `scripts/check-wasm-size.cjs` wired into `.github/workflows/contracts-ci.yml` (#93), recording the compiled `.wasm` byte size of all five contracts (`rwa-asset`, `registry`, `compliance`, `governance`, `oracle-adapter`), posting a size delta report on pull requests, and failing CI if any contract exceeds its configured byte budget.
- `scripts/check-soroban-sdk-version.cjs`, run in `.github/workflows/contracts-ci.yml`, rejecting per-crate `soroban-sdk` version overrides or duplicate `soroban-sdk` lockfile versions, plus the workspace SDK upgrade procedure in `CONTRIBUTING.md` (#95).
- `.devcontainer/devcontainer.json` with the pinned Rust toolchain (`rust-toolchain.toml`), `wasm32v1-none`, `stellar-cli` `27.1.0`, and Node.js 22 preinstalled, running `npm ci` in `sdk/` on creation (#81).
- `.vscode/tasks.json` defining `make build`, `make lint`, `make test`, and `npm test` grouped as build and test tasks (#82).
- `make doctor` target (`scripts/doctor.sh`) checking `rustc`, `wasm32v1-none`, `stellar-cli`, `node`, and `npm` versions against the repository's pinned expectations (#86).
- `ci`: Add `contracts/size-budget.json` establishing tracked byte thresholds for each contract, enforced in `.github/workflows/contracts-ci.yml` (#94).
- `ci`: Add `deny.toml` and `cargo-deny` check in `.github/workflows/security.yml` to enforce license compliance (Apache-2.0, MIT, BSD) and deny unknown registries (#91).
- Stage 1 developer-experience tooling:
- `.gitattributes` normalizing all tracked text to LF (`*.sh`, `*.rs`, `*.ts`, `*.md`, and the other text formats in the tree) and marking `Cargo.lock` and `package-lock.json` as generated. The tree was re-normalized once, and a new **Line Endings** workflow fails any pull request that reintroduces CRLF into the index (#85).
- A **Commitlint** workflow and `commitlint.config.cjs` enforcing the Conventional Commits style documented in `CONTRIBUTING.md`; a pull request with a non-conforming commit now fails the check. Allowed scopes are documented and now include the `vault` and `common` crates (#90).
Expand Down Expand Up @@ -79,7 +80,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed

- **BREAKING:** `rwa-asset.forced_transfer`'s `reason_hash` is `Bytes` end to end; the `ForcedTransfer` event's field was typed `BytesN<32>`, a straight compile error against the function's own `Bytes` parameter (#144, merged concurrently with #148–#151). Standardized on `Bytes` to match the empty-hash check below, rather than the fixed-size type, and renamed the error variant `NotForceTransferRole` to `NotForcedTransferRole` to match what the function actually raised (it never compiled under the other spelling either). `forced_transfer` now actually rejects an empty `reason_hash` with `InvalidReasonHash`, which existed as a documented error but was never checked; `InvalidForcedTransferParty` remains reserved and unraised, since the function deliberately treats a same-address call as a no-op (matching `transfer`/`transfer_from`) rather than rejecting it. Also fixed the same commit's `contracts/rwa-asset/src/error.rs` (`soroban_client` → `soroban_sdk`, `rwaError` → `RwaError`, `PartialE` → `PartialEq`, a doc typo) and its new tests, which called a `set_compliance_officer`/`NotComplianceOfficer` that never existed (the real role is `set_forced_transfer_role`/`NotForcedTransferRole`) and read `env.events().all()` through an SDK API it didn't import and didn't match this `soroban-sdk` version's shape.
- **BREAKING:** `rwa-asset::RwaError::NoPendingAdminTransfer` moves from discriminant `15` to `19`, and `compliance::ComplianceError::BatchTooLarge`/`NoPendingAdminTransfer` move from `6` to `7`/`8`. Both crates' error enums assigned the same numeric code to more than one variant (two in `rwa-asset`, three in `compliance`), which is a hard `E0081` compile error under the Rust version this workspace now builds with — found while adding `rwa-asset`'s new error variants for #148/#150. Moved rather than left colliding, since nothing has been deployed with the old values (ADR-003's discriminant freeze protects a *deployed* contract's clients, which none yet are). Also fixed, in the same two crates, a handful of files that failed to parse at all (duplicated `use`/`pub use` statements and a struct left without its closing brace) — both block `rwa-asset`'s own build via its `compliance` dev-dependency. `registry`, `oracle-adapter` and `governance` have the same class of pre-existing duplicate-discriminant and malformed-source defects; left alone here since neither blocks this change, and tracked separately.
- **BREAKING:** `rwa-asset::RwaError::NoPendingAdminTransfer` moves from discriminant `15` to `19`, and `compliance::ComplianceError::BatchTooLarge`/`NoPendingAdminTransfer` move from `6` to `7`/`8`. Both crates' error enums assigned the same numeric code to more than one variant (two in `rwa-asset`, three in `compliance`), which is a hard `E0081` compile error under the Rust version this workspace now builds with — found while adding `rwa-asset`'s new error variants for #148/#150. Moved rather than left colliding, since nothing has been deployed with the old values (ADR-003's discriminant freeze protects a *deployed* contract's clients, which none yet are). Also fixed, in the same two crates, a handful of files that failed to parse at all (duplicated `use`/`pub use` statements and a struct left without its closing brace) — both block `rwa-asset`'s own build via its `compliance` dev-dependency. Resolved the matching duplicate-discriminant and malformed-merge defects in `registry` (`NoPendingAdminTransfer` moved from `6` to `7`), `oracle-adapter` (`NoPendingAdminTransfer` moved from `14` to `15`), `governance` (`InvalidDepositAmount`, `NoPendingAdminTransfer`, `InvalidQuorum`, `InvalidPassThresholdBps`, and `DuplicateActiveProposal` assigned distinct discriminants `15`–`19`), and `tools/benchmark` so the full workspace compiles and passes `cargo clippy --workspace --all-targets --all-features -- -D warnings` and `cargo test --all --features testutils`.
- **BREAKING:** `rwa-asset.set_compliance` refuses a `min_level` above `MAX_COMPLIANCE_LEVEL` (3), with `InvalidComplianceLevel` (#139). A compliance record's own `level` is already capped at the same scale in `ComplianceContract::set_kyc`, so a higher `min_level` could never be satisfied by any verified party: every transfer would silently and permanently fail `NotCompliant` until an admin reconfigured it. A caller already passing 0-3 is unaffected.

## [0.3.0] - 2026-09-14
Expand Down
36 changes: 35 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,11 @@ Branch naming conventions:
make fmt
make lint

# Workspace tooling checks (soroban-sdk consistency, WASM size budgets, cargo-deny)
node scripts/check-soroban-sdk-version.cjs
node scripts/check-wasm-size.cjs
cargo deny check

# TypeScript SDK
make sdk-lint
make sdk-typecheck
Expand All @@ -266,7 +271,36 @@ make all

CI will reject PRs that fail formatting or linting.

### 4. Write tests
### 4. Upgrading `soroban-sdk` and Managing WASM Budgets

#### Upgrading `soroban-sdk` (#95)

All workspace crates inherit `soroban-sdk` from a single `[workspace.dependencies]` entry in the root [`Cargo.toml`](Cargo.toml). Individual crates must always declare `soroban-sdk = { workspace = true }` (with optional `features = ["testutils"]` in `[dev-dependencies]`) and must never pin a per-crate `version`, `git`, or `path` override. `node scripts/check-soroban-sdk-version.cjs` enforces this rule in CI.

To upgrade `soroban-sdk` across the workspace:

1. **Update the root manifest:** Edit `soroban-sdk = { version = "..." }` under `[workspace.dependencies]` in the root `Cargo.toml`.
2. **Align `stellar-cli` in CI:** If the major or minor version changes, update `STELLAR_CLI_VERSION` and `STELLAR_CLI_SHA256` in `.github/workflows/contracts-ci.yml` and `.github/workflows/testnet-smoke.yml`.
3. **Refresh `Cargo.lock` and verify consistency:**
```bash
cargo update -p soroban-sdk
node scripts/check-soroban-sdk-version.cjs
cargo deny check
```
4. **Rebuild contracts, verify WASM budgets, and run tests:**
```bash
make build
node scripts/check-wasm-size.cjs
node scripts/generate-oracle-parity-table.cjs
make test
make sdk-test
```

#### Contract WASM Size Budgets (#93, #94)

Each deployable contract (`rwa-asset`, `registry`, `compliance`, `governance`, `oracle-adapter`) has a byte budget tracked in [`contracts/size-budget.json`](contracts/size-budget.json). In CI, `.github/workflows/contracts-ci.yml` builds all five contracts, runs `node scripts/check-wasm-size.cjs` to compare their optimized `.wasm` sizes against `contracts/size-budget.json`, comments the size delta table on pull requests, and fails the build if any contract exceeds its `max_bytes` ceiling. Raising a contract's budget requires an explicit, reviewed edit to `contracts/size-budget.json`.

### 5. Write tests

See [Testing Requirements](#testing-requirements).

Expand Down
Loading
Loading