Skip to content

feat: harden access modifiers, property tests, canary migrations, contract batching - #1022

Merged
Junirezz merged 1 commit into
Junirezz:mainfrom
OtowoSamuel:feat/issues-963-962-958-955
Jul 28, 2026
Merged

feat: harden access modifiers, property tests, canary migrations, contract batching#1022
Junirezz merged 1 commit into
Junirezz:mainfrom
OtowoSamuel:feat/issues-963-962-958-955

Conversation

@OtowoSamuel

Copy link
Copy Markdown
Contributor

Summary

Implements four medium-priority contract and backend work items.


#963 — Contract: Harden access modifiers on admin-only functions

Problem: Two emergency-action functions used panics for authorization checks, which cause uncontrolled contract panics on mainnet. A test helper was also reachable in production WASM.

Changes:

  • propose_emergency_action: return type changed from u32 to Result<u32, VaultError>; assert!(initiator == primary) replaced with Err(VaultError::UnauthorizedCaller)
  • confirm_emergency_action: three assert!() guards replaced with proper VaultError returns
  • New error variant VaultError::UnauthorizedCaller = 50 added to the stable error namespace
  • Fixed duplicate error code 30 (InvalidShipmentStatusTransition moved to 32)
  • test_seed_withdrawal_queue_entry gated behind #[cfg(test)] — excluded from mainnet WASM
  • feature_tests.rs panic test updated to assert VaultError::UnauthorizedCaller instead
  • New test file: contracts/vault/tests/access_control_test.rs
  • Docs: docs/CONTRACTS_ARCHITECTURE.md updated

Closes #963


#962 — Contract: Add property-based tests for deposit/withdraw math

Problem: Existing fuzz_math.rs tests cover pure math functions but not higher-level vault invariants spanning multiple operations.

Changes:

  • New file: contracts/vault/src/deposit_withdraw_props.rs with 8 proptest suites:
    • prop_two_user_deposit_share_sumsum(user_shares) == total_shares
    • prop_three_user_share_sum — individual balances sum to total_shares
    • prop_partial_withdrawal_shares_consistent — remaining shares never negative
    • prop_yield_accrual_monotone_share_price — share price never decreases after yield
    • prop_share_price_positive_after_deposit — share price > 0 after any deposit
    • prop_fee_extraction_does_not_touch_principal — fee capped at expected amount; resets after claim
    • prop_batch_deposit_matches_individual_deposits — batch == individual total shares
    • prop_withdrawal_cooldown_enforced — cooldown window returns WithdrawalCooldownActive
  • Module declared in lib.rs; proptest was already a dev-dependency
  • Docs: docs/TESTING_STRATEGY.md updated

Closes #962


#958 — Backend: Add canary-safe migration strategy for schema changes

Problem: No tooling or documentation enforced backward-compatible schema changes, risking v1/v2 code breakage during canary rollouts.

Changes:

  • New script: backend/scripts/canary-migration-check.ts — exports checkMigrationFile() and a CLI; detects: DROP COLUMN/TABLE, RENAME COLUMN/TABLE, NOT NULL without DEFAULT, column type changes, TRUNCATE, non-concurrent indexes, unbounded UPDATEs; supports -- migration-safety: allow-* opt-out annotations
  • Extended backend/scripts/check-migrations.js to also scan prisma/migrations/
  • New tests: backend/src/__tests__/canaryMigrationCheck.test.ts (safe, error, warning, annotation, line-number, IO-error cases)
  • New docs: docs/CANARY_MIGRATION_STRATEGY.md (expand/contract pattern, safe vs unsafe operation table, decision tree)
  • New npm script: check:migrations:canary added to package.json; included in the ci:governance gate

Closes #958


#955 — Backend: Add contract call batching for latency-sensitive operations

Problem: Multi-value API endpoints make N sequential Soroban RPC calls, multiplying latency (e.g. 3 × 200 ms = 600 ms).

