Skip to content

feat: emergency pause, multi-pool router, loop-cap docs, proptest invariants#206

Closed
obanai9 wants to merge 1 commit into
Dgetsylver:mainfrom
obanai9:main
Closed

feat: emergency pause, multi-pool router, loop-cap docs, proptest invariants#206
obanai9 wants to merge 1 commit into
Dgetsylver:mainfrom
obanai9:main

Conversation

@obanai9

@obanai9 obanai9 commented May 29, 2026

Copy link
Copy Markdown

Summary

Implements four issues in a single PR, all changes confined to contracts/strategies/:

#45 – D4: Emergency pause admin role

  • storage.rs: added Admin and Paused keys to DataKey; new helpers set_admin, get_admin, set_paused, is_paused.
  • lib.rs: ninth constructor arg admin: Address; new public methods pause(), unpause(), paused(), get_admin().
  • pause() and unpause() are gated by the admin address set at init and emit an on-chain event (pause, state) → bool.
  • deposit() and harvest() return StrategyError::NotAuthorized when paused; withdraw() is unaffected so users can always exit.

#48 – D7: Multi-pool / multi-asset router contract

  • New crate contracts/strategies/router/ (Soroban cdylib).
  • PoolRouter stores a registry of strategy addresses with net-APY snapshots (bps) updated by the admin.
  • deposit(amount, from, preferred_strategy?) — queries the router's pre-deposit balance for accurate share pricing, picks the highest-APY strategy deterministically (or uses user override), issues proportional virtual shares.
  • withdraw(amount, from, to) — burns virtual shares, strategy sends equity directly to to.
  • balance(from) — returns user_vs / total_vs × strategy.balance(router).
  • Admin methods: add_strategy, remove_strategy, update_apy, set_admin.
  • README.md covers architecture, virtual-share accounting, routing algorithm, and migration path for existing single-pool depositors.

#49 – D8: Document (and name) the 20-loop hard cap

  • constants.rs: new MAX_LOOPS: u32 = 20 with a full rationale comment (Soroban instruction budget, diminishing returns, safety-ceiling vs. operator knob).
  • leverage.rs: loop_step_count uses MAX_LOOPS + 1 instead of bare 21; doc-comment references all three reasons.
  • README.md (new): module-level strategy documentation including a loop-progression table, full loop-cap rationale, init-arg reference, and key-invariants summary.

#50 – D9: Property / invariant tests for leverage math

  • Cargo.toml: added proptest = "1" to dev-dependencies.
  • New src/test_proptest.rs with ProptestConfig::with_cases(1_000) — seven invariants:
    1. total_supply >= total_borrow
    2. total_supply - total_borrow == initial (net equity)
    3. total_supply <= initial / (1 − c) (geometric-series bound)
    4. HF monotone in c_factor
    5. compute_step supply leg always equals balance
    6. compute_step final borrow is exactly 0
    7. No panic on extreme i128 inputs (saturating checked_mul)

Test plan

  • cargo test -p blend_leverage_strategy — existing unit tests pass; proptest runs 1 000 cases per property.
  • cargo check -p blend_leverage_router — router contract compiles without errors.
  • Verify BlendLeverageStrategy::pause() blocks deposit() and harvest(); withdraw() succeeds.
  • Verify PoolRouter::deposit routes to highest-APY strategy; user override selects preferred pool.
  • Confirm on-chain events are emitted for pause state changes and router deposits/withdrawals.

Closes #45
Closes #48
Closes #49
Closes #50

…iants

Implements four issues in one pass:

## Dgetsylver#45 – D4: Emergency pause admin role (blend_leverage)

- Added `Admin` and `Paused` keys to `DataKey` in `storage.rs`.
- Added `set_admin`, `get_admin`, `set_paused`, `is_paused` helpers in `storage.rs`.
- `__constructor` now accepts a ninth init arg (`admin: Address`) and persists it.
- New public methods on `BlendLeverageStrategy`:
  - `pause()` – admin-gated; blocks deposits and harvest, emits `(pause, state) → true`.
  - `unpause()` – admin-gated; resumes normal operation, emits `(pause, state) → false`.
  - `paused()` – read-only view returning current pause state.
  - `get_admin()` – read-only view returning the admin address.
