From efc910cb2a3c654486796df0a13b3b36587a190b Mon Sep 17 00:00:00 2001 From: laurentketterle-hub Date: Sun, 2 Aug 2026 08:31:48 +0200 Subject: [PATCH 1/2] docs: add missing project_registry and investment_vault events to EVENTS.md (Closes #330) --- EVENTS.md | 102 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/EVENTS.md b/EVENTS.md index 6b895bb..d14234d 100644 --- a/EVENTS.md +++ b/EVENTS.md @@ -18,6 +18,18 @@ Each event lists the public [`INTERFACE.md`](INTERFACE.md) function(s) that emit - **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)` @@ -48,8 +60,68 @@ Each event lists the public [`INTERFACE.md`](INTERFACE.md) function(s) that emit - **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)` @@ -61,3 +133,33 @@ Each event lists the public [`INTERFACE.md`](INTERFACE.md) function(s) that emit - **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
)` +- **Description**: Emitted when the admin sets or clears the emergency-admin address (#43). +- **Emitted by**: [`set_emergency_admin`](INTERFACE.md#investmentvault) From b2060831c0407f8c0e1c9cc7194e111e3edea7e3 Mon Sep 17 00:00:00 2001 From: laurentketterle-hub Date: Wed, 5 Aug 2026 11:15:11 +0200 Subject: [PATCH 2/2] docs: catalog all events in EVENTS.md + test snapshots + formatting consistency (Closes #330) --- .github/workflows/benchmarks.yml | 2 +- .github/workflows/changelog.yml | 6 +- .github/workflows/ci.yml | 1 - API.md | 27 +- CONTRACTS.md | 163 +++++----- CONTRIBUTING.md | 27 +- DEPLOYMENT.md | 2 +- EVENTS.md | 26 ++ INTERFACE.md | 266 +++++++-------- MIGRATION.md | 15 +- README.md | 153 ++++----- SECURITY.md | 7 +- adr/001-soroban-platform.md | 2 + adr/002-storage-patterns.md | 14 +- adr/003-share-vault-model.md | 2 + adr/004-security-model.md | 5 + adr/README.md | 12 +- docs/GOVERNANCE.md | 87 ++--- docs/INTEGRATION.md | 224 +++++++------ docs/NOTIFICATIONS.md | 100 +++--- docs/STORAGE.md | 302 +++++++++--------- gas-budgets.json | 49 ++- ...posit_vs_equivalent_single_deposits.1.json | 2 +- ...posit_vs_equivalent_single_deposits.2.json | 2 +- ...ch_vault_batch_deposit_two_accounts.1.json | 2 +- .../test/bench_vault_deposit.1.json | 2 +- ...r_functions_reject_non_admin_caller.1.json | 2 +- ...tch_deposit_mints_for_each_investor.1.json | 2 +- ...all_events_and_state_on_later_panic.1.json | 2 +- .../test/test_claim_queued_emits_event.1.json | 2 +- ...est_claim_settles_queued_redemption.1.json | 2 +- ...age_removes_zero_project_investment.1.json | 2 +- ...oncurrent_deposits_and_fund_project.1.json | 2 +- ...ructor_panics_with_invalid_registry.1.json | 2 +- ...st_conversion_empty_vault_is_1_to_1.1.json | 2 +- ...rsion_roundtrip_first_deposit_exact.1.json | 2 +- ...n_roundtrip_never_favors_withdrawer.1.json | 2 +- ...vert_to_shares_and_assets_roundtrip.1.json | 2 +- .../test_deposit_at_minimum_succeeds.1.json | 2 +- .../test_deposit_below_minimum_panics.1.json | 2 +- ...t_deposit_blocked_when_vault_paused.1.json | 2 +- .../test/test_deposit_emits_event.1.json | 2 +- ...st_deposit_proportional_after_first.1.json | 2 +- ...jects_when_exceeding_max_hbs_supply.1.json | 2 +- ...can_pause_and_unpause_without_owner.1.json | 2 +- ...y_pause_rejects_non_emergency_admin.1.json | 2 +- .../test/test_enable_secondary_trading.1.json | 2 +- ...nable_secondary_trading_emits_event.1.json | 2 +- .../test/test_fee_above_cap_panics.1.json | 2 +- ...t_first_deposit_mints_1_to_1_shares.1.json | 2 +- ..._flash_loan_fails_without_repayment.1.json | 2 +- ..._blocked_by_outstanding_investments.1.json | 2 +- ...full_withdrawal_with_no_investments.1.json | 2 +- ...project_allowed_when_thresholds_met.1.json | 2 +- ...ject_blocked_below_credit_threshold.1.json | 2 +- ...oject_blocked_below_green_threshold.1.json | 2 +- .../test_fund_project_cost_estimate.1.json | 2 +- .../test/test_fund_project_emits_event.1.json | 2 +- ...anics_when_amount_exceeds_available.1.json | 2 +- ..._project_panics_when_fully_depleted.1.json | 2 +- ...panics_with_out_of_range_project_id.1.json | 2 +- ...project_panics_with_zero_project_id.1.json | 2 +- ...nd_project_partial_funding_succeeds.1.json | 2 +- ...est_fund_project_records_investment.1.json | 2 +- ...und_project_rejects_paused_registry.1.json | 2 +- ...oject_rejects_self_funding_by_admin.1.json | 2 +- ..._call_exhausts_remaining_deployable.1.json | 2 +- ...ject_succeeds_with_valid_project_id.1.json | 2 +- ..._funding_thresholds_default_to_zero.1.json | 2 +- ...bs_token_info_after_trading_enabled.1.json | 2 +- ...s_token_info_before_trading_enabled.1.json | 2 +- ...roject_investment_zero_for_unfunded.1.json | 2 +- ...t_registry_returns_initial_registry.1.json | 2 +- ...tion_withdrawal_emits_warning_event.1.json | 2 +- .../test/test_initialize.1.json | 2 +- ...test_management_fee_set_emits_event.1.json | 2 +- .../test_multisig_batch_fund_projects.1.json | 2 +- ...ects_insufficient_funding_approvals.1.json | 2 +- .../test/test_nonzero_fee_accrual.1.json | 2 +- ...test_set_and_get_funding_thresholds.1.json | 2 +- ...et_funding_thresholds_is_admin_only.1.json | 2 +- .../test_set_registry_is_admin_only.1.json | 2 +- .../test_set_registry_updates_registry.1.json | 2 +- ..._set_registry_validates_new_address.1.json | 2 +- ...ed_emitter_persists_and_emits_event.1.json | 2 +- .../test_total_assets_after_deposit.1.json | 2 +- .../test_trading_disabled_by_default.1.json | 2 +- ...test_transfer_ownership_emits_event.1.json | 2 +- ..._transfer_to_vault_address_rejected.1.json | 2 +- ...nd_registry_reference_initial_state.1.json | 2 +- .../test_vault_deposit_cost_estimate.1.json | 2 +- ...ault_is_paused_getter_default_false.1.json | 2 +- ...migrate_state_rejects_wrong_version.1.json | 2 +- .../test/test_vault_pause_and_unpause.1.json | 2 +- ..._stored_version_blocks_normal_calls.1.json | 2 +- .../test/test_vault_state_version.1.json | 2 +- .../test/test_vault_with_zero_supply.1.json | 2 +- .../test_withdraw_at_minimum_succeeds.1.json | 2 +- .../test_withdraw_below_minimum_panics.1.json | 2 +- .../test/test_withdraw_emits_event.1.json | 2 +- ...nqueues_when_insufficient_liquidity.1.json | 2 +- ...thdraw_fails_when_all_usdc_deployed.1.json | 2 +- .../test_withdraw_queued_emits_event.1.json | 2 +- .../test/test_withdraw_returns_usdc.1.json | 2 +- ...st_withdraw_with_zero_supply_panics.1.json | 2 +- ...ithdrawal_rate_limiting_next_ledger.1.json | 2 +- ...ithdrawal_rate_limiting_same_ledger.1.json | 2 +- ...rawal_rate_limiting_transfer_locked.1.json | 2 +- .../test/test_zero_fee_parity.1.json | 2 +- ...edge_cases_are_handled_consistently.1.json | 2 +- ...aw_and_admin_flow_via_compiled_wasm.1.json | 2 +- notification-service/src/api.ts | 22 +- notification-service/src/config.test.ts | 4 +- notification-service/src/listener.test.ts | 25 +- notification-service/src/notifier.test.ts | 12 +- package-lock.json | 6 + ...h_registry_create_and_score_project.1.json | 2 +- .../test_full_heliobond_flow.1.json | 2 +- ...r_functions_reject_non_admin_caller.1.json | 2 +- .../test/test_certify_project.1.json | 2 +- .../test_certify_project_emits_event.1.json | 2 +- ...act_storage_removes_zero_collateral.1.json | 2 +- ..._create_project_blocked_when_paused.1.json | 2 +- ...e_project_by_non_whitelisted_panics.1.json | 7 +- ...eate_project_by_whitelisted_address.1.json | 2 +- .../test_create_project_emits_event.1.json | 2 +- ...t_create_project_records_created_at.1.json | 2 +- ...uality_score_changes_rate_correctly.1.json | 2 +- ...edit_quality_score_history_recorded.1.json | 2 +- .../test_deposit_and_get_collateral.1.json | 2 +- ...can_pause_and_unpause_without_owner.1.json | 2 +- ...y_pause_rejects_non_emergency_admin.1.json | 2 +- ...ng_limit_bps_scales_with_reputation.1.json | 2 +- .../test/test_get_all_projects.1.json | 2 +- ...ge_limit_larger_than_total_projects.1.json | 2 +- ...eturns_stable_ordering_across_pages.1.json | 2 +- ...jects_page_zero_limit_returns_empty.1.json | 2 +- ...et_score_history_nonexistent_panics.1.json | 7 +- ...elister_returns_initial_whitelister.1.json | 7 +- .../test/test_getters_work_when_paused.1.json | 2 +- ...itialize_sets_admin_and_whitelister.1.json | 9 +- .../test/test_interest_rate_mid_scores.1.json | 2 +- ...rest_rate_perfect_scores_is_minimum.1.json | 2 +- ...erest_rate_zero_scores_is_base_rate.1.json | 2 +- .../test_liquidate_collateral_by_admin.1.json | 2 +- .../test/test_maturity_date_is_mature.1.json | 2 +- ...grate_state_noop_on_current_version.1.json | 2 +- ...te_state_rejects_wrong_from_version.1.json | 7 +- ...st_multiple_creators_sequential_ids.1.json | 2 +- ...ltisig_update_impact_score_approved.1.json | 2 +- ...core_rejects_insufficient_approvals.1.json | 2 +- ...t_new_whitelister_can_set_whitelist.1.json | 2 +- ...non_owner_cannot_deposit_collateral.1.json | 2 +- .../test/test_owner_can_set_reputation.1.json | 2 +- ...ructor_deployment_and_initial_state.1.json | 2 +- ...ent_cost_estimate_and_initial_state.1.json | 2 +- .../test_registry_pause_and_unpause.1.json | 2 +- ...t_release_collateral_after_maturity.1.json | 2 +- .../test_reputation_above_100_panics.1.json | 7 +- .../test_reputation_can_be_updated.1.json | 2 +- .../test_reputation_defaults_to_zero.1.json | 7 +- ...d_event_contains_old_and_new_values.1.json | 2 +- ...re_history_multiple_updates_ordered.1.json | 2 +- ..._score_history_noop_does_not_append.1.json | 2 +- ...ore_history_records_entry_on_update.1.json | 2 +- .../test/test_sequential_project_ids.1.json | 2 +- .../test/test_set_and_get_reputation.1.json | 2 +- ..._set_creator_reputation_emits_event.1.json | 2 +- .../test_set_whitelist_emits_event.1.json | 2 +- .../test_set_whitelister_is_admin_only.1.json | 8 +- ...set_whitelister_updates_whitelister.1.json | 2 +- ..._stored_version_blocks_normal_calls.1.json | 2 +- .../test_state_version_matches_stored.1.json | 8 +- ...test_transfer_ownership_emits_event.1.json | 2 +- ...orized_caller_cannot_set_reputation.1.json | 7 +- ...quality_independent_of_green_impact.1.json | 2 +- ...redit_quality_score_boundary_values.1.json | 2 +- ...quality_score_noop_identical_values.1.json | 2 +- ...t_quality_score_out_of_range_panics.1.json | 2 +- ...update_credit_quality_score_success.1.json | 2 +- .../test/test_update_impact_score.1.json | 2 +- ...te_impact_score_blocked_when_paused.1.json | 2 +- ...update_impact_score_boundary_values.1.json | 2 +- ...est_update_impact_score_emits_event.1.json | 2 +- ...e_exceeds_100_panics_credit_quality.1.json | 2 +- ...ore_exceeds_100_panics_green_impact.1.json | 2 +- ...pdate_impact_score_max_value_panics.1.json | 2 +- ...ct_score_nonexistent_project_panics.1.json | 7 +- ..._impact_score_noop_identical_values.1.json | 2 +- .../test_update_score_non_admin_panics.1.json | 2 +- .../test_uri_above_max_length_panics.1.json | 2 +- .../test_uri_below_min_length_panics.1.json | 2 +- ...est_uri_exactly_max_length_accepted.1.json | 2 +- ...est_uri_exactly_min_length_accepted.1.json | 2 +- ...ial_characters_and_unicode_accepted.1.json | 2 +- ...metadata_hash_matches_recorded_hash.1.json | 2 +- ...fy_and_admin_flow_via_compiled_wasm.1.json | 2 +- 197 files changed, 1033 insertions(+), 927 deletions(-) create mode 100644 package-lock.json diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index d304451..7b574a5 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -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 diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index 460901d..ba9c66e 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -3,7 +3,7 @@ name: Changelog on: push: tags: - - 'v*' + - "v*" workflow_dispatch: inputs: tag: @@ -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 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c11d1b7..ba831d9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -275,4 +275,3 @@ jobs: if: github.ref == 'refs/heads/main' && github.event_name == 'push' id: deployment uses: actions/deploy-pages@v4 - diff --git a/API.md b/API.md index 0e36b38..bb9f208 100644 --- a/API.md +++ b/API.md @@ -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, @@ -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). @@ -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`. @@ -92,7 +102,9 @@ 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`. @@ -100,20 +112,25 @@ Deposits underlying tokens into the vault and mints shares. ```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. diff --git a/CONTRACTS.md b/CONTRACTS.md index 3776e2d..a5614dc 100644 --- a/CONTRACTS.md +++ b/CONTRACTS.md @@ -9,25 +9,25 @@ All functions live in the `InvestmentVault` or `ProjectRegistry` crates. Domain terms used throughout this document and the rest of the `contracts` docs. -| Term | Definition | -|---|---| -| **Credit quality score** | Oracle-set score (0–100) on a project's `ProjectData.credit_quality`, reflecting the creditworthiness of the underlying bond. Feeds into the project's interest rate via `compute_rate`; updated with `update_impact_score` or `update_credit_quality_score`. | -| **Green impact score** | Oracle-set score (0–100) on a project's `ProjectData.green_impact`, reflecting the environmental/climate benefit of the project. Feeds into the interest rate alongside credit quality and into `calculate_carbon_credits`; updated with `update_impact_score`. | -| **HBS token** | "Heliobond Shares" — the SEP-41 fungible token minted by `InvestmentVault` to represent an investor's proportional claim on the pooled USDC. Minted on `deposit`, burned on `withdraw`; see [Secondary Market Trading](#secondary-market-trading-issue-126) below. | -| **Whitelister** | The address authorised to grant or revoke project-creation rights via `set_whitelist`. A separate role from the contract owner/admin. | -| **Certification status** | `ProjectData.certification_status` (`None`, `Pending`, `Certified`, `Revoked`) — an independent attestation of a project's legitimacy, set by the whitelister or admin via `certify_project`. Distinct from the numeric credit/green scores. | -| **Maturity date** | Unix timestamp on `ProjectData.maturity_date` after which a project is considered mature (`is_mature`). `0` means open-ended (never matures). `release_collateral` requires maturity when one is set; `compact_archive` does not check it directly — it requires the project to already be archived instead. | -| **Interest rate (bps)** | The annualized rate, in basis points (10,000 bps = 100%), that `get_interest_rate` derives from a project's credit quality and green impact scores via `compute_rate`. | -| **Insurance premium / insurance fund** | A fixed 50 bps (`INSURANCE_PREMIUM_BPS`) cut of every vault deposit that accumulates in the vault's insurance fund. Paid out via `claim_insurance` to compensate investors when a project defaults. | -| **Management fee** | An optional, admin-configured fee (in bps, capped at `MAX_MANAGEMENT_FEE_BPS` = 500) deducted from each deposit before shares are minted, sent to a configured recipient via `set_management_fee`. | -| **Yield-per-share accumulator** | The vault's global, monotonically increasing `YieldPerShareAccum` value (scaled by `YIELD_SCALE`), used with each investor's last-claim checkpoint (`YieldDebt`) to compute claimable yield in O(1) without iterating investors. | -| **Multi-sig admin** | An optional `(signers, threshold)` configuration (`set_multisig_admin`) that requires `threshold` distinct signer approvals for critical operations instead of a single owner signature. `threshold = 0` disables it. | -| **Collateral** | Tokens deposited against a specific project (`deposit_collateral`) as security, released to the owner at maturity (`release_collateral`) or seized by the admin on default (`liquidate_collateral`). | -| **Archive / compaction** | Two-step lifecycle for retiring a project's storage footprint: `archive_project` flags a project inactive, and `compact_archive` later replaces its full `ProjectData` with a much smaller `ArchiveSummary` to reduce ongoing rent. | -| **Governance proposal** | A time-boxed on-chain vote (`create_proposal`, `cast_vote`, `execute_proposal`) that HBS holders use to approve or reject a described action; passes if `votes_for > votes_against` once voting closes. | -| **Carbon credits** | Units calculated from a project's green impact score and funding amount (`calculate_carbon_credits`), issuable to an address (`issue_carbon_credits`) and transferable independently of HBS or USDC balances. | -| **Bridge transfer** | Cross-chain movement of HBS value via a Wormhole-style message: `initiate_bridge_transfer` burns HBS and emits a VAA-verifiable message; `complete_bridge_transfer` verifies the VAA against trusted emitters and mints HBS on the destination side. | -| **State version / migration** | `STATE_VERSION` is the storage schema version a given contract build supports; `stored_state_version()` is what's actually persisted on-chain. `migrate_state` upgrades storage from an older version. See [MIGRATION.md](MIGRATION.md). | +| Term | Definition | +| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| **Credit quality score** | Oracle-set score (0–100) on a project's `ProjectData.credit_quality`, reflecting the creditworthiness of the underlying bond. Feeds into the project's interest rate via `compute_rate`; updated with `update_impact_score` or `update_credit_quality_score`. | +| **Green impact score** | Oracle-set score (0–100) on a project's `ProjectData.green_impact`, reflecting the environmental/climate benefit of the project. Feeds into the interest rate alongside credit quality and into `calculate_carbon_credits`; updated with `update_impact_score`. | +| **HBS token** | "Heliobond Shares" — the SEP-41 fungible token minted by `InvestmentVault` to represent an investor's proportional claim on the pooled USDC. Minted on `deposit`, burned on `withdraw`; see [Secondary Market Trading](#secondary-market-trading-issue-126) below. | +| **Whitelister** | The address authorised to grant or revoke project-creation rights via `set_whitelist`. A separate role from the contract owner/admin. | +| **Certification status** | `ProjectData.certification_status` (`None`, `Pending`, `Certified`, `Revoked`) — an independent attestation of a project's legitimacy, set by the whitelister or admin via `certify_project`. Distinct from the numeric credit/green scores. | +| **Maturity date** | Unix timestamp on `ProjectData.maturity_date` after which a project is considered mature (`is_mature`). `0` means open-ended (never matures). `release_collateral` requires maturity when one is set; `compact_archive` does not check it directly — it requires the project to already be archived instead. | +| **Interest rate (bps)** | The annualized rate, in basis points (10,000 bps = 100%), that `get_interest_rate` derives from a project's credit quality and green impact scores via `compute_rate`. | +| **Insurance premium / insurance fund** | A fixed 50 bps (`INSURANCE_PREMIUM_BPS`) cut of every vault deposit that accumulates in the vault's insurance fund. Paid out via `claim_insurance` to compensate investors when a project defaults. | +| **Management fee** | An optional, admin-configured fee (in bps, capped at `MAX_MANAGEMENT_FEE_BPS` = 500) deducted from each deposit before shares are minted, sent to a configured recipient via `set_management_fee`. | +| **Yield-per-share accumulator** | The vault's global, monotonically increasing `YieldPerShareAccum` value (scaled by `YIELD_SCALE`), used with each investor's last-claim checkpoint (`YieldDebt`) to compute claimable yield in O(1) without iterating investors. | +| **Multi-sig admin** | An optional `(signers, threshold)` configuration (`set_multisig_admin`) that requires `threshold` distinct signer approvals for critical operations instead of a single owner signature. `threshold = 0` disables it. | +| **Collateral** | Tokens deposited against a specific project (`deposit_collateral`) as security, released to the owner at maturity (`release_collateral`) or seized by the admin on default (`liquidate_collateral`). | +| **Archive / compaction** | Two-step lifecycle for retiring a project's storage footprint: `archive_project` flags a project inactive, and `compact_archive` later replaces its full `ProjectData` with a much smaller `ArchiveSummary` to reduce ongoing rent. | +| **Governance proposal** | A time-boxed on-chain vote (`create_proposal`, `cast_vote`, `execute_proposal`) that HBS holders use to approve or reject a described action; passes if `votes_for > votes_against` once voting closes. | +| **Carbon credits** | Units calculated from a project's green impact score and funding amount (`calculate_carbon_credits`), issuable to an address (`issue_carbon_credits`) and transferable independently of HBS or USDC balances. | +| **Bridge transfer** | Cross-chain movement of HBS value via a Wormhole-style message: `initiate_bridge_transfer` burns HBS and emits a VAA-verifiable message; `complete_bridge_transfer` verifies the VAA against trusted emitters and mints HBS on the destination side. | +| **State version / migration** | `STATE_VERSION` is the storage schema version a given contract build supports; `stored_state_version()` is what's actually persisted on-chain. `migrate_state` upgrades storage from an older version. See [MIGRATION.md](MIGRATION.md). | --- @@ -38,21 +38,21 @@ Constructor args: `admin: Address, whitelister: Address` ### Public Functions -| Function | Auth | Args | Returns | Events | -|---|---|---|---|---| -| `set_whitelist(account, status)` | whitelister | `account: Address, status: bool` | `()` | `WhitelistSet { account, status }` | -| `create_project(creator, uri, maturity_date)` | creator (whitelisted) | `creator: Address, uri: String, maturity_date: u64` | `u32` (project\_id) | `ProjectCreated { project_id, owner }` | -| `get_project(id)` | none | `id: u32` | `ProjectData` | — | -| `total_projects()` | none | — | `u32` | — | -| `get_all_projects()` | none | — | `Vec<(u32, ProjectData)>` | — | -| `update_impact_score(project_id, credit_quality, green_impact)` | admin (owner) | `project_id: u32, credit_quality: u32, green_impact: u32` | `()` | `ProjectUpdated`, `RateUpdated`, `ScoreChanged` | -| `update_credit_quality_score(project_id, credit_quality)` | admin (owner) | `project_id: u32, credit_quality: u32` (0–100) | `()` | `ScoreChanged` | -| `certify_project(caller, project_id, status)` | whitelister or admin | `caller: Address, project_id: u32, status: CertificationStatus` | `()` | `ProjectCertified { project_id, status }` | -| `is_mature(project_id)` | none | `project_id: u32` | `bool` | — | -| `create_proposal(proposer, description, voting_duration_secs)` | proposer | `proposer: Address, description: String, voting_duration_secs: u64` (≥ 86400) | `u32` (proposal\_id) | `ProposalCreated { proposal_id, proposer, voting_ends_at }` | -| `cast_vote(voter, proposal_id, support, weight)` | voter | `voter: Address, proposal_id: u32, support: bool, weight: i128` | `()` | `VoteCast { proposal_id, voter, support, weight }` | -| `execute_proposal(proposal_id)` | none | `proposal_id: u32` | `bool` (passed) | `ProposalExecuted { proposal_id, passed }` | -| `get_proposal(proposal_id)` | none | `proposal_id: u32` | `Proposal` | — | +| Function | Auth | Args | Returns | Events | +| --------------------------------------------------------------- | --------------------- | ----------------------------------------------------------------------------- | ------------------------- | ----------------------------------------------------------- | +| `set_whitelist(account, status)` | whitelister | `account: Address, status: bool` | `()` | `WhitelistSet { account, status }` | +| `create_project(creator, uri, maturity_date)` | creator (whitelisted) | `creator: Address, uri: String, maturity_date: u64` | `u32` (project\_id) | `ProjectCreated { project_id, owner }` | +| `get_project(id)` | none | `id: u32` | `ProjectData` | — | +| `total_projects()` | none | — | `u32` | — | +| `get_all_projects()` | none | — | `Vec<(u32, ProjectData)>` | — | +| `update_impact_score(project_id, credit_quality, green_impact)` | admin (owner) | `project_id: u32, credit_quality: u32, green_impact: u32` | `()` | `ProjectUpdated`, `RateUpdated`, `ScoreChanged` | +| `update_credit_quality_score(project_id, credit_quality)` | admin (owner) | `project_id: u32, credit_quality: u32` (0–100) | `()` | `ScoreChanged` | +| `certify_project(caller, project_id, status)` | whitelister or admin | `caller: Address, project_id: u32, status: CertificationStatus` | `()` | `ProjectCertified { project_id, status }` | +| `is_mature(project_id)` | none | `project_id: u32` | `bool` | — | +| `create_proposal(proposer, description, voting_duration_secs)` | proposer | `proposer: Address, description: String, voting_duration_secs: u64` (≥ 86400) | `u32` (proposal\_id) | `ProposalCreated { proposal_id, proposer, voting_ends_at }` | +| `cast_vote(voter, proposal_id, support, weight)` | voter | `voter: Address, proposal_id: u32, support: bool, weight: i128` | `()` | `VoteCast { proposal_id, voter, support, weight }` | +| `execute_proposal(proposal_id)` | none | `proposal_id: u32` | `bool` (passed) | `ProposalExecuted { proposal_id, passed }` | +| `get_proposal(proposal_id)` | none | `proposal_id: u32` | `Proposal` | — | ### Types @@ -80,10 +80,10 @@ pub struct Proposal { ### Score Functions Comparison -| Function | Scope | Emitted Events | -|---|---|---| -| `update_impact_score` | Sets both `credit_quality` AND `green_impact` atomically | `ProjectUpdated`, `RateUpdated`, `ScoreChanged` | -| `update_credit_quality_score` | Sets only `credit_quality`, leaves `green_impact` unchanged | `ScoreChanged` | +| Function | Scope | Emitted Events | +| ----------------------------- | ----------------------------------------------------------- | ----------------------------------------------- | +| `update_impact_score` | Sets both `credit_quality` AND `green_impact` atomically | `ProjectUpdated`, `RateUpdated`, `ScoreChanged` | +| `update_credit_quality_score` | Sets only `credit_quality`, leaves `green_impact` unchanged | `ScoreChanged` | The `ScoreChanged` event (#131) includes both old and new score values plus old and new interest rates, enabling off-chain notification services to calculate the exact delta without querying historical state. @@ -97,23 +97,24 @@ Token: HBS (Heliobond Shares) — SEP-41 fungible token via `FungibleToken` trai ### Constants -| Name | Value | Purpose | -|---|---|---| -| `MAX_DEPOSIT` | 1 billion USDC (7 dp) | Single-deposit ceiling | -| `INSURANCE_PREMIUM_BPS` | 50 | 0.5% of each deposit reserved for insurance fund | -| `MAX_MANAGEMENT_FEE_BPS` | 500 | 5% hard cap on admin-set management fee | -| `YIELD_SCALE` | 1e18 | Precision for yield-per-share accumulator | +| Name | Value | Purpose | +| ------------------------ | --------------------- | ------------------------------------------------ | +| `MAX_DEPOSIT` | 1 billion USDC (7 dp) | Single-deposit ceiling | +| `INSURANCE_PREMIUM_BPS` | 50 | 0.5% of each deposit reserved for insurance fund | +| `MAX_MANAGEMENT_FEE_BPS` | 500 | 5% hard cap on admin-set management fee | +| `YIELD_SCALE` | 1e18 | Precision for yield-per-share accumulator | ### Public Functions #### Core Deposit / Withdraw -| Function | Auth | Args | Returns | Events | -|---|---|---|---|---| -| `deposit(from, usdc_amount)` | from | `from: Address, usdc_amount: i128` (≤ MAX\_DEPOSIT) | `i128` (shares minted) | `Deposit { from, usdc_amount, shares_minted }` | -| `withdraw(from, shares_amount)` | from (via `burn`) | `from: Address, shares_amount: i128` | `i128` (USDC returned) | `Withdraw { from, shares_burned, usdc_returned }` | +| Function | Auth | Args | Returns | Events | +| ------------------------------- | ----------------- | --------------------------------------------------- | ---------------------- | ------------------------------------------------- | +| `deposit(from, usdc_amount)` | from | `from: Address, usdc_amount: i128` (≤ MAX\_DEPOSIT) | `i128` (shares minted) | `Deposit { from, usdc_amount, shares_minted }` | +| `withdraw(from, shares_amount)` | from (via `burn`) | `from: Address, shares_amount: i128` | `i128` (USDC returned) | `Withdraw { from, shares_burned, usdc_returned }` | **Deposit fee deduction order:** + 1. `insurance_premium = usdc_amount × 50 / 10_000` 2. `management_fee = usdc_amount × fee_bps / 10_000` 3. `investable = usdc_amount − insurance_premium − management_fee` @@ -121,43 +122,43 @@ Token: HBS (Heliobond Shares) — SEP-41 fungible token via `FungibleToken` trai #### Project Funding -| Function | Auth | Args | Returns | Events | -|---|---|---|---|---| -| `fund_project(project_id, amount)` | admin | `project_id: u32, amount: i128` | `()` | `ProjectFunded { project_id, amount, recipient }` | +| Function | Auth | Args | Returns | Events | +| ---------------------------------- | ----- | ------------------------------- | ------- | ------------------------------------------------- | +| `fund_project(project_id, amount)` | admin | `project_id: u32, amount: i128` | `()` | `ProjectFunded { project_id, amount, recipient }` | The insurance reserve is subtracted from available USDC before the check, preventing the admin from accidentally funding projects with insurance money. #### NAV Helpers -| Function | Auth | Args | Returns | -|---|---|---|---| -| `total_assets()` | none | — | `i128` (total USDC value) | -| `convert_to_shares(usdc_amount)` | none | `usdc_amount: i128` | `i128` | -| `convert_to_assets(shares_amount)` | none | `shares_amount: i128` | `i128` | -| `get_expected_returns()` | none | — | `i128` | +| Function | Auth | Args | Returns | +| ---------------------------------- | ---- | --------------------- | ------------------------- | +| `total_assets()` | none | — | `i128` (total USDC value) | +| `convert_to_shares(usdc_amount)` | none | `usdc_amount: i128` | `i128` | +| `convert_to_assets(shares_amount)` | none | `shares_amount: i128` | `i128` | +| `get_expected_returns()` | none | — | `i128` | #### Yield Distribution -| Function | Auth | Args | Returns | Events | -|---|---|---|---|---| -| `receive_yield(from, amount)` | admin | `from: Address, amount: i128` | `()` | `YieldReceived { from, amount }` | -| `claimable_yield(account)` | none | `account: Address` | `i128` | — | -| `claim_yield(from)` | from | `from: Address` | `i128` | `YieldClaimed { to, amount }` | -| `get_portfolio(account)` | none | `account: Address` | `PortfolioInfo` | — | +| Function | Auth | Args | Returns | Events | +| ----------------------------- | ----- | ----------------------------- | --------------- | -------------------------------- | +| `receive_yield(from, amount)` | admin | `from: Address, amount: i128` | `()` | `YieldReceived { from, amount }` | +| `claimable_yield(account)` | none | `account: Address` | `i128` | — | +| `claim_yield(from)` | from | `from: Address` | `i128` | `YieldClaimed { to, amount }` | +| `get_portfolio(account)` | none | `account: Address` | `PortfolioInfo` | — | #### Insurance Fund -| Function | Auth | Args | Returns | Events | -|---|---|---|---|---| -| `insurance_fund_balance()` | none | — | `i128` | — | -| `claim_insurance(project_id, recipient, amount)` | admin | `project_id: u32, recipient: Address, amount: i128` | `()` | `InsuranceClaimed { project_id, recipient, amount }` | +| Function | Auth | Args | Returns | Events | +| ------------------------------------------------ | ----- | --------------------------------------------------- | ------- | ---------------------------------------------------- | +| `insurance_fund_balance()` | none | — | `i128` | — | +| `claim_insurance(project_id, recipient, amount)` | admin | `project_id: u32, recipient: Address, amount: i128` | `()` | `InsuranceClaimed { project_id, recipient, amount }` | #### Management Fee (issue #7) -| Function | Auth | Args | Returns | Events | -|---|---|---|---|---| -| `set_management_fee(fee_bps, recipient)` | admin | `fee_bps: u32` (≤ 500), `recipient: Address` | `()` | `ManagementFeeSet { recipient, fee_bps }` | -| `get_management_fee_bps()` | none | — | `u32` | — | +| Function | Auth | Args | Returns | Events | +| ---------------------------------------- | ----- | -------------------------------------------- | ------- | ----------------------------------------- | +| `set_management_fee(fee_bps, recipient)` | admin | `fee_bps: u32` (≤ 500), `recipient: Address` | `()` | `ManagementFeeSet { recipient, fee_bps }` | +| `get_management_fee_bps()` | none | — | `u32` | — | The fee is `0` by default. Passing `fee_bps = 0` disables it. The hard cap of 500 bps (5%) is enforced on-chain and cannot be overridden. @@ -165,11 +166,11 @@ The fee is `0` by default. Passing `fee_bps = 0` disables it. The hard cap of 50 HBS is a SEP-41 fungible token and is natively tradeable on the Stellar DEX. These functions surface the official listing status so UIs and aggregators can discover the trading pair. -| Function | Auth | Args | Returns | Events | -|---|---|---|---|---| -| `enable_secondary_trading()` | admin | — | `()` | `TradingEnabled { enabled: true }` | -| `is_trading_enabled()` | none | — | `bool` | — | -| `get_hbs_token_info()` | none | — | `HBSTokenInfo` | — | +| Function | Auth | Args | Returns | Events | +| ---------------------------- | ----- | ---- | -------------- | ---------------------------------- | +| `enable_secondary_trading()` | admin | — | `()` | `TradingEnabled { enabled: true }` | +| `is_trading_enabled()` | none | — | `bool` | — | +| `get_hbs_token_info()` | none | — | `HBSTokenInfo` | — | ```rust pub struct HBSTokenInfo { @@ -181,6 +182,7 @@ pub struct HBSTokenInfo { ``` **DEX integration notes:** + - HBS contract ID (the vault address) is the SEP-41 asset identifier on Stellar - To list on Stellar DEX, create an offer using the Stellar SDK: `ManageOfferOp` or `PathPaymentOp` using the vault contract address as the asset code - Liquidity pools can be created via `ChangeTrustOp` against the HBS/USDC pair @@ -188,9 +190,9 @@ pub struct HBSTokenInfo { #### Misc -| Function | Auth | Args | Returns | -|---|---|---|---| -| `accepted_asset()` | none | — | `Address` (USDC SAC) | +| Function | Auth | Args | Returns | +| ------------------ | ---- | ---- | -------------------- | +| `accepted_asset()` | none | — | `Address` (USDC SAC) | ### Types @@ -289,4 +291,5 @@ hash (keep every uploaded hash recorded, see `deploy/testnet.json` and `scripts/check_deploy_wasm_hash.py`). State written under the new version may not be readable by the old WASM if the storage layout changed, so this is not a true undo. - - Emits `Withdraw` + +- Emits `Withdraw` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7f714bb..b5fca53 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 @@ -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 @@ -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 @@ -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) @@ -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`). --- diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 97a5648..d2fb556 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -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= investment_vault=` 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 *Settings → Environments*. +- 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 _Settings → Environments_. - `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. diff --git a/EVENTS.md b/EVENTS.md index d14234d..30241c8 100644 --- a/EVENTS.md +++ b/EVENTS.md @@ -7,90 +7,105 @@ 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). @@ -99,66 +114,77 @@ Each event lists the public [`INTERFACE.md`](INTERFACE.md) function(s) that emit ## 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
)` - **Description**: Emitted when the admin sets or clears the emergency-admin address (#43). diff --git a/INTERFACE.md b/INTERFACE.md index 26bb94b..18a23a9 100644 --- a/INTERFACE.md +++ b/INTERFACE.md @@ -26,13 +26,13 @@ invocation. Multi-sig errors: -| Error | Meaning | -| --- | --- | +| Error | Meaning | +| -------------------------- | ------------------------------------------------------------------ | | `InvalidMultiSigThreshold` | Threshold is 0, exceeds signer count, or stored config is invalid. | -| `TooManyMultiSigSigners` | More than 10 signers were supplied. | -| `NotMultiSigSigner` | An approval address is not in the configured signer set. | -| `DuplicateApproval` | The same signer appears more than once. | -| `InsufficientApprovals` | Fewer approvals than the configured threshold were supplied. | +| `TooManyMultiSigSigners` | More than 10 signers were supplied. | +| `NotMultiSigSigner` | An approval address is not in the configured signer set. | +| `DuplicateApproval` | The same signer appears more than once. | +| `InsufficientApprovals` | Fewer approvals than the configured threshold were supplied. | ## ProjectRegistry @@ -54,58 +54,58 @@ Multi-sig errors: ### Functions -| Function | Auth | Returns | Errors / Notes | -| --- | --- | --- | --- | -| `__constructor(admin: Address, whitelister: Address)` | none | none | Sets owner, whitelister, counters. | -| `set_whitelist(account: Address, status: bool)` | `whitelister` | none | Grants or revokes project creation rights. | -| `create_project(creator: Address, uri: String, maturity_date: u64, metadata_hash: BytesN<32>)` | `creator` | `u32` | Requires whitelist, URI length 8..512, future maturity when nonzero. `metadata_hash` is the content hash of the off-chain `uri` payload (#44). | -| `get_project(id: u32)` | none | `ProjectData` | `ProjectNotFound`. | -| `total_projects()` | none | `u32` | Highest assigned project id. | -| `verify_metadata_hash(project_id: u32, candidate_hash: BytesN<32>)` | none | `bool` | True if `candidate_hash` matches the hash recorded at creation (#44). | -| `update_impact_score(project_id: u32, credit_quality: u32, green_impact: u32)` | owner, or disabled when multi-sig is enabled | none | Scores 0..100. Use approval variant after enabling multi-sig. | -| `update_impact_score_approved(project_id: u32, credit_quality: u32, green_impact: u32, approvals: Vec
)` | multi-sig signers | none | Critical operation. | -| `update_credit_quality_score(project_id: u32, credit_quality: u32)` | owner, or disabled when multi-sig is enabled | none | Updates credit score only. No multi-sig-approved variant currently exists. | -| `get_score_history(project_id: u32)` | none | `Vec` | Chronological ring buffer of past score updates (#123). | -| `certify_project(caller: Address, project_id: u32, status: CertificationStatus)` | `caller` | none | Caller must be whitelister or owner. | -| `is_mature(project_id: u32)` | none | `bool` | False for open-ended projects. | -| `get_all_projects()` | none | `Vec<(u32, ProjectData)>` | O(n) over registered ids. | -| `get_projects_page(offset: u32, limit: u32)` | none | `Vec<(u32, ProjectData)>` | Non-archived projects with ID > `offset`, up to `limit` entries; stable ordering when paged with no writes in between (#269). | -| `create_proposal(proposer: Address, description: String, voting_duration_secs: u64)` | `proposer` | `u32` | Duration must be at least 86,400 seconds. | -| `cast_vote(voter: Address, proposal_id: u32, support: bool, weight: i128)` | `voter` | none | Weight must be positive; callers must supply verified HBS balance. | -| `execute_proposal(proposal_id: u32)` | none | `bool` | Callable after voting ends. | -| `get_proposal(proposal_id: u32)` | none | `Proposal` | `ProposalNotFound`. | -| `deposit_collateral(project_id: u32, depositor: Address, token: Address, amount: i128)` | `depositor` | none | Depositor must be project owner; amount positive. | -| `get_collateral(project_id: u32, token: Address)` | none | `i128` | Returns 0 if absent. | -| `release_collateral(project_id: u32, caller: Address, token: Address)` | `caller` | none | Caller must be project owner; project must be mature when maturity exists. | -| `liquidate_collateral(project_id: u32, token: Address, recipient: Address)` | owner, or disabled when multi-sig is enabled | none | Critical operation. | -| `liquidate_collateral_approved(project_id: u32, token: Address, recipient: Address, approvals: Vec
)` | multi-sig signers | none | Critical operation. | -| `set_multisig_admin(signers: Vec
, threshold: u32)` | owner | none | Configures 1..10 unique signers. | -| `clear_multisig_admin()` | owner | none | Restores owner-only critical operations. | -| `get_multisig_admin()` | none | `(Vec
, u32)` | Returns signers and threshold. | -| `get_interest_rate(project_id: u32)` | none | `u32` | Annualized bps from project scores. | -| `set_creator_reputation(caller: Address, creator: Address, score: u32)` | `caller` | none | Caller must be whitelister or owner; score 0..100. | -| `get_creator_reputation(creator: Address)` | none | `u32` | Defaults to 0. | -| `get_creator_funding_limit_bps(creator: Address)` | none | `u32` | Reputation-derived suggested limit. | -| `set_whitelister(new_whitelister: Address)` | owner | none | Replaces whitelister. | -| `get_whitelister()` | none | `Address` | Current whitelister. | -| `archive_project(project_id: u32)` | owner | none | Marks a project archived; excluded from `get_all_projects` by default (#26). | -| `delete_project(project_id: u32)` | owner | none | Rejects deletion when the project has active investments. | -| `get_all_projects_with_archived()` | none | `Vec<(u32, ProjectData)>` | Like `get_all_projects` but includes archived projects. | -| `compact_archive(project_id: u32)` | owner | none | Replaces a full `ProjectData` with a minimal `ArchiveSummary` (#73). Project must already be archived. | -| `get_archive_summary(project_id: u32)` | none | `ArchiveSummary` | Panics if the project hasn't been compacted. | -| `compact_storage(project_ids: Vec, tokens: Vec
)` | owner | `u32` | Removes zero-value collateral storage entries; returns count removed. | -| `pause()` | owner | none | Blocks state-mutating operations; getters remain available (#72). | -| `unpause()` | owner | none | Reverses `pause()`. | -| `is_paused()` | none | `bool` | Circuit-breaker status. | -| `set_emergency_admin(emergency_admin: Option
)` | owner | none | Configures (or clears) an address that can call `emergency_pause`/`emergency_unpause` without full owner privileges (#43). | -| `get_emergency_admin()` | none | `Option
` | Currently configured emergency admin, if any. | -| `emergency_pause(caller: Address)` | `caller` must be the configured emergency admin | none | Pauses without owner auth (#43). | -| `emergency_unpause(caller: Address)` | `caller` must be the configured emergency admin | none | Unpauses without owner auth (#43). | -| `health_check()` | none | `HealthStatus` | Consolidated status snapshot for monitoring integrations (#77). | -| `state_version()` | none | `u32` | Schema version supported by this contract build. | -| `stored_state_version()` | none | `u32` | Schema version recorded in instance storage; 0 for unversioned deployments. | -| `migrate_state(from_version: u32)` | owner | `u32` | Migrates storage from `from_version` to the current schema version. | -| `upgrade(new_wasm_hash: BytesN<32>)` | owner | none | Deploys new contract code at the current address. | +| Function | Auth | Returns | Errors / Notes | +| ---------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | +| `__constructor(admin: Address, whitelister: Address)` | none | none | Sets owner, whitelister, counters. | +| `set_whitelist(account: Address, status: bool)` | `whitelister` | none | Grants or revokes project creation rights. | +| `create_project(creator: Address, uri: String, maturity_date: u64, metadata_hash: BytesN<32>)` | `creator` | `u32` | Requires whitelist, URI length 8..512, future maturity when nonzero. `metadata_hash` is the content hash of the off-chain `uri` payload (#44). | +| `get_project(id: u32)` | none | `ProjectData` | `ProjectNotFound`. | +| `total_projects()` | none | `u32` | Highest assigned project id. | +| `verify_metadata_hash(project_id: u32, candidate_hash: BytesN<32>)` | none | `bool` | True if `candidate_hash` matches the hash recorded at creation (#44). | +| `update_impact_score(project_id: u32, credit_quality: u32, green_impact: u32)` | owner, or disabled when multi-sig is enabled | none | Scores 0..100. Use approval variant after enabling multi-sig. | +| `update_impact_score_approved(project_id: u32, credit_quality: u32, green_impact: u32, approvals: Vec
)` | multi-sig signers | none | Critical operation. | +| `update_credit_quality_score(project_id: u32, credit_quality: u32)` | owner, or disabled when multi-sig is enabled | none | Updates credit score only. No multi-sig-approved variant currently exists. | +| `get_score_history(project_id: u32)` | none | `Vec` | Chronological ring buffer of past score updates (#123). | +| `certify_project(caller: Address, project_id: u32, status: CertificationStatus)` | `caller` | none | Caller must be whitelister or owner. | +| `is_mature(project_id: u32)` | none | `bool` | False for open-ended projects. | +| `get_all_projects()` | none | `Vec<(u32, ProjectData)>` | O(n) over registered ids. | +| `get_projects_page(offset: u32, limit: u32)` | none | `Vec<(u32, ProjectData)>` | Non-archived projects with ID > `offset`, up to `limit` entries; stable ordering when paged with no writes in between (#269). | +| `create_proposal(proposer: Address, description: String, voting_duration_secs: u64)` | `proposer` | `u32` | Duration must be at least 86,400 seconds. | +| `cast_vote(voter: Address, proposal_id: u32, support: bool, weight: i128)` | `voter` | none | Weight must be positive; callers must supply verified HBS balance. | +| `execute_proposal(proposal_id: u32)` | none | `bool` | Callable after voting ends. | +| `get_proposal(proposal_id: u32)` | none | `Proposal` | `ProposalNotFound`. | +| `deposit_collateral(project_id: u32, depositor: Address, token: Address, amount: i128)` | `depositor` | none | Depositor must be project owner; amount positive. | +| `get_collateral(project_id: u32, token: Address)` | none | `i128` | Returns 0 if absent. | +| `release_collateral(project_id: u32, caller: Address, token: Address)` | `caller` | none | Caller must be project owner; project must be mature when maturity exists. | +| `liquidate_collateral(project_id: u32, token: Address, recipient: Address)` | owner, or disabled when multi-sig is enabled | none | Critical operation. | +| `liquidate_collateral_approved(project_id: u32, token: Address, recipient: Address, approvals: Vec
)` | multi-sig signers | none | Critical operation. | +| `set_multisig_admin(signers: Vec
, threshold: u32)` | owner | none | Configures 1..10 unique signers. | +| `clear_multisig_admin()` | owner | none | Restores owner-only critical operations. | +| `get_multisig_admin()` | none | `(Vec
, u32)` | Returns signers and threshold. | +| `get_interest_rate(project_id: u32)` | none | `u32` | Annualized bps from project scores. | +| `set_creator_reputation(caller: Address, creator: Address, score: u32)` | `caller` | none | Caller must be whitelister or owner; score 0..100. | +| `get_creator_reputation(creator: Address)` | none | `u32` | Defaults to 0. | +| `get_creator_funding_limit_bps(creator: Address)` | none | `u32` | Reputation-derived suggested limit. | +| `set_whitelister(new_whitelister: Address)` | owner | none | Replaces whitelister. | +| `get_whitelister()` | none | `Address` | Current whitelister. | +| `archive_project(project_id: u32)` | owner | none | Marks a project archived; excluded from `get_all_projects` by default (#26). | +| `delete_project(project_id: u32)` | owner | none | Rejects deletion when the project has active investments. | +| `get_all_projects_with_archived()` | none | `Vec<(u32, ProjectData)>` | Like `get_all_projects` but includes archived projects. | +| `compact_archive(project_id: u32)` | owner | none | Replaces a full `ProjectData` with a minimal `ArchiveSummary` (#73). Project must already be archived. | +| `get_archive_summary(project_id: u32)` | none | `ArchiveSummary` | Panics if the project hasn't been compacted. | +| `compact_storage(project_ids: Vec, tokens: Vec
)` | owner | `u32` | Removes zero-value collateral storage entries; returns count removed. | +| `pause()` | owner | none | Blocks state-mutating operations; getters remain available (#72). | +| `unpause()` | owner | none | Reverses `pause()`. | +| `is_paused()` | none | `bool` | Circuit-breaker status. | +| `set_emergency_admin(emergency_admin: Option
)` | owner | none | Configures (or clears) an address that can call `emergency_pause`/`emergency_unpause` without full owner privileges (#43). | +| `get_emergency_admin()` | none | `Option
` | Currently configured emergency admin, if any. | +| `emergency_pause(caller: Address)` | `caller` must be the configured emergency admin | none | Pauses without owner auth (#43). | +| `emergency_unpause(caller: Address)` | `caller` must be the configured emergency admin | none | Unpauses without owner auth (#43). | +| `health_check()` | none | `HealthStatus` | Consolidated status snapshot for monitoring integrations (#77). | +| `state_version()` | none | `u32` | Schema version supported by this contract build. | +| `stored_state_version()` | none | `u32` | Schema version recorded in instance storage; 0 for unversioned deployments. | +| `migrate_state(from_version: u32)` | owner | `u32` | Migrates storage from `from_version` to the current schema version. | +| `upgrade(new_wasm_hash: BytesN<32>)` | owner | none | Deploys new contract code at the current address. | ## InvestmentVault @@ -124,81 +124,81 @@ Compliance/reporting types: `ComplianceEventData`, `ReportingSnapshotData`, ### Functions -| Function | Auth | Returns | Errors / Notes | -| --- | --- | --- | --- | -| `__constructor(admin: Address, usdc_sac: Address, registry: Address)` | none | none | Validates registry via `total_projects()`, sets HBS metadata. | -| `deposit(from: Address, usdc_amount: i128)` | `from` | `i128` | Transfers USDC, deducts insurance premium and optional fee, mints shares. | -| `batch_deposit(deposits: Vec<(Address, i128)>)` | each depositor | `Vec` | Runs multiple deposits in order; keep batches small enough for Soroban resource limits. | -| `withdraw(from: Address, shares_amount: i128)` | `from` via burn | `i128` | Burns shares; may enqueue if liquid USDC is insufficient. | -| `claim()` | none | `i128` | Settles queued withdrawals FIFO. | -| `fund_project(project_id: u32, amount: i128)` | owner, or disabled when multi-sig is enabled | none | Critical operation; checks score thresholds and insurance reserve. | -| `fund_project_with_approvals(project_id: u32, amount: i128, approvals: Vec
)` | multi-sig signers | none | Critical operation. | -| `batch_fund_projects(fundings: Vec<(u32, i128)>, approvals: Vec
)` | owner when multi-sig disabled, otherwise multi-sig signers | none | Common batch funding path. | -| `receive_yield(from: Address, amount: i128)` | owner, or disabled when multi-sig is enabled | none | Transfers repayment USDC and updates yield accumulator. No multi-sig-approved variant currently exists. | -| `claim_yield(from: Address)` | `from` | `i128` | Pays accrued yield when liquid. | -| `get_project_investment(project_id: u32)` | none | `i128` | Cumulative USDC funded into `project_id`; 0 if never funded. | -| `max_hbs_supply()` | none | `i128` | Hard cap on total HBS share supply enforced by `deposit` (#20). | -| `claim_insurance(project_id: u32, recipient: Address, amount: i128)` | owner, or disabled when multi-sig is enabled | none | Critical operation; one claim per project. | -| `claim_insurance_with_approvals(project_id: u32, recipient: Address, amount: i128, approvals: Vec
)` | multi-sig signers | none | Critical operation. | -| `set_multisig_admin(signers: Vec
, threshold: u32)` | owner | none | Configures 1..10 unique signers. | -| `get_multisig_admin()` | none | `(Vec
, u32)` | Returns signers and threshold. InvestmentVault has no `clear_multisig_admin()`. | -| `get_expected_returns()` | none | `i128` | O(n) over registry projects. | -| `total_assets()` | none | `i128` | Liquid USDC + investments + expected returns. | -| `convert_to_shares(usdc_amount: i128)` | none | `i128` | ERC-4626-style conversion. | -| `convert_to_assets(shares_amount: i128)` | none | `i128` | ERC-4626-style conversion. | -| `get_utilization_bps()` | none | `u32` | Investments over liquid plus investments. | -| `claimable_yield(account: Address)` | none | `i128` | View-only accrued yield. | -| `get_portfolio(account: Address)` | none | `PortfolioInfo` | Investor analytics snapshot. | -| `insurance_fund_balance()` | none | `i128` | Stored insurance reserve. | -| `accepted_asset()` | none | `Address` | USDC SAC address. | -| `set_management_fee(fee_bps: u32, recipient: Address)` | owner | none | Fee capped at 500 bps. | -| `get_management_fee_bps()` | none | `u32` | Defaults to 0. | -| `enable_secondary_trading()` | owner | none | Sets HBS trading flag. | -| `is_trading_enabled()` | none | `bool` | Trading flag. | -| `set_funding_thresholds(min_credit_quality: u32, min_green_impact: u32)` | owner | none | Scores must be 0..100. | -| `get_min_credit_quality()` | none | `u32` | Defaults to 0. | -| `get_min_green_impact()` | none | `u32` | Defaults to 0. | -| `set_registry(new_registry: Address)` | owner | none | Validates registry and replaces dependency. | -| `get_registry()` | none | `Address` | Registry address. | -| `get_hbs_token_info()` | none | `HBSTokenInfo` | HBS metadata and trading flag. | -| `set_bridge(bridge: Address)` | owner | none | Configures bridge minter. | -| `bridge_mint(to: Address, amount: i128)` | bridge | none | Amount positive. | -| `bridge_burn(from: Address, amount: i128)` | `from` | none | Amount positive. | -| `set_wormhole_core(core: Address)` | owner | none | Configures Wormhole core. | -| `set_trusted_emitter(chain_id: u32, emitter_address: BytesN<32>, trusted: bool)` | owner | none | Updates trusted emitter map. | -| `initiate_bridge_transfer(from: Address, amount: i128, target_chain: u32, recipient: BytesN<32>, nonce: u64)` | `from` | `u64` | Burns HBS and publishes Wormhole message. | -| `complete_bridge_transfer(vaa: Bytes)` | none | none | Verifies VAA, trusted emitter, replay guard, mints HBS. | -| `set_flash_loan_fee(fee_bps: i128)` | owner | none | 0..1000 bps. | -| `flash_loan_fee()` | none | `i128` | Defaults to 30 bps. | -| `execute_flash_loan(initiator: Address, borrower: Address, amount: i128, data: Bytes)` | `initiator` | none | Calls borrower callback and collects amount plus fee. | -| `set_carbon_oracle(oracle: Address)` | owner | none | Configures oracle. | -| `set_carbon_credit_price(price: i128)` | oracle | none | Price positive. | -| `carbon_credit_price()` | none | `i128` | Defaults to 0. | -| `calculate_carbon_credits(project_id: u32, amount: i128)` | none | `CarbonCreditCalculation` | Uses project green impact. | -| `issue_carbon_credits(to: Address, project_id: u32, amount: i128)` | none | `i128` | Issues calculated credits when positive. | -| `transfer_carbon_credits(from: Address, to: Address, amount: i128)` | `from` | none | Balance must cover amount. | -| `carbon_credit_balance(address: Address)` | none | `i128` | Defaults to 0. | -| `set_max_transaction_amount(amount: i128)` | owner | none | Compliance cap; 0 disables. | -| `max_transaction_amount()` | none | `i128` | Defaults to 0. | -| `record_compliance_event(event_type: String, data: String)` | owner | none | Appends event. | -| `get_compliance_event(seq: u64)` | none | `ComplianceEventData` | Panics if missing. | -| `get_compliance_events(from: u64, to: u64)` | none | `Vec` | Inclusive range; skips missing entries. | -| `take_reporting_snapshot()` | owner | none | Captures latest reporting metrics. | -| `get_latest_snapshot()` | none | `ReportingSnapshotData` | Panics if no snapshot exists. | -| `export_regulatory_data()` | none | `RegulatoryReport` | Includes latest snapshot and up to 50 recent events. | -| `compact_storage()` | owner | `u32` | Removes zero-value `ProjectInvestment` entries; returns count removed (#88). | -| `pause()` | owner | none | Blocks state-mutating operations; getters remain available (#72). | -| `unpause()` | owner | none | Reverses `pause()`. | -| `is_paused()` | none | `bool` | Circuit-breaker status. | -| `set_emergency_admin(emergency_admin: Option
)` | owner | none | Configures (or clears) an address that can call `emergency_pause`/`emergency_unpause` without full owner privileges (#43). | -| `get_emergency_admin()` | none | `Option
` | Currently configured emergency admin, if any. | -| `emergency_pause(caller: Address)` | `caller` must be the configured emergency admin | none | Pauses without owner auth (#43). | -| `emergency_unpause(caller: Address)` | `caller` must be the configured emergency admin | none | Unpauses without owner auth (#43). | -| `health_check()` | none | `HealthStatus` | Consolidated status snapshot for monitoring integrations (#77). | -| `state_version()` | none | `u32` | Schema version supported by this contract build. | -| `stored_state_version()` | none | `u32` | Schema version recorded in instance storage; 0 for unversioned deployments. | -| `migrate_state(from_version: u32)` | owner | `u32` | Migrates storage from `from_version` to the current schema version. | -| `upgrade(new_wasm_hash: BytesN<32>)` | owner | none | Deploys new contract code at the current address. | +| Function | Auth | Returns | Errors / Notes | +| ------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- | -------------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| `__constructor(admin: Address, usdc_sac: Address, registry: Address)` | none | none | Validates registry via `total_projects()`, sets HBS metadata. | +| `deposit(from: Address, usdc_amount: i128)` | `from` | `i128` | Transfers USDC, deducts insurance premium and optional fee, mints shares. | +| `batch_deposit(deposits: Vec<(Address, i128)>)` | each depositor | `Vec` | Runs multiple deposits in order; keep batches small enough for Soroban resource limits. | +| `withdraw(from: Address, shares_amount: i128)` | `from` via burn | `i128` | Burns shares; may enqueue if liquid USDC is insufficient. | +| `claim()` | none | `i128` | Settles queued withdrawals FIFO. | +| `fund_project(project_id: u32, amount: i128)` | owner, or disabled when multi-sig is enabled | none | Critical operation; checks score thresholds and insurance reserve. | +| `fund_project_with_approvals(project_id: u32, amount: i128, approvals: Vec
)` | multi-sig signers | none | Critical operation. | +| `batch_fund_projects(fundings: Vec<(u32, i128)>, approvals: Vec
)` | owner when multi-sig disabled, otherwise multi-sig signers | none | Common batch funding path. | +| `receive_yield(from: Address, amount: i128)` | owner, or disabled when multi-sig is enabled | none | Transfers repayment USDC and updates yield accumulator. No multi-sig-approved variant currently exists. | +| `claim_yield(from: Address)` | `from` | `i128` | Pays accrued yield when liquid. | +| `get_project_investment(project_id: u32)` | none | `i128` | Cumulative USDC funded into `project_id`; 0 if never funded. | +| `max_hbs_supply()` | none | `i128` | Hard cap on total HBS share supply enforced by `deposit` (#20). | +| `claim_insurance(project_id: u32, recipient: Address, amount: i128)` | owner, or disabled when multi-sig is enabled | none | Critical operation; one claim per project. | +| `claim_insurance_with_approvals(project_id: u32, recipient: Address, amount: i128, approvals: Vec
)` | multi-sig signers | none | Critical operation. | +| `set_multisig_admin(signers: Vec
, threshold: u32)` | owner | none | Configures 1..10 unique signers. | +| `get_multisig_admin()` | none | `(Vec
, u32)` | Returns signers and threshold. InvestmentVault has no `clear_multisig_admin()`. | +| `get_expected_returns()` | none | `i128` | O(n) over registry projects. | +| `total_assets()` | none | `i128` | Liquid USDC + investments + expected returns. | +| `convert_to_shares(usdc_amount: i128)` | none | `i128` | ERC-4626-style conversion. | +| `convert_to_assets(shares_amount: i128)` | none | `i128` | ERC-4626-style conversion. | +| `get_utilization_bps()` | none | `u32` | Investments over liquid plus investments. | +| `claimable_yield(account: Address)` | none | `i128` | View-only accrued yield. | +| `get_portfolio(account: Address)` | none | `PortfolioInfo` | Investor analytics snapshot. | +| `insurance_fund_balance()` | none | `i128` | Stored insurance reserve. | +| `accepted_asset()` | none | `Address` | USDC SAC address. | +| `set_management_fee(fee_bps: u32, recipient: Address)` | owner | none | Fee capped at 500 bps. | +| `get_management_fee_bps()` | none | `u32` | Defaults to 0. | +| `enable_secondary_trading()` | owner | none | Sets HBS trading flag. | +| `is_trading_enabled()` | none | `bool` | Trading flag. | +| `set_funding_thresholds(min_credit_quality: u32, min_green_impact: u32)` | owner | none | Scores must be 0..100. | +| `get_min_credit_quality()` | none | `u32` | Defaults to 0. | +| `get_min_green_impact()` | none | `u32` | Defaults to 0. | +| `set_registry(new_registry: Address)` | owner | none | Validates registry and replaces dependency. | +| `get_registry()` | none | `Address` | Registry address. | +| `get_hbs_token_info()` | none | `HBSTokenInfo` | HBS metadata and trading flag. | +| `set_bridge(bridge: Address)` | owner | none | Configures bridge minter. | +| `bridge_mint(to: Address, amount: i128)` | bridge | none | Amount positive. | +| `bridge_burn(from: Address, amount: i128)` | `from` | none | Amount positive. | +| `set_wormhole_core(core: Address)` | owner | none | Configures Wormhole core. | +| `set_trusted_emitter(chain_id: u32, emitter_address: BytesN<32>, trusted: bool)` | owner | none | Updates trusted emitter map. | +| `initiate_bridge_transfer(from: Address, amount: i128, target_chain: u32, recipient: BytesN<32>, nonce: u64)` | `from` | `u64` | Burns HBS and publishes Wormhole message. | +| `complete_bridge_transfer(vaa: Bytes)` | none | none | Verifies VAA, trusted emitter, replay guard, mints HBS. | +| `set_flash_loan_fee(fee_bps: i128)` | owner | none | 0..1000 bps. | +| `flash_loan_fee()` | none | `i128` | Defaults to 30 bps. | +| `execute_flash_loan(initiator: Address, borrower: Address, amount: i128, data: Bytes)` | `initiator` | none | Calls borrower callback and collects amount plus fee. | +| `set_carbon_oracle(oracle: Address)` | owner | none | Configures oracle. | +| `set_carbon_credit_price(price: i128)` | oracle | none | Price positive. | +| `carbon_credit_price()` | none | `i128` | Defaults to 0. | +| `calculate_carbon_credits(project_id: u32, amount: i128)` | none | `CarbonCreditCalculation` | Uses project green impact. | +| `issue_carbon_credits(to: Address, project_id: u32, amount: i128)` | none | `i128` | Issues calculated credits when positive. | +| `transfer_carbon_credits(from: Address, to: Address, amount: i128)` | `from` | none | Balance must cover amount. | +| `carbon_credit_balance(address: Address)` | none | `i128` | Defaults to 0. | +| `set_max_transaction_amount(amount: i128)` | owner | none | Compliance cap; 0 disables. | +| `max_transaction_amount()` | none | `i128` | Defaults to 0. | +| `record_compliance_event(event_type: String, data: String)` | owner | none | Appends event. | +| `get_compliance_event(seq: u64)` | none | `ComplianceEventData` | Panics if missing. | +| `get_compliance_events(from: u64, to: u64)` | none | `Vec` | Inclusive range; skips missing entries. | +| `take_reporting_snapshot()` | owner | none | Captures latest reporting metrics. | +| `get_latest_snapshot()` | none | `ReportingSnapshotData` | Panics if no snapshot exists. | +| `export_regulatory_data()` | none | `RegulatoryReport` | Includes latest snapshot and up to 50 recent events. | +| `compact_storage()` | owner | `u32` | Removes zero-value `ProjectInvestment` entries; returns count removed (#88). | +| `pause()` | owner | none | Blocks state-mutating operations; getters remain available (#72). | +| `unpause()` | owner | none | Reverses `pause()`. | +| `is_paused()` | none | `bool` | Circuit-breaker status. | +| `set_emergency_admin(emergency_admin: Option
)` | owner | none | Configures (or clears) an address that can call `emergency_pause`/`emergency_unpause` without full owner privileges (#43). | +| `get_emergency_admin()` | none | `Option
` | Currently configured emergency admin, if any. | +| `emergency_pause(caller: Address)` | `caller` must be the configured emergency admin | none | Pauses without owner auth (#43). | +| `emergency_unpause(caller: Address)` | `caller` must be the configured emergency admin | none | Unpauses without owner auth (#43). | +| `health_check()` | none | `HealthStatus` | Consolidated status snapshot for monitoring integrations (#77). | +| `state_version()` | none | `u32` | Schema version supported by this contract build. | +| `stored_state_version()` | none | `u32` | Schema version recorded in instance storage; 0 for unversioned deployments. | +| `migrate_state(from_version: u32)` | owner | `u32` | Migrates storage from `from_version` to the current schema version. | +| `upgrade(new_wasm_hash: BytesN<32>)` | owner | none | Deploys new contract code at the current address. | ## Batch Operation Limits diff --git a/MIGRATION.md b/MIGRATION.md index 013c019..ad01f65 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -18,8 +18,8 @@ whenever a storage layout change requires a migration step. ## Storage Layout Versioning -| Key | Type | Notes | -|-----|------|-------| +| Key | Type | Notes | +| ------------------------- | ----- | --------------------------------------------------------- | | `StateVersion` (instance) | `u32` | Written at construction; read by `require_current_state`. | `require_current_state` rejects calls if the stored version does not match the @@ -34,8 +34,7 @@ introduced). 0 as compatible and does **not** reject calls in that case (0 is treated as equivalent to v1, since the v0→v1 migration involved no layout changes). `InvestmentVault`'s `require_current_state` has no such exception — it rejects -any stored version that isn't exactly the current `STATE_VERSION`, including -0. Both contracts reject any other mismatched version identically. +any stored version that isn't exactly the current `STATE_VERSION`, including 0. Both contracts reject any other mismatched version identically. ## Upgrade Procedure @@ -163,10 +162,10 @@ pub fn migrate_state(env: Env, from_version: u32) -> u32 { ## Version History -| Version | Contract | Description | -|---------|----------|-------------| -| 0 | Both | Pre-versioning deployments (treat as v1 state layout). | -| 1 | Both | Initial versioned deployment. No layout changes from v0. | +| Version | Contract | Description | +| ------- | -------- | -------------------------------------------------------- | +| 0 | Both | Pre-versioning deployments (treat as v1 state layout). | +| 1 | Both | Initial versioned deployment. No layout changes from v0. | ## Versioned Storage Patterns diff --git a/README.md b/README.md index 45dbd17..8efa3f6 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,10 @@ On-chain core of [Heliobond](https://heliobond.io) — a green bond platform built on Stellar. Two [Soroban](https://stellar.org/soroban) smart contracts manage the full lifecycle from project registration through investor deposits and capital disbursement. -| Contract | Crate | Purpose | -|---|---|---| +| Contract | Crate | Purpose | +| ----------------- | ------------------ | -------------------------------------------------------- | | `ProjectRegistry` | `project_registry` | Stores project metadata and oracle-updated impact scores | -| `InvestmentVault` | `investment_vault` | SEP-41 token vault; accepts USDC and mints HBS shares | +| `InvestmentVault` | `investment_vault` | SEP-41 token vault; accepts USDC and mints HBS shares | --- @@ -100,10 +100,10 @@ For details, see [`docs/GOVERNANCE.md`](./docs/GOVERNANCE.md). ### API & Rust Crate Documentation Rust docs are automatically generated and published via CI: -* Deployed reference: [https://BuildersWCT.github.io/contracts/](https://BuildersWCT.github.io/contracts/) -For the complete, up-to-date interface specification including all functions, parameters, and error codes, see [`INTERFACE.md`](./INTERFACE.md). +- Deployed reference: [https://BuildersWCT.github.io/contracts/](https://BuildersWCT.github.io/contracts/) +For the complete, up-to-date interface specification including all functions, parameters, and error codes, see [`INTERFACE.md`](./INTERFACE.md). ### ProjectRegistry @@ -117,23 +117,23 @@ Sets the `Ownable` owner to `admin` and records the `whitelister` address. **Public functions** -| Function | Auth required | Description | -|---|---|---| -| `set_whitelist(account, status)` | `Whitelister` | Grant or revoke whitelist status for a creator address | -| `create_project(creator, uri, maturity_date, metadata_hash)` | `creator` | Register a new project; requires whitelist; returns `project_id` | -| `get_project(id)` | none | Return `ProjectData` for a given `project_id`; panics if not found | -| `total_projects()` | none | Return the current project counter | -| `update_impact_score(project_id, credit_quality, green_impact)` | `Admin` | Set impact scores (0–100 each) for a project | -| `update_impact_score_approved(project_id, credit_quality, green_impact, approvals)` | Multi-sig signers | Multi-sig variant for critical operations | -| `update_credit_quality_score(project_id, credit_quality)` | `Admin` | Update credit score only | -| `get_projects_page(offset, limit)` | none | Paginated project listing with stable ordering | -| `get_all_projects()` | none | Return all non-archived projects | -| `certify_project(caller, project_id, status)` | Whitelister or Admin | Update project certification status | -| `create_proposal(proposer, description, voting_duration_secs)` | `proposer` | Create governance proposal | -| `cast_vote(voter, proposal_id, support, weight)` | `voter` | Vote on proposal with HBS weight | -| `transfer_ownership(new_owner)` | `Admin` | Transfer contract ownership | -| `set_multisig_admin(signers, threshold)` | `Admin` | Configure multi-sig for critical operations | -| `pause()` / `unpause()` | `Admin` | Circuit breaker for emergency pauses | +| Function | Auth required | Description | +| ----------------------------------------------------------------------------------- | -------------------- | ------------------------------------------------------------------ | +| `set_whitelist(account, status)` | `Whitelister` | Grant or revoke whitelist status for a creator address | +| `create_project(creator, uri, maturity_date, metadata_hash)` | `creator` | Register a new project; requires whitelist; returns `project_id` | +| `get_project(id)` | none | Return `ProjectData` for a given `project_id`; panics if not found | +| `total_projects()` | none | Return the current project counter | +| `update_impact_score(project_id, credit_quality, green_impact)` | `Admin` | Set impact scores (0–100 each) for a project | +| `update_impact_score_approved(project_id, credit_quality, green_impact, approvals)` | Multi-sig signers | Multi-sig variant for critical operations | +| `update_credit_quality_score(project_id, credit_quality)` | `Admin` | Update credit score only | +| `get_projects_page(offset, limit)` | none | Paginated project listing with stable ordering | +| `get_all_projects()` | none | Return all non-archived projects | +| `certify_project(caller, project_id, status)` | Whitelister or Admin | Update project certification status | +| `create_proposal(proposer, description, voting_duration_secs)` | `proposer` | Create governance proposal | +| `cast_vote(voter, proposal_id, support, weight)` | `voter` | Vote on proposal with HBS weight | +| `transfer_ownership(new_owner)` | `Admin` | Transfer contract ownership | +| `set_multisig_admin(signers, threshold)` | `Admin` | Configure multi-sig for critical operations | +| `pause()` / `unpause()` | `Admin` | Circuit breaker for emergency pauses | **ProjectData struct** @@ -166,25 +166,25 @@ Sets the `Ownable` owner to `admin`, stores USDC SAC and Registry addresses, ini **Public functions** -| Function | Auth required | Description | -|---|---|---| -| `deposit(from, usdc_amount)` | `from` | Transfer USDC from investor; mint HBS shares; return shares minted | -| `batch_deposit(deposits)` | Each depositor | Batch deposit for multiple investors | -| `withdraw(from, shares_amount)` | `from` | Burn HBS shares; enqueue if insufficient liquidity | -| `claim()` | none | Settle queued redemptions FIFO; return USDC paid out | -| `fund_project(project_id, amount)` | `Admin` | Cross-call Registry; transfer USDC to project owner | -| `fund_project_with_approvals(project_id, amount, approvals)` | Multi-sig signers | Multi-sig variant for critical operations | -| `batch_fund_projects(fundings, approvals)` | Admin or Multi-sig | Batch funding for multiple projects | -| `receive_yield(from, amount)` | `Admin` | Register interest/yield payments from projects | -| `claim_yield(from)` | `from` | Claim accrued yield when liquid | -| `total_assets()` | none | Return `liquid_USDC + investments + expected_returns` | -| `convert_to_shares(usdc_amount)` | none | Preview HBS for given USDC deposit (ERC-4626) | -| `convert_to_assets(shares_amount)` | none | Preview USDC for given HBS redemption (ERC-4626) | -| `get_expected_returns()` | none | Sum `investment × (credit_quality + green_impact) / 200` | -| `claim_insurance(project_id, recipient, amount)` | `Admin` | Authorize default insurance payouts | -| `set_multisig_admin(signers, threshold)` | `Admin` | Configure multi-sig for critical operations | -| `transfer_ownership(new_owner)` | `Admin` | Transfer contract ownership | -| `pause()` / `unpause()` | `Admin` | Circuit breaker for emergency pauses | +| Function | Auth required | Description | +| ------------------------------------------------------------ | ------------------ | ------------------------------------------------------------------ | +| `deposit(from, usdc_amount)` | `from` | Transfer USDC from investor; mint HBS shares; return shares minted | +| `batch_deposit(deposits)` | Each depositor | Batch deposit for multiple investors | +| `withdraw(from, shares_amount)` | `from` | Burn HBS shares; enqueue if insufficient liquidity | +| `claim()` | none | Settle queued redemptions FIFO; return USDC paid out | +| `fund_project(project_id, amount)` | `Admin` | Cross-call Registry; transfer USDC to project owner | +| `fund_project_with_approvals(project_id, amount, approvals)` | Multi-sig signers | Multi-sig variant for critical operations | +| `batch_fund_projects(fundings, approvals)` | Admin or Multi-sig | Batch funding for multiple projects | +| `receive_yield(from, amount)` | `Admin` | Register interest/yield payments from projects | +| `claim_yield(from)` | `from` | Claim accrued yield when liquid | +| `total_assets()` | none | Return `liquid_USDC + investments + expected_returns` | +| `convert_to_shares(usdc_amount)` | none | Preview HBS for given USDC deposit (ERC-4626) | +| `convert_to_assets(shares_amount)` | none | Preview USDC for given HBS redemption (ERC-4626) | +| `get_expected_returns()` | none | Sum `investment × (credit_quality + green_impact) / 200` | +| `claim_insurance(project_id, recipient, amount)` | `Admin` | Authorize default insurance payouts | +| `set_multisig_admin(signers, threshold)` | `Admin` | Configure multi-sig for critical operations | +| `transfer_ownership(new_owner)` | `Admin` | Transfer contract ownership | +| `pause()` / `unpause()` | `Admin` | Circuit breaker for emergency pauses | The vault also exposes the full SEP-41 `FungibleToken` interface (`balance`, `transfer`, `allowance`, `approve`, etc.) and `FungibleBurnable` (`burn`, `burn_from`) from `stellar-tokens`. @@ -275,49 +275,50 @@ Every state-changing function emits a structured event. Topics are indexed by th ### InvestmentVault -| Event | Topics | Data | Emitted by | -|---|---|---|---| -| `Deposit` | `from` (Address) | `usdc_amount`, `shares_minted` (i128) | `deposit()` | -| `Withdraw` | `from` (Address) | `shares_burned`, `usdc_returned` (i128) | `withdraw()` — immediate path | -| `WithdrawQueued` | `from` (Address) | `shares_burned`, `usdc_owed` (i128) | `withdraw()` — queued path (insufficient liquidity) | -| `WithdrawClaimed` | `to` (Address) | `usdc_paid` (i128), `claim_index` (u64) | `claim()` | -| `ProjectFunded` | `project_id` (u32) | `amount` (i128), `recipient` (Address) | `fund_project()` | -| `YieldReceived` | `from` (Address) | `amount` (i128) | `receive_yield()` | -| `YieldClaimed` | `to` (Address) | `amount` (i128) | `claim_yield()` | -| `InsuranceClaimed` | `project_id` (u32) | `recipient` (Address), `amount` (i128) | `claim_insurance()` | -| `OwnershipTransfer` | (library) | `new_owner` (Address) | `transfer_ownership()` — emitted by `stellar-access` | -| `OwnershipTransferCompleted` | (library) | `new_owner` (Address) | `accept_ownership()` — emitted by `stellar-access` | -| `OwnershipRenounced` | (library) | — | `renounce_ownership()` — emitted by `stellar-access` | +| Event | Topics | Data | Emitted by | +| ---------------------------- | ------------------ | --------------------------------------- | ---------------------------------------------------- | +| `Deposit` | `from` (Address) | `usdc_amount`, `shares_minted` (i128) | `deposit()` | +| `Withdraw` | `from` (Address) | `shares_burned`, `usdc_returned` (i128) | `withdraw()` — immediate path | +| `WithdrawQueued` | `from` (Address) | `shares_burned`, `usdc_owed` (i128) | `withdraw()` — queued path (insufficient liquidity) | +| `WithdrawClaimed` | `to` (Address) | `usdc_paid` (i128), `claim_index` (u64) | `claim()` | +| `ProjectFunded` | `project_id` (u32) | `amount` (i128), `recipient` (Address) | `fund_project()` | +| `YieldReceived` | `from` (Address) | `amount` (i128) | `receive_yield()` | +| `YieldClaimed` | `to` (Address) | `amount` (i128) | `claim_yield()` | +| `InsuranceClaimed` | `project_id` (u32) | `recipient` (Address), `amount` (i128) | `claim_insurance()` | +| `OwnershipTransfer` | (library) | `new_owner` (Address) | `transfer_ownership()` — emitted by `stellar-access` | +| `OwnershipTransferCompleted` | (library) | `new_owner` (Address) | `accept_ownership()` — emitted by `stellar-access` | +| `OwnershipRenounced` | (library) | — | `renounce_ownership()` — emitted by `stellar-access` | ### ProjectRegistry -| Event | Topics | Data | Emitted by | -|---|---|---|---| -| `ProjectCreated` | `project_id` (u32), `owner` (Address) | — | `create_project()` | -| `ProjectUpdated` | `project_id` (u32) | `credit_quality`, `green_impact` (u32) | `update_impact_score()` (only when values change) | -| `ScoreChanged` | `project_id` (u32) | `old_credit_quality`, `new_credit_quality`, `old_green_impact`, `new_green_impact`, `old_rate_bps`, `new_rate_bps` (u32) | `update_impact_score()`, `update_credit_quality_score()` (#131) | -| `WhitelistSet` | `account` (Address) | `status` (bool) | `set_whitelist()` | -| `ProjectCertified` | `project_id` (u32) | `status` (CertificationStatus) | `certify_project()` | -| `ProposalCreated` | `proposal_id` (u32) | `proposer` (Address), `voting_ends_at` (u64) | `create_proposal()` | -| `VoteCast` | `proposal_id` (u32) | `voter` (Address), `support` (bool), `weight` (i128) | `cast_vote()` | -| `ProposalExecuted` | `proposal_id` (u32) | `passed` (bool) | `execute_proposal()` | -| `OwnershipTransfer` | (library) | `new_owner` (Address) | `transfer_ownership()` — emitted by `stellar-access` | -| `OwnershipTransferCompleted` | (library) | `new_owner` (Address) | `accept_ownership()` — emitted by `stellar-access` | -| `OwnershipRenounced` | (library) | — | `renounce_ownership()` — emitted by `stellar-access` | +| Event | Topics | Data | Emitted by | +| ---------------------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------- | +| `ProjectCreated` | `project_id` (u32), `owner` (Address) | — | `create_project()` | +| `ProjectUpdated` | `project_id` (u32) | `credit_quality`, `green_impact` (u32) | `update_impact_score()` (only when values change) | +| `ScoreChanged` | `project_id` (u32) | `old_credit_quality`, `new_credit_quality`, `old_green_impact`, `new_green_impact`, `old_rate_bps`, `new_rate_bps` (u32) | `update_impact_score()`, `update_credit_quality_score()` (#131) | +| `WhitelistSet` | `account` (Address) | `status` (bool) | `set_whitelist()` | +| `ProjectCertified` | `project_id` (u32) | `status` (CertificationStatus) | `certify_project()` | +| `ProposalCreated` | `proposal_id` (u32) | `proposer` (Address), `voting_ends_at` (u64) | `create_proposal()` | +| `VoteCast` | `proposal_id` (u32) | `voter` (Address), `support` (bool), `weight` (i128) | `cast_vote()` | +| `ProposalExecuted` | `proposal_id` (u32) | `passed` (bool) | `execute_proposal()` | +| `OwnershipTransfer` | (library) | `new_owner` (Address) | `transfer_ownership()` — emitted by `stellar-access` | +| `OwnershipTransferCompleted` | (library) | `new_owner` (Address) | `accept_ownership()` — emitted by `stellar-access` | +| `OwnershipRenounced` | (library) | — | `renounce_ownership()` — emitted by `stellar-access` | --- ## Tech Stack -| Component | Version | -|---|---| -| Language | Rust (edition 2021, `#![no_std]`) | -| Soroban SDK | `soroban-sdk = 26.1.0` | -| OZ stellar-tokens | `stellar-tokens = 0.7.2` | -| OZ stellar-access | `stellar-access = 0.7.2` | -| OZ stellar-macros | `stellar-macros = 0.7.2` | -| Compile target | `wasm32v1-none` | -| Release profile | LTO, `opt-level = "z"`, `panic = "abort"` | +| Component | Version | +| ----------------- | ----------------------------------------- | +| Language | Rust (edition 2021, `#![no_std]`) | +| Soroban SDK | `soroban-sdk = 26.1.0` | +| OZ stellar-tokens | `stellar-tokens = 0.7.2` | +| OZ stellar-access | `stellar-access = 0.7.2` | +| OZ stellar-macros | `stellar-macros = 0.7.2` | +| Compile target | `wasm32v1-none` | +| Release profile | LTO, `opt-level = "z"`, `panic = "abort"` | ## Storage Rent Considerations + Soroban charges rent for persistent storage. Projects, whitelists, and investments occupy persistent storage. To minimize costs, older inactive projects should be compacted using the `compact_archive` function, which reduces the storage footprint from ~580 bytes down to ~52 bytes. Instance storage is used for global configuration to lower per-access fees. diff --git a/SECURITY.md b/SECURITY.md index e25eb84..0ac0f4f 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -15,11 +15,13 @@ This is testnet, pre-production software. The smart contracts have not yet been ## Threat Model and Trust Assumptions ### Trust Boundaries + - **Project Registry & Investment Vault**: These contracts trust each other explicitly for interoperability where documented. Administrative functions are restricted to a multi-sig or single highly-trusted admin key. - **Oracles and External Data**: We assume our selected oracles (if any) provide accurate and timely data. Any compromise of the oracle may lead to incorrect valuations or interest rate calculations. - **End Users**: Users are responsible for securing their own private keys. The contracts do not have a mechanism to recover funds sent to the wrong address or lost due to compromised keys. ### Known Limitations + - The contracts currently rely on a centralized whitelister for project creation. - Maximum URI lengths and specific string size bounds are strictly enforced to prevent ledger bloat. @@ -31,7 +33,7 @@ contract, walk through this checklist (#265): - [ ] **Auth checks**: Does this function require the right caller's authorization? `#[only_owner]` for admin-only actions, an explicit `caller.require_auth()` for actions gated to a specific non-admin - party (e.g. a project owner). Confirm the check happens *before* any + party (e.g. a project owner). Confirm the check happens _before_ any state mutation, not after. If the function is admin-only, add it to `test_all_only_owner_functions_reject_non_admin_caller` in the relevant crate's `test.rs` (#266) — don't rely solely on a one-off @@ -58,7 +60,7 @@ contract, walk through this checklist (#265): table for its contract, and update `ProjectData`/other type docs if you changed a struct. `scripts/check_interface_docs.py` (wired into CI, #273) will fail the build if you forget — but it only catches missing - *names*, not incorrect auth/return/notes columns, so still write them + _names_, not incorrect auth/return/notes columns, so still write them accurately by hand. - [ ] **Tests**: A happy-path test, at least one negative/panic test for the primary validation failure mode, and (if admin-gated) an entry in @@ -74,6 +76,7 @@ contract, walk through this checklist (#265): ## Incident Response Procedures If a critical vulnerability is discovered and verified: + 1. **Triage**: The core team will assess the severity and potential impact within 24 hours. 2. **Mitigation**: If necessary and feasible, administrative functions may be used to pause certain contract operations to prevent further exploitation. 3. **Patch & Deploy**: A fix will be developed, tested, and deployed as a contract upgrade. diff --git a/adr/001-soroban-platform.md b/adr/001-soroban-platform.md index c456789..3cdb2b2 100644 --- a/adr/001-soroban-platform.md +++ b/adr/001-soroban-platform.md @@ -27,11 +27,13 @@ Key reasons: ## Consequences **Positive:** + - Very low invocation cost (fractions of a cent per call). - Soroban's host-managed storage tiers (instance / persistent / temporary) give predictable data lifecycle without manual expiry logic. - `stellar_tokens` and `stellar_access` crates provide audited primitives for fungible tokens and ownership. **Negative / trade-offs:** + - Smaller developer ecosystem than EVM; fewer ready-made audit firms. - No native multi-sig in contracts (must be handled at the Stellar account layer). - `no_std` limits available Rust crates; anything requiring `std` must be avoided or rewritten. diff --git a/adr/002-storage-patterns.md b/adr/002-storage-patterns.md index 6d0e0f6..4257780 100644 --- a/adr/002-storage-patterns.md +++ b/adr/002-storage-patterns.md @@ -6,17 +6,18 @@ Soroban offers three storage tiers: -| Tier | Lifetime | Cost | Use case | -|------|----------|------|----------| -| Instance | Lives as long as the contract instance | Cheapest reads | Config set once, read often | -| Persistent | Survives as long as rent is paid | Moderate | Long-lived per-entity state | -| Temporary | Automatically expires after TTL | Cheapest writes | Short-lived scratch state | +| Tier | Lifetime | Cost | Use case | +| ---------- | -------------------------------------- | --------------- | --------------------------- | +| Instance | Lives as long as the contract instance | Cheapest reads | Config set once, read often | +| Persistent | Survives as long as rent is paid | Moderate | Long-lived per-entity state | +| Temporary | Automatically expires after TTL | Cheapest writes | Short-lived scratch state | Every persistent entry has a TTL that must be extended (rent paid) or the entry is evicted. Incorrect partitioning means either paying unnecessary rent or losing data. ## Decision **Instance storage** holds contract-level configuration that never changes after deployment: + - `VaultKey::UsdcSac` — the USDC SAC address - `VaultKey::Registry` — the ProjectRegistry contract address - `DataKey::Whitelister` — the whitelister address @@ -25,6 +26,7 @@ Every persistent entry has a TTL that must be extended (rent paid) or the entry Rationale: instance storage is bumped automatically when any function is invoked on the contract, so no explicit TTL management is needed for these entries. **Persistent storage** holds per-entity state that must outlive individual invocations: + - `DataKey::Project(id)` — project metadata - `DataKey::Whitelist(addr)` — per-address whitelist status - `VaultKey::ProjectInvestment(id)` — USDC invested per project @@ -37,9 +39,11 @@ Rationale: project records and investment ledgers must survive indefinitely. Ren ## Consequences **Positive:** + - No manual TTL calls needed for instance-stored config. - Clean separation: adding a new config value → instance; adding a new per-entity record → persistent. **Negative / trade-offs:** + - Persistent entries can be evicted if a project is never touched for a long time. Operators must either invoke the contract periodically or monitor for approaching TTL expiry. - `ProjectCounter` in instance storage means it is trivially readable but also updated on every project creation, slightly increasing instance storage cost over time (Soroban charges for updated bytes). diff --git a/adr/003-share-vault-model.md b/adr/003-share-vault-model.md index 2fec0de..e941357 100644 --- a/adr/003-share-vault-model.md +++ b/adr/003-share-vault-model.md @@ -26,11 +26,13 @@ Key reasons: ## Consequences **Positive:** + - Share price naturally incorporates all value in the vault including projected returns. - LPs can trade HBS on any SEP-41-compatible DEX as a secondary exit. - First depositor receives 1:1 shares (guarded by `total_shares == 0 || total_assets == 0` check). **Negative / trade-offs:** + - Share price depends on `get_expected_returns`, which reads every project in the registry. This is O(n) in the number of projects; gas cost grows linearly. A future optimisation may maintain a running expected-return accumulator. - The share model means early LPs dilute later LPs if returns are recognised before new deposits — this is standard vault behaviour but must be communicated clearly to users. - Rounding is in favour of the vault (integer truncation), which may leave tiny dust amounts unclaimable. diff --git a/adr/004-security-model.md b/adr/004-security-model.md index 5afa2e7..d5dd0ec 100644 --- a/adr/004-security-model.md +++ b/adr/004-security-model.md @@ -10,11 +10,13 @@ Two distinct trust boundaries exist in the protocol: 2. **Project creators** — third parties submitting green projects. Must be vetted before they can create projects, but not trusted with admin power. Options for admin access control: + - **Multisig at the Stellar account layer** — admin is a Stellar account with multiple signers; threshold enforced by the network, not the contract. - **Role list in contract** — contract stores a list of addresses with specific roles. - **Single owner in contract** — contract stores one owner address; owner can be a multisig account. Options for creator access control: + - **Open permissionless** — anyone can create a project. - **NFT-gated** — creator must hold a specific NFT. - **Whitelist** — a designated whitelister address approves creator addresses. @@ -26,6 +28,7 @@ Options for creator access control: **Creator access control:** use a dedicated `whitelister` address stored in instance storage. The whitelister calls `set_whitelist(account, true/false)` to approve or revoke creators. Only whitelisted addresses can call `create_project`. Rationale: + - Single-owner is simple and auditable. Multisig complexity (threshold, key rotation) is handled at the Stellar account layer where it belongs, not duplicated in the contract. - A separate whitelister role decouples day-to-day project onboarding from protocol admin. The admin can be a cold multisig; the whitelister can be a warmer operational key. - A simple boolean whitelist is sufficient for the current scale. Graduated tiers or KYC attestation can be added later without breaking the existing interface. @@ -33,11 +36,13 @@ Rationale: ## Consequences **Positive:** + - `#[only_owner]` is a single-line, compiler-enforced guard. Hard to accidentally omit. - Whitelister and owner can be different accounts, limiting blast radius if either is compromised. - No on-chain role enumeration — no function to list all owners or whitelisters, reducing attack surface. **Negative / trade-offs:** + - Single owner is a single point of failure if the owner key is lost. Mitigation: owner should be a Stellar multisig account with threshold ≥ 2. - There is no on-chain timelock on `fund_project`. A compromised owner could immediately drain USDC to any project's registered owner address. Mitigation: use a multisig owner and monitor `project_funded` events. - Whitelist revocation (`set_whitelist(addr, false)`) does not remove existing projects created by the revoked address. Existing projects remain valid. diff --git a/adr/README.md b/adr/README.md index fc6fac2..c765720 100644 --- a/adr/README.md +++ b/adr/README.md @@ -4,12 +4,12 @@ This directory contains Architecture Decision Records (ADRs) for the Heliobond s ## Index -| # | Title | Status | -|---|-------|--------| -| [001](001-soroban-platform.md) | Use Soroban / Stellar for smart contracts | Accepted | -| [002](002-storage-patterns.md) | Persistent vs instance storage partitioning | Accepted | -| [003](003-share-vault-model.md) | ERC-4626-inspired share vault for investments | Accepted | -| [004](004-security-model.md) | Owner-only admin pattern and whitelist access control | Accepted | +| # | Title | Status | +| ------------------------------- | ----------------------------------------------------- | -------- | +| [001](001-soroban-platform.md) | Use Soroban / Stellar for smart contracts | Accepted | +| [002](002-storage-patterns.md) | Persistent vs instance storage partitioning | Accepted | +| [003](003-share-vault-model.md) | ERC-4626-inspired share vault for investments | Accepted | +| [004](004-security-model.md) | Owner-only admin pattern and whitelist access control | Accepted | ## When to write a new ADR diff --git a/docs/GOVERNANCE.md b/docs/GOVERNANCE.md index 2b00dcf..b7b99eb 100644 --- a/docs/GOVERNANCE.md +++ b/docs/GOVERNANCE.md @@ -9,38 +9,42 @@ This document outlines the governance model of the Heliobond platform, detailing Heliobond contracts distinguish between three primary roles: **Admin (Owner)**, **Whitelister**, and **Project Creators (Whitelisted)**. This structure is designed to decouple contract administration from operational tasks. ### 1. Admin (Owner) + The Admin role manages critical protocol configuration and capital allocation. -* **Implementation:** Employs the `stellar-access::ownable` single-owner pattern. -* **Ownership Transfer:** Follows a secure 2-step transfer process (`transfer_ownership` followed by `accept_ownership` from the new owner) to avoid accidental transfer to incorrect addresses. -* **Visibility:** Emits custom, project-specific `OwnershipTransferred` events in both contracts, ensuring that ownership change proposals are auditable off-chain. + +- **Implementation:** Employs the `stellar-access::ownable` single-owner pattern. +- **Ownership Transfer:** Follows a secure 2-step transfer process (`transfer_ownership` followed by `accept_ownership` from the new owner) to avoid accidental transfer to incorrect addresses. +- **Visibility:** Emits custom, project-specific `OwnershipTransferred` events in both contracts, ensuring that ownership change proposals are auditable off-chain. #### Admin Capabilities -| Action | Contract | Description | -|---|---|---| -| `update_impact_score` | `ProjectRegistry` | Sets `credit_quality` and `green_impact` scores (0–100) for a project. | -| `update_credit_quality_score` | `ProjectRegistry` | Updates only the `credit_quality` score, preserving the existing `green_impact` score. | -| `certify_project` | `ProjectRegistry` | Updates the certification status of a registered project (shares this capability with the Whitelister). | -| `fund_project` | `InvestmentVault` | Disburses capital from the vault to the registered project creator's address. | -| `receive_yield` | `InvestmentVault` | Registers interest/yield payments received from project owners. | -| `claim_insurance` | `InvestmentVault` | Authorizes default insurance payouts to affected investors from the insurance reserve. | -| `set_management_fee` | `InvestmentVault` | Sets the vault management fee (hard-capped at 5.00% / 500 bps). | -| `enable_secondary_trading` | `InvestmentVault` | Enables DEX listing discovery and updates official secondary market listing status. | -| `pause` / `unpause` | Both | Temporarily freezes deposits, withdrawals, and proposal voting under emergency conditions. | +| Action | Contract | Description | +| ----------------------------- | ----------------- | ------------------------------------------------------------------------------------------------------- | +| `update_impact_score` | `ProjectRegistry` | Sets `credit_quality` and `green_impact` scores (0–100) for a project. | +| `update_credit_quality_score` | `ProjectRegistry` | Updates only the `credit_quality` score, preserving the existing `green_impact` score. | +| `certify_project` | `ProjectRegistry` | Updates the certification status of a registered project (shares this capability with the Whitelister). | +| `fund_project` | `InvestmentVault` | Disburses capital from the vault to the registered project creator's address. | +| `receive_yield` | `InvestmentVault` | Registers interest/yield payments received from project owners. | +| `claim_insurance` | `InvestmentVault` | Authorizes default insurance payouts to affected investors from the insurance reserve. | +| `set_management_fee` | `InvestmentVault` | Sets the vault management fee (hard-capped at 5.00% / 500 bps). | +| `enable_secondary_trading` | `InvestmentVault` | Enables DEX listing discovery and updates official secondary market listing status. | +| `pause` / `unpause` | Both | Temporarily freezes deposits, withdrawals, and proposal voting under emergency conditions. | ### 2. Whitelister + An operational role focused on onboarding project creators and certifying projects. Separating this role prevents daily tasks from requiring the high-security Admin key. #### Whitelister Capabilities -| Action | Contract | Description | -|---|---|---| -| `set_whitelist` | `ProjectRegistry` | Approves or revokes creator addresses, granting or denying project registration rights. | +| Action | Contract | Description | +| ----------------- | ----------------- | ------------------------------------------------------------------------------------------------- | +| `set_whitelist` | `ProjectRegistry` | Approves or revokes creator addresses, granting or denying project registration rights. | | `certify_project` | `ProjectRegistry` | Updates the certification status of a registered project (shares this capability with the Admin). | ### 3. Project Creators (Whitelisted) -* Must be explicitly whitelisted by the Whitelister. -* Can call `create_project` to register new project metadata (IPFS URI and maturity date) on-chain. + +- Must be explicitly whitelisted by the Whitelister. +- Can call `create_project` to register new project metadata (IPFS URI and maturity date) on-chain. --- @@ -52,13 +56,14 @@ Both contracts support an optional multisig gate on their most sensitive Admin a Each contract exposes the same three functions (owner-only unless noted): -| Function | Contract | Effect | -|---|---|---| -| `set_multisig_admin(signers, threshold)` | Both | Sets the approver list and required approval count. `#[only_owner]`. | -| `get_multisig_admin()` | Both | Returns `(signers, threshold)`. No auth required — anyone can read the current config. | -| `clear_multisig_admin()` | `ProjectRegistry` | Resets `threshold` to `0` and `signers` to empty, disabling multisig. `#[only_owner]`. | +| Function | Contract | Effect | +| ---------------------------------------- | ----------------- | -------------------------------------------------------------------------------------- | +| `set_multisig_admin(signers, threshold)` | Both | Sets the approver list and required approval count. `#[only_owner]`. | +| `get_multisig_admin()` | Both | Returns `(signers, threshold)`. No auth required — anyone can read the current config. | +| `clear_multisig_admin()` | `ProjectRegistry` | Resets `threshold` to `0` and `signers` to empty, disabling multisig. `#[only_owner]`. | `set_multisig_admin` validates the config before storing it: + - `signers.len()` must not exceed `MAX_MULTISIG_SIGNERS` (10 in both contracts) — panics with `TooManyMultiSigSigners` otherwise. - `threshold` must be greater than `0` and no greater than `signers.len()` — panics with `InvalidMultiSigThreshold` otherwise (so you cannot require more approvals than there are signers, and cannot set a threshold with no signers). - `signers` must not contain duplicate addresses — panics with `DuplicateApproval` otherwise. @@ -66,6 +71,7 @@ Each contract exposes the same three functions (owner-only unless noted): ### How it changes call behaviour Once `threshold > 0`, the plain single-owner variant of a gated function (e.g. `fund_project`, `claim_insurance`) becomes unusable — it panics via an internal `require_multisig_disabled` guard. Callers must switch to the `_with_approvals` variant instead (e.g. `fund_project_with_approvals`, `claim_insurance_with_approvals`, `update_impact_score_approved`), passing a `Vec
` of the approving signers. For each address in that list, `require_admin_approval`: + 1. Panics with `DuplicateApproval` if it already appeared earlier in the same list. 2. Panics with `NotMultiSigSigner` if it isn't in the stored `signers` set. 3. Calls `.require_auth()` on it — every listed approver must independently authorize the transaction, not just be named in the list. @@ -90,28 +96,31 @@ Because `set_multisig_admin` is itself `#[only_owner]` and not gated by the mult To prepare the platform for future decentralization, a preliminary governance proposal system is built directly into the `ProjectRegistry` contract. ### 1. Proposal Creation (`create_proposal`) -* **Eligibility:** Any whitelisted address may propose a governance change. -* **Parameters:** Requires a text description and a voting period. -* **Constraint:** The voting duration must be at least `MIN_VOTING_PERIOD` (86,400 seconds / 24 hours) to prevent flash proposals. + +- **Eligibility:** Any whitelisted address may propose a governance change. +- **Parameters:** Requires a text description and a voting period. +- **Constraint:** The voting duration must be at least `MIN_VOTING_PERIOD` (86,400 seconds / 24 hours) to prevent flash proposals. ### 2. Casting Votes (`cast_vote`) -* **Eligibility:** Any token holder can vote. -* **Mechanism:** Votes are cast as either `support` (for) or `against`. -* **Voting Weight:** A voter's weight corresponds to their HBS (Heliobond Shares) balance. + +- **Eligibility:** Any token holder can vote. +- **Mechanism:** Votes are cast as either `support` (for) or `against`. +- **Voting Weight:** A voter's weight corresponds to their HBS (Heliobond Shares) balance. > [!WARNING] > **On-Chain Voting Weight Limitation** > > In the current version, the `cast_vote` function takes the vote `weight` as a direct parameter supplied by the caller, **without verifying it against the actual HBS token balance on-chain**. > -> * **Current Mitigation:** Off-chain clients and indexers must query the `InvestmentVault` contract via `balance(voter)` during simulation and submit the correct value. Any proposal executed with invalid or inflated vote weights must be filtered out or rejected during off-chain validation before executing any manual steps. -> * **Future Fix:** A cross-contract call from `ProjectRegistry` to `InvestmentVault::balance(voter)` will be integrated into `cast_vote` to enforce the voting weight programmatically. +> - **Current Mitigation:** Off-chain clients and indexers must query the `InvestmentVault` contract via `balance(voter)` during simulation and submit the correct value. Any proposal executed with invalid or inflated vote weights must be filtered out or rejected during off-chain validation before executing any manual steps. +> - **Future Fix:** A cross-contract call from `ProjectRegistry` to `InvestmentVault::balance(voter)` will be integrated into `cast_vote` to enforce the voting weight programmatically. ### 3. Proposal Execution (`execute_proposal`) -* **Eligibility:** Anyone may trigger execution once the voting period has elapsed. -* **Rule:** The proposal passes if `votes_for > votes_against`. -* **State Change:** The proposal is marked as `executed` to prevent double-execution or late voting. -* **Impact:** In the current phase, proposal execution is informational/social (signaling consensus) and does not automatically trigger state changes in contract configurations. + +- **Eligibility:** Anyone may trigger execution once the voting period has elapsed. +- **Rule:** The proposal passes if `votes_for > votes_against`. +- **State Change:** The proposal is marked as `executed` to prevent double-execution or late voting. +- **Impact:** In the current phase, proposal execution is informational/social (signaling consensus) and does not automatically trigger state changes in contract configurations. --- @@ -138,6 +147,6 @@ graph TD Community members can participate in Heliobond governance through the following channels: -* **Holding HBS:** Acquisition of HBS shares grants voting power. The larger your share of the pool, the more influence your votes carry. -* **Submitting Proposals:** Whitelisted creators can propose updates to the protocol rules, fees, or whitelisting guidelines. -* **Auditing Protocol Operations:** Because every governance action emits structured events (`ProposalCreated`, `VoteCast`, `ProposalExecuted`, `OwnershipTransferred`), users can run independent indexers to monitor and verify all administrative decisions. +- **Holding HBS:** Acquisition of HBS shares grants voting power. The larger your share of the pool, the more influence your votes carry. +- **Submitting Proposals:** Whitelisted creators can propose updates to the protocol rules, fees, or whitelisting guidelines. +- **Auditing Protocol Operations:** Because every governance action emits structured events (`ProposalCreated`, `VoteCast`, `ProposalExecuted`, `OwnershipTransferred`), users can run independent indexers to monitor and verify all administrative decisions. diff --git a/docs/INTEGRATION.md b/docs/INTEGRATION.md index 7688399..911e72c 100644 --- a/docs/INTEGRATION.md +++ b/docs/INTEGRATION.md @@ -13,6 +13,7 @@ npm install @stellar/stellar-sdk ``` You also need: + - A funded Stellar account (testnet faucet: [friendbot](https://friendbot.stellar.org)) - The deployed contract IDs (see deployment summary in CI or ask the team) @@ -20,11 +21,11 @@ You also need: ## Contract addresses -| Contract | Testnet ID | Description | -|----------|-----------|-------------| -| `ProjectRegistry` | `CXXX…` | Manages projects, whitelist, governance | -| `InvestmentVault` | `CYYY…` | Manages deposits, shares, yield, insurance | -| USDC SAC | `CZZZ…` | USDC Stellar Asset Contract on testnet | +| Contract | Testnet ID | Description | +| ----------------- | ---------- | ------------------------------------------ | +| `ProjectRegistry` | `CXXX…` | Manages projects, whitelist, governance | +| `InvestmentVault` | `CYYY…` | Manages deposits, shares, yield, insurance | +| USDC SAC | `CZZZ…` | USDC Stellar Asset Contract on testnet | > Replace `CX…`, `CY…`, `CZ…` with the actual IDs from the latest deployment. @@ -64,6 +65,7 @@ sequenceDiagram ``` Notes: + - `fund_project` is the only call where the vault reads from the registry (`get_project`) — this is how it resolves the payout address without storing project ownership itself. - `cast_vote`'s weight is supplied by the caller, not fetched on-chain — see the [governance doc](./GOVERNANCE.md#on-chain-proposal--voting-mechanism) for why, and what to verify off-chain before trusting a vote. @@ -84,11 +86,11 @@ import { } from "@stellar/stellar-sdk"; const RPC_URL = "https://soroban-testnet.stellar.org"; -const server = new SorobanRpc.Server(RPC_URL); -const network = Networks.TESTNET; +const server = new SorobanRpc.Server(RPC_URL); +const network = Networks.TESTNET; -const keypair = Keypair.fromSecret("SXXX…your secret key…"); -const account = await server.getAccount(keypair.publicKey()); +const keypair = Keypair.fromSecret("SXXX…your secret key…"); +const account = await server.getAccount(keypair.publicKey()); ``` --- @@ -103,7 +105,7 @@ async function invokeContract( keypair: Keypair, ): Promise { const contract = new Contract(contractId); - const account = await server.getAccount(keypair.publicKey()); + const account = await server.getAccount(keypair.publicKey()); const tx = new TransactionBuilder(account, { fee: "100000", @@ -130,7 +132,7 @@ async function invokeContract( // Poll for confirmation let response = await server.getTransaction(result.hash); while (response.status === "NOT_FOUND") { - await new Promise(r => setTimeout(r, 1000)); + await new Promise((r) => setTimeout(r, 1000)); response = await server.getTransaction(result.hash); } if (response.status !== "SUCCESS") { @@ -151,12 +153,14 @@ const REGISTRY = "CXXX…"; const result = await server.simulateTransaction( new TransactionBuilder(account, { fee: "100", networkPassphrase: network }) - .addOperation(new Contract(REGISTRY).call( - "get_whitelist", - nativeToScVal(keypair.publicKey(), { type: "address" }), - )) + .addOperation( + new Contract(REGISTRY).call( + "get_whitelist", + nativeToScVal(keypair.publicKey(), { type: "address" }), + ), + ) .setTimeout(30) - .build() + .build(), ); const isWhitelisted: boolean = scValToNative((result as any).result.retval); ``` @@ -164,32 +168,36 @@ const isWhitelisted: boolean = scValToNative((result as any).result.retval); ### Create a project ```typescript -const projectId = scValToNative(await invokeContract( - REGISTRY, - "create_project", - [ - nativeToScVal(keypair.publicKey(), { type: "address" }), // creator - nativeToScVal("ipfs://QmYourHash", { type: "string" }), // uri - nativeToScVal(0n, { type: "u64" }), // maturity_date (0 = open-ended) - ], - keypair, -)); +const projectId = scValToNative( + await invokeContract( + REGISTRY, + "create_project", + [ + nativeToScVal(keypair.publicKey(), { type: "address" }), // creator + nativeToScVal("ipfs://QmYourHash", { type: "string" }), // uri + nativeToScVal(0n, { type: "u64" }), // maturity_date (0 = open-ended) + ], + keypair, + ), +); console.log("Project created with ID:", projectId); ``` ### Create a governance proposal ```typescript -const proposalId = scValToNative(await invokeContract( - REGISTRY, - "create_proposal", - [ - nativeToScVal(keypair.publicKey(), { type: "address" }), - nativeToScVal("Increase insurance premium to 1%", { type: "string" }), - nativeToScVal(BigInt(7 * 24 * 3600), { type: "u64" }), // 7-day voting period - ], - keypair, -)); +const proposalId = scValToNative( + await invokeContract( + REGISTRY, + "create_proposal", + [ + nativeToScVal(keypair.publicKey(), { type: "address" }), + nativeToScVal("Increase insurance premium to 1%", { type: "string" }), + nativeToScVal(BigInt(7 * 24 * 3600), { type: "u64" }), // 7-day voting period + ], + keypair, + ), +); ``` ### Cast a vote @@ -202,12 +210,14 @@ const VAULT = "CYYY…"; // Read HBS balance (view — no auth, no fee beyond simulation) const balanceSim = await server.simulateTransaction( new TransactionBuilder(account, { fee: "100", networkPassphrase: network }) - .addOperation(new Contract(VAULT).call( - "balance", - nativeToScVal(keypair.publicKey(), { type: "address" }), - )) + .addOperation( + new Contract(VAULT).call( + "balance", + nativeToScVal(keypair.publicKey(), { type: "address" }), + ), + ) .setTimeout(30) - .build() + .build(), ); const hbsBalance: bigint = scValToNative((balanceSim as any).result.retval); @@ -216,9 +226,9 @@ await invokeContract( "cast_vote", [ nativeToScVal(keypair.publicKey(), { type: "address" }), - nativeToScVal(proposalId, { type: "u32" }), - nativeToScVal(true, { type: "bool" }), // support = true - nativeToScVal(hbsBalance, { type: "i128" }), + nativeToScVal(proposalId, { type: "u32" }), + nativeToScVal(true, { type: "bool" }), // support = true + nativeToScVal(hbsBalance, { type: "i128" }), ], keypair, ); @@ -242,23 +252,25 @@ await invokeContract( "approve", [ nativeToScVal(keypair.publicKey(), { type: "address" }), // from - nativeToScVal(VAULT, { type: "address" }), // spender - nativeToScVal(DEPOSIT_AMOUNT, { type: "i128" }), - nativeToScVal(99999999n, { type: "u32" }), // expiration_ledger + nativeToScVal(VAULT, { type: "address" }), // spender + nativeToScVal(DEPOSIT_AMOUNT, { type: "i128" }), + nativeToScVal(99999999n, { type: "u32" }), // expiration_ledger ], keypair, ); // Step 2: deposit -const sharesMinted = scValToNative(await invokeContract( - VAULT, - "deposit", - [ - nativeToScVal(keypair.publicKey(), { type: "address" }), - nativeToScVal(DEPOSIT_AMOUNT, { type: "i128" }), - ], - keypair, -)); +const sharesMinted = scValToNative( + await invokeContract( + VAULT, + "deposit", + [ + nativeToScVal(keypair.publicKey(), { type: "address" }), + nativeToScVal(DEPOSIT_AMOUNT, { type: "i128" }), + ], + keypair, + ), +); console.log("Shares minted:", sharesMinted.toString()); ``` @@ -267,12 +279,14 @@ console.log("Shares minted:", sharesMinted.toString()); ```typescript const portfolioSim = await server.simulateTransaction( new TransactionBuilder(account, { fee: "100", networkPassphrase: network }) - .addOperation(new Contract(VAULT).call( - "get_portfolio", - nativeToScVal(keypair.publicKey(), { type: "address" }), - )) + .addOperation( + new Contract(VAULT).call( + "get_portfolio", + nativeToScVal(keypair.publicKey(), { type: "address" }), + ), + ) .setTimeout(30) - .build() + .build(), ); const portfolio = scValToNative((portfolioSim as any).result.retval); console.log("Portfolio:", portfolio); @@ -288,27 +302,31 @@ console.log("Portfolio:", portfolio); ### Claim yield ```typescript -const claimed = scValToNative(await invokeContract( - VAULT, - "claim_yield", - [nativeToScVal(keypair.publicKey(), { type: "address" })], - keypair, -)); +const claimed = scValToNative( + await invokeContract( + VAULT, + "claim_yield", + [nativeToScVal(keypair.publicKey(), { type: "address" })], + keypair, + ), +); console.log("Yield claimed (USDC stroops):", claimed.toString()); ``` ### Withdraw shares ```typescript -const usdcReturned = scValToNative(await invokeContract( - VAULT, - "withdraw", - [ - nativeToScVal(keypair.publicKey(), { type: "address" }), - nativeToScVal(sharesMinted / 2n, { type: "i128" }), // redeem half - ], - keypair, -)); +const usdcReturned = scValToNative( + await invokeContract( + VAULT, + "withdraw", + [ + nativeToScVal(keypair.publicKey(), { type: "address" }), + nativeToScVal(sharesMinted / 2n, { type: "i128" }), // redeem half + ], + keypair, + ), +); ``` --- @@ -317,16 +335,16 @@ const usdcReturned = scValToNative(await invokeContract( All contract panics surface as Soroban `HostError` codes in the simulation or transaction result. Common patterns: -| Panic message | Cause | Resolution | -|---------------|-------|------------| -| `"not whitelisted"` | Creator not in whitelist | Ask admin to call `set_whitelist` | -| `"deposit must be positive"` | Amount ≤ 0 | Validate input before submitting | -| `"deposit exceeds maximum"` | Amount > 1 billion USDC | Split into multiple deposits | -| `"uri too short"` | URI < 8 bytes | Provide a valid IPFS or HTTPS URI | -| `"insufficient deployable USDC"` | Vault liquid balance minus insurance reserve < amount | Wait for more deposits or reduce amount | -| `"already voted"` | Voter already cast a vote on this proposal | UI should check `has_voted` before showing vote button | -| `"voting period too short"` | Duration < 86 400 s | Use at least 1 day | -| `"insurance already claimed"` | Payout already made for this project | Check `InsuranceClaimed` state first | +| Panic message | Cause | Resolution | +| -------------------------------- | ----------------------------------------------------- | ------------------------------------------------------ | +| `"not whitelisted"` | Creator not in whitelist | Ask admin to call `set_whitelist` | +| `"deposit must be positive"` | Amount ≤ 0 | Validate input before submitting | +| `"deposit exceeds maximum"` | Amount > 1 billion USDC | Split into multiple deposits | +| `"uri too short"` | URI < 8 bytes | Provide a valid IPFS or HTTPS URI | +| `"insufficient deployable USDC"` | Vault liquid balance minus insurance reserve < amount | Wait for more deposits or reduce amount | +| `"already voted"` | Voter already cast a vote on this proposal | UI should check `has_voted` before showing vote button | +| `"voting period too short"` | Duration < 86 400 s | Use at least 1 day | +| `"insurance already claimed"` | Payout already made for this project | Check `InsuranceClaimed` state first | ```typescript try { @@ -349,9 +367,11 @@ Listen to contract events using the Stellar Horizon API or an indexer: ```typescript // Via Horizon (events endpoint) const resp = await fetch( - `https://horizon-testnet.stellar.org/contracts/${VAULT}/events?limit=20` + `https://horizon-testnet.stellar.org/contracts/${VAULT}/events?limit=20`, ); -const { _embedded: { records } } = await resp.json(); +const { + _embedded: { records }, +} = await resp.json(); records.forEach((ev: any) => { console.log(ev.type, ev.value); }); @@ -359,17 +379,17 @@ records.forEach((ev: any) => { Key event topics by contract: -| Contract | Topic | Fired when | -|----------|-------|-----------| -| `InvestmentVault` | `deposit` | Investor deposits USDC | -| `InvestmentVault` | `withdraw` | Investor withdraws | -| `InvestmentVault` | `yield_received` | Owner posts yield | -| `InvestmentVault` | `yield_claimed` | Investor claims yield | -| `InvestmentVault` | `insurance_claimed` | Default payout made | -| `ProjectRegistry` | `project_created` | New project registered | -| `ProjectRegistry` | `project_updated` | Impact scores updated | -| `ProjectRegistry` | `score_changed` | Score changed (includes old + new values) | -| `ProjectRegistry` | `project_certified` | Certification status changed | -| `ProjectRegistry` | `proposal_created` | Governance proposal opened | -| `ProjectRegistry` | `vote_cast` | Vote recorded | -| `ProjectRegistry` | `proposal_executed` | Proposal finalised | +| Contract | Topic | Fired when | +| ----------------- | ------------------- | ----------------------------------------- | +| `InvestmentVault` | `deposit` | Investor deposits USDC | +| `InvestmentVault` | `withdraw` | Investor withdraws | +| `InvestmentVault` | `yield_received` | Owner posts yield | +| `InvestmentVault` | `yield_claimed` | Investor claims yield | +| `InvestmentVault` | `insurance_claimed` | Default payout made | +| `ProjectRegistry` | `project_created` | New project registered | +| `ProjectRegistry` | `project_updated` | Impact scores updated | +| `ProjectRegistry` | `score_changed` | Score changed (includes old + new values) | +| `ProjectRegistry` | `project_certified` | Certification status changed | +| `ProjectRegistry` | `proposal_created` | Governance proposal opened | +| `ProjectRegistry` | `vote_cast` | Vote recorded | +| `ProjectRegistry` | `proposal_executed` | Proposal finalised | diff --git a/docs/NOTIFICATIONS.md b/docs/NOTIFICATIONS.md index ef754e5..704c9dd 100644 --- a/docs/NOTIFICATIONS.md +++ b/docs/NOTIFICATIONS.md @@ -15,15 +15,15 @@ The `ProjectRegistry` contract emits `ScoreChanged` whenever an impact score is ### Event Structure -| Field | Type | Description | -|-------|------|-------------| -| `project_id` (topic) | `u32` | The project whose scores changed | -| `old_credit_quality` | `u32` | Previous credit quality (0–100) | -| `new_credit_quality` | `u32` | New credit quality (0–100) | -| `old_green_impact` | `u32` | Previous green impact (0–100) | -| `new_green_impact` | `u32` | New green impact (0–100) | -| `old_rate_bps` | `u32` | Previous interest rate in basis points (500–1000) | -| `new_rate_bps` | `u32` | New interest rate in basis points (500–1000) | +| Field | Type | Description | +| -------------------- | ----- | ------------------------------------------------- | +| `project_id` (topic) | `u32` | The project whose scores changed | +| `old_credit_quality` | `u32` | Previous credit quality (0–100) | +| `new_credit_quality` | `u32` | New credit quality (0–100) | +| `old_green_impact` | `u32` | Previous green impact (0–100) | +| `new_green_impact` | `u32` | New green impact (0–100) | +| `old_rate_bps` | `u32` | Previous interest rate in basis points (500–1000) | +| `new_rate_bps` | `u32` | New interest rate in basis points (500–1000) | ### When It Fires @@ -64,14 +64,14 @@ Off-chain consumers that decode the raw Soroban event (as `notification-service/ } ``` -| Field | Type | Description | -|-------|------|--------------| -| `project_id` | `number` | The project whose scores changed (decoded from the event's topic) | -| `old_credit_quality` / `new_credit_quality` | `number` | Credit quality before/after (0–100) | -| `old_green_impact` / `new_green_impact` | `number` | Green impact before/after (0–100) | -| `old_rate_bps` / `new_rate_bps` | `number` | Interest rate in basis points before/after (500–1000) | -| `timestamp` | `number` | Unix timestamp (seconds) of the ledger close time | -| `ledger` | `number` | Stellar ledger sequence number the event was emitted in | +| Field | Type | Description | +| ------------------------------------------- | -------- | ----------------------------------------------------------------- | +| `project_id` | `number` | The project whose scores changed (decoded from the event's topic) | +| `old_credit_quality` / `new_credit_quality` | `number` | Credit quality before/after (0–100) | +| `old_green_impact` / `new_green_impact` | `number` | Green impact before/after (0–100) | +| `old_rate_bps` / `new_rate_bps` | `number` | Interest rate in basis points before/after (500–1000) | +| `timestamp` | `number` | Unix timestamp (seconds) of the ledger close time | +| `ledger` | `number` | Stellar ledger sequence number the event was emitted in | All fields are required — the decoder rejects any raw event that doesn't decode to every field above as a finite number, rather than passing through `null`/`NaN`. @@ -93,13 +93,13 @@ Soroban RPC ──► Listener ──► Investor Index ──► Notifier ### Components -| Component | File | Description | -|-----------|------|-------------| -| `Listener` | `src/listener.ts` | Polls Soroban RPC for `ScoreChanged` events | -| `Store` | `src/db.ts` | SQLite database for investor preferences and project-investor index | +| Component | File | Description | +| ---------- | ----------------- | ------------------------------------------------------------------- | +| `Listener` | `src/listener.ts` | Polls Soroban RPC for `ScoreChanged` events | +| `Store` | `src/db.ts` | SQLite database for investor preferences and project-investor index | | `Notifier` | `src/notifier.ts` | Dispatches email (nodemailer) and webhook (HTTP POST) notifications | -| `API` | `src/api.ts` | Express REST API for managing notification preferences | -| `Config` | `src/config.ts` | Environment-based configuration | +| `API` | `src/api.ts` | Express REST API for managing notification preferences | +| `Config` | `src/config.ts` | Environment-based configuration | ### Quick Start @@ -113,21 +113,21 @@ npm run dev ### Configuration -| Environment Variable | Default | Description | -|---------------------|---------|-------------| -| `STELLAR_RPC_URL` | `https://soroban-testnet.stellar.org` | Soroban RPC endpoint | -| `STELLAR_NETWORK_PASSPHRASE` | Testnet passphrase | Network passphrase | -| `REGISTRY_CONTRACT_ID` | (required) | Deployed `ProjectRegistry` contract ID | -| `VAULT_CONTRACT_ID` | (optional) | Deployed `InvestmentVault` contract ID | -| `DB_PATH` | `./data/notifications.db` | SQLite database path | -| `POLL_INTERVAL_MS` | `30000` | Event polling interval | -| `FROM_EMAIL` | — | Sender email address | -| `SMTP_HOST` | — | SMTP server hostname | -| `SMTP_PORT` | `587` | SMTP port | -| `SMTP_SECURE` | `false` | Use TLS for SMTP | -| `SMTP_USER` | — | SMTP username | -| `SMTP_PASS` | — | SMTP password | -| `API_PORT` | `3000` | REST API port | +| Environment Variable | Default | Description | +| ---------------------------- | ------------------------------------- | -------------------------------------- | +| `STELLAR_RPC_URL` | `https://soroban-testnet.stellar.org` | Soroban RPC endpoint | +| `STELLAR_NETWORK_PASSPHRASE` | Testnet passphrase | Network passphrase | +| `REGISTRY_CONTRACT_ID` | (required) | Deployed `ProjectRegistry` contract ID | +| `VAULT_CONTRACT_ID` | (optional) | Deployed `InvestmentVault` contract ID | +| `DB_PATH` | `./data/notifications.db` | SQLite database path | +| `POLL_INTERVAL_MS` | `30000` | Event polling interval | +| `FROM_EMAIL` | — | Sender email address | +| `SMTP_HOST` | — | SMTP server hostname | +| `SMTP_PORT` | `587` | SMTP port | +| `SMTP_SECURE` | `false` | Use TLS for SMTP | +| `SMTP_USER` | — | SMTP username | +| `SMTP_PASS` | — | SMTP password | +| `API_PORT` | `3000` | REST API port | ### REST API @@ -140,6 +140,7 @@ npm run dev **PUT `/preferences/:address`** — Create or update a preference. Request body: + ```json { "email": "investor@example.com", @@ -149,12 +150,12 @@ Request body: } ``` -| Field | Type | Required | Description | -|-------|------|----------|-------------| -| `email` | `string` | No | Email address for email notifications | -| `webhook_url` | `string` | No | HTTPS URL for webhook POST notifications | -| `enabled` | `boolean` | No (default: true) | Master toggle for notifications | -| `min_delta` | `number` | No (default: 1) | Minimum absolute score change (0–100) to trigger a notification | +| Field | Type | Required | Description | +| ------------- | --------- | ------------------ | --------------------------------------------------------------- | +| `email` | `string` | No | Email address for email notifications | +| `webhook_url` | `string` | No | HTTPS URL for webhook POST notifications | +| `enabled` | `boolean` | No (default: true) | Master toggle for notifications | +| `min_delta` | `number` | No (default: 1) | Minimum absolute score change (0–100) to trigger a notification | At least one of `email` or `webhook_url` must be provided. Both can be set simultaneously. @@ -166,13 +167,14 @@ At least one of `email` or `webhook_url` must be provided. Both can be set simul Query parameters: -| Param | Type | Default | Description | -|-------|------|---------|-------------| -| `limit` | `number` | `50` (max `200`) | Maximum number of entries to return | -| `offset` | `number` | `0` | Number of entries to skip | -| `investor_address` | `string` | — | Restrict results to a single investor | +| Param | Type | Default | Description | +| ------------------ | -------- | ---------------- | ------------------------------------- | +| `limit` | `number` | `50` (max `200`) | Maximum number of entries to return | +| `offset` | `number` | `0` | Number of entries to skip | +| `investor_address` | `string` | — | Restrict results to a single investor | Response body: + ```json { "items": [ diff --git a/docs/STORAGE.md b/docs/STORAGE.md index da3422e..d69137a 100644 --- a/docs/STORAGE.md +++ b/docs/STORAGE.md @@ -8,11 +8,11 @@ This document enumerates every storage key used by the Heliobond contracts, the ## Storage tiers -| Tier | Lifetime | Rent | Typical use | -|------|----------|------|-------------| -| **Instance** | As long as the contract instance is live | Bumped automatically on every invocation | Config set once, read often; global state read on almost every call | -| **Persistent** | Until TTL expires (rent must be paid) | Charged per byte per ledger | Long-lived per-entity state | -| **Temporary** | Automatic expiry after TTL | Cheapest writes | Not currently used | +| Tier | Lifetime | Rent | Typical use | +| -------------- | ---------------------------------------- | ---------------------------------------- | ------------------------------------------------------------------- | +| **Instance** | As long as the contract instance is live | Bumped automatically on every invocation | Config set once, read often; global state read on almost every call | +| **Persistent** | Until TTL expires (rent must be paid) | Charged per byte per ledger | Long-lived per-entity state | +| **Temporary** | Automatic expiry after TTL | Cheapest writes | Not currently used | See [ADR-002](../adr/002-storage-patterns.md) for the rationale behind this partitioning. @@ -24,62 +24,62 @@ Every storage key prefix across both contracts, its Rust value type, storage tie the contract/module that owns it. This is a flat index for quick lookup; see the per-contract sections below for field-level detail, size estimates, and access patterns. -| Prefix | Rust type | Tier | Owner | -|--------|-----------|------|-------| -| `StateVersion` | `u32` | Instance | `project_registry` | -| `Whitelister` | `Address` | Instance | `project_registry` | -| `ProjectCounter` | `u32` | Instance | `project_registry` | -| `ProposalCounter` | `u32` | Instance | `project_registry` | -| `MultiSigSigners` | `Vec
` | Instance | `project_registry` | -| `MultiSigThreshold` | `u32` | Instance | `project_registry` | -| `Paused` | `bool` | Instance | `project_registry` | -| `EmergencyAdmin` | `Option
` | Instance | `project_registry` | -| `Project(u32)` | `ProjectData` | Persistent | `project_registry` | -| `Whitelist(Address)` | `bool` | Persistent | `project_registry` | -| `Proposal(u32)` | `Proposal` | Persistent | `project_registry` | -| `HasVoted(u32, Address)` | `bool` | Persistent | `project_registry` | -| `Collateral(u32, Address)` | `i128` | Persistent | `project_registry` | -| `CreatorReputation(Address)` | `u32` | Persistent | `project_registry` | -| `Arch(u32)` | `ArchiveSummary` | Persistent | `project_registry` | -| `ScoreHistorySlot(u32, u32)` | `ScoreHistoryEntry` | Persistent | `project_registry` | -| `ScoreHistoryTotal(u32)` | `u32` | Persistent | `project_registry` | -| `StateVersion` | `u32` | Instance | `investment_vault` | -| `UsdcSac` | `Address` | Instance | `investment_vault` | -| `Registry` | `Address` | Instance | `investment_vault` | -| `CachedTotalAssets` | `i128` | Instance | `investment_vault` | -| `ManagementFeeBps` | `u32` | Instance | `investment_vault` | -| `ManagementFeeRecipient` | `Address` | Instance | `investment_vault` | -| `TradingEnabled` | `bool` | Instance | `investment_vault` | -| `MinCreditQuality` | `u32` | Instance | `investment_vault` | -| `MinGreenImpact` | `u32` | Instance | `investment_vault` | -| `Bridge` | `Address` | Instance | `investment_vault` | -| `FlashLoanFee` | `i128` | Instance | `investment_vault` | -| `CarbonOracle` | `Address` | Instance | `investment_vault` | -| `CarbonCreditPrice` | `i128` | Instance | `investment_vault` | -| `MaxTransactionAmount` | `i128` | Instance | `investment_vault` | -| `MultiSigSigners` | `Vec
` | Instance | `investment_vault` | -| `MultiSigThreshold` | `u32` | Instance | `investment_vault` | -| `Paused` | `bool` | Instance | `investment_vault` | -| `ComplianceEventCounter` | `u64` | Instance | `investment_vault` | -| `ReportingSnapshot` | `ReportingSnapshotData` | Instance | `investment_vault` | -| `EmergencyAdmin` | `Option
` | Instance | `investment_vault` | -| `CachedExpectedReturns` | `i128` | Persistent (dead — never read, see Migration notes) | `investment_vault` | -| `TotalInvestments` | `i128` | Persistent | `investment_vault` | -| `ProjectInvestment(u32)` | `i128` | Persistent | `investment_vault` | -| `YieldPerShareAccum` | `i128` | Persistent | `investment_vault` | -| `YieldDebt(Address)` | `i128` | Persistent | `investment_vault` | -| `InsuranceFund` | `i128` | Persistent | `investment_vault` | -| `InsuranceClaimed(u32)` | `bool` | Persistent | `investment_vault` | -| `TotalDeposited(Address)` | `i128` | Persistent | `investment_vault` | -| `QueueHead` | `u64` | Persistent | `investment_vault` | -| `QueueTail` | `u64` | Persistent | `investment_vault` | -| `QueueEntry(u64)` | `QueuedClaim` | Persistent | `investment_vault` | -| `CarbonCreditBalance(Address)` | `i128` | Persistent | `investment_vault` | -| `ComplianceEvent(u64)` | `ComplianceEventData` | Persistent | `investment_vault` | -| `LastDeposit(Address)` | `u32` | Persistent | `investment_vault` | -| `WormholeCore` | `Address` | Instance | `investment_vault` (`BridgeDataKey`) | -| `TrustedEmitter(u32, BytesN<32>)` | `bool` | Persistent | `investment_vault` (`BridgeDataKey`) | -| `ConsumedVaa(BytesN<32>)` | `bool` | Persistent | `investment_vault` (`BridgeDataKey`) | +| Prefix | Rust type | Tier | Owner | +| --------------------------------- | ----------------------- | --------------------------------------------------- | ------------------------------------ | +| `StateVersion` | `u32` | Instance | `project_registry` | +| `Whitelister` | `Address` | Instance | `project_registry` | +| `ProjectCounter` | `u32` | Instance | `project_registry` | +| `ProposalCounter` | `u32` | Instance | `project_registry` | +| `MultiSigSigners` | `Vec
` | Instance | `project_registry` | +| `MultiSigThreshold` | `u32` | Instance | `project_registry` | +| `Paused` | `bool` | Instance | `project_registry` | +| `EmergencyAdmin` | `Option
` | Instance | `project_registry` | +| `Project(u32)` | `ProjectData` | Persistent | `project_registry` | +| `Whitelist(Address)` | `bool` | Persistent | `project_registry` | +| `Proposal(u32)` | `Proposal` | Persistent | `project_registry` | +| `HasVoted(u32, Address)` | `bool` | Persistent | `project_registry` | +| `Collateral(u32, Address)` | `i128` | Persistent | `project_registry` | +| `CreatorReputation(Address)` | `u32` | Persistent | `project_registry` | +| `Arch(u32)` | `ArchiveSummary` | Persistent | `project_registry` | +| `ScoreHistorySlot(u32, u32)` | `ScoreHistoryEntry` | Persistent | `project_registry` | +| `ScoreHistoryTotal(u32)` | `u32` | Persistent | `project_registry` | +| `StateVersion` | `u32` | Instance | `investment_vault` | +| `UsdcSac` | `Address` | Instance | `investment_vault` | +| `Registry` | `Address` | Instance | `investment_vault` | +| `CachedTotalAssets` | `i128` | Instance | `investment_vault` | +| `ManagementFeeBps` | `u32` | Instance | `investment_vault` | +| `ManagementFeeRecipient` | `Address` | Instance | `investment_vault` | +| `TradingEnabled` | `bool` | Instance | `investment_vault` | +| `MinCreditQuality` | `u32` | Instance | `investment_vault` | +| `MinGreenImpact` | `u32` | Instance | `investment_vault` | +| `Bridge` | `Address` | Instance | `investment_vault` | +| `FlashLoanFee` | `i128` | Instance | `investment_vault` | +| `CarbonOracle` | `Address` | Instance | `investment_vault` | +| `CarbonCreditPrice` | `i128` | Instance | `investment_vault` | +| `MaxTransactionAmount` | `i128` | Instance | `investment_vault` | +| `MultiSigSigners` | `Vec
` | Instance | `investment_vault` | +| `MultiSigThreshold` | `u32` | Instance | `investment_vault` | +| `Paused` | `bool` | Instance | `investment_vault` | +| `ComplianceEventCounter` | `u64` | Instance | `investment_vault` | +| `ReportingSnapshot` | `ReportingSnapshotData` | Instance | `investment_vault` | +| `EmergencyAdmin` | `Option
` | Instance | `investment_vault` | +| `CachedExpectedReturns` | `i128` | Persistent (dead — never read, see Migration notes) | `investment_vault` | +| `TotalInvestments` | `i128` | Persistent | `investment_vault` | +| `ProjectInvestment(u32)` | `i128` | Persistent | `investment_vault` | +| `YieldPerShareAccum` | `i128` | Persistent | `investment_vault` | +| `YieldDebt(Address)` | `i128` | Persistent | `investment_vault` | +| `InsuranceFund` | `i128` | Persistent | `investment_vault` | +| `InsuranceClaimed(u32)` | `bool` | Persistent | `investment_vault` | +| `TotalDeposited(Address)` | `i128` | Persistent | `investment_vault` | +| `QueueHead` | `u64` | Persistent | `investment_vault` | +| `QueueTail` | `u64` | Persistent | `investment_vault` | +| `QueueEntry(u64)` | `QueuedClaim` | Persistent | `investment_vault` | +| `CarbonCreditBalance(Address)` | `i128` | Persistent | `investment_vault` | +| `ComplianceEvent(u64)` | `ComplianceEventData` | Persistent | `investment_vault` | +| `LastDeposit(Address)` | `u32` | Persistent | `investment_vault` | +| `WormholeCore` | `Address` | Instance | `investment_vault` (`BridgeDataKey`) | +| `TrustedEmitter(u32, BytesN<32>)` | `bool` | Persistent | `investment_vault` (`BridgeDataKey`) | +| `ConsumedVaa(BytesN<32>)` | `bool` | Persistent | `investment_vault` (`BridgeDataKey`) | `StateVersion`, `MultiSigSigners`, `MultiSigThreshold`, `Paused`, and `EmergencyAdmin` are independent per-contract prefixes: each contract's `DataKey`/`VaultKey` enum is scoped to @@ -93,10 +93,10 @@ lives alongside `VaultKey` for the Wormhole bridge feature. Soroban encodes `#[contracttype]` enum keys as XDR `SCVal`. The variant name is stored as a `Symbol`; shorter names reduce per-key byte cost. -| Key shape | XDR encoding | Approximate key bytes | -|-----------|-------------|----------------------| -| Unit variant (e.g. `StateVersion`) | `Symbol("StateVersion")` | name length + 4 overhead | -| Tuple variant (e.g. `Project(u32)`) | `Map {Symbol("Project") → u32}` | name length + 4 + 4 (u32) | +| Key shape | XDR encoding | Approximate key bytes | +| --------------------------------------------------- | ---------------------------------- | --------------------------- | +| Unit variant (e.g. `StateVersion`) | `Symbol("StateVersion")` | name length + 4 overhead | +| Tuple variant (e.g. `Project(u32)`) | `Map {Symbol("Project") → u32}` | name length + 4 + 4 (u32) | | Tuple variant with Address (e.g. `Whitelist(addr)`) | `Map {Symbol("Whitelist") → addr}` | name length + 4 + 32 (addr) | **New keys should use short variant names** (4 characters or fewer where practical) to reduce per-entry cost. Existing names are stable after deployment — do not rename variants without a migration. @@ -111,26 +111,26 @@ Example: `DataKey::Arch(u32)` (4 chars) vs `ArchiveSummary(u32)` (13 chars) save All configuration and counters are in instance storage. The instance TTL is bumped on every contract invocation, so no explicit TTL management is needed. -| Key (`DataKey` variant) | Rust type | Key bytes | Description | -|-------------------------|-----------|-----------|-------------| -| `StateVersion` | `u32` | ~16 | Storage schema version | -| `Whitelister` | `Address` | ~18 | Address authorised to whitelist creators | -| `ProjectCounter` | `u32` | ~18 | Auto-incrementing project ID | -| `ProposalCounter` | `u32` | ~20 | Auto-incrementing governance proposal ID | -| `MultiSigSigners` | `Vec
` | ~18 | Multi-sig signer set for admin ops | -| `MultiSigThreshold` | `u32` | ~22 | Required approval count | +| Key (`DataKey` variant) | Rust type | Key bytes | Description | +| ----------------------- | -------------- | --------- | ---------------------------------------- | +| `StateVersion` | `u32` | ~16 | Storage schema version | +| `Whitelister` | `Address` | ~18 | Address authorised to whitelist creators | +| `ProjectCounter` | `u32` | ~18 | Auto-incrementing project ID | +| `ProposalCounter` | `u32` | ~20 | Auto-incrementing governance proposal ID | +| `MultiSigSigners` | `Vec
` | ~18 | Multi-sig signer set for admin ops | +| `MultiSigThreshold` | `u32` | ~22 | Required approval count | ### Persistent storage -| Key | Rust type | Key bytes | Value bytes (approx) | Description | -|-----|-----------|-----------|---------------------|-------------| -| `DataKey::Project(u32)` | `ProjectData` | ~12 | ~132–580 | Full project record keyed by ID | -| `DataKey::Whitelist(Address)` | `bool` | ~42 | 1 | `true` if address is whitelisted | -| `DataKey::Proposal(u32)` | `Proposal` | ~13 | ~100+ | Governance proposal keyed by ID | -| `DataKey::HasVoted(u32, Address)` | `bool` | ~47 | 1 | `true` if address has voted on proposal | -| `DataKey::Collateral(u32, Address)` | `i128` | ~47 | 16 | Collateral balance for (project, token) | -| `DataKey::CreatorReputation(Address)` | `u32` | ~49 | 4 | Reputation score 0–100 for a creator | -| `DataKey::Arch(u32)` | `ArchiveSummary` | ~9 | ~52 | Compact record for compacted projects (#73) | +| Key | Rust type | Key bytes | Value bytes (approx) | Description | +| ------------------------------------- | ---------------- | --------- | -------------------- | ------------------------------------------- | +| `DataKey::Project(u32)` | `ProjectData` | ~12 | ~132–580 | Full project record keyed by ID | +| `DataKey::Whitelist(Address)` | `bool` | ~42 | 1 | `true` if address is whitelisted | +| `DataKey::Proposal(u32)` | `Proposal` | ~13 | ~100+ | Governance proposal keyed by ID | +| `DataKey::HasVoted(u32, Address)` | `bool` | ~47 | 1 | `true` if address has voted on proposal | +| `DataKey::Collateral(u32, Address)` | `i128` | ~47 | 16 | Collateral balance for (project, token) | +| `DataKey::CreatorReputation(Address)` | `u32` | ~49 | 4 | Reputation score 0–100 for a creator | +| `DataKey::Arch(u32)` | `ArchiveSummary` | ~9 | ~52 | Compact record for compacted projects (#73) | #### `ProjectData` layout @@ -184,27 +184,27 @@ pub struct Proposal { All configuration and global aggregate caches are in instance storage. -| Key (`VaultKey` variant) | Rust type | Key bytes | Description | -|--------------------------|-----------|-----------|-------------| -| `StateVersion` | `u32` | ~16 | Storage schema version | -| `UsdcSac` | `Address` | ~11 | USDC Stellar Asset Contract address | -| `Registry` | `Address` | ~12 | `project_registry` contract address | -| `ManagementFeeBps` | `u32` | ~20 | Optional management fee in bps (0–500) | -| `ManagementFeeRecipient` | `Address` | ~27 | Fee recipient address | -| `TradingEnabled` | `bool` | ~18 | Whether secondary market trading is active | -| `MinCreditQuality` | `u32` | ~20 | Minimum credit quality threshold for funding | -| `MinGreenImpact` | `u32` | ~17 | Minimum green impact threshold for funding | -| `Bridge` | `Address` | ~9 | Bridge contract address | -| `FlashLoanFee` | `i128` | ~16 | Flash loan fee in bps | -| `CarbonOracle` | `Address` | ~15 | Carbon credit oracle address | -| `CarbonCreditPrice` | `i128` | ~21 | Carbon credit price in USD micro-units | -| `MaxTransactionAmount` | `i128` | ~25 | Compliance transaction limit (0 = no limit) | -| `MultiSigSigners` | `Vec
` | ~18 | Multi-sig signer set | -| `MultiSigThreshold` | `u32` | ~22 | Required approval count | -| `Paused` | `bool` | ~9 | Circuit-breaker pause state | -| `ComplianceEventCounter` | `u64` | ~27 | Compliance event sequence counter | -| `ReportingSnapshot` | `ReportingSnapshotData` | ~22 | Latest regulatory snapshot | -| `CachedTotalAssets` | `i128` | ~21 | NAV cache — updated on deposit/withdraw/yield (#85) | +| Key (`VaultKey` variant) | Rust type | Key bytes | Description | +| ------------------------ | ----------------------- | --------- | --------------------------------------------------- | +| `StateVersion` | `u32` | ~16 | Storage schema version | +| `UsdcSac` | `Address` | ~11 | USDC Stellar Asset Contract address | +| `Registry` | `Address` | ~12 | `project_registry` contract address | +| `ManagementFeeBps` | `u32` | ~20 | Optional management fee in bps (0–500) | +| `ManagementFeeRecipient` | `Address` | ~27 | Fee recipient address | +| `TradingEnabled` | `bool` | ~18 | Whether secondary market trading is active | +| `MinCreditQuality` | `u32` | ~20 | Minimum credit quality threshold for funding | +| `MinGreenImpact` | `u32` | ~17 | Minimum green impact threshold for funding | +| `Bridge` | `Address` | ~9 | Bridge contract address | +| `FlashLoanFee` | `i128` | ~16 | Flash loan fee in bps | +| `CarbonOracle` | `Address` | ~15 | Carbon credit oracle address | +| `CarbonCreditPrice` | `i128` | ~21 | Carbon credit price in USD micro-units | +| `MaxTransactionAmount` | `i128` | ~25 | Compliance transaction limit (0 = no limit) | +| `MultiSigSigners` | `Vec
` | ~18 | Multi-sig signer set | +| `MultiSigThreshold` | `u32` | ~22 | Required approval count | +| `Paused` | `bool` | ~9 | Circuit-breaker pause state | +| `ComplianceEventCounter` | `u64` | ~27 | Compliance event sequence counter | +| `ReportingSnapshot` | `ReportingSnapshotData` | ~22 | Latest regulatory snapshot | +| `CachedTotalAssets` | `i128` | ~21 | NAV cache — updated on deposit/withdraw/yield (#85) | `CachedTotalAssets` was moved from persistent to instance storage (#85): it is written on almost every state-changing operation and read on every asset query, so instance storage eliminates separate persistent reads and removes its individual rent obligation. @@ -212,21 +212,21 @@ All configuration and global aggregate caches are in instance storage. ### Persistent storage -| Key | Rust type | Key bytes | Value bytes | Description | -|-----|-----------|-----------|-------------|-------------| -| `VaultKey::TotalInvestments` | `i128` | ~21 | 16 | Cumulative USDC sent to projects | -| `VaultKey::ProjectInvestment(u32)` | `i128` | ~25 | 16 | USDC invested in a specific project | -| `VaultKey::YieldPerShareAccum` | `i128` | ~24 | 16 | Global yield-per-share accumulator (×10¹⁸) | -| `VaultKey::YieldDebt(Address)` | `i128` | ~42 | 16 | Per-investor yield checkpoint at last claim | -| `VaultKey::InsuranceFund` | `i128` | ~17 | 16 | Insurance fund USDC balance | -| `VaultKey::InsuranceClaimed(u32)` | `bool` | ~23 | 1 | `true` once insurance payout made for project | -| `VaultKey::TotalDeposited(Address)` | `i128` | ~46 | 16 | Lifetime USDC deposited by an investor | -| `VaultKey::QueueHead` | `u64` | ~14 | 8 | Oldest unprocessed redemption queue entry | -| `VaultKey::QueueTail` | `u64` | ~14 | 8 | Next free redemption queue index | -| `VaultKey::QueueEntry(u64)` | `QueuedClaim` | ~15 | ~48 | A queued redemption by index | -| `VaultKey::CarbonCreditBalance(Address)` | `i128` | ~30 | 16 | Carbon credit balance per address | -| `VaultKey::ComplianceEvent(u64)` | `ComplianceEventData` | ~22 | ~100+ | A compliance event record | -| `VaultKey::InsuranceClaimed(u32)` | `bool` | ~23 | 1 | One-time insurance claim flag per project | +| Key | Rust type | Key bytes | Value bytes | Description | +| ---------------------------------------- | --------------------- | --------- | ----------- | --------------------------------------------- | +| `VaultKey::TotalInvestments` | `i128` | ~21 | 16 | Cumulative USDC sent to projects | +| `VaultKey::ProjectInvestment(u32)` | `i128` | ~25 | 16 | USDC invested in a specific project | +| `VaultKey::YieldPerShareAccum` | `i128` | ~24 | 16 | Global yield-per-share accumulator (×10¹⁸) | +| `VaultKey::YieldDebt(Address)` | `i128` | ~42 | 16 | Per-investor yield checkpoint at last claim | +| `VaultKey::InsuranceFund` | `i128` | ~17 | 16 | Insurance fund USDC balance | +| `VaultKey::InsuranceClaimed(u32)` | `bool` | ~23 | 1 | `true` once insurance payout made for project | +| `VaultKey::TotalDeposited(Address)` | `i128` | ~46 | 16 | Lifetime USDC deposited by an investor | +| `VaultKey::QueueHead` | `u64` | ~14 | 8 | Oldest unprocessed redemption queue entry | +| `VaultKey::QueueTail` | `u64` | ~14 | 8 | Next free redemption queue index | +| `VaultKey::QueueEntry(u64)` | `QueuedClaim` | ~15 | ~48 | A queued redemption by index | +| `VaultKey::CarbonCreditBalance(Address)` | `i128` | ~30 | 16 | Carbon credit balance per address | +| `VaultKey::ComplianceEvent(u64)` | `ComplianceEventData` | ~22 | ~100+ | A compliance event record | +| `VaultKey::InsuranceClaimed(u32)` | `bool` | ~23 | 1 | One-time insurance claim flag per project | --- @@ -274,16 +274,16 @@ Call `compact_archive(project_id)` when ALL of the following hold: Soroban charges rent based on **entry size in bytes × ledger TTL**. The following are rough estimates. -| Entry | Key bytes | Value bytes | Total | Notes | -|-------|-----------|-------------|-------|-------| -| `ProjectData` (max URI) | ~12 | ~580 | ~592 | Dominant cost per project | -| `ProjectData` (64-byte IPFS CID) | ~12 | ~132 | ~144 | Typical cost | -| `ArchiveSummary` | ~9 | ~52 | ~61 | After `compact_archive` | -| `Proposal` (short description) | ~13 | ~100 | ~113 | Depends on description length | -| `HasVoted(id, addr)` | ~47 | 1 | ~48 | One per voter per proposal | -| `YieldDebt(addr)` | ~42 | 16 | ~58 | One per investor who claims yield | -| `TotalDeposited(addr)` | ~46 | 16 | ~62 | One per depositing investor | -| `ProjectInvestment(id)` | ~25 | 16 | ~41 | One per funded project | +| Entry | Key bytes | Value bytes | Total | Notes | +| -------------------------------- | --------- | ----------- | ----- | --------------------------------- | +| `ProjectData` (max URI) | ~12 | ~580 | ~592 | Dominant cost per project | +| `ProjectData` (64-byte IPFS CID) | ~12 | ~132 | ~144 | Typical cost | +| `ArchiveSummary` | ~9 | ~52 | ~61 | After `compact_archive` | +| `Proposal` (short description) | ~13 | ~100 | ~113 | Depends on description length | +| `HasVoted(id, addr)` | ~47 | 1 | ~48 | One per voter per proposal | +| `YieldDebt(addr)` | ~42 | 16 | ~58 | One per investor who claims yield | +| `TotalDeposited(addr)` | ~46 | 16 | ~62 | One per depositing investor | +| `ProjectInvestment(id)` | ~25 | 16 | ~41 | One per funded project | Instance storage is billed as a single ledger entry for all instance keys combined; total instance size for the vault is approximately 400–600 bytes (configuration only, no per-entity data). @@ -293,11 +293,11 @@ Instance storage is billed as a single ledger entry for all instance keys combin Cross-contract calls are the most expensive single operation in Soroban. Each call costs several thousand instructions beyond the callee's own work. -| Operation | Cross-contract calls | Notes | -|-----------|---------------------|-------| -| `fund_project` | 1 (`get_project`) | Previously 2 — `total_projects()` removed (#87) | -| `get_expected_returns` | 1 + N (`total_projects` + per-funded project `get_project`) | N = number of funded projects | -| `calculate_carbon_credits` | 1 (`get_project`) | Cannot be reduced further | +| Operation | Cross-contract calls | Notes | +| -------------------------- | ----------------------------------------------------------- | ----------------------------------------------- | +| `fund_project` | 1 (`get_project`) | Previously 2 — `total_projects()` removed (#87) | +| `get_expected_returns` | 1 + N (`total_projects` + per-funded project `get_project`) | N = number of funded projects | +| `calculate_carbon_credits` | 1 (`get_project`) | Cannot be reduced further | `fund_project_internal` was optimised to make a single cross-contract call (`get_project`) instead of two (`total_projects` + `get_project`). The project-not-found case is handled by `get_project`'s own error path. A local check (`project_id == 0`) rejects the invalid zero ID without a cross-contract call. @@ -305,24 +305,24 @@ Cross-contract calls are the most expensive single operation in Soroban. Each ca ## Access patterns -| Operation | Keys read | Keys written | -|-----------|-----------|--------------| -| `create_project` | `StateVersion`, `Whitelist(creator)`, `ProjectCounter` | `Project(id)`, `ProjectCounter` | -| `archive_project` | `Project(id)` | `Project(id)` | -| `compact_archive` | `Project(id)` | `Arch(id)` — removes `Project(id)` | -| `get_archive_summary` | `Arch(id)` | — | -| `update_impact_score` | `Project(id)` | `Project(id)` (skipped if no-op) | -| `certify_project` | `Whitelister`, owner (via `get_owner`) | `Project(id)` | -| `create_proposal` | `ProposalCounter` | `Proposal(id)`, `ProposalCounter` | -| `cast_vote` | `HasVoted(id, addr)`, `Proposal(id)` | `Proposal(id)`, `HasVoted(id, addr)` | -| `execute_proposal` | `Proposal(id)` | `Proposal(id)` | -| `deposit` | `UsdcSac`, `InsuranceFund`, `TotalDeposited(from)`, `CachedTotalAssets` | `InsuranceFund`, `TotalDeposited(from)`, `CachedTotalAssets` | -| `withdraw` | `UsdcSac`, `CachedTotalAssets` | `CachedTotalAssets` | -| `fund_project` | `Registry`, `UsdcSac`, `InsuranceFund`, `ProjectInvestment(id)`, `TotalInvestments` + 1 cross-contract `get_project` | `ProjectInvestment(id)`, `TotalInvestments` | -| `receive_yield` | `YieldPerShareAccum` | `YieldPerShareAccum` | -| `claim_yield` | `YieldPerShareAccum`, `YieldDebt(from)`, `UsdcSac`, `CachedTotalAssets` | `YieldDebt(from)`, `CachedTotalAssets` | -| `get_portfolio` | `YieldPerShareAccum`, `YieldDebt(addr)`, `TotalDeposited(addr)` | — | -| `claim_insurance` | `InsuranceFund`, `InsuranceClaimed(id)` | `InsuranceFund`, `InsuranceClaimed(id)` | +| Operation | Keys read | Keys written | +| --------------------- | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | +| `create_project` | `StateVersion`, `Whitelist(creator)`, `ProjectCounter` | `Project(id)`, `ProjectCounter` | +| `archive_project` | `Project(id)` | `Project(id)` | +| `compact_archive` | `Project(id)` | `Arch(id)` — removes `Project(id)` | +| `get_archive_summary` | `Arch(id)` | — | +| `update_impact_score` | `Project(id)` | `Project(id)` (skipped if no-op) | +| `certify_project` | `Whitelister`, owner (via `get_owner`) | `Project(id)` | +| `create_proposal` | `ProposalCounter` | `Proposal(id)`, `ProposalCounter` | +| `cast_vote` | `HasVoted(id, addr)`, `Proposal(id)` | `Proposal(id)`, `HasVoted(id, addr)` | +| `execute_proposal` | `Proposal(id)` | `Proposal(id)` | +| `deposit` | `UsdcSac`, `InsuranceFund`, `TotalDeposited(from)`, `CachedTotalAssets` | `InsuranceFund`, `TotalDeposited(from)`, `CachedTotalAssets` | +| `withdraw` | `UsdcSac`, `CachedTotalAssets` | `CachedTotalAssets` | +| `fund_project` | `Registry`, `UsdcSac`, `InsuranceFund`, `ProjectInvestment(id)`, `TotalInvestments` + 1 cross-contract `get_project` | `ProjectInvestment(id)`, `TotalInvestments` | +| `receive_yield` | `YieldPerShareAccum` | `YieldPerShareAccum` | +| `claim_yield` | `YieldPerShareAccum`, `YieldDebt(from)`, `UsdcSac`, `CachedTotalAssets` | `YieldDebt(from)`, `CachedTotalAssets` | +| `get_portfolio` | `YieldPerShareAccum`, `YieldDebt(addr)`, `TotalDeposited(addr)` | — | +| `claim_insurance` | `InsuranceFund`, `InsuranceClaimed(id)` | `InsuranceFund`, `InsuranceClaimed(id)` | --- diff --git a/gas-budgets.json b/gas-budgets.json index 99447b9..0e917fa 100644 --- a/gas-budgets.json +++ b/gas-budgets.json @@ -15,14 +15,45 @@ }, "storage_key_sizes": { "note": "Key bytes are XDR Symbol overhead + variant name length (#82). Shorter names reduce per-entry rent.", - "project_registry.Project(u32)": { "key_bytes": 12, "value_bytes_typical": 132, "value_bytes_max": 580 }, - "project_registry.Arch(u32)": { "key_bytes": 9, "value_bytes": 52, "note": "Compact archive key — 4-char name demonstrates compact key pattern (#73, #82)" }, - "project_registry.Whitelist(Address)": { "key_bytes": 42, "value_bytes": 1 }, - "project_registry.HasVoted(u32,Address)": { "key_bytes": 47, "value_bytes": 1 }, - "project_registry.Collateral(u32,Address)": { "key_bytes": 47, "value_bytes": 16 }, - "investment_vault.ProjectInvestment(u32)": { "key_bytes": 25, "value_bytes": 16 }, - "investment_vault.YieldDebt(Address)": { "key_bytes": 42, "value_bytes": 16 }, - "investment_vault.TotalDeposited(Address)": { "key_bytes": 46, "value_bytes": 16 }, - "investment_vault.CachedTotalAssets": { "key_bytes": 21, "value_bytes": 16, "storage_tier": "instance", "note": "Moved to instance (#85): read/written on every state change, no separate rent needed" } + "project_registry.Project(u32)": { + "key_bytes": 12, + "value_bytes_typical": 132, + "value_bytes_max": 580 + }, + "project_registry.Arch(u32)": { + "key_bytes": 9, + "value_bytes": 52, + "note": "Compact archive key — 4-char name demonstrates compact key pattern (#73, #82)" + }, + "project_registry.Whitelist(Address)": { + "key_bytes": 42, + "value_bytes": 1 + }, + "project_registry.HasVoted(u32,Address)": { + "key_bytes": 47, + "value_bytes": 1 + }, + "project_registry.Collateral(u32,Address)": { + "key_bytes": 47, + "value_bytes": 16 + }, + "investment_vault.ProjectInvestment(u32)": { + "key_bytes": 25, + "value_bytes": 16 + }, + "investment_vault.YieldDebt(Address)": { + "key_bytes": 42, + "value_bytes": 16 + }, + "investment_vault.TotalDeposited(Address)": { + "key_bytes": 46, + "value_bytes": 16 + }, + "investment_vault.CachedTotalAssets": { + "key_bytes": 21, + "value_bytes": 16, + "storage_tier": "instance", + "note": "Moved to instance (#85): read/written on every state change, no separate rent needed" + } } } diff --git a/investment_vault/test_snapshots/test/bench_batch_deposit_vs_equivalent_single_deposits.1.json b/investment_vault/test_snapshots/test/bench_batch_deposit_vs_equivalent_single_deposits.1.json index 2b21f8e..f881e02 100644 --- a/investment_vault/test_snapshots/test/bench_batch_deposit_vs_equivalent_single_deposits.1.json +++ b/investment_vault/test_snapshots/test/bench_batch_deposit_vs_equivalent_single_deposits.1.json @@ -1308,4 +1308,4 @@ "failed_call": false } ] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/bench_batch_deposit_vs_equivalent_single_deposits.2.json b/investment_vault/test_snapshots/test/bench_batch_deposit_vs_equivalent_single_deposits.2.json index 3c9e9f0..edac554 100644 --- a/investment_vault/test_snapshots/test/bench_batch_deposit_vs_equivalent_single_deposits.2.json +++ b/investment_vault/test_snapshots/test/bench_batch_deposit_vs_equivalent_single_deposits.2.json @@ -1173,4 +1173,4 @@ "failed_call": false } ] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/bench_vault_batch_deposit_two_accounts.1.json b/investment_vault/test_snapshots/test/bench_vault_batch_deposit_two_accounts.1.json index 2b21f8e..f881e02 100644 --- a/investment_vault/test_snapshots/test/bench_vault_batch_deposit_two_accounts.1.json +++ b/investment_vault/test_snapshots/test/bench_vault_batch_deposit_two_accounts.1.json @@ -1308,4 +1308,4 @@ "failed_call": false } ] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/bench_vault_deposit.1.json b/investment_vault/test_snapshots/test/bench_vault_deposit.1.json index 98dbe08..7423b32 100644 --- a/investment_vault/test_snapshots/test/bench_vault_deposit.1.json +++ b/investment_vault/test_snapshots/test/bench_vault_deposit.1.json @@ -935,4 +935,4 @@ "failed_call": false } ] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_all_only_owner_functions_reject_non_admin_caller.1.json b/investment_vault/test_snapshots/test/test_all_only_owner_functions_reject_non_admin_caller.1.json index c4282fd..3c3852d 100644 --- a/investment_vault/test_snapshots/test/test_all_only_owner_functions_reject_non_admin_caller.1.json +++ b/investment_vault/test_snapshots/test/test_all_only_owner_functions_reject_non_admin_caller.1.json @@ -553,4 +553,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_batch_deposit_mints_for_each_investor.1.json b/investment_vault/test_snapshots/test/test_batch_deposit_mints_for_each_investor.1.json index 8e8d1da..fa5be9d 100644 --- a/investment_vault/test_snapshots/test/test_batch_deposit_mints_for_each_investor.1.json +++ b/investment_vault/test_snapshots/test/test_batch_deposit_mints_for_each_investor.1.json @@ -1107,4 +1107,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_batch_fund_projects_rolls_back_all_events_and_state_on_later_panic.1.json b/investment_vault/test_snapshots/test/test_batch_fund_projects_rolls_back_all_events_and_state_on_later_panic.1.json index 788634b..f67bca9 100644 --- a/investment_vault/test_snapshots/test/test_batch_fund_projects_rolls_back_all_events_and_state_on_later_panic.1.json +++ b/investment_vault/test_snapshots/test/test_batch_fund_projects_rolls_back_all_events_and_state_on_later_panic.1.json @@ -1367,4 +1367,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_claim_queued_emits_event.1.json b/investment_vault/test_snapshots/test/test_claim_queued_emits_event.1.json index 677cb79..8274de4 100644 --- a/investment_vault/test_snapshots/test/test_claim_queued_emits_event.1.json +++ b/investment_vault/test_snapshots/test/test_claim_queued_emits_event.1.json @@ -1581,4 +1581,4 @@ "failed_call": false } ] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_claim_settles_queued_redemption.1.json b/investment_vault/test_snapshots/test/test_claim_settles_queued_redemption.1.json index 66bb372..a370314 100644 --- a/investment_vault/test_snapshots/test/test_claim_settles_queued_redemption.1.json +++ b/investment_vault/test_snapshots/test/test_claim_settles_queued_redemption.1.json @@ -1513,4 +1513,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_compact_storage_removes_zero_project_investment.1.json b/investment_vault/test_snapshots/test/test_compact_storage_removes_zero_project_investment.1.json index b06a76c..3174df1 100644 --- a/investment_vault/test_snapshots/test/test_compact_storage_removes_zero_project_investment.1.json +++ b/investment_vault/test_snapshots/test/test_compact_storage_removes_zero_project_investment.1.json @@ -1215,4 +1215,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_concurrent_deposits_and_fund_project.1.json b/investment_vault/test_snapshots/test/test_concurrent_deposits_and_fund_project.1.json index 390e3e3..5ca5473 100644 --- a/investment_vault/test_snapshots/test/test_concurrent_deposits_and_fund_project.1.json +++ b/investment_vault/test_snapshots/test/test_concurrent_deposits_and_fund_project.1.json @@ -1704,4 +1704,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_constructor_panics_with_invalid_registry.1.json b/investment_vault/test_snapshots/test/test_constructor_panics_with_invalid_registry.1.json index 922c2af..fe94a9c 100644 --- a/investment_vault/test_snapshots/test/test_constructor_panics_with_invalid_registry.1.json +++ b/investment_vault/test_snapshots/test/test_constructor_panics_with_invalid_registry.1.json @@ -221,4 +221,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_conversion_empty_vault_is_1_to_1.1.json b/investment_vault/test_snapshots/test/test_conversion_empty_vault_is_1_to_1.1.json index fc69b93..038e845 100644 --- a/investment_vault/test_snapshots/test/test_conversion_empty_vault_is_1_to_1.1.json +++ b/investment_vault/test_snapshots/test/test_conversion_empty_vault_is_1_to_1.1.json @@ -517,4 +517,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_conversion_roundtrip_first_deposit_exact.1.json b/investment_vault/test_snapshots/test/test_conversion_roundtrip_first_deposit_exact.1.json index 0013294..082635d 100644 --- a/investment_vault/test_snapshots/test/test_conversion_roundtrip_first_deposit_exact.1.json +++ b/investment_vault/test_snapshots/test/test_conversion_roundtrip_first_deposit_exact.1.json @@ -511,4 +511,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_conversion_roundtrip_never_favors_withdrawer.1.json b/investment_vault/test_snapshots/test/test_conversion_roundtrip_never_favors_withdrawer.1.json index 41213c9..abfe50a 100644 --- a/investment_vault/test_snapshots/test/test_conversion_roundtrip_never_favors_withdrawer.1.json +++ b/investment_vault/test_snapshots/test/test_conversion_roundtrip_never_favors_withdrawer.1.json @@ -847,4 +847,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_convert_to_shares_and_assets_roundtrip.1.json b/investment_vault/test_snapshots/test/test_convert_to_shares_and_assets_roundtrip.1.json index c1f468a..dd36733 100644 --- a/investment_vault/test_snapshots/test/test_convert_to_shares_and_assets_roundtrip.1.json +++ b/investment_vault/test_snapshots/test/test_convert_to_shares_and_assets_roundtrip.1.json @@ -835,4 +835,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_deposit_at_minimum_succeeds.1.json b/investment_vault/test_snapshots/test/test_deposit_at_minimum_succeeds.1.json index 9c0136a..13dee4f 100644 --- a/investment_vault/test_snapshots/test/test_deposit_at_minimum_succeeds.1.json +++ b/investment_vault/test_snapshots/test/test_deposit_at_minimum_succeeds.1.json @@ -935,4 +935,4 @@ "failed_call": false } ] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_deposit_below_minimum_panics.1.json b/investment_vault/test_snapshots/test/test_deposit_below_minimum_panics.1.json index 67c17d0..da411fa 100644 --- a/investment_vault/test_snapshots/test/test_deposit_below_minimum_panics.1.json +++ b/investment_vault/test_snapshots/test/test_deposit_below_minimum_panics.1.json @@ -602,4 +602,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_deposit_blocked_when_vault_paused.1.json b/investment_vault/test_snapshots/test/test_deposit_blocked_when_vault_paused.1.json index 4a230ce..1e5a383 100644 --- a/investment_vault/test_snapshots/test/test_deposit_blocked_when_vault_paused.1.json +++ b/investment_vault/test_snapshots/test/test_deposit_blocked_when_vault_paused.1.json @@ -649,4 +649,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_deposit_emits_event.1.json b/investment_vault/test_snapshots/test/test_deposit_emits_event.1.json index 98dbe08..7423b32 100644 --- a/investment_vault/test_snapshots/test/test_deposit_emits_event.1.json +++ b/investment_vault/test_snapshots/test/test_deposit_emits_event.1.json @@ -935,4 +935,4 @@ "failed_call": false } ] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_deposit_proportional_after_first.1.json b/investment_vault/test_snapshots/test/test_deposit_proportional_after_first.1.json index b6bff21..f975edf 100644 --- a/investment_vault/test_snapshots/test/test_deposit_proportional_after_first.1.json +++ b/investment_vault/test_snapshots/test/test_deposit_proportional_after_first.1.json @@ -1383,4 +1383,4 @@ "failed_call": false } ] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_deposit_rejects_when_exceeding_max_hbs_supply.1.json b/investment_vault/test_snapshots/test/test_deposit_rejects_when_exceeding_max_hbs_supply.1.json index 52f42ec..3e0b5f4 100644 --- a/investment_vault/test_snapshots/test/test_deposit_rejects_when_exceeding_max_hbs_supply.1.json +++ b/investment_vault/test_snapshots/test/test_deposit_rejects_when_exceeding_max_hbs_supply.1.json @@ -3083,4 +3083,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_emergency_admin_can_pause_and_unpause_without_owner.1.json b/investment_vault/test_snapshots/test/test_emergency_admin_can_pause_and_unpause_without_owner.1.json index 1c74518..2ae048a 100644 --- a/investment_vault/test_snapshots/test/test_emergency_admin_can_pause_and_unpause_without_owner.1.json +++ b/investment_vault/test_snapshots/test/test_emergency_admin_can_pause_and_unpause_without_owner.1.json @@ -653,4 +653,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_emergency_pause_rejects_non_emergency_admin.1.json b/investment_vault/test_snapshots/test/test_emergency_pause_rejects_non_emergency_admin.1.json index 5e1ac56..de62e4c 100644 --- a/investment_vault/test_snapshots/test/test_emergency_pause_rejects_non_emergency_admin.1.json +++ b/investment_vault/test_snapshots/test/test_emergency_pause_rejects_non_emergency_admin.1.json @@ -559,4 +559,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_enable_secondary_trading.1.json b/investment_vault/test_snapshots/test/test_enable_secondary_trading.1.json index c427694..ff74396 100644 --- a/investment_vault/test_snapshots/test/test_enable_secondary_trading.1.json +++ b/investment_vault/test_snapshots/test/test_enable_secondary_trading.1.json @@ -555,4 +555,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_enable_secondary_trading_emits_event.1.json b/investment_vault/test_snapshots/test/test_enable_secondary_trading_emits_event.1.json index 4b7102f..8e44b73 100644 --- a/investment_vault/test_snapshots/test/test_enable_secondary_trading_emits_event.1.json +++ b/investment_vault/test_snapshots/test/test_enable_secondary_trading_emits_event.1.json @@ -584,4 +584,4 @@ "failed_call": false } ] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_fee_above_cap_panics.1.json b/investment_vault/test_snapshots/test/test_fee_above_cap_panics.1.json index ef83482..10844cc 100644 --- a/investment_vault/test_snapshots/test/test_fee_above_cap_panics.1.json +++ b/investment_vault/test_snapshots/test/test_fee_above_cap_panics.1.json @@ -508,4 +508,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_first_deposit_mints_1_to_1_shares.1.json b/investment_vault/test_snapshots/test/test_first_deposit_mints_1_to_1_shares.1.json index 450f0ab..9e54e5f 100644 --- a/investment_vault/test_snapshots/test/test_first_deposit_mints_1_to_1_shares.1.json +++ b/investment_vault/test_snapshots/test/test_first_deposit_mints_1_to_1_shares.1.json @@ -837,4 +837,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_flash_loan_fails_without_repayment.1.json b/investment_vault/test_snapshots/test/test_flash_loan_fails_without_repayment.1.json index 32d5ef3..aba7f9a 100644 --- a/investment_vault/test_snapshots/test/test_flash_loan_fails_without_repayment.1.json +++ b/investment_vault/test_snapshots/test/test_flash_loan_fails_without_repayment.1.json @@ -599,4 +599,4 @@ "failed_call": true } ] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_full_withdrawal_blocked_by_outstanding_investments.1.json b/investment_vault/test_snapshots/test/test_full_withdrawal_blocked_by_outstanding_investments.1.json index ea9eb0c..89ac607 100644 --- a/investment_vault/test_snapshots/test/test_full_withdrawal_blocked_by_outstanding_investments.1.json +++ b/investment_vault/test_snapshots/test/test_full_withdrawal_blocked_by_outstanding_investments.1.json @@ -1180,4 +1180,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_full_withdrawal_with_no_investments.1.json b/investment_vault/test_snapshots/test/test_full_withdrawal_with_no_investments.1.json index 85ca447..206fd17 100644 --- a/investment_vault/test_snapshots/test/test_full_withdrawal_with_no_investments.1.json +++ b/investment_vault/test_snapshots/test/test_full_withdrawal_with_no_investments.1.json @@ -880,4 +880,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_fund_project_allowed_when_thresholds_met.1.json b/investment_vault/test_snapshots/test/test_fund_project_allowed_when_thresholds_met.1.json index 612b066..f688df6 100644 --- a/investment_vault/test_snapshots/test/test_fund_project_allowed_when_thresholds_met.1.json +++ b/investment_vault/test_snapshots/test/test_fund_project_allowed_when_thresholds_met.1.json @@ -1373,4 +1373,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_fund_project_blocked_below_credit_threshold.1.json b/investment_vault/test_snapshots/test/test_fund_project_blocked_below_credit_threshold.1.json index 91bc673..f6ff1b9 100644 --- a/investment_vault/test_snapshots/test/test_fund_project_blocked_below_credit_threshold.1.json +++ b/investment_vault/test_snapshots/test/test_fund_project_blocked_below_credit_threshold.1.json @@ -1125,4 +1125,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_fund_project_blocked_below_green_threshold.1.json b/investment_vault/test_snapshots/test/test_fund_project_blocked_below_green_threshold.1.json index f5c1f9b..98f0bf2 100644 --- a/investment_vault/test_snapshots/test/test_fund_project_blocked_below_green_threshold.1.json +++ b/investment_vault/test_snapshots/test/test_fund_project_blocked_below_green_threshold.1.json @@ -1125,4 +1125,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_fund_project_cost_estimate.1.json b/investment_vault/test_snapshots/test/test_fund_project_cost_estimate.1.json index 963ccd5..c74f400 100644 --- a/investment_vault/test_snapshots/test/test_fund_project_cost_estimate.1.json +++ b/investment_vault/test_snapshots/test/test_fund_project_cost_estimate.1.json @@ -1249,4 +1249,4 @@ "failed_call": false } ] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_fund_project_emits_event.1.json b/investment_vault/test_snapshots/test/test_fund_project_emits_event.1.json index 65eece6..9994daa 100644 --- a/investment_vault/test_snapshots/test/test_fund_project_emits_event.1.json +++ b/investment_vault/test_snapshots/test/test_fund_project_emits_event.1.json @@ -1249,4 +1249,4 @@ "failed_call": false } ] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_fund_project_panics_when_amount_exceeds_available.1.json b/investment_vault/test_snapshots/test/test_fund_project_panics_when_amount_exceeds_available.1.json index 0f219a6..138b586 100644 --- a/investment_vault/test_snapshots/test/test_fund_project_panics_when_amount_exceeds_available.1.json +++ b/investment_vault/test_snapshots/test/test_fund_project_panics_when_amount_exceeds_available.1.json @@ -1059,4 +1059,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_fund_project_panics_when_fully_depleted.1.json b/investment_vault/test_snapshots/test/test_fund_project_panics_when_fully_depleted.1.json index bc4f3a1..2d88a2f 100644 --- a/investment_vault/test_snapshots/test/test_fund_project_panics_when_fully_depleted.1.json +++ b/investment_vault/test_snapshots/test/test_fund_project_panics_when_fully_depleted.1.json @@ -1180,4 +1180,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_fund_project_panics_with_out_of_range_project_id.1.json b/investment_vault/test_snapshots/test/test_fund_project_panics_with_out_of_range_project_id.1.json index ef77532..88ec381 100644 --- a/investment_vault/test_snapshots/test/test_fund_project_panics_with_out_of_range_project_id.1.json +++ b/investment_vault/test_snapshots/test/test_fund_project_panics_with_out_of_range_project_id.1.json @@ -834,4 +834,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_fund_project_panics_with_zero_project_id.1.json b/investment_vault/test_snapshots/test/test_fund_project_panics_with_zero_project_id.1.json index ef77532..88ec381 100644 --- a/investment_vault/test_snapshots/test/test_fund_project_panics_with_zero_project_id.1.json +++ b/investment_vault/test_snapshots/test/test_fund_project_panics_with_zero_project_id.1.json @@ -834,4 +834,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_fund_project_partial_funding_succeeds.1.json b/investment_vault/test_snapshots/test/test_fund_project_partial_funding_succeeds.1.json index ac9c18a..4c4a21f 100644 --- a/investment_vault/test_snapshots/test/test_fund_project_partial_funding_succeeds.1.json +++ b/investment_vault/test_snapshots/test/test_fund_project_partial_funding_succeeds.1.json @@ -1222,4 +1222,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_fund_project_records_investment.1.json b/investment_vault/test_snapshots/test/test_fund_project_records_investment.1.json index ef77532..88ec381 100644 --- a/investment_vault/test_snapshots/test/test_fund_project_records_investment.1.json +++ b/investment_vault/test_snapshots/test/test_fund_project_records_investment.1.json @@ -834,4 +834,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_fund_project_rejects_paused_registry.1.json b/investment_vault/test_snapshots/test/test_fund_project_rejects_paused_registry.1.json index 0eaa836..23a3957 100644 --- a/investment_vault/test_snapshots/test/test_fund_project_rejects_paused_registry.1.json +++ b/investment_vault/test_snapshots/test/test_fund_project_rejects_paused_registry.1.json @@ -1106,4 +1106,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_fund_project_rejects_self_funding_by_admin.1.json b/investment_vault/test_snapshots/test/test_fund_project_rejects_self_funding_by_admin.1.json index 52ce08a..cadfb7e 100644 --- a/investment_vault/test_snapshots/test/test_fund_project_rejects_self_funding_by_admin.1.json +++ b/investment_vault/test_snapshots/test/test_fund_project_rejects_self_funding_by_admin.1.json @@ -1059,4 +1059,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_fund_project_second_call_exhausts_remaining_deployable.1.json b/investment_vault/test_snapshots/test/test_fund_project_second_call_exhausts_remaining_deployable.1.json index 869b8ff..9cacad0 100644 --- a/investment_vault/test_snapshots/test/test_fund_project_second_call_exhausts_remaining_deployable.1.json +++ b/investment_vault/test_snapshots/test/test_fund_project_second_call_exhausts_remaining_deployable.1.json @@ -1180,4 +1180,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_fund_project_succeeds_with_valid_project_id.1.json b/investment_vault/test_snapshots/test/test_fund_project_succeeds_with_valid_project_id.1.json index df7de6f..40748b2 100644 --- a/investment_vault/test_snapshots/test/test_fund_project_succeeds_with_valid_project_id.1.json +++ b/investment_vault/test_snapshots/test/test_fund_project_succeeds_with_valid_project_id.1.json @@ -1180,4 +1180,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_funding_thresholds_default_to_zero.1.json b/investment_vault/test_snapshots/test/test_funding_thresholds_default_to_zero.1.json index 157490f..2f3544a 100644 --- a/investment_vault/test_snapshots/test/test_funding_thresholds_default_to_zero.1.json +++ b/investment_vault/test_snapshots/test/test_funding_thresholds_default_to_zero.1.json @@ -509,4 +509,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_get_hbs_token_info_after_trading_enabled.1.json b/investment_vault/test_snapshots/test/test_get_hbs_token_info_after_trading_enabled.1.json index c427694..ff74396 100644 --- a/investment_vault/test_snapshots/test/test_get_hbs_token_info_after_trading_enabled.1.json +++ b/investment_vault/test_snapshots/test/test_get_hbs_token_info_after_trading_enabled.1.json @@ -555,4 +555,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_get_hbs_token_info_before_trading_enabled.1.json b/investment_vault/test_snapshots/test/test_get_hbs_token_info_before_trading_enabled.1.json index a861089..e81846b 100644 --- a/investment_vault/test_snapshots/test/test_get_hbs_token_info_before_trading_enabled.1.json +++ b/investment_vault/test_snapshots/test/test_get_hbs_token_info_before_trading_enabled.1.json @@ -508,4 +508,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_get_project_investment_zero_for_unfunded.1.json b/investment_vault/test_snapshots/test/test_get_project_investment_zero_for_unfunded.1.json index 157490f..2f3544a 100644 --- a/investment_vault/test_snapshots/test/test_get_project_investment_zero_for_unfunded.1.json +++ b/investment_vault/test_snapshots/test/test_get_project_investment_zero_for_unfunded.1.json @@ -509,4 +509,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_get_registry_returns_initial_registry.1.json b/investment_vault/test_snapshots/test/test_get_registry_returns_initial_registry.1.json index a861089..e81846b 100644 --- a/investment_vault/test_snapshots/test/test_get_registry_returns_initial_registry.1.json +++ b/investment_vault/test_snapshots/test/test_get_registry_returns_initial_registry.1.json @@ -508,4 +508,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_high_utilization_withdrawal_emits_warning_event.1.json b/investment_vault/test_snapshots/test/test_high_utilization_withdrawal_emits_warning_event.1.json index 5a6ad0b..bacf22c 100644 --- a/investment_vault/test_snapshots/test/test_high_utilization_withdrawal_emits_warning_event.1.json +++ b/investment_vault/test_snapshots/test/test_high_utilization_withdrawal_emits_warning_event.1.json @@ -1356,4 +1356,4 @@ "failed_call": false } ] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_initialize.1.json b/investment_vault/test_snapshots/test/test_initialize.1.json index e33f821..8c6ea3f 100644 --- a/investment_vault/test_snapshots/test/test_initialize.1.json +++ b/investment_vault/test_snapshots/test/test_initialize.1.json @@ -509,4 +509,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_management_fee_set_emits_event.1.json b/investment_vault/test_snapshots/test/test_management_fee_set_emits_event.1.json index 471ee00..b869ae0 100644 --- a/investment_vault/test_snapshots/test/test_management_fee_set_emits_event.1.json +++ b/investment_vault/test_snapshots/test/test_management_fee_set_emits_event.1.json @@ -606,4 +606,4 @@ "failed_call": false } ] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_multisig_batch_fund_projects.1.json b/investment_vault/test_snapshots/test/test_multisig_batch_fund_projects.1.json index 558e720..0836859 100644 --- a/investment_vault/test_snapshots/test/test_multisig_batch_fund_projects.1.json +++ b/investment_vault/test_snapshots/test/test_multisig_batch_fund_projects.1.json @@ -1666,4 +1666,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_multisig_rejects_insufficient_funding_approvals.1.json b/investment_vault/test_snapshots/test/test_multisig_rejects_insufficient_funding_approvals.1.json index 605665c..741b24d 100644 --- a/investment_vault/test_snapshots/test/test_multisig_rejects_insufficient_funding_approvals.1.json +++ b/investment_vault/test_snapshots/test/test_multisig_rejects_insufficient_funding_approvals.1.json @@ -588,4 +588,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_nonzero_fee_accrual.1.json b/investment_vault/test_snapshots/test/test_nonzero_fee_accrual.1.json index 55bad44..28d7024 100644 --- a/investment_vault/test_snapshots/test/test_nonzero_fee_accrual.1.json +++ b/investment_vault/test_snapshots/test/test_nonzero_fee_accrual.1.json @@ -953,4 +953,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_set_and_get_funding_thresholds.1.json b/investment_vault/test_snapshots/test/test_set_and_get_funding_thresholds.1.json index 8ebb94b..c979165 100644 --- a/investment_vault/test_snapshots/test/test_set_and_get_funding_thresholds.1.json +++ b/investment_vault/test_snapshots/test/test_set_and_get_funding_thresholds.1.json @@ -575,4 +575,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_set_funding_thresholds_is_admin_only.1.json b/investment_vault/test_snapshots/test/test_set_funding_thresholds_is_admin_only.1.json index c4bbb93..ed8dc73 100644 --- a/investment_vault/test_snapshots/test/test_set_funding_thresholds_is_admin_only.1.json +++ b/investment_vault/test_snapshots/test/test_set_funding_thresholds_is_admin_only.1.json @@ -532,4 +532,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_set_registry_is_admin_only.1.json b/investment_vault/test_snapshots/test/test_set_registry_is_admin_only.1.json index e403d04..4214b14 100644 --- a/investment_vault/test_snapshots/test/test_set_registry_is_admin_only.1.json +++ b/investment_vault/test_snapshots/test/test_set_registry_is_admin_only.1.json @@ -665,4 +665,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_set_registry_updates_registry.1.json b/investment_vault/test_snapshots/test/test_set_registry_updates_registry.1.json index 099dfe2..7b26a2e 100644 --- a/investment_vault/test_snapshots/test/test_set_registry_updates_registry.1.json +++ b/investment_vault/test_snapshots/test/test_set_registry_updates_registry.1.json @@ -680,4 +680,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_set_registry_validates_new_address.1.json b/investment_vault/test_snapshots/test/test_set_registry_validates_new_address.1.json index ef83482..10844cc 100644 --- a/investment_vault/test_snapshots/test/test_set_registry_validates_new_address.1.json +++ b/investment_vault/test_snapshots/test/test_set_registry_validates_new_address.1.json @@ -508,4 +508,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_set_trusted_emitter_persists_and_emits_event.1.json b/investment_vault/test_snapshots/test/test_set_trusted_emitter_persists_and_emits_event.1.json index ad607bc..221a17b 100644 --- a/investment_vault/test_snapshots/test/test_set_trusted_emitter_persists_and_emits_event.1.json +++ b/investment_vault/test_snapshots/test/test_set_trusted_emitter_persists_and_emits_event.1.json @@ -668,4 +668,4 @@ "failed_call": false } ] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_total_assets_after_deposit.1.json b/investment_vault/test_snapshots/test/test_total_assets_after_deposit.1.json index fe86ba6..7fa1fd7 100644 --- a/investment_vault/test_snapshots/test/test_total_assets_after_deposit.1.json +++ b/investment_vault/test_snapshots/test/test_total_assets_after_deposit.1.json @@ -834,4 +834,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_trading_disabled_by_default.1.json b/investment_vault/test_snapshots/test/test_trading_disabled_by_default.1.json index a861089..e81846b 100644 --- a/investment_vault/test_snapshots/test/test_trading_disabled_by_default.1.json +++ b/investment_vault/test_snapshots/test/test_trading_disabled_by_default.1.json @@ -508,4 +508,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_transfer_ownership_emits_event.1.json b/investment_vault/test_snapshots/test/test_transfer_ownership_emits_event.1.json index 5f2ce00..aaed9df 100644 --- a/investment_vault/test_snapshots/test/test_transfer_ownership_emits_event.1.json +++ b/investment_vault/test_snapshots/test/test_transfer_ownership_emits_event.1.json @@ -662,4 +662,4 @@ "failed_call": false } ] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_transfer_to_vault_address_rejected.1.json b/investment_vault/test_snapshots/test/test_transfer_to_vault_address_rejected.1.json index ef77532..88ec381 100644 --- a/investment_vault/test_snapshots/test/test_transfer_to_vault_address_rejected.1.json +++ b/investment_vault/test_snapshots/test/test_transfer_to_vault_address_rejected.1.json @@ -834,4 +834,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_vault_constructor_and_registry_reference_initial_state.1.json b/investment_vault/test_snapshots/test/test_vault_constructor_and_registry_reference_initial_state.1.json index 9fb8861..0101711 100644 --- a/investment_vault/test_snapshots/test/test_vault_constructor_and_registry_reference_initial_state.1.json +++ b/investment_vault/test_snapshots/test/test_vault_constructor_and_registry_reference_initial_state.1.json @@ -1187,4 +1187,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_vault_deposit_cost_estimate.1.json b/investment_vault/test_snapshots/test/test_vault_deposit_cost_estimate.1.json index 48c5a2d..b6dbe24 100644 --- a/investment_vault/test_snapshots/test/test_vault_deposit_cost_estimate.1.json +++ b/investment_vault/test_snapshots/test/test_vault_deposit_cost_estimate.1.json @@ -935,4 +935,4 @@ "failed_call": false } ] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_vault_is_paused_getter_default_false.1.json b/investment_vault/test_snapshots/test/test_vault_is_paused_getter_default_false.1.json index a861089..e81846b 100644 --- a/investment_vault/test_snapshots/test/test_vault_is_paused_getter_default_false.1.json +++ b/investment_vault/test_snapshots/test/test_vault_is_paused_getter_default_false.1.json @@ -508,4 +508,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_vault_migrate_state_rejects_wrong_version.1.json b/investment_vault/test_snapshots/test/test_vault_migrate_state_rejects_wrong_version.1.json index a861089..e81846b 100644 --- a/investment_vault/test_snapshots/test/test_vault_migrate_state_rejects_wrong_version.1.json +++ b/investment_vault/test_snapshots/test/test_vault_migrate_state_rejects_wrong_version.1.json @@ -508,4 +508,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_vault_pause_and_unpause.1.json b/investment_vault/test_snapshots/test/test_vault_pause_and_unpause.1.json index 553fb49..4121da0 100644 --- a/investment_vault/test_snapshots/test/test_vault_pause_and_unpause.1.json +++ b/investment_vault/test_snapshots/test/test_vault_pause_and_unpause.1.json @@ -591,4 +591,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_vault_stale_stored_version_blocks_normal_calls.1.json b/investment_vault/test_snapshots/test/test_vault_stale_stored_version_blocks_normal_calls.1.json index 157f61d..475c369 100644 --- a/investment_vault/test_snapshots/test/test_vault_stale_stored_version_blocks_normal_calls.1.json +++ b/investment_vault/test_snapshots/test/test_vault_stale_stored_version_blocks_normal_calls.1.json @@ -603,4 +603,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_vault_state_version.1.json b/investment_vault/test_snapshots/test/test_vault_state_version.1.json index 157490f..2f3544a 100644 --- a/investment_vault/test_snapshots/test/test_vault_state_version.1.json +++ b/investment_vault/test_snapshots/test/test_vault_state_version.1.json @@ -509,4 +509,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_vault_with_zero_supply.1.json b/investment_vault/test_snapshots/test/test_vault_with_zero_supply.1.json index a2b51d2..1a66e60 100644 --- a/investment_vault/test_snapshots/test/test_vault_with_zero_supply.1.json +++ b/investment_vault/test_snapshots/test/test_vault_with_zero_supply.1.json @@ -937,4 +937,4 @@ "failed_call": false } ] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_withdraw_at_minimum_succeeds.1.json b/investment_vault/test_snapshots/test/test_withdraw_at_minimum_succeeds.1.json index acc475b..78ac37c 100644 --- a/investment_vault/test_snapshots/test/test_withdraw_at_minimum_succeeds.1.json +++ b/investment_vault/test_snapshots/test/test_withdraw_at_minimum_succeeds.1.json @@ -980,4 +980,4 @@ "failed_call": false } ] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_withdraw_below_minimum_panics.1.json b/investment_vault/test_snapshots/test/test_withdraw_below_minimum_panics.1.json index 60f0200..20561f3 100644 --- a/investment_vault/test_snapshots/test/test_withdraw_below_minimum_panics.1.json +++ b/investment_vault/test_snapshots/test/test_withdraw_below_minimum_panics.1.json @@ -834,4 +834,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_withdraw_emits_event.1.json b/investment_vault/test_snapshots/test/test_withdraw_emits_event.1.json index 6e9898d..3ffebf5 100644 --- a/investment_vault/test_snapshots/test/test_withdraw_emits_event.1.json +++ b/investment_vault/test_snapshots/test/test_withdraw_emits_event.1.json @@ -980,4 +980,4 @@ "failed_call": false } ] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_withdraw_enqueues_when_insufficient_liquidity.1.json b/investment_vault/test_snapshots/test/test_withdraw_enqueues_when_insufficient_liquidity.1.json index e4b8fb9..dd394ac 100644 --- a/investment_vault/test_snapshots/test/test_withdraw_enqueues_when_insufficient_liquidity.1.json +++ b/investment_vault/test_snapshots/test/test_withdraw_enqueues_when_insufficient_liquidity.1.json @@ -1294,4 +1294,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_withdraw_fails_when_all_usdc_deployed.1.json b/investment_vault/test_snapshots/test/test_withdraw_fails_when_all_usdc_deployed.1.json index bc4f3a1..2d88a2f 100644 --- a/investment_vault/test_snapshots/test/test_withdraw_fails_when_all_usdc_deployed.1.json +++ b/investment_vault/test_snapshots/test/test_withdraw_fails_when_all_usdc_deployed.1.json @@ -1180,4 +1180,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_withdraw_queued_emits_event.1.json b/investment_vault/test_snapshots/test/test_withdraw_queued_emits_event.1.json index e023d42..d29d31e 100644 --- a/investment_vault/test_snapshots/test/test_withdraw_queued_emits_event.1.json +++ b/investment_vault/test_snapshots/test/test_withdraw_queued_emits_event.1.json @@ -1365,4 +1365,4 @@ "failed_call": false } ] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_withdraw_returns_usdc.1.json b/investment_vault/test_snapshots/test/test_withdraw_returns_usdc.1.json index 13278f7..2b1ac36 100644 --- a/investment_vault/test_snapshots/test/test_withdraw_returns_usdc.1.json +++ b/investment_vault/test_snapshots/test/test_withdraw_returns_usdc.1.json @@ -879,4 +879,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_withdraw_with_zero_supply_panics.1.json b/investment_vault/test_snapshots/test/test_withdraw_with_zero_supply_panics.1.json index ef83482..10844cc 100644 --- a/investment_vault/test_snapshots/test/test_withdraw_with_zero_supply_panics.1.json +++ b/investment_vault/test_snapshots/test/test_withdraw_with_zero_supply_panics.1.json @@ -508,4 +508,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_withdrawal_rate_limiting_next_ledger.1.json b/investment_vault/test_snapshots/test/test_withdrawal_rate_limiting_next_ledger.1.json index 6e9898d..3ffebf5 100644 --- a/investment_vault/test_snapshots/test/test_withdrawal_rate_limiting_next_ledger.1.json +++ b/investment_vault/test_snapshots/test/test_withdrawal_rate_limiting_next_ledger.1.json @@ -980,4 +980,4 @@ "failed_call": false } ] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_withdrawal_rate_limiting_same_ledger.1.json b/investment_vault/test_snapshots/test/test_withdrawal_rate_limiting_same_ledger.1.json index ef77532..88ec381 100644 --- a/investment_vault/test_snapshots/test/test_withdrawal_rate_limiting_same_ledger.1.json +++ b/investment_vault/test_snapshots/test/test_withdrawal_rate_limiting_same_ledger.1.json @@ -834,4 +834,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_withdrawal_rate_limiting_transfer_locked.1.json b/investment_vault/test_snapshots/test/test_withdrawal_rate_limiting_transfer_locked.1.json index b2d1811..7ef1afa 100644 --- a/investment_vault/test_snapshots/test/test_withdrawal_rate_limiting_transfer_locked.1.json +++ b/investment_vault/test_snapshots/test/test_withdrawal_rate_limiting_transfer_locked.1.json @@ -933,4 +933,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_zero_fee_parity.1.json b/investment_vault/test_snapshots/test/test_zero_fee_parity.1.json index 5e594ff..0523c92 100644 --- a/investment_vault/test_snapshots/test/test_zero_fee_parity.1.json +++ b/investment_vault/test_snapshots/test/test_zero_fee_parity.1.json @@ -901,4 +901,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/test/test_zero_supply_edge_cases_are_handled_consistently.1.json b/investment_vault/test_snapshots/test/test_zero_supply_edge_cases_are_handled_consistently.1.json index c3f9532..f5091fe 100644 --- a/investment_vault/test_snapshots/test/test_zero_supply_edge_cases_are_handled_consistently.1.json +++ b/investment_vault/test_snapshots/test/test_zero_supply_edge_cases_are_handled_consistently.1.json @@ -513,4 +513,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/investment_vault/test_snapshots/wasm_test/wasm_smoke/test_deposit_withdraw_and_admin_flow_via_compiled_wasm.1.json b/investment_vault/test_snapshots/wasm_test/wasm_smoke/test_deposit_withdraw_and_admin_flow_via_compiled_wasm.1.json index e00f55f..6a0ca8a 100644 --- a/investment_vault/test_snapshots/wasm_test/wasm_smoke/test_deposit_withdraw_and_admin_flow_via_compiled_wasm.1.json +++ b/investment_vault/test_snapshots/wasm_test/wasm_smoke/test_deposit_withdraw_and_admin_flow_via_compiled_wasm.1.json @@ -1450,4 +1450,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/notification-service/src/api.ts b/notification-service/src/api.ts index 126a2bb..b4d1a87 100644 --- a/notification-service/src/api.ts +++ b/notification-service/src/api.ts @@ -43,7 +43,10 @@ export function createApi( const origin = req.headers.origin; if (origin && options.allowedOrigins?.includes(origin)) { res.setHeader("Access-Control-Allow-Origin", origin); - res.setHeader("Access-Control-Allow-Methods", "GET, PUT, DELETE, OPTIONS"); + res.setHeader( + "Access-Control-Allow-Methods", + "GET, PUT, DELETE, OPTIONS", + ); res.setHeader("Access-Control-Allow-Headers", "Content-Type"); } if (req.method === "OPTIONS") { @@ -133,7 +136,11 @@ export function createApi( return; } - if (webhook_url !== undefined && typeof webhook_url === "string" && webhook_url.length > 0) { + if ( + webhook_url !== undefined && + typeof webhook_url === "string" && + webhook_url.length > 0 + ) { try { new URL(webhook_url); } catch { @@ -147,8 +154,15 @@ export function createApi( return; } - if (min_delta !== undefined && (typeof min_delta !== "number" || !Number.isFinite(min_delta) || min_delta < 0)) { - res.status(400).json({ error: "min_delta must be a non-negative number" }); + if ( + min_delta !== undefined && + (typeof min_delta !== "number" || + !Number.isFinite(min_delta) || + min_delta < 0) + ) { + res + .status(400) + .json({ error: "min_delta must be a non-negative number" }); return; } diff --git a/notification-service/src/config.test.ts b/notification-service/src/config.test.ts index 2b41771..485b8c4 100644 --- a/notification-service/src/config.test.ts +++ b/notification-service/src/config.test.ts @@ -50,9 +50,7 @@ describe("loadConfig", () => { const config = loadConfig(); expect(config.rpc_url).toBe("https://soroban-testnet.stellar.org"); - expect(config.network_passphrase).toBe( - "Test SDF Network ; September 2015", - ); + expect(config.network_passphrase).toBe("Test SDF Network ; September 2015"); expect(config.db_path).toBe("./data/notifications.db"); expect(config.poll_interval_ms).toBe(30000); expect(config.api_port).toBe(3000); diff --git a/notification-service/src/listener.test.ts b/notification-service/src/listener.test.ts index 53af230..23fdf03 100644 --- a/notification-service/src/listener.test.ts +++ b/notification-service/src/listener.test.ts @@ -239,19 +239,18 @@ describe("pollScoreChanges reconnects after a dropped RPC connection", () => { .mockResolvedValueOnce({ sequence: 100 }); // Second poll returns an event - getEventsMock - .mockResolvedValueOnce({ - events: [ - { - value: buildScoreChangedEvent( - ["score_changed", 7], - buildDataMap(FULL_SCORES), - ), - ledger: 100, - timestamp: TIMESTAMP, - }, - ], - }); + getEventsMock.mockResolvedValueOnce({ + events: [ + { + value: buildScoreChangedEvent( + ["score_changed", 7], + buildDataMap(FULL_SCORES), + ), + ledger: 100, + timestamp: TIMESTAMP, + }, + ], + }); const handle = await pollScoreChanges( config, diff --git a/notification-service/src/notifier.test.ts b/notification-service/src/notifier.test.ts index 431a138..9d97657 100644 --- a/notification-service/src/notifier.test.ts +++ b/notification-service/src/notifier.test.ts @@ -131,8 +131,8 @@ describe("Notifier retry behavior on a failed delivery", () => { }); it("does not record a notification or dedup key when webhook returns a server error", async () => { - fetchMock.mockImplementationOnce(async () => - new Response("Internal Server Error", { status: 500 }), + fetchMock.mockImplementationOnce( + async () => new Response("Internal Server Error", { status: 500 }), ); const store = makeStore(); @@ -173,13 +173,13 @@ describe("Notifier retry behavior on a failed delivery", () => { } as unknown as Store; // First attempt: webhook fails - fetchMock.mockImplementationOnce(async () => - new Response("bad gateway", { status: 502 }), + fetchMock.mockImplementationOnce( + async () => new Response("bad gateway", { status: 502 }), ); // Redelivery: webhook succeeds - fetchMock.mockImplementationOnce(async () => - new Response(null, { status: 200 }), + fetchMock.mockImplementationOnce( + async () => new Response(null, { status: 200 }), ); // Use a config without email transport — webhook-only path diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..9a150cb --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "heliobond_contracts", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/project_registry/test_snapshots/test/bench_registry_create_and_score_project.1.json b/project_registry/test_snapshots/test/bench_registry_create_and_score_project.1.json index 7bf1997..e506652 100644 --- a/project_registry/test_snapshots/test/bench_registry_create_and_score_project.1.json +++ b/project_registry/test_snapshots/test/bench_registry_create_and_score_project.1.json @@ -615,4 +615,4 @@ "failed_call": false } ] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/integration/test_full_heliobond_flow.1.json b/project_registry/test_snapshots/test/integration/test_full_heliobond_flow.1.json index a809991..b710579 100644 --- a/project_registry/test_snapshots/test/integration/test_full_heliobond_flow.1.json +++ b/project_registry/test_snapshots/test/integration/test_full_heliobond_flow.1.json @@ -1537,4 +1537,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_all_only_owner_functions_reject_non_admin_caller.1.json b/project_registry/test_snapshots/test/test_all_only_owner_functions_reject_non_admin_caller.1.json index 8b6da7b..6d39a5d 100644 --- a/project_registry/test_snapshots/test/test_all_only_owner_functions_reject_non_admin_caller.1.json +++ b/project_registry/test_snapshots/test/test_all_only_owner_functions_reject_non_admin_caller.1.json @@ -382,4 +382,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_certify_project.1.json b/project_registry/test_snapshots/test/test_certify_project.1.json index 489ec47..30b78c9 100644 --- a/project_registry/test_snapshots/test/test_certify_project.1.json +++ b/project_registry/test_snapshots/test/test_certify_project.1.json @@ -389,4 +389,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_certify_project_emits_event.1.json b/project_registry/test_snapshots/test/test_certify_project_emits_event.1.json index 0c6eab1..d17cc00 100644 --- a/project_registry/test_snapshots/test/test_certify_project_emits_event.1.json +++ b/project_registry/test_snapshots/test/test_certify_project_emits_event.1.json @@ -421,4 +421,4 @@ "failed_call": false } ] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_compact_storage_removes_zero_collateral.1.json b/project_registry/test_snapshots/test/test_compact_storage_removes_zero_collateral.1.json index 19a5f82..b3358c4 100644 --- a/project_registry/test_snapshots/test/test_compact_storage_removes_zero_collateral.1.json +++ b/project_registry/test_snapshots/test/test_compact_storage_removes_zero_collateral.1.json @@ -819,4 +819,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_create_project_blocked_when_paused.1.json b/project_registry/test_snapshots/test/test_create_project_blocked_when_paused.1.json index 40a968b..0d0c0cb 100644 --- a/project_registry/test_snapshots/test/test_create_project_blocked_when_paused.1.json +++ b/project_registry/test_snapshots/test/test_create_project_blocked_when_paused.1.json @@ -235,4 +235,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_create_project_by_non_whitelisted_panics.1.json b/project_registry/test_snapshots/test/test_create_project_by_non_whitelisted_panics.1.json index 6ed76ea..ef65075 100644 --- a/project_registry/test_snapshots/test/test_create_project_by_non_whitelisted_panics.1.json +++ b/project_registry/test_snapshots/test/test_create_project_by_non_whitelisted_panics.1.json @@ -4,10 +4,7 @@ "nonce": 0, "mux_id": 0 }, - "auth": [ - [], - [] - ], + "auth": [[], []], "ledger": { "protocol_version": 26, "sequence_number": 0, @@ -119,4 +116,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_create_project_by_whitelisted_address.1.json b/project_registry/test_snapshots/test/test_create_project_by_whitelisted_address.1.json index 5fa87eb..8d9116e 100644 --- a/project_registry/test_snapshots/test/test_create_project_by_whitelisted_address.1.json +++ b/project_registry/test_snapshots/test/test_create_project_by_whitelisted_address.1.json @@ -345,4 +345,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_create_project_emits_event.1.json b/project_registry/test_snapshots/test/test_create_project_emits_event.1.json index 99468cc..3cb01da 100644 --- a/project_registry/test_snapshots/test/test_create_project_emits_event.1.json +++ b/project_registry/test_snapshots/test/test_create_project_emits_event.1.json @@ -370,4 +370,4 @@ "failed_call": false } ] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_create_project_records_created_at.1.json b/project_registry/test_snapshots/test/test_create_project_records_created_at.1.json index 8f52623..f1d1cc0 100644 --- a/project_registry/test_snapshots/test/test_create_project_records_created_at.1.json +++ b/project_registry/test_snapshots/test/test_create_project_records_created_at.1.json @@ -344,4 +344,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_credit_quality_score_changes_rate_correctly.1.json b/project_registry/test_snapshots/test/test_credit_quality_score_changes_rate_correctly.1.json index 59db8f4..0d9180c 100644 --- a/project_registry/test_snapshots/test/test_credit_quality_score_changes_rate_correctly.1.json +++ b/project_registry/test_snapshots/test/test_credit_quality_score_changes_rate_correctly.1.json @@ -570,4 +570,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_credit_quality_score_history_recorded.1.json b/project_registry/test_snapshots/test/test_credit_quality_score_history_recorded.1.json index 86ae4c1..09c7942 100644 --- a/project_registry/test_snapshots/test/test_credit_quality_score_history_recorded.1.json +++ b/project_registry/test_snapshots/test/test_credit_quality_score_history_recorded.1.json @@ -510,4 +510,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_deposit_and_get_collateral.1.json b/project_registry/test_snapshots/test/test_deposit_and_get_collateral.1.json index 7ee4893..4c40f9c 100644 --- a/project_registry/test_snapshots/test/test_deposit_and_get_collateral.1.json +++ b/project_registry/test_snapshots/test/test_deposit_and_get_collateral.1.json @@ -753,4 +753,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_emergency_admin_can_pause_and_unpause_without_owner.1.json b/project_registry/test_snapshots/test/test_emergency_admin_can_pause_and_unpause_without_owner.1.json index 6c9ffe8..cc2b867 100644 --- a/project_registry/test_snapshots/test/test_emergency_admin_can_pause_and_unpause_without_owner.1.json +++ b/project_registry/test_snapshots/test/test_emergency_admin_can_pause_and_unpause_without_owner.1.json @@ -264,4 +264,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_emergency_pause_rejects_non_emergency_admin.1.json b/project_registry/test_snapshots/test/test_emergency_pause_rejects_non_emergency_admin.1.json index 8b95379..00e99f9 100644 --- a/project_registry/test_snapshots/test/test_emergency_pause_rejects_non_emergency_admin.1.json +++ b/project_registry/test_snapshots/test/test_emergency_pause_rejects_non_emergency_admin.1.json @@ -170,4 +170,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_funding_limit_bps_scales_with_reputation.1.json b/project_registry/test_snapshots/test/test_funding_limit_bps_scales_with_reputation.1.json index 3c80581..b7d9483 100644 --- a/project_registry/test_snapshots/test/test_funding_limit_bps_scales_with_reputation.1.json +++ b/project_registry/test_snapshots/test/test_funding_limit_bps_scales_with_reputation.1.json @@ -238,4 +238,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_get_all_projects.1.json b/project_registry/test_snapshots/test/test_get_all_projects.1.json index 842faef..499158a 100644 --- a/project_registry/test_snapshots/test/test_get_all_projects.1.json +++ b/project_registry/test_snapshots/test/test_get_all_projects.1.json @@ -500,4 +500,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_get_projects_page_limit_larger_than_total_projects.1.json b/project_registry/test_snapshots/test/test_get_projects_page_limit_larger_than_total_projects.1.json index 94eeb86..b0f9dc0 100644 --- a/project_registry/test_snapshots/test/test_get_projects_page_limit_larger_than_total_projects.1.json +++ b/project_registry/test_snapshots/test/test_get_projects_page_limit_larger_than_total_projects.1.json @@ -656,4 +656,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_get_projects_page_returns_stable_ordering_across_pages.1.json b/project_registry/test_snapshots/test/test_get_projects_page_returns_stable_ordering_across_pages.1.json index 8271b3a..b46170f 100644 --- a/project_registry/test_snapshots/test/test_get_projects_page_returns_stable_ordering_across_pages.1.json +++ b/project_registry/test_snapshots/test/test_get_projects_page_returns_stable_ordering_across_pages.1.json @@ -971,4 +971,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_get_projects_page_zero_limit_returns_empty.1.json b/project_registry/test_snapshots/test/test_get_projects_page_zero_limit_returns_empty.1.json index b9c9879..83308bc 100644 --- a/project_registry/test_snapshots/test/test_get_projects_page_zero_limit_returns_empty.1.json +++ b/project_registry/test_snapshots/test/test_get_projects_page_zero_limit_returns_empty.1.json @@ -344,4 +344,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_get_score_history_nonexistent_panics.1.json b/project_registry/test_snapshots/test/test_get_score_history_nonexistent_panics.1.json index 699d274..15b347c 100644 --- a/project_registry/test_snapshots/test/test_get_score_history_nonexistent_panics.1.json +++ b/project_registry/test_snapshots/test/test_get_score_history_nonexistent_panics.1.json @@ -4,10 +4,7 @@ "nonce": 0, "mux_id": 0 }, - "auth": [ - [], - [] - ], + "auth": [[], []], "ledger": { "protocol_version": 26, "sequence_number": 0, @@ -119,4 +116,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_get_whitelister_returns_initial_whitelister.1.json b/project_registry/test_snapshots/test/test_get_whitelister_returns_initial_whitelister.1.json index 699d274..15b347c 100644 --- a/project_registry/test_snapshots/test/test_get_whitelister_returns_initial_whitelister.1.json +++ b/project_registry/test_snapshots/test/test_get_whitelister_returns_initial_whitelister.1.json @@ -4,10 +4,7 @@ "nonce": 0, "mux_id": 0 }, - "auth": [ - [], - [] - ], + "auth": [[], []], "ledger": { "protocol_version": 26, "sequence_number": 0, @@ -119,4 +116,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_getters_work_when_paused.1.json b/project_registry/test_snapshots/test/test_getters_work_when_paused.1.json index 8bd2f15..9e758de 100644 --- a/project_registry/test_snapshots/test/test_getters_work_when_paused.1.json +++ b/project_registry/test_snapshots/test/test_getters_work_when_paused.1.json @@ -393,4 +393,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_initialize_sets_admin_and_whitelister.1.json b/project_registry/test_snapshots/test/test_initialize_sets_admin_and_whitelister.1.json index e8e697e..5b5b7be 100644 --- a/project_registry/test_snapshots/test/test_initialize_sets_admin_and_whitelister.1.json +++ b/project_registry/test_snapshots/test/test_initialize_sets_admin_and_whitelister.1.json @@ -4,12 +4,7 @@ "nonce": 0, "mux_id": 0 }, - "auth": [ - [], - [], - [], - [] - ], + "auth": [[], [], [], []], "ledger": { "protocol_version": 26, "sequence_number": 0, @@ -121,4 +116,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_interest_rate_mid_scores.1.json b/project_registry/test_snapshots/test/test_interest_rate_mid_scores.1.json index 722f23d..1a6e5a7 100644 --- a/project_registry/test_snapshots/test/test_interest_rate_mid_scores.1.json +++ b/project_registry/test_snapshots/test/test_interest_rate_mid_scores.1.json @@ -471,4 +471,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_interest_rate_perfect_scores_is_minimum.1.json b/project_registry/test_snapshots/test/test_interest_rate_perfect_scores_is_minimum.1.json index addaa5f..e80620c 100644 --- a/project_registry/test_snapshots/test/test_interest_rate_perfect_scores_is_minimum.1.json +++ b/project_registry/test_snapshots/test/test_interest_rate_perfect_scores_is_minimum.1.json @@ -471,4 +471,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_interest_rate_zero_scores_is_base_rate.1.json b/project_registry/test_snapshots/test/test_interest_rate_zero_scores_is_base_rate.1.json index b9c9879..83308bc 100644 --- a/project_registry/test_snapshots/test/test_interest_rate_zero_scores_is_base_rate.1.json +++ b/project_registry/test_snapshots/test/test_interest_rate_zero_scores_is_base_rate.1.json @@ -344,4 +344,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_liquidate_collateral_by_admin.1.json b/project_registry/test_snapshots/test/test_liquidate_collateral_by_admin.1.json index e0b18d8..98f670f 100644 --- a/project_registry/test_snapshots/test/test_liquidate_collateral_by_admin.1.json +++ b/project_registry/test_snapshots/test/test_liquidate_collateral_by_admin.1.json @@ -820,4 +820,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_maturity_date_is_mature.1.json b/project_registry/test_snapshots/test/test_maturity_date_is_mature.1.json index 56192de..965f8b8 100644 --- a/project_registry/test_snapshots/test/test_maturity_date_is_mature.1.json +++ b/project_registry/test_snapshots/test/test_maturity_date_is_mature.1.json @@ -345,4 +345,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_migrate_state_noop_on_current_version.1.json b/project_registry/test_snapshots/test/test_migrate_state_noop_on_current_version.1.json index 4609e52..6639218 100644 --- a/project_registry/test_snapshots/test/test_migrate_state_noop_on_current_version.1.json +++ b/project_registry/test_snapshots/test/test_migrate_state_noop_on_current_version.1.json @@ -157,4 +157,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_migrate_state_rejects_wrong_from_version.1.json b/project_registry/test_snapshots/test/test_migrate_state_rejects_wrong_from_version.1.json index 699d274..15b347c 100644 --- a/project_registry/test_snapshots/test/test_migrate_state_rejects_wrong_from_version.1.json +++ b/project_registry/test_snapshots/test/test_migrate_state_rejects_wrong_from_version.1.json @@ -4,10 +4,7 @@ "nonce": 0, "mux_id": 0 }, - "auth": [ - [], - [] - ], + "auth": [[], []], "ledger": { "protocol_version": 26, "sequence_number": 0, @@ -119,4 +116,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_multiple_creators_sequential_ids.1.json b/project_registry/test_snapshots/test/test_multiple_creators_sequential_ids.1.json index 6c213dc..8a61164 100644 --- a/project_registry/test_snapshots/test/test_multiple_creators_sequential_ids.1.json +++ b/project_registry/test_snapshots/test/test_multiple_creators_sequential_ids.1.json @@ -996,4 +996,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_multisig_update_impact_score_approved.1.json b/project_registry/test_snapshots/test/test_multisig_update_impact_score_approved.1.json index d3cf614..837de7c 100644 --- a/project_registry/test_snapshots/test/test_multisig_update_impact_score_approved.1.json +++ b/project_registry/test_snapshots/test/test_multisig_update_impact_score_approved.1.json @@ -620,4 +620,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_multisig_update_impact_score_rejects_insufficient_approvals.1.json b/project_registry/test_snapshots/test/test_multisig_update_impact_score_rejects_insufficient_approvals.1.json index 788ccde..c70dab9 100644 --- a/project_registry/test_snapshots/test/test_multisig_update_impact_score_rejects_insufficient_approvals.1.json +++ b/project_registry/test_snapshots/test/test_multisig_update_impact_score_rejects_insufficient_approvals.1.json @@ -424,4 +424,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_new_whitelister_can_set_whitelist.1.json b/project_registry/test_snapshots/test/test_new_whitelister_can_set_whitelist.1.json index 4c3f7e3..b355511 100644 --- a/project_registry/test_snapshots/test/test_new_whitelister_can_set_whitelist.1.json +++ b/project_registry/test_snapshots/test/test_new_whitelister_can_set_whitelist.1.json @@ -409,4 +409,4 @@ "failed_call": false } ] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_non_owner_cannot_deposit_collateral.1.json b/project_registry/test_snapshots/test/test_non_owner_cannot_deposit_collateral.1.json index f92ce7d..f5364dd 100644 --- a/project_registry/test_snapshots/test/test_non_owner_cannot_deposit_collateral.1.json +++ b/project_registry/test_snapshots/test/test_non_owner_cannot_deposit_collateral.1.json @@ -602,4 +602,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_owner_can_set_reputation.1.json b/project_registry/test_snapshots/test/test_owner_can_set_reputation.1.json index b012826..3e46991 100644 --- a/project_registry/test_snapshots/test/test_owner_can_set_reputation.1.json +++ b/project_registry/test_snapshots/test/test_owner_can_set_reputation.1.json @@ -191,4 +191,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_registry_constructor_deployment_and_initial_state.1.json b/project_registry/test_snapshots/test/test_registry_constructor_deployment_and_initial_state.1.json index c93b7ef..ce23746 100644 --- a/project_registry/test_snapshots/test/test_registry_constructor_deployment_and_initial_state.1.json +++ b/project_registry/test_snapshots/test/test_registry_constructor_deployment_and_initial_state.1.json @@ -736,4 +736,4 @@ "failed_call": false } ] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_registry_constructor_deployment_cost_estimate_and_initial_state.1.json b/project_registry/test_snapshots/test/test_registry_constructor_deployment_cost_estimate_and_initial_state.1.json index 94a1cee..62b2e5e 100644 --- a/project_registry/test_snapshots/test/test_registry_constructor_deployment_cost_estimate_and_initial_state.1.json +++ b/project_registry/test_snapshots/test/test_registry_constructor_deployment_cost_estimate_and_initial_state.1.json @@ -687,4 +687,4 @@ "failed_call": false } ] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_registry_pause_and_unpause.1.json b/project_registry/test_snapshots/test/test_registry_pause_and_unpause.1.json index e9971a5..e369023 100644 --- a/project_registry/test_snapshots/test/test_registry_pause_and_unpause.1.json +++ b/project_registry/test_snapshots/test/test_registry_pause_and_unpause.1.json @@ -203,4 +203,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_release_collateral_after_maturity.1.json b/project_registry/test_snapshots/test/test_release_collateral_after_maturity.1.json index 42f8907..9322c6b 100644 --- a/project_registry/test_snapshots/test/test_release_collateral_after_maturity.1.json +++ b/project_registry/test_snapshots/test/test_release_collateral_after_maturity.1.json @@ -768,4 +768,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_reputation_above_100_panics.1.json b/project_registry/test_snapshots/test/test_reputation_above_100_panics.1.json index 6ed76ea..ef65075 100644 --- a/project_registry/test_snapshots/test/test_reputation_above_100_panics.1.json +++ b/project_registry/test_snapshots/test/test_reputation_above_100_panics.1.json @@ -4,10 +4,7 @@ "nonce": 0, "mux_id": 0 }, - "auth": [ - [], - [] - ], + "auth": [[], []], "ledger": { "protocol_version": 26, "sequence_number": 0, @@ -119,4 +116,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_reputation_can_be_updated.1.json b/project_registry/test_snapshots/test/test_reputation_can_be_updated.1.json index 6c267e0..bb872ad 100644 --- a/project_registry/test_snapshots/test/test_reputation_can_be_updated.1.json +++ b/project_registry/test_snapshots/test/test_reputation_can_be_updated.1.json @@ -236,4 +236,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_reputation_defaults_to_zero.1.json b/project_registry/test_snapshots/test/test_reputation_defaults_to_zero.1.json index 6ed76ea..ef65075 100644 --- a/project_registry/test_snapshots/test/test_reputation_defaults_to_zero.1.json +++ b/project_registry/test_snapshots/test/test_reputation_defaults_to_zero.1.json @@ -4,10 +4,7 @@ "nonce": 0, "mux_id": 0 }, - "auth": [ - [], - [] - ], + "auth": [[], []], "ledger": { "protocol_version": 26, "sequence_number": 0, @@ -119,4 +116,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_score_changed_event_contains_old_and_new_values.1.json b/project_registry/test_snapshots/test/test_score_changed_event_contains_old_and_new_values.1.json index b9ec7e5..63f496b 100644 --- a/project_registry/test_snapshots/test/test_score_changed_event_contains_old_and_new_values.1.json +++ b/project_registry/test_snapshots/test/test_score_changed_event_contains_old_and_new_values.1.json @@ -615,4 +615,4 @@ "failed_call": false } ] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_score_history_multiple_updates_ordered.1.json b/project_registry/test_snapshots/test/test_score_history_multiple_updates_ordered.1.json index 9f3c517..52f8cc7 100644 --- a/project_registry/test_snapshots/test/test_score_history_multiple_updates_ordered.1.json +++ b/project_registry/test_snapshots/test/test_score_history_multiple_updates_ordered.1.json @@ -671,4 +671,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_score_history_noop_does_not_append.1.json b/project_registry/test_snapshots/test/test_score_history_noop_does_not_append.1.json index 621581a..ba350ff 100644 --- a/project_registry/test_snapshots/test/test_score_history_noop_does_not_append.1.json +++ b/project_registry/test_snapshots/test/test_score_history_noop_does_not_append.1.json @@ -516,4 +516,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_score_history_records_entry_on_update.1.json b/project_registry/test_snapshots/test/test_score_history_records_entry_on_update.1.json index 60d0ac5..b0c1484 100644 --- a/project_registry/test_snapshots/test/test_score_history_records_entry_on_update.1.json +++ b/project_registry/test_snapshots/test/test_score_history_records_entry_on_update.1.json @@ -471,4 +471,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_sequential_project_ids.1.json b/project_registry/test_snapshots/test/test_sequential_project_ids.1.json index 842faef..499158a 100644 --- a/project_registry/test_snapshots/test/test_sequential_project_ids.1.json +++ b/project_registry/test_snapshots/test/test_sequential_project_ids.1.json @@ -500,4 +500,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_set_and_get_reputation.1.json b/project_registry/test_snapshots/test/test_set_and_get_reputation.1.json index 8dcdfe2..271179b 100644 --- a/project_registry/test_snapshots/test/test_set_and_get_reputation.1.json +++ b/project_registry/test_snapshots/test/test_set_and_get_reputation.1.json @@ -191,4 +191,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_set_creator_reputation_emits_event.1.json b/project_registry/test_snapshots/test/test_set_creator_reputation_emits_event.1.json index c5b932a..ea40cf7 100644 --- a/project_registry/test_snapshots/test/test_set_creator_reputation_emits_event.1.json +++ b/project_registry/test_snapshots/test/test_set_creator_reputation_emits_event.1.json @@ -292,4 +292,4 @@ "failed_call": false } ] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_set_whitelist_emits_event.1.json b/project_registry/test_snapshots/test/test_set_whitelist_emits_event.1.json index 2f8ce57..55ac175 100644 --- a/project_registry/test_snapshots/test/test_set_whitelist_emits_event.1.json +++ b/project_registry/test_snapshots/test/test_set_whitelist_emits_event.1.json @@ -220,4 +220,4 @@ "failed_call": false } ] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_set_whitelister_is_admin_only.1.json b/project_registry/test_snapshots/test/test_set_whitelister_is_admin_only.1.json index f890e6d..53267d9 100644 --- a/project_registry/test_snapshots/test/test_set_whitelister_is_admin_only.1.json +++ b/project_registry/test_snapshots/test/test_set_whitelister_is_admin_only.1.json @@ -4,11 +4,7 @@ "nonce": 1, "mux_id": 0 }, - "auth": [ - [], - [], - [] - ], + "auth": [[], [], []], "ledger": { "protocol_version": 26, "sequence_number": 0, @@ -143,4 +139,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_set_whitelister_updates_whitelister.1.json b/project_registry/test_snapshots/test/test_set_whitelister_updates_whitelister.1.json index 1e06602..a606271 100644 --- a/project_registry/test_snapshots/test/test_set_whitelister_updates_whitelister.1.json +++ b/project_registry/test_snapshots/test/test_set_whitelister_updates_whitelister.1.json @@ -158,4 +158,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_stale_stored_version_blocks_normal_calls.1.json b/project_registry/test_snapshots/test/test_stale_stored_version_blocks_normal_calls.1.json index ac72207..b457191 100644 --- a/project_registry/test_snapshots/test/test_stale_stored_version_blocks_normal_calls.1.json +++ b/project_registry/test_snapshots/test/test_stale_stored_version_blocks_normal_calls.1.json @@ -189,4 +189,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_state_version_matches_stored.1.json b/project_registry/test_snapshots/test/test_state_version_matches_stored.1.json index f09eace..d0ea818 100644 --- a/project_registry/test_snapshots/test/test_state_version_matches_stored.1.json +++ b/project_registry/test_snapshots/test/test_state_version_matches_stored.1.json @@ -4,11 +4,7 @@ "nonce": 0, "mux_id": 0 }, - "auth": [ - [], - [], - [] - ], + "auth": [[], [], []], "ledger": { "protocol_version": 26, "sequence_number": 0, @@ -120,4 +116,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_transfer_ownership_emits_event.1.json b/project_registry/test_snapshots/test/test_transfer_ownership_emits_event.1.json index ec1c9c8..2acc35c 100644 --- a/project_registry/test_snapshots/test/test_transfer_ownership_emits_event.1.json +++ b/project_registry/test_snapshots/test/test_transfer_ownership_emits_event.1.json @@ -273,4 +273,4 @@ "failed_call": false } ] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_unauthorized_caller_cannot_set_reputation.1.json b/project_registry/test_snapshots/test/test_unauthorized_caller_cannot_set_reputation.1.json index f54e33f..5a8143e 100644 --- a/project_registry/test_snapshots/test/test_unauthorized_caller_cannot_set_reputation.1.json +++ b/project_registry/test_snapshots/test/test_unauthorized_caller_cannot_set_reputation.1.json @@ -4,10 +4,7 @@ "nonce": 0, "mux_id": 0 }, - "auth": [ - [], - [] - ], + "auth": [[], []], "ledger": { "protocol_version": 26, "sequence_number": 0, @@ -119,4 +116,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_update_credit_quality_independent_of_green_impact.1.json b/project_registry/test_snapshots/test/test_update_credit_quality_independent_of_green_impact.1.json index 6e7e2fe..7f42c22 100644 --- a/project_registry/test_snapshots/test/test_update_credit_quality_independent_of_green_impact.1.json +++ b/project_registry/test_snapshots/test/test_update_credit_quality_independent_of_green_impact.1.json @@ -568,4 +568,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_update_credit_quality_score_boundary_values.1.json b/project_registry/test_snapshots/test/test_update_credit_quality_score_boundary_values.1.json index eeeacdc..0ba532b 100644 --- a/project_registry/test_snapshots/test/test_update_credit_quality_score_boundary_values.1.json +++ b/project_registry/test_snapshots/test/test_update_credit_quality_score_boundary_values.1.json @@ -511,4 +511,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_update_credit_quality_score_noop_identical_values.1.json b/project_registry/test_snapshots/test/test_update_credit_quality_score_noop_identical_values.1.json index b59b666..b798c60 100644 --- a/project_registry/test_snapshots/test/test_update_credit_quality_score_noop_identical_values.1.json +++ b/project_registry/test_snapshots/test/test_update_credit_quality_score_noop_identical_values.1.json @@ -511,4 +511,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_update_credit_quality_score_out_of_range_panics.1.json b/project_registry/test_snapshots/test/test_update_credit_quality_score_out_of_range_panics.1.json index b9c9879..83308bc 100644 --- a/project_registry/test_snapshots/test/test_update_credit_quality_score_out_of_range_panics.1.json +++ b/project_registry/test_snapshots/test/test_update_credit_quality_score_out_of_range_panics.1.json @@ -344,4 +344,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_update_credit_quality_score_success.1.json b/project_registry/test_snapshots/test/test_update_credit_quality_score_success.1.json index e99ae39..7c48383 100644 --- a/project_registry/test_snapshots/test/test_update_credit_quality_score_success.1.json +++ b/project_registry/test_snapshots/test/test_update_credit_quality_score_success.1.json @@ -468,4 +468,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_update_impact_score.1.json b/project_registry/test_snapshots/test/test_update_impact_score.1.json index 4f67249..0898a22 100644 --- a/project_registry/test_snapshots/test/test_update_impact_score.1.json +++ b/project_registry/test_snapshots/test/test_update_impact_score.1.json @@ -471,4 +471,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_update_impact_score_blocked_when_paused.1.json b/project_registry/test_snapshots/test/test_update_impact_score_blocked_when_paused.1.json index c332b05..c51d334 100644 --- a/project_registry/test_snapshots/test/test_update_impact_score_blocked_when_paused.1.json +++ b/project_registry/test_snapshots/test/test_update_impact_score_blocked_when_paused.1.json @@ -391,4 +391,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_update_impact_score_boundary_values.1.json b/project_registry/test_snapshots/test/test_update_impact_score_boundary_values.1.json index 780f010..ad984aa 100644 --- a/project_registry/test_snapshots/test/test_update_impact_score_boundary_values.1.json +++ b/project_registry/test_snapshots/test/test_update_impact_score_boundary_values.1.json @@ -517,4 +517,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_update_impact_score_emits_event.1.json b/project_registry/test_snapshots/test/test_update_impact_score_emits_event.1.json index b9ec7e5..63f496b 100644 --- a/project_registry/test_snapshots/test/test_update_impact_score_emits_event.1.json +++ b/project_registry/test_snapshots/test/test_update_impact_score_emits_event.1.json @@ -615,4 +615,4 @@ "failed_call": false } ] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_update_impact_score_exceeds_100_panics_credit_quality.1.json b/project_registry/test_snapshots/test/test_update_impact_score_exceeds_100_panics_credit_quality.1.json index b9c9879..83308bc 100644 --- a/project_registry/test_snapshots/test/test_update_impact_score_exceeds_100_panics_credit_quality.1.json +++ b/project_registry/test_snapshots/test/test_update_impact_score_exceeds_100_panics_credit_quality.1.json @@ -344,4 +344,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_update_impact_score_exceeds_100_panics_green_impact.1.json b/project_registry/test_snapshots/test/test_update_impact_score_exceeds_100_panics_green_impact.1.json index b9c9879..83308bc 100644 --- a/project_registry/test_snapshots/test/test_update_impact_score_exceeds_100_panics_green_impact.1.json +++ b/project_registry/test_snapshots/test/test_update_impact_score_exceeds_100_panics_green_impact.1.json @@ -344,4 +344,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_update_impact_score_max_value_panics.1.json b/project_registry/test_snapshots/test/test_update_impact_score_max_value_panics.1.json index b9c9879..83308bc 100644 --- a/project_registry/test_snapshots/test/test_update_impact_score_max_value_panics.1.json +++ b/project_registry/test_snapshots/test/test_update_impact_score_max_value_panics.1.json @@ -344,4 +344,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_update_impact_score_nonexistent_project_panics.1.json b/project_registry/test_snapshots/test/test_update_impact_score_nonexistent_project_panics.1.json index 699d274..15b347c 100644 --- a/project_registry/test_snapshots/test/test_update_impact_score_nonexistent_project_panics.1.json +++ b/project_registry/test_snapshots/test/test_update_impact_score_nonexistent_project_panics.1.json @@ -4,10 +4,7 @@ "nonce": 0, "mux_id": 0 }, - "auth": [ - [], - [] - ], + "auth": [[], []], "ledger": { "protocol_version": 26, "sequence_number": 0, @@ -119,4 +116,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_update_impact_score_noop_identical_values.1.json b/project_registry/test_snapshots/test/test_update_impact_score_noop_identical_values.1.json index 2422555..6bb8297 100644 --- a/project_registry/test_snapshots/test/test_update_impact_score_noop_identical_values.1.json +++ b/project_registry/test_snapshots/test/test_update_impact_score_noop_identical_values.1.json @@ -516,4 +516,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_update_score_non_admin_panics.1.json b/project_registry/test_snapshots/test/test_update_score_non_admin_panics.1.json index 2699c16..b88c70b 100644 --- a/project_registry/test_snapshots/test/test_update_score_non_admin_panics.1.json +++ b/project_registry/test_snapshots/test/test_update_score_non_admin_panics.1.json @@ -368,4 +368,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_uri_above_max_length_panics.1.json b/project_registry/test_snapshots/test/test_uri_above_max_length_panics.1.json index 594e368..4424e3a 100644 --- a/project_registry/test_snapshots/test/test_uri_above_max_length_panics.1.json +++ b/project_registry/test_snapshots/test/test_uri_above_max_length_panics.1.json @@ -188,4 +188,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_uri_below_min_length_panics.1.json b/project_registry/test_snapshots/test/test_uri_below_min_length_panics.1.json index 594e368..4424e3a 100644 --- a/project_registry/test_snapshots/test/test_uri_below_min_length_panics.1.json +++ b/project_registry/test_snapshots/test/test_uri_below_min_length_panics.1.json @@ -188,4 +188,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_uri_exactly_max_length_accepted.1.json b/project_registry/test_snapshots/test/test_uri_exactly_max_length_accepted.1.json index 6cde3b9..c7104c8 100644 --- a/project_registry/test_snapshots/test/test_uri_exactly_max_length_accepted.1.json +++ b/project_registry/test_snapshots/test/test_uri_exactly_max_length_accepted.1.json @@ -344,4 +344,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_uri_exactly_min_length_accepted.1.json b/project_registry/test_snapshots/test/test_uri_exactly_min_length_accepted.1.json index ddb7059..2d84c43 100644 --- a/project_registry/test_snapshots/test/test_uri_exactly_min_length_accepted.1.json +++ b/project_registry/test_snapshots/test/test_uri_exactly_min_length_accepted.1.json @@ -370,4 +370,4 @@ "failed_call": false } ] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_uri_with_special_characters_and_unicode_accepted.1.json b/project_registry/test_snapshots/test/test_uri_with_special_characters_and_unicode_accepted.1.json index 4c372ac..75d0041 100644 --- a/project_registry/test_snapshots/test/test_uri_with_special_characters_and_unicode_accepted.1.json +++ b/project_registry/test_snapshots/test/test_uri_with_special_characters_and_unicode_accepted.1.json @@ -345,4 +345,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/test/test_verify_metadata_hash_matches_recorded_hash.1.json b/project_registry/test_snapshots/test/test_verify_metadata_hash_matches_recorded_hash.1.json index 9eb5d34..4b30c5e 100644 --- a/project_registry/test_snapshots/test/test_verify_metadata_hash_matches_recorded_hash.1.json +++ b/project_registry/test_snapshots/test/test_verify_metadata_hash_matches_recorded_hash.1.json @@ -345,4 +345,4 @@ ] }, "events": [] -} \ No newline at end of file +} diff --git a/project_registry/test_snapshots/wasm_test/wasm_smoke/test_create_certify_and_admin_flow_via_compiled_wasm.1.json b/project_registry/test_snapshots/wasm_test/wasm_smoke/test_create_certify_and_admin_flow_via_compiled_wasm.1.json index d306332..108b878 100644 --- a/project_registry/test_snapshots/wasm_test/wasm_smoke/test_create_certify_and_admin_flow_via_compiled_wasm.1.json +++ b/project_registry/test_snapshots/wasm_test/wasm_smoke/test_create_certify_and_admin_flow_via_compiled_wasm.1.json @@ -625,4 +625,4 @@ ] }, "events": [] -} \ No newline at end of file +}