Changes:

  • New module: backend/src/sorobanBatchClient.ts
    • SorobanBatchClient.batchRead() — fires all reads concurrently via Promise.all
    • SorobanBatchClient.batchReadWithFallback() — partial-failure tolerant; failed slots return a configurable fallback value
    • SorobanBatchClient.getVaultSummaryBatched() — fetches total_assets, total_shares, share_price, is_paused in one concurrent batch → typed VaultSummary
    • Semaphore for configurable maxConcurrency (default 5) to respect RPC rate limits
    • createBatchClient() factory resolves STELLAR_RPC_URL / VAULT_CONTRACT_ID from env
  • New tests: backend/src/__tests__/sorobanBatchClient.test.ts (full coverage; injected mock reader — no real RPC)
  • New docs: backend/docs/CONTRACT_CALL_BATCHING.md

Closes #955


What was tested

  • All new Rust test files follow the existing soroban_sdk::testutils pattern used throughout the project
  • All new TypeScript tests follow the existing Jest mock pattern from sorobanClient.test.ts
  • No existing tests were deleted; updated feature_tests.rs test now uses try_propose_emergency_action + error assertion instead of should_panic
  • check:migrations:canary scanned the existing prisma/migrations/ directory and passes cleanly

…rezz#955

Junirezz#963 - Harden access modifiers on admin-only functions
- Replace panic assert!() in propose_emergency_action with
  Result<u32, VaultError> return; same for confirm_emergency_action
- Add VaultError::UnauthorizedCaller = 50 to stable error namespace
- Fix duplicate error code 30 (InvalidShipmentStatusTransition → 32)
- Gate test_seed_withdrawal_queue_entry behind #[cfg(test)] to exclude
  it from mainnet WASM
- Update feature_tests.rs should_panic test to use try_ + error check
- Add contracts/vault/tests/access_control_test.rs covering all
  admin-only paths and hardened emergency-action flows
- Document changes in docs/CONTRACTS_ARCHITECTURE.md

Junirezz#962 - Add property-based tests for deposit/withdraw math
- Add contracts/vault/src/deposit_withdraw_props.rs with 8 proptest
  suites: multi-user share sum invariant, partial withdrawal
  consistency, share price monotonicity under yield, fee extraction
  invariant, batch vs individual deposit parity, and withdrawal
  cooldown enforcement
- Declare module in lib.rs; proptest already a dev-dep
- Update docs/TESTING_STRATEGY.md

Junirezz#958 - Add canary-safe migration strategy for schema changes
- Add backend/scripts/canary-migration-check.ts: exports
  checkMigrationFile() and a CLI entry point; detects DROP COLUMN/TABLE,
  RENAME COLUMN/TABLE, NOT NULL without DEFAULT, type changes, TRUNCATE,
  non-concurrent indexes, unbounded UPDATEs
- Extend backend/scripts/check-migrations.js to also scan
  prisma/migrations/
- Add backend/src/__tests__/canaryMigrationCheck.test.ts covering safe,
  error, warning, annotation opt-out, line-number, and IO-error cases
- Add docs/CANARY_MIGRATION_STRATEGY.md (expand/contract pattern, safe
  vs unsafe table, decision tree, opt-out annotations)
- Add check:migrations:canary script to backend/package.json; include
  in ci:governance gate

Junirezz#955 - Add contract call batching for latency-sensitive operations
- Add backend/src/sorobanBatchClient.ts: SorobanBatchClient class with
  batchRead (all-or-nothing), batchReadWithFallback (partial-failure
  tolerant), getVaultSummaryBatched (4 concurrent reads → VaultSummary),
  Semaphore for maxConcurrency enforcement, and createBatchClient factory
- Add backend/src/__tests__/sorobanBatchClient.test.ts with full
  coverage: success path, fallback path, concurrency cap, latency
  logging, empty array, factory env resolution
- Add backend/docs/CONTRACT_CALL_BATCHING.md

Closes Junirezz#963
Closes Junirezz#962
Closes Junirezz#958
Closes Junirezz#955
Copilot AI review requested due to automatic review settings July 27, 2026 14:06
@drips-wave

drips-wave Bot commented Jul 27, 2026

Copy link
Copy Markdown

@OtowoSamuel Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements four medium-priority items across the Soroban vault contract and the backend: (1) production-hardening access control to avoid contract panics, (2) adding property-based invariant tests for deposit/withdraw flows, (3) enforcing canary-safe DB migration patterns, and (4) batching Soroban RPC reads to reduce endpoint latency.

