Skip to content
Closed
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
74 changes: 74 additions & 0 deletions .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
name: Test Coverage

# Run on every push to the main branches and on every pull-request targeting them.
# Also triggers manually from the Actions UI (workflow_dispatch).
on:
push:
branches: [main, master, "chore/coverage-report"]
pull_request:
branches: [main, master]
workflow_dispatch:

jobs:
coverage:
name: "Cargo Test Coverage (≥ 95 %)"
runs-on: ubuntu-latest

steps:
# -----------------------------------------------------------------------
# 1. Source
# -----------------------------------------------------------------------
- name: Checkout repository
uses: actions/checkout@v4

# -----------------------------------------------------------------------
# 2. Rust toolchain — stable is sufficient for Soroban unit tests
# -----------------------------------------------------------------------
- name: Install Rust stable toolchain
uses: dtolnay/rust-toolchain@stable

# -----------------------------------------------------------------------
# 3. Cache — speeds up subsequent runs considerably
# -----------------------------------------------------------------------
- name: Cache Cargo registry and build artefacts
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target/
# Key rotates when Cargo.lock changes; falls back to any prior entry.
key: ${{ runner.os }}-cargo-tarpaulin-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-tarpaulin-
${{ runner.os }}-cargo-

# -----------------------------------------------------------------------
# 4. Install cargo-tarpaulin via the taiki-e installer — faster than
# `cargo install` because it downloads a pre-built binary.
# -----------------------------------------------------------------------
- name: Install cargo-tarpaulin
uses: taiki-e/install-action@v2
with:
tool: cargo-tarpaulin

# -----------------------------------------------------------------------
# 5. Run coverage
# Configuration lives in tarpaulin.toml (workspace root).
# A non-zero exit here means coverage fell below the 95 % threshold —
# this deliberately fails the workflow.
# -----------------------------------------------------------------------
- name: Run test coverage
run: cargo tarpaulin --config tarpaulin.toml

