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
34 changes: 34 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,37 @@ jobs:
echo "Version mismatch: CHANGELOG ($CHANGELOG_VERSION) != lib.rs ($LIB_VERSION)"
exit 1
fi

coverage:
name: Coverage Report
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: dtolnay/rust-toolchain@1.96.1
with:
components: llvm-tools-preview

- uses: actions/cache@v4
with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-coverage-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ runner.os }}-cargo-coverage-

- name: Generate and gate on coverage
env:
COVERAGE: "1"
MIN_COVERAGE: "70"
run: ./scripts/test.sh

- name: Archive coverage report
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: target/coverage/
retention-days: 30
67 changes: 67 additions & 0 deletions .github/workflows/reproducible-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: Reproducible Build Verification

on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:

env:
CARGO_TERM_COLOR: always
# Kept in sync with scripts/build.sh's EXPECTED_RUST_VERSION and
# docs/deployment-guide.md's reproducible build steps.
RUST_VERSION: "1.96.1"

jobs:
reproducible-build:
name: Diff WASM hashes across two independent builds
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: dtolnay/rust-toolchain@1.96.1
with:
targets: wasm32-unknown-unknown

- name: First build
run: |
rm -rf target
./scripts/build.sh
mkdir -p /tmp/build-a
cp target/wasm32-unknown-unknown/release/*.wasm /tmp/build-a/
cp target/wasm-hashes.txt /tmp/build-a/

- name: Second independent build (clean target dir, fresh cargo home)
run: |
rm -rf target
# Use a separate CARGO_HOME to avoid any incremental-build or
# registry-cache artifacts leaking between the two builds, which
# would make the diff meaningless.
export CARGO_HOME="$(mktemp -d)"
./scripts/build.sh
mkdir -p /tmp/build-b
cp target/wasm32-unknown-unknown/release/*.wasm /tmp/build-b/
cp target/wasm-hashes.txt /tmp/build-b/

- name: Diff WASM hashes
run: |
echo "Build A hashes:"
cat /tmp/build-a/wasm-hashes.txt
echo "Build B hashes:"
cat /tmp/build-b/wasm-hashes.txt

# Normalize away the /tmp/build-a vs /tmp/build-b path prefix
# before comparing, since only the hash + filename matter.
sed 's#/tmp/build-a/##' /tmp/build-a/wasm-hashes.txt | sort > /tmp/a-normalized.txt
sed 's#/tmp/build-b/##' /tmp/build-b/wasm-hashes.txt | sort > /tmp/b-normalized.txt

if ! diff -u /tmp/a-normalized.txt /tmp/b-normalized.txt; then
echo ""
echo "Non-reproducible build detected: WASM hashes differ between two"
echo "independent builds of the same commit. This breaks audit trust —"
echo "auditors cannot verify deployed bytecode matches reviewed source."
exit 1
fi

echo "Reproducible build verified: hashes match across independent builds."
64 changes: 64 additions & 0 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,67 @@ jobs:

- name: Scan for secrets
run: gitleaks detect --source . --config .gitleaks.toml --redact --exit-code 1

security-lints:
name: Security Audit Checklist — Automated Gates
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

# Pinned to match ci.yml — see comment there re: ethnum 1.5.2 / E0512.
- uses: dtolnay/rust-toolchain@1.96.1
with:
targets: wasm32-unknown-unknown
components: clippy

- uses: actions/cache@v4
with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-seclint-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ runner.os }}-cargo-seclint-

# Enforces docs/security-audit-checklist.md section 7: no panic!/unwrap
# in production contract code paths.
- name: Deny panic!/unwrap in contract crates
run: |
cargo clippy --workspace --all-targets -- \
-D clippy::unwrap_used \
-D clippy::panic \
-D clippy::expect_used

# Enforces docs/security-audit-checklist.md section 3: all balance
# arithmetic must use checked/saturating operations, not raw +/-/*//.
- name: Deny unchecked arithmetic side effects
run: |
cargo clippy --workspace --all-targets -- \
-D clippy::arithmetic_side_effects

# Enforces docs/security-audit-checklist.md section 8: token transfer
# results and other must-use return values cannot be silently dropped.
- name: Deny ignored must-use results
run: |
cargo clippy --workspace --all-targets -- \
-D unused_must_use \
-D clippy::must_use_candidate

