Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
020e208
fix: remove dead calculate_interest_rate function (#331)
laurentketterle-hub Aug 5, 2026
f14fbf6
fix: add #[allow(dead_code)] to unused storage wrapper functions (#331)
laurentketterle-hub Aug 5, 2026
71b5a66
fix: combined CI fixes — close braces + enum discriminants + dead cod…
laurentketterle-hub Aug 5, 2026
b68e71d
fix: add remaining missing braces in events.rs and lib.rs
laurentketterle-hub Aug 5, 2026
264e792
fix: add missing closing brace in test_volume_fee_tier_is_admin_only …
laurentketterle-hub Aug 5, 2026
eac1059
fix: remove unused 'window' variable in check_deposit_lock causing CI…
laurentketterle-hub Aug 5, 2026
1b2f410
fix: resolve compilation errors - fix create_project calls and test f…
laurentketterle-hub Aug 5, 2026
91f4d17
fix: close unclosed test + restore array-body test case in notificati…
laurentketterle-hub Aug 5, 2026
c6e0db2
fix: CI failures - prettier formatting + suppress unnameable_test_ite…
laurentketterle-hub Aug 5, 2026
225231d
fix(ci): resolve Rust compilation errors - close impl block, fix modu…
noreply Aug 7, 2026
d6017e9
fix: remove stray closing brace in investment_vault/src/lib.rs
noreply Aug 7, 2026
b6496ba
fix: resolve compilation errors and docs drift
noreply Aug 7, 2026
53a672a
fix: rustfmt formatting in test.rs
noreply Aug 7, 2026
a9720fd
docs: add missing project_registry and investment_vault events to EVE…
laurentketterle-hub Aug 2, 2026
83f0b93
docs: catalog all events in EVENTS.md + test snapshots + formatting c…
laurentketterle-hub Aug 5, 2026
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
2 changes: 1 addition & 1 deletion .github/workflows/benchmarks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0 # need history to compare against baseline
fetch-depth: 0 # need history to compare against baseline

- name: Install Rust
uses: dtolnay/rust-toolchain@stable
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/changelog.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: Changelog
on:
push:
tags:
- 'v*'
- "v*"
workflow_dispatch:
inputs:
tag:
Expand All @@ -24,12 +24,12 @@ jobs:
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0 # need full history for conventional-changelog
fetch-depth: 0 # need full history for conventional-changelog

- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: '20'
node-version: "20"

- name: Install conventional-changelog-cli
run: npm install -g conventional-changelog-cli conventional-recommended-bump conventional-changelog-angular
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -275,4 +275,3 @@ jobs:
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
id: deployment
uses: actions/deploy-pages@v4

27 changes: 22 additions & 5 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ The `ProjectRegistry` contract manages project lifecycle, certification, reputat
}
```
- **`CertificationStatus`**: `None`, `Pending`, `Certified`, `Revoked`.
- **`Proposal`**:
- **`Proposal`**:
```rust
pub struct Proposal {
pub description: String,
Expand All @@ -37,9 +37,11 @@ The `ProjectRegistry` contract manages project lifecycle, certification, reputat
### Key Functions

#### `create_project(env: Env, creator: Address, uri: String, maturity_date: u64) -> u32`
Creates a new project.

Creates a new project.

- **Auth**: `creator` must authorize.
- **Parameters**:
- **Parameters**:
- `creator`: Project owner. Must be whitelisted.
- `uri`: Project metadata URI.
- `maturity_date`: Future Unix timestamp (0 for open-ended).
Expand All @@ -48,26 +50,34 @@ Creates a new project.
```javascript
const tx = await contract.invoke({
method: "create_project",
args: [creator, "https://example.com/project1", 0]
args: [creator, "https://example.com/project1", 0],
});
```

#### `get_project(env: Env, id: u32) -> ProjectData`

Returns the state of a project.

- **Errors**: `ProjectNotFound`.

#### `update_impact_score(env: Env, project_id: u32, credit_quality: u32, green_impact: u32)`

Updates the impact score (admin only).

- **Auth**: Admin.
- **Errors**: `ProjectNotFound`.

#### `deposit_collateral(env: Env, project_id: u32, depositor: Address, token: Address, amount: i128)`

Deposits collateral for a project.

- **Auth**: `depositor`.
- **Errors**: `ProjectNotFound`, `AmountMustBePositive`.

#### `certify_project(env: Env, project_id: u32, status: CertificationStatus)`

Updates a project's certification status.

- **Auth**: Admin.
- **Errors**: `ProjectNotFound`.

Expand All @@ -92,28 +102,35 @@ The `InvestmentVault` contract handles funding projects, claiming yields, and wi
### Key Functions

#### `deposit(env: Env, caller: Address, amount: i128)`

Deposits underlying tokens into the vault and mints shares.

- **Auth**: `caller`.
- **Parameters**: `amount` to deposit.
- **Errors**: `VaultIsPaused`, `AmountMustBePositive`, `VaultCapExceeded`.
- **Example Usage**:
```javascript
const tx = await vault.invoke({
method: "deposit",
args: [caller, 100000000] // 10 tokens with 7 decimals
args: [caller, 100000000], // 10 tokens with 7 decimals
});
```

#### `withdraw(env: Env, caller: Address, share_amount: i128)`

Burns shares and returns underlying tokens.

- **Auth**: `caller`.
- **Errors**: `VaultIsPaused`, `AmountMustBePositive`, `InsufficientShares`.

#### `fund_project(env: Env, project_id: u32, amount: i128)`

Funds a registered project (admin only).

- **Auth**: Admin.
- **Errors**: `VaultIsPaused`, `InsufficientVaultFunds`, `ProjectNotCertified`.

## General Considerations & Panics

- All base token values have 7 decimal places unless noted.
- Contract will panic on arithmetic overflow or if SDK constraints are violated.
163 changes: 83 additions & 80 deletions CONTRACTS.md

Large diffs are not rendered by default.

27 changes: 15 additions & 12 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ Browse [open issues](https://github.com/heliobond/contracts/issues). Issues tagg

### Prerequisites

| Tool | Version | Install |
|------|---------|---------|
| Rust | stable (≥ 1.78) | `rustup update stable` |
| wasm target | `wasm32v1-none` | `rustup target add wasm32v1-none` |
| Stellar CLI | ≥ 26.1.0 | [docs.stellar.org/tools/cli](https://developers.stellar.org/docs/tools/cli) |
| Tool | Version | Install |
| ----------- | --------------- | --------------------------------------------------------------------------- |
| Rust | stable (≥ 1.78) | `rustup update stable` |
| wasm target | `wasm32v1-none` | `rustup target add wasm32v1-none` |
| Stellar CLI | ≥ 26.1.0 | [docs.stellar.org/tools/cli](https://developers.stellar.org/docs/tools/cli) |

```bash
# Clone and verify the setup
Expand Down Expand Up @@ -67,6 +67,7 @@ We use [Conventional Commits](https://www.conventionalcommits.org/). The changel
**Scopes:** `investment_vault`, `project_registry`, `ci`, `adr` (or omit for cross-cutting)

Examples:

```
feat(investment_vault): add MAX_DEPOSIT cap to prevent overflow
fix(project_registry): guard u32 counter against overflow at u32::MAX
Expand All @@ -78,7 +79,8 @@ ci: add WASM size budget check to CI

`CHANGELOG.md` is generated automatically, not written by hand — do not edit it in your PR. [`.github/workflows/changelog.yml`](.github/workflows/changelog.yml) runs on every `v*` tag push (or manually via `workflow_dispatch`), scans commit history with `conventional-changelog` (Angular preset), regenerates `CHANGELOG.md`, and publishes it as the GitHub release notes.

This means your changelog entry *is* your commit message, so it has to follow the format above correctly:
This means your changelog entry _is_ your commit message, so it has to follow the format above correctly:

- `feat:` commits bump the minor version and appear under "Features"
- `fix:` (and anything else conventional-changelog treats as a fix) bumps patch and appears under "Bug Fixes"
- A `BREAKING CHANGE:` footer, or a `!` after the type/scope (e.g. `feat(investment_vault)!: ...`), bumps major
Expand All @@ -101,16 +103,17 @@ project_registry/src/test.rs ← registry tests

### What to test

| Change type | Minimum tests required |
|-------------|----------------------|
| New function | Happy path + at least one error case |
| Bug fix | Regression test that would have caught the original bug |
| Change type | Minimum tests required |
| -------------------------------------- | -------------------------------------------------------- |
| New function | Happy path + at least one error case |
| Bug fix | Regression test that would have caught the original bug |
| Edge case guard (overflow, zero, etc.) | Test that triggers the guard and asserts the panic/error |
| Math / share calculations | Rounding test + extreme value test |
| Math / share calculations | Rounding test + extreme value test |

### Money paths are sacred

Anything touching `deposit`, `withdraw`, `fund_project`, or share math needs tests for:

- First deposit into an empty vault
- Vault with non-zero assets and shares
- Rounding direction (truncation should favour the vault, never the user)
Expand All @@ -131,7 +134,7 @@ cargo test --all -- --nocapture # see println! output
- **No `std`** — contracts are `#![no_std]`. Do not add `std`-dependent crates.
- **No panics in library paths** — panics in `#[contractimpl]` are fine (they become Soroban errors); panics inside utility functions called from tests are not.
- **Events for every state change** — every mutation must emit a Soroban event so the indexer can reconstruct state. See `events.rs` in each contract.
- **Comments on non-obvious decisions** — explain *why*, not *what*. Reference the issue number for workarounds (`// #112: cap prevents i128 overflow in share calc`).
- **Comments on non-obvious decisions** — explain _why_, not _what_. Reference the issue number for workarounds (`// #112: cap prevents i128 overflow in share calc`).

---

Expand Down
2 changes: 1 addition & 1 deletion DEPLOYMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,6 @@ Both calls should return `1`.
- `stored_state_version` returns `0` after an upgrade: call `migrate_state --from_version 0` as the contract owner, then re-run verification.
- `deploy/testnet.json` has empty `project_registry` / `investment_vault` fields: this is the checked-in template's starting state before any deploy has run. Don't script against it directly — use the contract IDs printed by `make deploy-testnet` or the Deploy workflow's own output/summary; the manifest is only guaranteed current for `testnet` after a successful GitHub Actions deploy.
- `HASH MISMATCH` from `scripts/check_deploy_wasm_hash.py` or the Deploy workflow's "Verify on-chain WASM hash" step: the on-chain contract doesn't match the locally built WASM recorded in the manifest. Confirm the deploy you expect actually landed on the contract ID in `deploy/testnet.json`; if the on-chain upgrade was intentional, re-run `python3 scripts/check_deploy_wasm_hash.py update deploy/testnet.json project_registry=<wasm> investment_vault=<wasm>` to resync the manifest.
- Deploy workflow fails immediately in the `deploy` job with an auth/signing error: the `STELLAR_SECRET_KEY` secret must be configured on the GitHub **Environment** matching the chosen `network` input (`testnet` or `mainnet`), not just as a repo-level secret — check *SettingsEnvironments*.
- Deploy workflow fails immediately in the `deploy` job with an auth/signing error: the `STELLAR_SECRET_KEY` secret must be configured on the GitHub **Environment** matching the chosen `network` input (`testnet` or `mainnet`), not just as a repo-level secret — check _SettingsEnvironments_.
- `deploy/testnet.json`'s `"network"` field still says `"testnet"` after a mainnet deploy: only `network: testnet` runs trigger the "Update deploy manifest" step (`if: github.event.inputs.network == 'testnet'` in `deploy.yml`); mainnet contract IDs are not written back to this file and must be tracked separately.
- Deploy succeeded but `deploy/testnet.json` wasn't updated in git: the workflow's "Commit updated deploy manifest" step pushes directly to `main` — if branch protection blocks direct pushes, that step fails silently after a working deploy. Manually apply the same `project_registry` / `investment_vault` / `*_wasm_hash` updates and commit as a follow-up.
128 changes: 128 additions & 0 deletions EVENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,57 +7,185 @@ Each event lists the public [`INTERFACE.md`](INTERFACE.md) function(s) that emit
## Project Registry Events

### `project_created`

- **Topics**: `["project", "created"]`
- **Data**: `(project_id: u32, creator: Address)`
- **Description**: Emitted when a new project is created in the registry.
- **Emitted by**: [`create_project`](INTERFACE.md#projectregistry)

### `score_changed`

- **Topics**: `["score_changed", project_id: u32]`
- **Data** (Map, keyed by field name): `{old_credit_quality: u32, new_credit_quality: u32, old_green_impact: u32, new_green_impact: u32, old_rate_bps: u32, new_rate_bps: u32}`
- **Description**: Emitted when a project's impact scores and corresponding interest rate are updated.
- **Emitted by**: [`update_impact_score`](INTERFACE.md#projectregistry) / [`update_impact_score_approved`](INTERFACE.md#projectregistry) (both scores), [`update_credit_quality_score`](INTERFACE.md#projectregistry) (credit quality only)

### `project_updated`

- **Topics**: `["project", "updated"]`
- **Data**: `(project_id: u32, credit_quality: u32, green_impact: u32)`
- **Description**: Emitted when the oracle updates a project's credit-quality / green-impact scores (#6).
- **Emitted by**: [`update_impact_score`](INTERFACE.md#projectregistry)

### `rate_updated`

- **Topics**: `["project", "rate_updated"]`
- **Data**: `(project_id: u32, rate_bps: u32)`
- **Description**: Emitted when a project's interest rate is recalculated (#129).
- **Emitted by**: [`recalculate_rate`](INTERFACE.md#projectregistry)

### `project_archived`

- **Topics**: `["project", "archived"]`
- **Data**: `(project_id: u32)`
- **Description**: Emitted when a project is archived.
- **Emitted by**: [`archive_project`](INTERFACE.md#projectregistry)

### `project_deleted`

- **Topics**: `["project", "deleted"]`
- **Data**: `(project_id: u32)`
- **Description**: Emitted when a project is completely deleted.
- **Emitted by**: [`delete_project`](INTERFACE.md#projectregistry)

### `project_compacted`

- **Topics**: `["project", "compacted"]`
- **Data**: `(project_id: u32)`
- **Description**: Emitted when a project's storage footprint is reduced.
- **Emitted by**: [`compact_archive`](INTERFACE.md#projectregistry)

### `collateral_deposited`

- **Topics**: `["project", "collateral_deposited"]`
- **Data**: `(project_id: u32, token: Address, depositor: Address, amount: i128)`
- **Description**: Emitted when collateral is added for a project.
- **Emitted by**: [`deposit_collateral`](INTERFACE.md#projectregistry)

### `collateral_released`

- **Topics**: `["project", "collateral_released"]`
- **Data**: `(project_id: u32, token: Address, receiver: Address, amount: i128)`
- **Description**: Emitted when collateral is returned to the project owner.
- **Emitted by**: [`release_collateral`](INTERFACE.md#projectregistry)

### `collateral_liquidated`

- **Topics**: `["project", "collateral_liquidated"]`
- **Data**: `(project_id: u32, token: Address, recipient: Address, amount: i128)`
- **Description**: Emitted when collateral is liquidated by the admin (#128).
- **Emitted by**: [`liquidate_collateral`](INTERFACE.md#projectregistry)

### `whitelist_set`

- **Topics**: `["project", "whitelist_set"]`
- **Data**: `(account: Address, status: bool)`
- **Description**: Emitted when an account's whitelist status is changed.
- **Emitted by**: [`set_whitelist`](INTERFACE.md#projectregistry)

### `project_certified`

- **Topics**: `["project", "certified"]`
- **Data**: `(project_id: u32, status: CertificationStatus)`
- **Description**: Emitted when a project's certification status is updated (#130).
- **Emitted by**: [`certify_project`](INTERFACE.md#projectregistry)

### `proposal_created`

- **Topics**: `["governance", "proposal_created"]`
- **Data**: `(proposal_id: u32, proposer: Address, voting_ends_at: u64)`
- **Description**: Emitted when a governance proposal is created (#134).
- **Emitted by**: [`create_proposal`](INTERFACE.md#projectregistry)

### `vote_cast`

- **Topics**: `["governance", "vote_cast"]`
- **Data**: `(proposal_id: u32, voter: Address, support: bool, weight: i128)`
- **Description**: Emitted when a vote is cast on a proposal (#134).
- **Emitted by**: [`cast_vote`](INTERFACE.md#projectregistry)

### `proposal_executed`

- **Topics**: `["governance", "proposal_executed"]`
- **Data**: `(proposal_id: u32, passed: bool)`
- **Description**: Emitted when a proposal is finalised (#134).
- **Emitted by**: [`execute_proposal`](INTERFACE.md#projectregistry)

## Investment Vault Events

### `deposit`

- **Topics**: `["vault", "deposit"]`
- **Data**: `(from: Address, usdc_amount: i128, shares_minted: i128)`
- **Description**: Emitted when an investor deposits USDC and receives vault shares.
- **Emitted by**: [`deposit`](INTERFACE.md#investmentvault)

### `withdraw`

- **Topics**: `["vault", "withdraw"]`
- **Data**: `(from: Address, shares_burned: i128, usdc_returned: i128)`
- **Description**: Emitted when an investor burns shares and withdraws USDC.
- **Emitted by**: [`withdraw`](INTERFACE.md#investmentvault)

### `withdraw_queued`

- **Topics**: `["vault", "withdraw_queued"]`
- **Data**: `(from: Address, shares_burned: i128, usdc_owed: i128)`
- **Description**: Emitted when a withdrawal is queued because liquid USDC is insufficient (#3). Shares are burned immediately; USDC will be paid when claim() is called.
- **Emitted by**: [`queue_withdrawal`](INTERFACE.md#investmentvault)

### `withdraw_claimed`

- **Topics**: `["vault", "withdraw_claimed"]`
- **Data**: `(to: Address, usdc_paid: i128, claim_index: u64)`
- **Description**: Emitted when a queued redemption claim is settled by claim() (#3).
- **Emitted by**: [`claim`](INTERFACE.md#investmentvault)

### `project_funded`

- **Topics**: `["vault", "project_funded"]`
- **Data**: `(project_id: u32, amount: i128, recipient: Address)`
- **Description**: Emitted when the vault transfers USDC from the vault to a project's owner.
- **Emitted by**: [`fund_project`](INTERFACE.md#investmentvault), [`fund_project_with_approvals`](INTERFACE.md#investmentvault), [`batch_fund_projects`](INTERFACE.md#investmentvault)

### `yield_received`

- **Topics**: `["vault", "yield_received"]`
- **Data**: `(from: Address, amount: i128)`
- **Description**: Emitted when yield repayment USDC is received from a project and folded into the yield-per-share accumulator for later claims.
- **Emitted by**: [`receive_yield`](INTERFACE.md#investmentvault)

### `yield_claimed`

- **Topics**: `["vault", "yield_claimed"]`
- **Data**: `(to: Address, amount: i128)`
- **Description**: Emitted when a shareholder claims accumulated yield (#125).
- **Emitted by**: [`claim_yield`](INTERFACE.md#investmentvault)

### `insurance_claimed`

- **Topics**: `["vault", "insurance_claimed"]`
- **Data**: `(project_id: u32, recipient: Address, amount: i128)`
- **Description**: Emitted when an insurance payout is made for a defaulted project (#135).
- **Emitted by**: [`claim_insurance`](INTERFACE.md#investmentvault)

### `paused`

- **Topics**: `["vault", "paused"]`
- **Data**: `()` (no data)
- **Description**: Emitted when the vault is paused (emergency stop).
- **Emitted by**: [`pause`](INTERFACE.md#investmentvault)

### `unpaused`

- **Topics**: `["vault", "unpaused"]`
- **Data**: `()` (no data)
- **Description**: Emitted when the vault is unpaused.
- **Emitted by**: [`unpause`](INTERFACE.md#investmentvault)

### `emergency_admin_changed`

- **Topics**: `["vault", "emergency_admin_changed"]`
- **Data**: `(new_emergency_admin: Option<Address>)`
- **Description**: Emitted when the admin sets or clears the emergency-admin address (#43).
- **Emitted by**: [`set_emergency_admin`](INTERFACE.md#investmentvault)
Loading
Loading