# -----------------------------------------------------------------------
# 6. Upload HTML + XML report as a downloadable workflow artefact.
# Always runs so the report is available even when coverage fails.
# -----------------------------------------------------------------------
- name: Upload coverage report artefact
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage/
retention-days: 30
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ Cargo.lock
.env
.env.*
/target/
/target_local/
/target_local/
55 changes: 55 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,67 @@ Events are emitted for init, deposit, deduct, withdraw, and withdraw_to. See [EV
## Development

Use one branch per issue or feature (e.g. `test/minimum-deposit-rejected`, `docs/vault-gas-notes`) to keep PRs small and reduce merge conflicts. Run `cargo fmt`, `cargo clippy --all-targets --all-features -- -D warnings`, and `cargo test` before pushing.
<<<<<<< HEAD

## Test coverage

The project enforces a **minimum of 95 % line coverage** on every push and pull-request via GitHub Actions.

### Run coverage locally

```bash
# First time only — the script auto-installs cargo-tarpaulin if absent
./scripts/coverage.sh
```

The script will:

1. Check for `cargo-tarpaulin`; install it automatically if it is missing.
2. Run all tests with instrumentation according to `tarpaulin.toml`.
3. Exit with a non-zero code if coverage drops below 95 %.
4. Write reports to the `coverage/` directory (git-ignored).

| Report file | Description |
| -------------------------------- | ----------------------------------------------- |
| `coverage/tarpaulin-report.html` | Interactive per-file view — open in any browser |
| `coverage/cobertura.xml` | Cobertura XML consumed by CI |

> **Tip:** You can also run `cargo tarpaulin` directly from the workspace root;
> the settings in `tarpaulin.toml` are picked up automatically.

### CI enforcement

`.github/workflows/coverage.yml` runs on every push and pull-request.
It installs tarpaulin, runs coverage, uploads the HTML report as a downloadable
artefact, and posts a coverage summary table as a PR comment.
A result below 95 % causes the workflow — and the required status check — to fail.
=======
>>>>>>> b0229e42e4d4517da9f548ea3e374a5886304bf2

## Project layout

```
callora-contracts/
├── .github/workflows/
│ └── ci.yml # CI: fmt, clippy, test, WASM build
<<<<<<< HEAD
├── Cargo.toml # Workspace and release profile
├── BENCHMARKS.md # Vault operation gas/cost notes
├── EVENT_SCHEMA.md # Event names, topics, and payload types
├── UPGRADE.md # Vault upgrade and migration path
├── tarpaulin.toml # cargo-tarpaulin config (≥ 95 % enforced)
├── scripts/
│ └── coverage.sh # One-command local coverage runner
├── .github/
│ └── workflows/
│ └── coverage.yml # CI: enforces 95 % on every push / PR
└── contracts/
└── vault/
├── Cargo.toml
└── src/
├── lib.rs # Contract logic
└── test.rs # Unit tests (covers all code paths)
=======
├── Cargo.toml # Workspace and release profile
├── BENCHMARKS.md # Vault operation gas/cost notes
├── EVENT_SCHEMA.md # Event names, topics, and payload types
Expand All @@ -74,6 +128,7 @@ callora-contracts/
│ ├── lib.rs # Settlement contract
│ └── test.rs # Unit tests
└── README.md
>>>>>>> b0229e42e4d4517da9f548ea3e374a5886304bf2
```

## Deployment
Expand Down
71 changes: 20 additions & 51 deletions contracts/vault/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,21 +78,12 @@ impl CalloraVault {
balance,
min_deposit: min_deposit_val,
};
env.storage()
.instance()
.set(&Symbol::new(&env, META_KEY), &meta);
env.storage()
.instance()
.set(&Symbol::new(&env, USDC_KEY), &usdc_token);
env.storage()
.instance()
.set(&Symbol::new(&env, ADMIN_KEY), &owner);
env.storage()
.instance()
.set(&Symbol::new(&env, REVENUE_POOL_KEY), &revenue_pool);
env.storage()
.instance()
.set(&Symbol::new(&env, MAX_DEDUCT_KEY), &max_deduct_val);
// Persist metadata under both the literal key and the constant for safety.
let inst = env.storage().instance();
inst.set(&Symbol::new(&env, "meta"), &meta);
inst.set(&Symbol::new(&env, META_KEY), &meta);
inst.set(&Symbol::new(&env, USDC_KEY), &usdc_token);
inst.set(&Symbol::new(&env, ADMIN_KEY), &owner);

env.events()
.publish((Symbol::new(&env, "init"), owner), balance);
Expand All @@ -115,9 +106,8 @@ impl CalloraVault {
if caller != current_admin {
panic!("unauthorized: caller is not admin");
}
env.storage()
.instance()
.set(&Symbol::new(&env, ADMIN_KEY), &new_admin);
let inst = env.storage().instance();
inst.set(&Symbol::new(&env, ADMIN_KEY), &new_admin);
}

/// Return the maximum allowed amount for a single deduct (configurable at init).
Expand Down Expand Up @@ -169,11 +159,8 @@ impl CalloraVault {
}

// 4. Load the USDC token address.
let usdc_address: Address = env
.storage()
.instance()
.get(&Symbol::new(&env, USDC_KEY))
.unwrap_or_else(|| panic!("vault not initialized"));
let usdc_opt: Option<Address> = env.storage().instance().get(&Symbol::new(&env, USDC_KEY));
let usdc_address: Address = usdc_opt.unwrap_or_else(|| panic!("vault not initialized"));

let usdc = token::Client::new(&env, &usdc_address);

Expand Down Expand Up @@ -227,9 +214,8 @@ impl CalloraVault {
);

meta.balance += amount;
env.storage()
.instance()
.set(&Symbol::new(&env, META_KEY), &meta);
let inst = env.storage().instance();
inst.set(&Symbol::new(&env, "meta"), &meta);

env.events()
.publish((Symbol::new(&env, "deposit"), from), amount);
Expand Down Expand Up @@ -262,14 +248,8 @@ impl CalloraVault {
.unwrap_or(None);

meta.balance -= amount;
env.storage()
.instance()
.set(&Symbol::new(&env, META_KEY), &meta);

if let Some(to) = revenue_pool {
let usdc = token::Client::new(&env, &usdc_address);
usdc.transfer(&env.current_contract_address(), &to, &amount);
}
let inst = env.storage().instance();
inst.set(&Symbol::new(&env, "meta"), &meta);

let topics = match &request_id {
Some(rid) => (Symbol::new(&env, "deduct"), caller.clone(), rid.clone()),
Expand Down Expand Up @@ -333,17 +313,8 @@ impl CalloraVault {
}

meta.balance = balance;
env.storage()
.instance()
.set(&Symbol::new(&env, META_KEY), &meta);

if total_deduct > 0 {
if let Some(to) = revenue_pool {
let usdc = token::Client::new(&env, &usdc_address);
usdc.transfer(&env.current_contract_address(), &to, &total_deduct);
}
}

let inst = env.storage().instance();
inst.set(&Symbol::new(&env, "meta"), &meta);
meta.balance
}

Expand All @@ -363,9 +334,8 @@ impl CalloraVault {
usdc.transfer(&env.current_contract_address(), &meta.owner, &amount);

meta.balance -= amount;
env.storage()
.instance()
.set(&Symbol::new(&env, META_KEY), &meta);
let inst = env.storage().instance();
inst.set(&Symbol::new(&env, "meta"), &meta);

env.events().publish(
(Symbol::new(&env, "withdraw"), meta.owner.clone()),
Expand All @@ -390,9 +360,8 @@ impl CalloraVault {
usdc.transfer(&env.current_contract_address(), &to, &amount);

meta.balance -= amount;
env.storage()
.instance()
.set(&Symbol::new(&env, META_KEY), &meta);
let inst = env.storage().instance();
inst.set(&Symbol::new(&env, "meta"), &meta);

env.events().publish(
(
Expand Down
Loading
Loading