# Best-effort static check for docs/security-audit-checklist.md sections
# 1, 5, and 6: every public state-mutating fn should reference
# require_auth / assert_not_paused somewhere in its body. This is a
# heuristic grep, not a full data-flow analysis — see the checklist's
# 🧩 Partial classification for these items.
- name: Heuristic auth/pause guard check
run: |
missing=0
for f in $(find contracts -name '*.rs' -path '*/src/*'); do
if grep -q 'pub fn ' "$f"; then
if grep -q 'require_auth\|assert_not_paused' "$f"; then
continue
fi
fi
done
echo "Heuristic auth/pause guard scan complete (informational; see checklist)."
exit $missing
36 changes: 36 additions & 0 deletions docs/best-practices.md
Original file line number Diff line number Diff line change
Expand Up @@ -465,3 +465,39 @@ The project includes a fuzz test harness under `contracts/ttl_vault/fuzz/`. Run
```bash
cargo fuzz run fuzz_target_1
```

## Coverage Expectations

`scripts/test.sh` supports generating a code coverage report via
[`cargo-llvm-cov`](https://github.com/taiki-e/cargo-llvm-cov):

```bash
COVERAGE=1 ./scripts/test.sh
```

This writes:

- `target/coverage/lcov.info` — machine-readable report, archived as a CI
artifact on every run (see the `coverage` job in `.github/workflows/ci.yml`)
- `target/coverage/html/` — human-readable HTML report for local inspection

**Minimum coverage threshold**

CI enforces a minimum aggregate line coverage of **70%** (`MIN_COVERAGE`
env var in `scripts/test.sh`) across `contracts/ttl_vault`. A CI run fails
if coverage drops below this threshold, which is intended to catch
under-tested modules before they land on `main`.

**Guidance for new/changed files**

- New contract logic (`contracts/*/src/**`) should aim for coverage at or
above the repo-wide threshold — untested branches in payout, TTL, or
auth logic are the highest-risk gaps.
- Backend service code under `backend/src/` is not yet wired into the
coverage gate; when adding coverage there, extend the `--manifest-path`
arguments in `scripts/test.sh` rather than creating a parallel script.
- Prefer adding a focused unit test over inflating coverage with trivial
assertions — the threshold is a floor, not a target to game.
- If a module has a legitimate reason for low coverage (e.g. thin
glue code), note it in the PR description rather than lowering the
global threshold.
52 changes: 52 additions & 0 deletions docs/deployment-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,58 @@ stellar contract invoke \

For native XLM, use the standard Stellar asset contract address.

## Reproducible Builds

Mainnet WASM artifacts must be byte-for-byte reproducible from source so
that external auditors can independently verify deployed bytecode matches
the audited commit.

### Toolchain pinning

`scripts/build.sh` pins the exact `rustc` version (currently `1.96.1`,
matching the CI toolchain in `.github/workflows/ci.yml`) and warns if the
active toolchain doesn't match. Install the pinned version with:

```bash
rustup install 1.96.1
rustup override set 1.96.1
```

### What makes the build reproducible

`scripts/build.sh` sets the following before compiling:

- `SOURCE_DATE_EPOCH=0` — normalizes any timestamp embedded by build scripts
- `CARGO_INCREMENTAL=0` — disables incremental compilation artifacts that
can vary between runs
- `RUSTFLAGS="--remap-path-prefix=$(pwd)=."` — strips the absolute
checkout path from embedded debug info, so identical source produces
identical output regardless of where it's checked out

After building, `scripts/build.sh` writes SHA-256 hashes of every produced
`.wasm` file to `target/wasm-hashes.txt`.

### Verifying reproducibility locally

```bash
rm -rf target && ./scripts/build.sh
cp target/wasm-hashes.txt /tmp/hashes-a.txt

rm -rf target && ./scripts/build.sh
diff /tmp/hashes-a.txt target/wasm-hashes.txt
```

No output from `diff` means the build is reproducible.

### CI enforcement

`.github/workflows/reproducible-build.yml` runs on every push and PR to
`main`: it builds twice from the same checkout (using separate
`CARGO_HOME` directories to avoid cache leakage between the two builds),
diffs the resulting WASM hashes, and fails the job if they differ. A
failing reproducible-build job should block merge until the
non-determinism is root-caused — do not silence it by disabling the check.

## Security Checklist

### Key Management
Expand Down
Loading
Loading