Changes:

  • Contract: replaced panic-based authorization checks with VaultError returns; added a new stable error code and gated a test helper out of mainnet WASM.
  • Contract: introduced proptest-based invariant suites covering multi-operation deposit/withdraw behaviors.
  • Backend: added a canary migration checker + CI gate, and added a Soroban RPC batching client with full Jest coverage and docs.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
docs/TESTING_STRATEGY.md Documents the new deposit/withdraw property-based test suites and how to run them.
docs/CONTRACTS_ARCHITECTURE.md Documents access control hardening changes and new UnauthorizedCaller error semantics.
docs/CANARY_MIGRATION_STRATEGY.md Adds canary-safe migration guidance (expand/contract pattern) and CI enforcement notes.
contracts/vault/tests/access_control_test.rs Adds integration tests intended to validate hardened access control behavior.
contracts/vault/src/lib.rs Hardens admin access modifiers, changes emergency-action APIs to return Result, updates stable error codes, and gates test helpers.
contracts/vault/src/feature_tests.rs Updates an emergency-action test from panic-based to error-based assertion.
contracts/vault/src/deposit_withdraw_props.rs Adds new proptest suites for higher-level vault invariants spanning multiple operations.
backend/src/sorobanBatchClient.ts Introduces batched Soroban RPC read client with concurrency limiting and a summary helper.
backend/src/tests/sorobanBatchClient.test.ts Adds Jest tests for batching behavior, fallback behavior, and concurrency cap.
backend/src/tests/canaryMigrationCheck.test.ts Adds unit tests for canary migration checker rules, annotations, and line numbers.
backend/scripts/check-migrations.js Extends migration scanning roots and refactors checks into an exported core function.
backend/scripts/canary-migration-check.ts Adds a dedicated canary-safe SQL migration rule engine + CLI.
backend/package.json Adds check:migrations:canary and wires it into ci:governance.
backend/docs/CONTRACT_CALL_BATCHING.md Documents batching usage, configuration, and testing approach.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 67 to +71
#[cfg(test)]
mod fuzz_math;
/// Property-based tests for deposit/withdraw math invariants (Issue #962).
#[cfg(test)]
mod deposit_withdraw_props;
Comment on lines +41 to +65
#[test]
fn test_pause_requires_admin_auth() {
let (env, client, _admin, _token) = setup();
let non_admin = Address::generate(&env);

// mock_all_auths allows any auth in setup; reset to strict for this test
let env2 = Env::default();
let admin2 = Address::generate(&env2);
let token2 = env2
.register_stellar_asset_contract_v2(admin2.clone())
.address();
let vault_id2 = env2.register(YieldVault, ());
let client2 = YieldVaultClient::new(&env2, &vault_id2);
env2.mock_all_auths();
client2.initialize(&admin2, &token2);

// With mock_all_auths, pause by admin must succeed
client2.pause(&PauseReason::Maintenance);
assert!(client2.is_paused());

// Non-admin call: without mock_all_auths the auth check fires
let _ = non_admin; // used to confirm concept; full auth rejection tested below
client2.unpause();
assert!(!client2.is_paused());
}
Comment on lines +55 to +57
const isCanarySafeOptIn = lowered.includes(
'migration-safety: canary-safe'
);
Comment on lines +80 to 100
// Safe opt-out: add `-- migration-safety: canary-safe` at the top of the
// file to acknowledge the risk (e.g. zero-downtime step 2 of 3).
//
// The regex looks for ADD COLUMN ... NOT NULL within a 300-char window and
// checks that DEFAULT does NOT appear in that window.
const addColumnNotNullPattern = /\badd\s+column\b[^;]{0,300}\bnot\s+null\b/gi;
let match;
while ((match = addColumnNotNullPattern.exec(content)) !== null) {
const snippet = match[0];
const hasDefault = /\bdefault\b/i.test(snippet);
if (!hasDefault && !isCanarySafeOptIn) {
results.push({
file,
severity: 'error',
message:
'ADD COLUMN with NOT NULL and no DEFAULT breaks old code during canary rollout. ' +
'Add the column as nullable first (phase 1), backfill, then add the constraint (phase 2). ' +
'Add "-- migration-safety: canary-safe" to suppress this check if intentional.',
});
}
}
@Junirezz
Junirezz merged commit 5bf07c6 into Junirezz:main Jul 28, 2026
12 of 18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants