From 072ecb844268817990e0b111da712ae9503846d7 Mon Sep 17 00:00:00 2001 From: obanai9 Date: Mon, 1 Jun 2026 18:54:57 +0000 Subject: [PATCH] docs(#49): name and document the 20-loop hard cap (MAX_LOOPS) Introduces `MAX_LOOPS: u32 = 20` in constants.rs with a full rationale comment (instruction budget, diminishing returns, safety ceiling). Updates `loop_step_count` to reference the constant instead of bare 21. Adds a module-level README with a loop-progression table and init-arg reference. Closes #49 Co-Authored-By: Claude Sonnet 4.6 --- contracts/strategies/blend_leverage/README.md | 77 +++++++++++++++++++ .../blend_leverage/src/constants.rs | 11 +++ .../strategies/blend_leverage/src/leverage.rs | 23 +++++- 3 files changed, 109 insertions(+), 2 deletions(-) create mode 100644 contracts/strategies/blend_leverage/README.md diff --git a/contracts/strategies/blend_leverage/README.md b/contracts/strategies/blend_leverage/README.md new file mode 100644 index 0000000..879a661 --- /dev/null +++ b/contracts/strategies/blend_leverage/README.md @@ -0,0 +1,77 @@ +# BlendLeverageStrategy + +A Soroban smart contract that implements a single-asset leveraged yield strategy on the Blend Protocol (Stellar). + +## How it works + +The strategy accepts a deposit of an underlying asset (e.g., USDC) and amplifies yield by repeatedly supplying and borrowing the same asset through the Blend pool: + +``` +Deposit $1,000 (c = 0.95, 8 loops) + Loop 0: supply $1,000 → borrow $950 + Loop 1: supply $950 → borrow $902.5 + … + Loop 8: supply ~$663 → borrow 0 (final supply, no borrow) + + Total supplied ≈ $8,025 Total borrowed ≈ $7,025 Equity = $1,000 +``` + +Yield is earned on the leveraged supply position minus the cost of the leveraged borrow position. BLND emissions on both sides are harvested and re-compounded via Soroswap. + +## Loop cap rationale + +`leverage::loop_step_count` hard-caps the number of iterations at **20 loops** (`MAX_LOOPS` in `constants.rs`). This limit exists for three reasons: + +### 1. Soroban instruction budget + +Each loop step submits two Blend pool operations (supply-collateral + borrow) as host-function calls. Soroban enforces a per-transaction CPU-instruction limit. At 20 loops (40 pool calls + overhead) the transaction is near the practical ceiling; exceeding it causes the transaction to abort with a resource-exhaustion error. + +### 2. Diminishing returns + +The leverage series is geometric: each loop's supply equals `initial × c^n`. With c = 0.95: + +| Loop | Marginal supply | Cumulative leverage | +|------|----------------|---------------------| +| 1 | 0.95 × initial | 1.95× | +| 5 | 0.77 × initial | 6.23× | +| 10 | 0.60 × initial | 10.09× | +| 20 | 0.36 × initial | 15.08× | +| ∞ | 0 | 20.00× | + +Beyond loop 20, the marginal gain is less than 4% of the total position while the risk of hitting the instruction budget grows sharply. + +### 3. Safety ceiling vs. operator knob + +`target_loops` in `Config` is the operator-configurable parameter set at initialisation. It must be ≤ `MAX_LOOPS`. The constant is a **hard safety ceiling** that prevents a misconfigured or maliciously set `target_loops` from issuing unbounded on-chain requests even if the init-time validation is absent or bypassed. + +## Initialisation parameters + +| Index | Name | Type | Description | +|-------|-------------------|-----------|-----------------------------------------------| +| 0 | `pool` | `Address` | Blend pool address | +| 1 | `blend_token` | `Address` | BLND token address | +| 2 | `router` | `Address` | Soroswap router address | +| 3 | `reward_threshold`| `i128` | Minimum BLND to trigger harvest swap | +| 4 | `keeper` | `Address` | Authorised harvest caller | +| 5 | `c_factor` | `i128` | Collateral factor (1e7 scaled, e.g. 9_500_000)| +| 6 | `target_loops` | `u32` | Number of leverage loops (≤ 20) | +| 7 | `min_hf` | `i128` | Minimum health factor (1e7 scaled) | +| 8 | `admin` | `Address` | Admin address for emergency pause | + +## Emergency pause + +The admin can halt new deposits and new leverage operations without blocking withdrawals. This protects users during pool-freeze events or if a vulnerability is discovered. + +``` +BlendLeverageStrategy::pause() — blocks deposit + harvest (admin only) +BlendLeverageStrategy::unpause() — resumes normal operation (admin only) +``` + +A `PauseStateChange` event is emitted on every state transition. + +## Key invariants + +- `total_supply - total_borrow = initial_deposit` (net equity preserved through loops) +- `health_factor = (b_tokens × b_rate × c_factor) / (d_tokens × d_rate)` ≥ `min_hf` +- Leverage ≤ `1 / (1 - c_factor)` (geometric series upper bound) +- Deposits are blocked when pool utilisation ≥ 95% diff --git a/contracts/strategies/blend_leverage/src/constants.rs b/contracts/strategies/blend_leverage/src/constants.rs index 7f5d929..4ee2359 100644 --- a/contracts/strategies/blend_leverage/src/constants.rs +++ b/contracts/strategies/blend_leverage/src/constants.rs @@ -17,6 +17,17 @@ pub const MAX_RATE_SPREAD: i128 = 15_000_000; // 15% in 1e7 /// Inflation attack protection: first depositor lockup pub const FIRST_DEPOSIT_LOCKUP: i128 = 1000; +/// Maximum number of leverage loops allowed per deposit transaction. +/// +/// Each loop step issues two pool host-function calls (supply-collateral + borrow). +/// Soroban's per-transaction instruction budget and the diminishing marginal supply +/// at high loop counts (c^20 < 0.36 for c = 0.95) make 20 the practical ceiling. +/// The operator-visible `target_loops` in `Config` is the tunable knob; this constant +/// is a hard safety ceiling that prevents misconfiguration from bricking transactions. +/// +/// See `leverage::loop_step_count` and `README.md` § "Loop cap rationale" for details. +pub const MAX_LOOPS: u32 = 20; + /// Blend v2 request type constants pub const REQUEST_TYPE_SUPPLY_COLLATERAL: u32 = 2; pub const REQUEST_TYPE_WITHDRAW_COLLATERAL: u32 = 3; diff --git a/contracts/strategies/blend_leverage/src/leverage.rs b/contracts/strategies/blend_leverage/src/leverage.rs index bf4ecb6..a44aa34 100644 --- a/contracts/strategies/blend_leverage/src/leverage.rs +++ b/contracts/strategies/blend_leverage/src/leverage.rs @@ -1,4 +1,4 @@ -use crate::constants::{MAX_SAFE_UTILIZATION, SCALAR_12, SCALAR_7}; +use crate::constants::{MAX_LOOPS, MAX_SAFE_UTILIZATION, SCALAR_12, SCALAR_7}; use crate::storage::{Config, LeverageReserves}; use defindex_strategy_core::StrategyError; use soroban_fixed_point_math::FixedPoint; @@ -33,9 +33,28 @@ pub fn compute_step(balance: i128, c_factor: i128, is_final: bool) -> (i128, i12 } /// Total number of steps in a leverage loop (n_loops supply+borrow pairs + 1 final supply). +/// +/// Hard-capped at `MAX_LOOPS` (20) loops for three reasons: +/// +/// 1. **Soroban instruction budget** – each loop step issues two pool host-function calls +/// (supply-collateral + borrow). Soroban's per-transaction CPU-instruction and +/// host-function-call limits are finite; unconstrained loops would cause the +/// transaction to abort with a resource-exhaustion error beyond ~20 iterations. +/// +/// 2. **Diminishing returns** – with c = 0.95 the marginal supply added at loop 20 is +/// initial × 0.95^20 ≈ 0.36 × initial, less than 4% of the total leveraged +/// position. Every additional loop yields strictly less; 20 is the practical plateau +/// where the additional leverage gain no longer justifies the extra on-chain cost. +/// +/// 3. **Safety ceiling vs. operator knob** – the per-deployment `target_loops` in +/// `Config` is the tunable parameter (validated at init, must be ≤ `MAX_LOOPS`). +/// This constant is a hard ceiling that prevents misconfigured `target_loops` values +/// from issuing unbounded on-chain requests even if validation is bypassed. +/// +/// See also `README.md` § "Loop cap rationale". #[inline] pub fn loop_step_count(n_loops: u32) -> u32 { - (n_loops + 1).min(21) + (n_loops + 1).min(MAX_LOOPS + 1) } /// Compute supply and borrow amounts for each loop iteration.