Skip to content

Commit ab51167

Browse files
Merge pull request #13 from Multyr/kpi4/epochedqueue-cutover
feat: cut production over to EpochedQueueModule (Renzo-style epoch qu…
2 parents f451716 + f12cc50 commit ab51167

82 files changed

Lines changed: 5400 additions & 3705 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎docs/access-control.md‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ bytes32 public constant KEEPER_ROLE = keccak256("KEEPER_ROLE");
8383
bytes32 public constant PARAM_ROLE = keccak256("PARAM_ROLE");
8484
```
8585

86-
This contract uses a `governor` address + OpenZeppelin `AccessControl`-style `bytes32` role constants. It is **NOT** imported by `AdminModule`, `QueueModule`, `ERC4626Module`, or any active module on branch `reorg/runbook-docs-consolidate-01a.3`. The production role system is the `roleOf[selector]` mapping described above.
86+
This contract uses a `governor` address + OpenZeppelin `AccessControl`-style `bytes32` role constants. It is **NOT** imported by `AdminModule`, `EpochedQueueModule`, `ERC4626Module`, or any active module. The production role system is the `roleOf[selector]` mapping described above.
8787

8888
---
8989

@@ -184,9 +184,13 @@ Key permissionless functions:
184184
| Function | Module | Notes |
185185
|----------|--------|-------|
186186
| `deposit` | ERC4626Module | Subject to pause checks |
187-
| `requestClaim` | QueueModule | Subject to pause + anti-spam checks |
188-
| `settleFeesAndProcessQueue` | QueueModule | Callable by anyone (keeper pattern) |
189-
| `compactQueue` | QueueModule | Cleanup, always callable |
187+
| `requestEpochWithdrawal` | EpochedQueueModule | Subject to pause + `minClaimAmount` floor |
188+
| `requestInstantWithdrawal` | EpochedQueueModule | Same floor; falls back to the queue when the cap or free liquidity blocks it |
189+
| `cancelEpochWithdrawal` | EpochedQueueModule | Claim owner only, and only while the epoch is `Open` |
190+
| `closeCurrentEpoch` | EpochedQueueModule | Callable by anyone once the epoch duration has elapsed (keeper pattern) |
191+
| `fundEpoch` | EpochedQueueModule | Callable by anyone; no-op when the epoch is already funded |
192+
| `claimEpochAssets` / `batchClaimEpochAssets` | EpochedQueueModule | Claim owner only, self-service, no keeper required |
193+
| `syncOldestUnfundedEpoch` | EpochedQueueModule | Cursor maintenance, always callable |
190194
| `acceptOwnership` | AdminModule | Must be `pendingOwner` (checked internally) |
191195
| `markMatured` | FixedMaturityModule | Any address, once maturityTs reached |
192196
| `markFundingFailed` | FixedMaturityModule | Any address, once deadline + net < min |
@@ -245,7 +249,7 @@ struct Layout {
245249

246250
These functions bypass normal ERC-20 transfer logic and directly adjust share balances. They are used by:
247251

248-
- **QueueModule** — burns user shares during queue settlement, mints fee shares to `feeCollector`
252+
- **EpochedQueueModule** — burns escrowed shares as each claim is paid, transfers fee shares to `feeCollector` in one batch at epoch close, mints perf-fee shares on crystallization
249253
- **FixedMaturityModule** — mints/burns during lifecycle transitions (e.g., `_applyFinalPerformanceFee`)
250254

251255
Access check:

‎docs/architecture.md‎

Lines changed: 69 additions & 52 deletions
Large diffs are not rendered by default.

‎docs/audit-scope.md‎

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ graph TD
4646
|--------|------|------|
4747
| `CoreVault` | `src/core/CoreVault.sol:36` | Diamond-lite proxy; delegatecalls to modules |
4848
| `ERC4626Module` | `src/core/modules/ERC4626Module.sol:166` | Deposit/mint/withdraw/redeem; ERC-4626 compliance |
49-
| `QueueModule` | `src/core/modules/QueueModule.sol:207` | Async withdrawal queue; settlement; anti-spam |
49+
| `EpochedQueueModule` | `src/core/modules/EpochedQueueModule.sol` | Async withdrawal queue; epoch batching; settlement |
5050
| `AdminModule` | `src/core/modules/AdminModule.sol:73` | Governance, timelocks, pause, ecosystem wiring |
5151
| `LiquidityOpsModule` | `src/core/modules/LiquidityOpsModule.sol:32` | Deploy/rebalance/realize liquidity to/from strategies |
5252
| `FeeCollector` | `src/core/modules/FeeCollector.sol:17` | Fee distribution to treasury/ops/safety reserve |
@@ -60,7 +60,7 @@ graph TD
6060
| `ExitFeeLib` | `src/core/libraries/ExitFeeLib.sol:29` | Fee computation for all 3 exit modes |
6161
| `CoreStorage` | `src/core/storage/CoreStorage.sol:38` | EIP-7201 namespaced core storage |
6262
| `FeeStorage` | `src/core/storage/FeeStorage.sol:55` | Fee params + perf fee storage |
63-
| `QueueStorage` | `src/core/storage/QueueStorage.sol:24` | Queue head/claims/pendingShares |
63+
| `EpochQueueStorage` | `src/core/modules/EpochedQueueModule.sol:29` | Epoch/claim state, escrowedShares, outstandingClaimCount (retired `QueueStorage.sol` kept only as a reserved EIP-7201 slot, no longer in scope for live logic) |
6464
| `BatchGuardrails` | `src/core/modules/BatchGuardrails.sol:20` | Batch call validation (peripheral) |
6565

6666
### 2.2 Core Protocol — FixedMaturity Extension
@@ -78,7 +78,7 @@ The FM extension adds state machine governance on top of OpenEnded CoreVault. Th
7878

7979
**Minimal guards in existing modules** (not new logic — just early-return gates):
8080
- `ERC4626Module.sol` (3 gating calls)
81-
- `QueueModule.sol` (2 gating calls)
81+
- `EpochedQueueModule.sol` (2 gating calls)
8282
- `LiquidityOpsModule.sol` (2 gating calls)
8383

8484
**Architectural constraint**: FM extension must not modify any OpenEnded execution path. All FM logic is isolated in FM files.
@@ -146,15 +146,15 @@ src/core/
146146
│ ├── FeeCollector.sol
147147
│ ├── FixedMaturityModule.sol
148148
│ ├── LiquidityOpsModule.sol
149-
│ ├── QueueModule.sol
149+
│ ├── EpochedQueueModule.sol
150150
│ ├── StrategyHealthRegistry.sol
151151
│ ├── StrategyRouter.sol
152152
│ └── VaultUpkeep.sol # Keeper (out of scope)
153153
└── storage/
154154
├── CoreStorage.sol # EIP-7201 main
155155
├── FeeStorage.sol # EIP-7201 fee
156156
├── FixedMaturityStorage.sol # EIP-7201 FM
157-
└── QueueStorage.sol # EIP-7201 queue
157+
└── QueueStorage.sol # EIP-7201 -- retired/reserved slot only, EpochQueueStorage lives inside EpochedQueueModule.sol
158158
```
159159

160160
Total: 51 `.sol` files in `src/core/`.
@@ -172,8 +172,8 @@ Total: 51 `.sol` files in `src/core/`.
172172
| L1 | **Owner key is single point of control** | Design choice | No on-chain DAO. Mitigated by vetoer + timelock. Deployment to multi-sig (Safe) is recommended. |
173173
| L2 | **`FLAG_SYSTEM_SEALED` does not freeze `roleOf[selector]`** | Known gap (AC8) | Owner can change per-function roles post-seal. Timelock provides recourse window. |
174174
| L3 | **`forceWithdrawAll` is best-effort (F-03: resolved)** | Design choice, mitigated | Delivers `min(hot, targetAssets)` — still no guarantee of full liquidity. Previously this could silently deliver an arbitrarily small fill (up to ~90%+ of value if strategies were frozen/illiquid); now a mandatory `minAssetsOut` parameter reverts the whole call (`SlippageExceeded`, no state change) if the fill falls short. See `test/sprint-test/ForceWithdrawAll_SlippagePOC.t.sol`. |
175-
| L4 | **Settlement loop is partial** | Design choice | Gas safety exit at `gasleft() > 150_000`. Queue resumes in next call. Settlement is not atomic for large queues. |
176-
| L5 | **INSTANT fallback stores `immediate=false`** | Design choice | INSTANT requests that fall back to queue are re-classified as standard queue entries (no epoch cap). Fixed in shadow report (BUG 6). |
175+
| L4 | **Settlement is epoch-wide, pull-based** | Design choice (superseded L4) | `EpochedQueueModule` replaced the retired per-claim FIFO settle loop (gas-bounded `_settleLoop`) with `closeCurrentEpoch()`/`fundEpoch()` (epoch-wide, O(1) in claim count) and `claimEpochAssets()` (pull-based, per user). No gas-safety partial-exit is needed since no single call iterates over claims. |
176+
| L5 | **INSTANT fallback always becomes a standard epoch claim** | Design choice | `requestInstantWithdrawal` requests that fail `_canInstant()` fall back to the exact same code path as `requestEpochWithdrawal` — there is no `immediate` flag on `EpochClaim` to mis-set (the BUG 6 class in the retired `QueueModule` is eliminated by construction, not by a fix). |
177177
| L6 | **`preMaturityForceExitPenaltyBps` max 50%** | Design constraint | Hard cap at 5000 bps validated in `configureFixedMaturity`. |
178178
| L7 | **`BatchGuardrails.sol` is peripheral** | Design choice | Not part of CoreVault module dispatch. Separate validation layer, not enforced at core level. |
179179
| L8 | **`Roles.sol` and `Ownable2StepMixin.sol` are legacy** | Legacy artifact | pragma 0.8.24, NOT imported by active modules. Present in codebase but not deployed. |
@@ -184,6 +184,11 @@ Total: 51 `.sol` files in `src/core/`.
184184

185185
The Final Shadow Readiness Report (2026-04-10) found 7 bugs during a 1950-test, 10K-operation fork replay. All 7 were fixed before the first audit. Regression tests for each are in `test/unit/core/AuditFix_Regression.t.sol`.
186186

187+
> **Note**: BUG 4, 6, and 7 below refer to `QueueModule.sol`, which has since been deleted and
188+
> replaced by `EpochedQueueModule.sol` (epoch-batched settlement) — see §9 for why the new
189+
> module's settlement path should be treated as fresh audit surface rather than a pure
190+
> regression check against these historical findings.
191+
187192
| Bug | Severity | File fixed | What was wrong |
188193
|-----|---------|------------|---------------|
189194
| BUG 1 | P0 | `ERC4626Module.sol` | `previewDeposit` returned gross instead of net (OZ non-compliance) |
@@ -213,7 +218,7 @@ These are known and accepted before the first audit:
213218
| Component | Unit | Invariant | Fuzz | Fork | Halmos | Echidna |
214219
|-----------|------|-----------|------|------|--------|---------|
215220
| CoreVault deposit/withdraw | ✅ | ✅ SI-1..6 | ✅ | ✅ | — | ✅ |
216-
| QueueModule settlement | ✅ | ✅ QI-1..6 | ✅ | ✅ | — | — |
221+
| EpochedQueueModule settlement | ✅ | ✅ QI-1..6 | ✅ | ✅ | — | — |
217222
| AdminModule governance | ✅ | ✅ GI-1..7 | — | — | — | — |
218223
| Fee computation | ✅ | ✅ SI-4 | ✅ | ✅ | — | ✅ |
219224
| FixedMaturity lifecycle | ✅ | ✅ FM-inv | — | — | — | — |
@@ -238,7 +243,7 @@ Cross-referenced to test files for auditor traceability:
238243
| SI-2 | `totalSupply * sharePrice ≈ totalAssets` | `test/invariants/CoreVault_System_Invariants.t.sol` |
239244
| SI-3 | No user can withdraw more than deposited (minus fees) | `test/invariants/CoreVault_System_Invariants.t.sol` |
240245
| SI-4 | Fees never exceed configured maximums | `test/invariants/CoreVault_System_Invariants.t.sol` |
241-
| SI-5 | `queueLength() == number of open claims` | `test/invariants/CoreVault_ClaimsQueue_Invariants.t.sol` |
246+
| SI-5 | `outstandingClaimCount() == number of unclaimed claims across all epochs` | `test/invariants/CoreVault_ClaimsQueue_Invariants.t.sol` |
242247
| SI-6 | NAV is consistent with underlying balances | `test/invariants/CoreVault_System_Invariants.t.sol` |
243248
| QI-1 | No claim in queue has `grossAssets < minClaimAmount` | `test/invariants/CoreVault_ClaimsQueue_Invariants.t.sol` |
244249
| QI-4 | A claim cannot be settled twice | `test/invariants/CoreVault_ClaimsQueue_Invariants.t.sol` |
@@ -262,10 +267,10 @@ Cross-referenced to test files for auditor traceability:
262267
|---|---------------|------|-----------|---------------|
263268
| T1 | **Owner key compromise** | HIGH | Timelock delay + vetoer revoke window. `freezeParams` makes fees immutable post-hardening. | GI-3,4; governance unit tests |
264269
| T2 | **Share price manipulation via donation** | HIGH | Fixed in shadow report (BUG 3: non-dilutive fee transfer). Dead deposit seeds baseline. | SI-2; `AuditFix_Regression`; `SharePriceCollapse_Security.t.sol` |
265-
| T3 | **Queue exhaustion / gas DoS** | MEDIUM | Gas safety exit at 150K remaining. Partial settlement resumes in next call. `compactQueue` callable by anyone. | SI-5; `ExitEngine_StressTest`; e2e |
270+
| T3 | **Queue exhaustion / gas DoS** | MEDIUM | `fundEpoch()` liquidity pull is O(1) regardless of claim count (single call per epoch, not a per-claim scan). Settlement is pull-based (`claimEpochAssets`) so an individual user's claim cost never depends on other users' claims. | SI-5; `ExitEngine_StressTest`; `Hardening_GasAndChaos` gas characterization |
266271
| T4 | **Reentrancy via strategy callback** | MEDIUM | `FLAG_REENTRANCY_LOCKED` guards `deployToStrategies`, `rebalanceStrategies`. W2 rule: external calls are try/catch. | `CoreVault_DiamondLite_Reentrancy`; `ForceWithdraw_Reentrancy` |
267272
| T5 | **EIP-7201 storage slot collision** | HIGH (mitigated) | 4 namespaces verified via `EIP7201Compliance.t.sol` (5 tests). Fixed post-FINDING-OOS-03. | `test/security/EIP7201Compliance.t.sol` |
268-
| T6 | **Unauthorized processorMint/Burn** | CRITICAL | `isAuthorizedModule[addr]` gate — only QueueModule and FixedMaturityModule authorized at deploy. | `CoreVault_DiamondLite_AccessControl` |
273+
| T6 | **Unauthorized processorMint/Burn** | CRITICAL | `isAuthorizedModule[addr]` gate — only EpochedQueueModule and FixedMaturityModule authorized at deploy. | `CoreVault_DiamondLite_AccessControl` |
269274
| T7 | **FixedMaturity capital lock** | MEDIUM | `markMatured()` is permissionless; `preMaturityForceExitPenaltyBps ≤ 50%` hard cap. `forceWithdrawAll` available, now with a `minAssetsOut` floor (F-03). | FM invariant tests; `ForceWithdraw_*` |
270275
| T8 | **Fee parameter ratchet via short timelock** | MEDIUM | H3: post-seal min delay floor 1 day. Guardian can pause while vetoer revokes. | `CoreVault_ParamTimelock`; `Governance_Seal_Invariants` |
271276

@@ -275,7 +280,7 @@ Cross-referenced to test files for auditor traceability:
275280

276281
Priority ranking based on value at risk and complexity:
277282

278-
1. **Settlement loop and share accounting** (CRITICAL): `QueueModule._settleLoop`, `_convertToAssetsCached`, `_crystallize`. Four shadow-report HIGH bugs were found here. The PPS must remain exact across all settlement sequences.
283+
1. **Settlement and share accounting** (CRITICAL): `EpochedQueueModule.closeCurrentEpoch`, `fundEpoch`, `claimEpochAssets`, `_crystallize`. This is a substantially rewritten implementation (epoch-batched, not per-claim FIFO) that replaced the retired `QueueModule` post-first-audit — it has NOT yet been through the same shadow-report fork-replay process that found the four HIGH bugs listed in §5.1 for the old module. `_crystallize` was ported verbatim and inherits BUG-4-equivalent regression coverage; the epoch close/fund/claim path is new and should be treated as fresh audit surface, not a regression check. PPS must remain exact — locked once at `closeCurrentEpoch()`, applied identically to every claim in that epoch.
279284

280285
2. **ERC-4626 compliance at boundaries** (HIGH): `previewDeposit`, `previewWithdraw`, `mint` fee symmetry with `deposit`. Boundary conditions (totalAssets=0, totalSupply=0) and mint/deposit equivalence.
281286

@@ -293,13 +298,15 @@ Priority ranking based on value at risk and complexity:
293298

294299
The following specific checks are recommended based on the internal shadow report and architecture review:
295300

296-
**Settlement loop** (`src/core/modules/QueueModule.sol:407`):
297-
- [ ] Verify `_convertToAssetsCached` uses snapshot BEFORE `_burn` (BUG 4 regression)
298-
- [ ] Verify `immediate=false` for all queued claims (BUG 6 regression)
301+
**Settlement: close / fund / claim** (`src/core/modules/EpochedQueueModule.sol:327-513`):
302+
- [ ] Verify `ppsAtClose` is computed BEFORE any shares are burned, and never recomputed after (BUG 4-equivalent regression, new code path)
303+
- [ ] Verify every `EpochClaim` is treated identically regardless of how it was created (`requestEpochWithdrawal` vs. `requestInstantWithdrawal` fallback) — there is no `immediate` flag to mis-set (BUG 6 class eliminated by construction, verify no reintroduction)
299304
- [ ] Verify `feeShares` rounded UP (rounding in favour of protocol)
300-
- [ ] Verify `cachedTA / cachedTS` snapshot is set once per batch (deterministic PPS)
301-
- [ ] Verify `gasleft() > 150_000` guard prevents out-of-gas in settle loop
302-
- [ ] Verify INSTANT cap consumption: only for INSTANT-mode, never STANDARD/FORCE
305+
- [ ] Verify `ppsAtClose` is locked once per epoch at `closeCurrentEpoch()` and used identically by every claim in that epoch regardless of claim order (deterministic PPS)
306+
- [ ] Verify `fundEpoch()` only transitions to `Funded` when `hot >= totalNetAssets` — no partial-funding state can be marked `Funded`
307+
- [ ] Verify `outstandingClaimCount` persists across `closeCurrentEpoch()` (does not reset like per-epoch `claimCount`) — dynamic-cap bypass class, already fixed pre-cutover, verify no regression
308+
- [ ] Verify INSTANT cap consumption: only for the settled-immediately path in `requestInstantWithdrawal`, never for STANDARD claims at any point in their lifecycle
309+
- [ ] Verify `_notifyIncentivesExit` gas cost per claim stays well under keeper gas limits (BUG 7 was a QueueModule/IncentivesEngine issue — confirm the same call pattern in `EpochedQueueModule` doesn't reintroduce it)
303310

304311
**Deposit and mint** (`src/core/modules/ERC4626Module.sol:166`):
305312
- [ ] Verify `previewDeposit(X) == deposit(X).shares` (ERC-4626 compliance, BUG 1 regression)
@@ -352,7 +359,7 @@ The following specific checks are recommended based on the internal shadow repor
352359
|--------|------|-------|
353360
| `CoreVault` | `src/core/CoreVault.sol:36` | Diamond-lite proxy; delegatecall dispatch |
354361
| `ERC4626Module` | `src/core/modules/ERC4626Module.sol:166` | Deposit/withdraw; BUG 2+3 fixed here |
355-
| `QueueModule._settleLoop` | `src/core/modules/QueueModule.sol:407` | Settlement; BUG 4+6 fixed here |
362+
| `EpochedQueueModule` close/fund/claim | `src/core/modules/EpochedQueueModule.sol:327-513` | Settlement; supersedes retired `QueueModule._settleLoop` where BUG 4+6 were fixed |
356363
| `AdminModule` | `src/core/modules/AdminModule.sol:73` | Governance; all timelocks |
357364
| `FeeCollector` | `src/core/modules/FeeCollector.sol:17` | Immutable governor; GI-2 |
358365
| `FixedMaturityModule` | `src/core/modules/FixedMaturityModule.sol:52` | FM state machine; FM audit scope |

0 commit comments

Comments
 (0)