- `deposit()` and `harvest()` return `StrategyError::NotAuthorized` immediately when paused;
  `withdraw()` is unaffected, preserving user access to funds at all times.

Closes Dgetsylver#45

## Dgetsylver#48 – D7: Multi-pool / multi-asset router contract

- New crate `contracts/strategies/router/` (cdylib, soroban-sdk 25.3.0).
- `PoolRouter` contract (`src/lib.rs`):
  - Maintains a registry of strategy addresses with net-APY snapshots (basis points).
  - `deposit(amount, from, preferred_strategy?)` – pulls asset from user, queries the
    router's existing balance in the chosen strategy for accurate share pricing, deposits
    via `StrategyClient`, and issues proportional virtual shares to the user.
  - Deterministic pool selection: iterates the registry and picks the highest
    `net_apy_bps`; ties broken by insertion order for stability.
  - User override: if `preferred_strategy` is `Some` and registered, it is used directly.
  - `withdraw(amount, from, to)` – burns virtual shares proportionally; strategy sends
    equity directly to `to` without routing through the router.
  - `balance(from)` – returns `user_vs / total_vs × strategy.balance(router)`.
  - Admin methods: `add_strategy`, `remove_strategy`, `update_apy`, `set_admin`.
  - View helpers: `best_strategy()`, `strategies()`, `admin()`, `asset()`.
  - Events: `(RouterDeposit, from)` and `(RouterWithdraw, from)`.
- `src/storage.rs`: `StrategyEntry`, `UserPosition`, per-strategy virtual-share tracking,
  per-user position persistence with 120-day TTL extension.
- `README.md`: architecture description, virtual-share accounting example, migration path
  for existing single-pool depositors (withdraw + re-deposit flow), routing algorithm docs.

Closes Dgetsylver#48

## Dgetsylver#49 – D8: Document (and name) the 20-loop hard cap (blend_leverage)

- Added `MAX_LOOPS: u32 = 20` constant to `constants.rs` with a full rationale comment
  covering (1) Soroban instruction budget, (2) diminishing marginal returns, and (3) the
  distinction between the safety ceiling and the operator-tunable `target_loops`.
- `leverage.rs`: `loop_step_count` now uses `MAX_LOOPS + 1` instead of the bare literal
  `21`, and carries an expanded doc-comment referencing all three reasons plus the README.
- `README.md` (new): module-level documentation for the blend_leverage strategy including
  a loop-progression table, the full loop-cap rationale, init-arg reference, and a summary
  of key invariants.

Closes Dgetsylver#49

## Dgetsylver#50 – D9: Property / invariant tests for leverage math (blend_leverage)

- Added `proptest = "1"` to `[dev-dependencies]` in `Cargo.toml`.
- New test module `src/test_proptest.rs` with `ProptestConfig::with_cases(1_000)`:
  1. `inv_total_supply_gte_total_borrow` – supply ≥ borrow for any (initial, c, n).
  2. `inv_net_equity_equals_initial` – supply − borrow = initial always.
  3. `inv_leverage_bounded_by_geometric_series` – supply ≤ initial / (1 − c).
  4. `inv_hf_monotone_in_c_factor` – higher c_factor → higher or equal HF for fixed
     b/d positions and rates.
  5. `inv_compute_step_supply_equals_balance` – supply leg always equals `balance`.
  6. `inv_compute_step_final_borrow_is_zero` – final step borrow is exactly 0.
  7. `inv_no_panic_on_extreme_inputs` – `compute_totals` does not panic on large i128
     values; saturating `checked_mul` keeps outputs non-negative.
- Registered the module in `lib.rs` under `#[cfg(test)]`.

Closes Dgetsylver#50
@drips-wave

drips-wave Bot commented May 29, 2026

Copy link
Copy Markdown

@obanai9 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

@hugo-heer

Copy link
Copy Markdown
Collaborator

Hey @obanai9 , thanks for your contribution ! Could you please make a PR per issue ?

@Dgetsylver

Copy link
Copy Markdown
Owner

Closing in favor of #232, #233, #234 and #235, which split this branch's exact content into focused PRs (same +1079/−3 across the same 11 files). Review feedback will land on those. For future work, please use a feature branch per change rather than your fork's main — PRs from main can't be updated independently.

@Dgetsylver Dgetsylver closed this Jun 13, 2026
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