Skip to content

fix: no more optimistic and double counting across epochs - #106

Merged
matteoettam09 merged 5 commits into
mainfrom
issue-20
Nov 19, 2025
Merged

fix: no more optimistic and double counting across epochs#106
matteoettam09 merged 5 commits into
mainfrom
issue-20

Conversation

@matteoettam09

@matteoettam09 matteoettam09 commented Nov 19, 2025

Copy link
Copy Markdown
Member

Summary by Sourcery

Fix optimistic double counting by limiting pending deposit and redeem calculations to the first MAX_FULFILL_BATCH_SIZE requests per epoch and add tests for batch limit accounting

Bug Fixes:

  • Limit pendingDeposit to sum only the amounts from the first MAX_FULFILL_BATCH_SIZE deposit requests
  • Limit pendingRedeem to sum only the shares from the first MAX_FULFILL_BATCH_SIZE redeem requests

Tests:

  • Add BatchLimitAccounting test suite to validate batch limit behavior

Summary by CodeRabbit

  • New Features

    • Added a configurable max fulfill batch size (with getter/setter and event) and protocol-level pause/unpause controls.
  • Bug Fixes

    • Pending deposit/redeem reporting and fulfillment now respect a configurable per-call batch limit, producing accurate processable amounts.
  • API

    • pendingDeposit and pendingRedeem now accept a batch-size parameter; previous fixed batch constant removed.
  • Tests

    • Added comprehensive tests covering batch-limit accounting and fulfillment synchronization.

…correct share price calculation and no protocol halts

Test Coverage
BatchLimitAccounting.test.ts validates:
Returns full amount when < 150 requests
Returns only first 150 when > 150 requests
Handles varying amounts correctly
Works with redeem requests
Multi-epoch scenarios (no double-counting)
Integration with fulfillDeposit()/fulfillRedeem()
Edge cases (0, 1, 150, 151 requests)
@immunefi-magnus

Copy link
Copy Markdown

🛡️ Immunefi PR Reviews

We noticed that your project isn't set up for automatic code reviews. If you'd like this PR reviewed by the Immunefi team, you can request it manually using the link below:

🔗 Send this PR in for review

Once submitted, we'll take care of assigning a reviewer and follow up here.

@sourcery-ai

sourcery-ai Bot commented Nov 19, 2025

Copy link
Copy Markdown

Reviewer's Guide

Refactored pendingDeposit and pendingRedeem to compute processable amounts/shares limited by MAX_FULFILL_BATCH_SIZE, added tests to validate batch limit behavior, and updated corresponding artifacts.

Class diagram for updated OrionVault pendingDeposit and pendingRedeem methods

classDiagram
    class OrionVault {
        +pendingDeposit() uint256
        +pendingRedeem() uint256
        -_depositRequests
        -_redeemRequests
        -MAX_FULFILL_BATCH_SIZE
    }
    OrionVault : pendingDeposit() now sums only first MAX_FULFILL_BATCH_SIZE deposit requests
    OrionVault : pendingRedeem() now sums only first MAX_FULFILL_BATCH_SIZE redeem requests
    OrionVault --> "*" _depositRequests
    OrionVault --> "*" _redeemRequests
Loading

Flow diagram for batch-limited pendingDeposit and pendingRedeem calculation

flowchart TD
    A["pendingDeposit() called"] --> B["Get _depositRequests length"]
    B --> C{"length == 0?"}
    C -- Yes --> D["Return 0"]
    C -- No --> E["Set batchSize = min(length, MAX_FULFILL_BATCH_SIZE)"]
    E --> F["Sum amounts of first batchSize deposit requests"]
    F --> G["Return processableAmount"]

    H["pendingRedeem() called"] --> I["Get _redeemRequests length"]
    I --> J{"length == 0?"}
    J -- Yes --> K["Return 0"]
    J -- No --> L["Set batchSize = min(length, MAX_FULFILL_BATCH_SIZE)"]
    L --> M["Sum shares of first batchSize redeem requests"]
    M --> N["Return processableShares"]
Loading

File-Level Changes

Change Details Files
Refined pendingDeposit to calculate processable deposit amounts up to batch size
  • Replaced static _pendingDeposit return with dynamic loop over first MAX_FULFILL_BATCH_SIZE deposit requests
  • Handled empty queue case explicitly
  • Capped batch size using MAX_FULFILL_BATCH_SIZE constant
contracts/vaults/OrionVault.sol
Refined pendingRedeem to calculate processable redeemable shares up to batch size
  • Replaced static _pendingRedeem return with dynamic loop over first MAX_FULFILL_BATCH_SIZE redeem requests
  • Handled empty queue case explicitly
  • Capped batch size using MAX_FULFILL_BATCH_SIZE constant
contracts/vaults/OrionVault.sol
Added batch limit accounting tests
  • Introduced BatchLimitAccounting.test.ts to verify that only MAX_FULFILL_BATCH_SIZE requests are counted per epoch
  • Asserted correct sums for deposits and redeems under various queue lengths
test/BatchLimitAccounting.test.ts
Updated compiled artifacts to reflect interface changes
  • Regenerated TransparentVaultFactory artifact
  • Regenerated OrionTransparentVault artifact
artifacts/contracts/factories/TransparentVaultFactory.sol/TransparentVaultFactory.json
artifacts/contracts/vaults/OrionTransparentVault.sol/OrionTransparentVault.json

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Nov 19, 2025

Copy link
Copy Markdown

Walkthrough

Removed cached pending totals and per-vault constant; pendingDeposit/pendingRedeem now accept a fulfillBatchSize and compute sums from request queues up to that size. Fulfill functions use config.maxFulfillBatchSize() for batching. Tests and config/orchestrator call sites updated accordingly.

Changes

Cohort / File(s) Change Summary
Vault implementation
contracts/vaults/OrionVault.sol, artifacts/contracts/vaults/OrionVault.sol/OrionVault.json
Removed _pendingDeposit, _pendingRedeem state and MAX_FULFILL_BATCH_SIZE constant. pendingDeposit and pendingRedeem signatures changed to accept uint256 fulfillBatchSize; they now compute sums from request queues up to the provided batch size. Fulfill functions use config.maxFulfillBatchSize() and no longer update cached totals.
Configuration
contracts/OrionConfig.sol, contracts/interfaces/IOrionConfig.sol, artifacts/contracts/OrionConfig.sol/OrionConfig.json, artifacts/contracts/interfaces/IOrionConfig.sol/IOrionConfig.json
Added uint256 public maxFulfillBatchSize (initialized to 150), maxFulfillBatchSize() getter, setMaxFulfillBatchSize(uint256) setter and MaxFulfillBatchSizeUpdated event. Also consolidated guardian/pause/unpause methods.
Orchestrators / callers
contracts/orchestrators/InternalStatesOrchestrator.sol, contracts/orchestrators/LiquidityOrchestrator.sol, artifacts/.../LiquidityOrchestrator.json
Call sites updated to read maxFulfillBatchSize once and pass it into vault pendingDeposit(...)/pendingRedeem(...) calls. Local batching logic now driven by config value.
Events library
contracts/libraries/EventsLib.sol, artifacts/contracts/libraries/EventsLib.sol/EventsLib.json
Added MaxFulfillBatchSizeUpdated(uint256 indexed) event to EventsLib ABI.
Tests updated / added
test/BatchLimitAccounting.test.ts, test/*.test.ts (many tests), test/orchestrator/*
Added BatchLimitAccounting.test.ts covering batch-limit accounting and fulfillment synchronization. Numerous tests updated to call pendingDeposit(await orionConfig.maxFulfillBatchSize()) / pendingRedeem(await orionConfig.maxFulfillBatchSize()) and to read orionConfig.maxFulfillBatchSize() instead of vault constant.
Artifacts / bytecode-only updates
artifacts/contracts/** (multiple JSONs: PriceAdapter, ExecutionAdapter, strategies, orchestrators, etc.)
Rebuilt artifacts: updated bytecode / deployedBytecode strings in many compiled artifacts without ABI signature changes (except where ABIs were intentionally extended above).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Orchestrator as Orchestrator
  participant Config as OrionConfig
  participant Vault as OrionVault

  Note over Orchestrator,Config: Preprocessing / batch-size retrieval
  Orchestrator->>Config: maxFulfillBatchSize()
  Config-->>Orchestrator: batchSize

  Note over Orchestrator,Vault: Query processable amounts (batch-aware)
  Orchestrator->>Vault: pendingDeposit(batchSize)
  Vault-->>Orchestrator: depositProcessableAmount
  Orchestrator->>Vault: pendingRedeem(batchSize)
  Vault-->>Orchestrator: redeemProcessableAmount

  alt depositProcessableAmount > 0
    Orchestrator->>Vault: fulfillDeposit(batchSize)  %% uses config internally for loop limit
    Vault-->>Orchestrator: sharesMinted / assetsMoved
  end

  alt redeemProcessableAmount > 0
    Orchestrator->>Vault: fulfillRedeem(batchSize)
    Vault-->>Orchestrator: assetsReturned / sharesBurned
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

  • Focus review on:
    • contracts/vaults/OrionVault.sol: correctness of iteration/aggregation in new parameterized pendingDeposit/pendingRedeem and that no cached state regressions remain.
    • contracts/OrionConfig.sol: access control and validation in setMaxFulfillBatchSize, and pause/unpause/guardian consolidation.
    • Orchestrator call sites (InternalStatesOrchestrator.sol, LiquidityOrchestrator.sol) to ensure batchSize is read once and propagated correctly.
    • New test test/BatchLimitAccounting.test.ts for deterministic assumptions matching implementation.

Possibly related PRs

  • fix: fulfillRedeem, unit tests #71 — touches fulfillRedeem changes and access control in orbit of vault fulfillment logic.
  • Dev #73 — modifies vault/orchestrator fulfillment paths and interactions; related to batching and fulfillment sequencing.
  • Dev #91 — adjusts orchestrator handling of pending deposit/redeem flows and ERC20 transfer ordering; overlaps with batch-aware orchestrator updates.

Poem

🐰 I counted queues in moonlit math, one hop, one nibble, one path,
Up to one-fifty, I tally the heap, no cached crumbs left for me to keep.
Config whispers the batch-size tune, orchestrators hum, fulfillments croon.
A little rabbit dances light, batching tidy through the night. ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the main objective: fixing optimistic double counting by limiting pending calculations to a batch size, which aligns with the core changes removing cached state variables and implementing batch-size-aware pending calculations.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch issue-20

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@sourcery-ai sourcery-ai Bot 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.

Hey there - I've reviewed your changes - here's some feedback:

  • Consider extracting the batching sum logic in pendingDeposit and pendingRedeem into a reusable internal helper to reduce code duplication.
  • Review the gas impact of looping up to MAX_FULFILL_BATCH_SIZE in view functions, as on-chain iterations could become expensive at higher batch sizes.
  • Ensure tests include scenarios where the request count is below, equal to, and exceeds MAX_FULFILL_BATCH_SIZE to validate correct boundary behavior.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Consider extracting the batching sum logic in pendingDeposit and pendingRedeem into a reusable internal helper to reduce code duplication.
- Review the gas impact of looping up to MAX_FULFILL_BATCH_SIZE in view functions, as on-chain iterations could become expensive at higher batch sizes.
- Ensure tests include scenarios where the request count is below, equal to, and exceeds MAX_FULFILL_BATCH_SIZE to validate correct boundary behavior.

## Individual Comments

### Comment 1
<location> `test/BatchLimitAccounting.test.ts:28-130` </location>
<code_context>

</code_context>

<issue_to_address>
**issue (code-quality):** Avoid function declarations, favouring function assignment expressions, inside blocks. ([`avoid-function-declarations-in-blocks`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/TypeScript/Default-Rules/avoid-function-declarations-in-blocks))

<details><summary>Explanation</summary>Function declarations may be hoisted in Javascript, but the behaviour is inconsistent between browsers.
Hoisting is generally confusing and should be avoided. Rather than using function declarations inside blocks, you
should use function expressions, which create functions in-scope.
</details>
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@codecov

codecov Bot commented Nov 19, 2025

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
test/BatchLimitAccounting.test.ts (1)

223-238: Clarify request aggregation comments and consider adding a >MAX_FULFILL_BATCH_SIZE coverage test

  • In the “Simulated batch limit behavior” block, comments conflict:

    • Line 224 claims requestDeposit overwrites the previous request per user,
    • Line 232 correctly notes it actually increments the amount for that address.
      This is a bit confusing for readers; it would help to align these comments with the contract behavior (one aggregated request per address).
  • The documentation section explains the 200‑request scenario and the 150‑request batch limit, but the suite never actually exercises pendingDeposit/pendingRedeem with more than 150 unique requesters. If practical, adding a targeted test that constructs >MAX_FULFILL_BATCH_SIZE distinct LP addresses (e.g., via impersonation of pre‑funded accounts) would validate the fix in the exact edge case described in the docs.

Neither of these are blockers, but they would make the tests more self‑consistent and strengthen confidence in the batch‑limit behavior.

Also applies to: 301-319

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0321cce and 3ab4a4f.

📒 Files selected for processing (2)
  • contracts/vaults/OrionVault.sol (1 hunks)
  • test/BatchLimitAccounting.test.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
test/BatchLimitAccounting.test.ts (2)
test/orchestrator/Orchestrators.test.ts (3)
  • MIN_DEPOSIT (2542-2716)
  • orionConfig (2642-2660)
  • orionConfig (2662-2678)
test/OrionVaultExchangeRate.test.ts (3)
  • loadFixture (408-454)
  • loadFixture (458-483)
  • it (353-405)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Sourcery review
  • GitHub Check: Build, Lint and Test
🔇 Additional comments (1)
test/BatchLimitAccounting.test.ts (1)

28-129: Fixture wiring and environment setup look solid

The deployment fixture cleanly wires the mock underlying asset, config, both orchestrators, vault factory, registry, and vault instance, then pre‑funds and approves all test users. This gives a realistic environment for the batch‑limit tests without unnecessary complexity. No issues from a correctness perspective.

Comment thread contracts/vaults/OrionVault.sol Outdated

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
test/MinimumAmountDOS.test.ts (1)

445-445: Use the MAX_FULFILL_BATCH_SIZE variable instead of hardcoding.

Line 445 hardcodes 150n in the calculation, but the test already retrieves the batch size dynamically on line 322. This creates inconsistency and the test will fail if the default config value changes.

Apply this diff to use the variable:

-      const capitalRequired = (MIN_DEPOSIT * 150n) / 10n ** 6n; // 150 = MAX_FULFILL_BATCH_SIZE
+      const capitalRequired = (MIN_DEPOSIT * MAX_FULFILL_BATCH_SIZE) / 10n ** 6n;

Note: You'll need to ensure MAX_FULFILL_BATCH_SIZE is accessible in this test's scope, or retrieve it again within this test case.

♻️ Duplicate comments (1)
contracts/vaults/OrionVault.sol (1)

576-591: pendingDeposit/pendingRedeem still don’t match the batch actually processed in fulfillDeposit/fulfillRedeem (and uint16 loop index is unsafe with configurable batch size)

pendingDeposit / pendingRedeem now correctly cap their sums to batchSize = min(length, fulfillBatchSize) and iterate via at(i) over indices 0 .. batchSize-1. However, fulfillDeposit / fulfillRedeem process batches using at(0) on each iteration combined with remove(user), which internally uses swap‑and‑pop in EnumerableMap. When length > batchSize, the actual set of processed entries in a fulfill call is {index 0, last, second‑last, ...} rather than the fixed prefix 0 .. batchSize-1 used in pending*. This means:

  • The depositTotalAssets / redeemTotalAssets values computed off‑chain or by orchestrators via pending* can diverge from the amounts actually fulfilled in that epoch under high load (more than fulfillBatchSize unique requesters with heterogeneous sizes).
  • Share/asset accounting for the epoch can be incorrect, since fulfill* uses depositTotalAssets / redeemTotalAssets to derive per-user share/asset conversions but then applies them to a different subset of requests than what pending* summed.
  • This is the same structural issue that was flagged in the previous review and still needs to be addressed.

On top of this, all four loops use uint16 i while batchSize is uint256. If either fulfillBatchSize (for pending*) or config.maxFulfillBatchSize() / queue length (for fulfill*) ever exceed type(uint16).max (65,535), the ++i increment will overflow and revert due to Solidity 0.8 checked arithmetic, effectively DoSing these paths for misconfigured batch sizes or very large queues.

To make pending* and fulfill* strictly consistent, and to remove the implicit uint16 limit, you can:

  1. Keep pending* summing at(i) for i in [0, batchSize).
  2. Change fulfillDeposit / fulfillRedeem to:
    • First snapshot the first batchSize entries via at(i) into memory arrays of (user, amount/shares), and
    • Then iterate over that snapshot to process and remove(user) by key.
  3. Use uint256 as the loop index type everywhere.

Illustrative diff (adapt as needed):

@@
-    function pendingDeposit(uint256 fulfillBatchSize) external view returns (uint256) {
-        uint256 length = _depositRequests.length();
-        if (length == 0) {
-            return 0;
-        }
-
-        uint256 batchSize = Math.min(length, fulfillBatchSize);
-        uint256 processableAmount = 0;
-
-        for (uint16 i = 0; i < batchSize; ++i) {
-            // slither-disable-next-line unused-return
-            (, uint256 amount) = _depositRequests.at(i);
-            processableAmount += amount;
-        }
-
-        return processableAmount;
-    }
+    function pendingDeposit(uint256 fulfillBatchSize) external view returns (uint256) {
+        uint256 length = _depositRequests.length();
+        if (length == 0) {
+            return 0;
+        }
+
+        uint256 batchSize = Math.min(length, fulfillBatchSize);
+        uint256 processableAmount = 0;
+
+        for (uint256 i = 0; i < batchSize; ++i) {
+            // slither-disable-next-line unused-return
+            (, uint256 amount) = _depositRequests.at(i);
+            processableAmount += amount;
+        }
+
+        return processableAmount;
+    }
@@
-    function pendingRedeem(uint256 fulfillBatchSize) external view returns (uint256) {
-        uint256 length = _redeemRequests.length();
-        if (length == 0) {
-            return 0;
-        }
-
-        uint256 batchSize = Math.min(length, fulfillBatchSize);
-        uint256 processableShares = 0;
-
-        for (uint16 i = 0; i < batchSize; ++i) {
-            // slither-disable-next-line unused-return
-            (, uint256 shares) = _redeemRequests.at(i);
-            processableShares += shares;
-        }
-
-        return processableShares;
-    }
+    function pendingRedeem(uint256 fulfillBatchSize) external view returns (uint256) {
+        uint256 length = _redeemRequests.length();
+        if (length == 0) {
+            return 0;
+        }
+
+        uint256 batchSize = Math.min(length, fulfillBatchSize);
+        uint256 processableShares = 0;
+
+        for (uint256 i = 0; i < batchSize; ++i) {
+            // slither-disable-next-line unused-return
+            (, uint256 shares) = _redeemRequests.at(i);
+            processableShares += shares;
+        }
+
+        return processableShares;
+    }
@@
     function fulfillDeposit(uint256 depositTotalAssets) external onlyLiquidityOrchestrator nonReentrant {
         uint256 length = _depositRequests.length();
         if (length == 0) {
             return;
         }
 
         uint256 batchSize = Math.min(length, config.maxFulfillBatchSize());
         uint16 currentEpoch = internalStatesOrchestrator.epochCounter();
@@
-        // Process requests in batch
-        uint256 processedAmount = 0;
-        for (uint16 i = 0; i < batchSize; ++i) {
-            // Get request by index (index 0 since we remove as we go)
-            (address user, uint256 amount) = _depositRequests.at(0);
-
-            // slither-disable-next-line unused-return
-            _depositRequests.remove(user);
+        // Snapshot first `batchSize` requests so selection matches `pendingDeposit`
+        address[] memory users = new address[](batchSize);
+        uint256[] memory amounts = new uint256[](batchSize);
+        for (uint256 i = 0; i < batchSize; ++i) {
+            (users[i], amounts[i]) = _depositRequests.at(i);
+        }
+
+        // Process requests in batch
+        uint256 processedAmount = 0;
+        for (uint256 i = 0; i < batchSize; ++i) {
+            address user = users[i];
+            uint256 amount = amounts[i];
+
+            // slither-disable-next-line unused-return
+            _depositRequests.remove(user);
@@
     function fulfillRedeem(uint256 redeemTotalAssets) external onlyLiquidityOrchestrator nonReentrant {
         uint256 length = _redeemRequests.length();
         if (length == 0) {
             return;
         }
 
         uint256 batchSize = Math.min(length, config.maxFulfillBatchSize());
         uint16 currentEpoch = internalStatesOrchestrator.epochCounter();
@@
-        // Process requests in batch
-        uint256 processedShares = 0;
-        for (uint16 i = 0; i < batchSize; ++i) {
-            // Get request by index (index 0 since we remove as we go)
-            (address user, uint256 shares) = _redeemRequests.at(0);
-
-            // slither-disable-next-line unused-return
-            _redeemRequests.remove(user);
+        // Snapshot first `batchSize` requests so selection matches `pendingRedeem`
+        address[] memory users = new address[](batchSize);
+        uint256[] memory sharesList = new uint256[](batchSize);
+        for (uint256 i = 0; i < batchSize; ++i) {
+            (users[i], sharesList[i]) = _redeemRequests.at(i);
+        }
+
+        // Process requests in batch
+        uint256 processedShares = 0;
+        for (uint256 i = 0; i < batchSize; ++i) {
+            address user = users[i];
+            uint256 shares = sharesList[i];
+
+            // slither-disable-next-line unused-return
+            _redeemRequests.remove(user);

This keeps:

  • The set of requests selected by pendingDeposit / pendingRedeem and fulfillDeposit / fulfillRedeem exactly aligned (the same indices 0..batchSize-1).
  • The batch bounded by the same fulfillBatchSize / config.maxFulfillBatchSize() parameter.
  • Loop indices as uint256, removing the hidden assumption that batchSize <= 65,535.

Also applies to: 595-610, 623-655, 658-693

🧹 Nitpick comments (3)
test/orchestrator/Orchestrators.test.ts (1)

1263-1264: Tests now correctly use config-driven fulfill batch size

Switching all pendingDeposit/pendingRedeem calls to pass await orionConfig.maxFulfillBatchSize() keeps the orchestrator tests aligned with the new batched accounting semantics, and the DOS test deriving MAX_FULFILL_BATCH_SIZE from config avoids hard-coding protocol constants.

One minor coupling: the DOS scenario still asserts capitalInUnits === 15000n, so changing the default maxFulfillBatchSize will intentionally break this test; if you expect that parameter to change often, consider expressing the expectation symbolically in terms of MAX_FULFILL_BATCH_SIZE instead of the numeric 15000.

Also applies to: 1466-1473, 1519-1523, 1710-1715, 2647-2660, 2676-2678

contracts/OrionConfig.sol (1)

63-65: Batch-size config and pause controls are sound; consider a couple of guards

The new maxFulfillBatchSize parameter (default 150) plus setMaxFulfillBatchSize() gating on isSystemIdle() and size != 0 fits the intended batching model, and the guardian-based pauseAll/unpauseAll wiring into both orchestrators looks correct.

Two optional hardening tweaks you might consider:

  • In pauseAll / unpauseAll, early-revert if internalStatesOrchestrator or liquidityOrchestrator is still unset to avoid no-op calls against zero addresses.
  • If you expect very large maxFulfillBatchSize values to be dangerous for gas, add a conservative upper bound (e.g., revert if size exceeds a protocol constant), so misconfiguration can’t accidentally create an unfulfillable epoch.

Also applies to: 112-113, 196-204, 206-235

test/BatchLimitAccounting.test.ts (1)

281-301: Strengthen the >maxFulfillBatchSize assertion for pendingDeposit

The “documentation” test correctly demonstrates that with maxFulfillBatchSize set below the number of deposits, pendingDeposit(batchSize) should be less than the total queued deposits. To catch more subtle regressions, you could assert the exact expected amount:

- const pendingDeposit = await vault.pendingDeposit(await config.maxFulfillBatchSize());
-
- void expect(pendingDeposit).to.be.lessThan(DEPOSIT_AMOUNT * BigInt(numUsers));
+ const batchSize = await config.maxFulfillBatchSize();
+ const pendingDeposit = await vault.pendingDeposit(batchSize);
+
+ const expectedProcessable = DEPOSIT_AMOUNT * batchSize;
+ void expect(pendingDeposit).to.equal(expectedProcessable);

(This assumes each of the first batchSize requests is for DEPOSIT_AMOUNT, which matches how this test is constructed.)

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3ab4a4f and f3b0904.

📒 Files selected for processing (27)
  • artifacts/contracts/OrionConfig.sol/OrionConfig.json (4 hunks)
  • artifacts/contracts/execution/OrionAssetERC4626ExecutionAdapter.sol/OrionAssetERC4626ExecutionAdapter.json (1 hunks)
  • artifacts/contracts/interfaces/IOrionConfig.sol/IOrionConfig.json (2 hunks)
  • artifacts/contracts/interfaces/IOrionTransparentVault.sol/IOrionTransparentVault.json (2 hunks)
  • artifacts/contracts/interfaces/IOrionVault.sol/IOrionVault.json (2 hunks)
  • artifacts/contracts/libraries/EventsLib.sol/EventsLib.json (2 hunks)
  • artifacts/contracts/orchestrators/LiquidityOrchestrator.sol/LiquidityOrchestrator.json (1 hunks)
  • artifacts/contracts/price/OrionAssetERC4626PriceAdapter.sol/OrionAssetERC4626PriceAdapter.json (1 hunks)
  • artifacts/contracts/price/PriceAdapterRegistry.sol/PriceAdapterRegistry.json (1 hunks)
  • artifacts/contracts/strategies/KBestTvlWeightedAverage.sol/KBestTvlWeightedAverage.json (1 hunks)
  • artifacts/contracts/test/KBestTvlWeightedAverageInvalid.sol/KBestTvlWeightedAverageInvalid.json (1 hunks)
  • artifacts/contracts/vaults/OrionVault.sol/OrionVault.json (2 hunks)
  • contracts/OrionConfig.sol (3 hunks)
  • contracts/interfaces/IOrionConfig.sol (1 hunks)
  • contracts/interfaces/IOrionVault.sol (1 hunks)
  • contracts/libraries/EventsLib.sol (1 hunks)
  • contracts/orchestrators/InternalStatesOrchestrator.sol (3 hunks)
  • contracts/orchestrators/LiquidityOrchestrator.sol (1 hunks)
  • contracts/vaults/OrionVault.sol (3 hunks)
  • test/BatchLimitAccounting.test.ts (1 hunks)
  • test/MinimumAmountDOS.test.ts (1 hunks)
  • test/OrionConfigVault.test.ts (4 hunks)
  • test/ProtocolPause.test.ts (3 hunks)
  • test/RedeemBeforeDepositOrder.test.ts (6 hunks)
  • test/orchestrator/OrchestratorPerformUpkeep.test.ts (4 hunks)
  • test/orchestrator/Orchestrators.test.ts (6 hunks)
  • test/orchestrator/OrchestratorsZeroState.test.ts (2 hunks)
✅ Files skipped from review due to trivial changes (2)
  • artifacts/contracts/price/PriceAdapterRegistry.sol/PriceAdapterRegistry.json
  • artifacts/contracts/test/KBestTvlWeightedAverageInvalid.sol/KBestTvlWeightedAverageInvalid.json
🧰 Additional context used
🧬 Code graph analysis (2)
artifacts/contracts/execution/OrionAssetERC4626ExecutionAdapter.sol/OrionAssetERC4626ExecutionAdapter.json (1)
test/Adapters.test.ts (2)
  • OrionAssetERC4626ExecutionAdapterFactory (209-225)
  • orionConfig (16-381)
test/orchestrator/Orchestrators.test.ts (1)
test/orchestrator/OrchestratorConfiguration.test.ts (1)
  • ABSOLUTE_VAULT_DEPOSIT (85-705)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build, Lint and Test
  • GitHub Check: Sourcery review
🔇 Additional comments (23)
artifacts/contracts/strategies/KBestTvlWeightedAverage.sol/KBestTvlWeightedAverage.json (1)

222-223: Verify bytecode changes align with source contract modifications.

This is a compiled artifact file with updated bytecode and deployedBytecode. While bytecode changes are expected when the underlying contract changes, I cannot verify the correctness of raw bytecode hex strings. The ABI remains unchanged, which is good.

To ensure the new bytecode is correct, I need to review the actual source contract (contracts/strategies/KBestTvlWeightedAverage.sol) to confirm it aligns with the PR's batching logic changes.

Can you provide or point to the source contract file that was modified? This will help me verify that:

  1. The contract logic correctly implements the MaxFulfillBatchSize batching changes mentioned in the PR
  2. The compiled bytecode reflects those source changes accurately
artifacts/contracts/execution/OrionAssetERC4626ExecutionAdapter.sol/OrionAssetERC4626ExecutionAdapter.json (1)

172-173: Build artifact—bytecode regeneration is expected; verify source code instead.

This is a compiler-generated artifact file. The bytecode and deployedBytecode changes on lines 172–173 are expected outcomes when the underlying Solidity source changes. These hex strings cannot be meaningfully reviewed by humans.

Since the ABI (lines 5–171) remains unchanged, the public interface is stable. Verify the logic changes by reviewing the source Solidity file (contracts/execution/OrionAssetERC4626ExecutionAdapter.sol) rather than this artifact. Ensure build artifacts are regenerated automatically during CI/CD rather than manually versioned in the repository if not already doing so.

artifacts/contracts/price/OrionAssetERC4626PriceAdapter.sol/OrionAssetERC4626PriceAdapter.json (1)

110-111: Artifact bytecode change is justified and necessary.

The IOrionConfig interface was modified to include the maxFulfillBatchSize() function as part of this PR. Since OrionAssetERC4626PriceAdapter imports and depends on IOrionConfig, recompilation occurs automatically when the dependency changes, resulting in the updated bytecode. This is expected behavior—the artifact should remain in the PR.

artifacts/contracts/interfaces/IOrionConfig.sol/IOrionConfig.json (1)

329-341: LGTM! Configuration interface properly extended.

The maxFulfillBatchSize getter and setMaxFulfillBatchSize setter follow standard patterns for configuration management in the ABI.

Also applies to: 498-510

artifacts/contracts/vaults/OrionVault.sol/OrionVault.json (1)

1246-1262: LGTM! ABI correctly reflects interface changes.

The OrionVault ABI properly includes the new fulfillBatchSize parameter for both pendingDeposit and pendingRedeem functions.

Also applies to: 1265-1281

artifacts/contracts/orchestrators/LiquidityOrchestrator.sol/LiquidityOrchestrator.json (1)

889-890: LGTM! Bytecode updated as expected.

The bytecode changes reflect the implementation updates to pass maxFulfillBatchSize() when calling vault's pendingDeposit and pendingRedeem methods.

contracts/libraries/EventsLib.sol (1)

40-42: LGTM! Event properly declared.

The MaxFulfillBatchSizeUpdated event follows the established pattern for configuration update events in EventsLib. The indexed parameter enables efficient event filtering.

test/MinimumAmountDOS.test.ts (1)

322-323: LGTM! Correctly retrieves batch size from config.

The test now dynamically retrieves the batch size from config.maxFulfillBatchSize() instead of using a hardcoded constant, which properly aligns with the configurable batch size changes.

test/RedeemBeforeDepositOrder.test.ts (1)

91-92: LGTM! All test calls properly updated.

All calls to vault.pendingDeposit() and vault.pendingRedeem() have been consistently updated to pass the batch size parameter retrieved from orionConfig.maxFulfillBatchSize().

Also applies to: 237-238, 368-369, 388-388, 401-402, 417-417

artifacts/contracts/interfaces/IOrionVault.sol/IOrionVault.json (1)

801-817: LGTM! Interface ABI correctly updated.

The IOrionVault interface artifact properly reflects the addition of the fulfillBatchSize parameter to pendingDeposit and pendingRedeem methods.

Also applies to: 820-836

test/ProtocolPause.test.ts (1)

324-442: Batch-size-aware pendingDeposit checks match the new vault API

Using await config.maxFulfillBatchSize() in all pendingDeposit assertions keeps these pause/unpause integration tests aligned with the updated interface, and the single-request scenarios ensure the batch cap cannot hide pending work.

test/OrionConfigVault.test.ts (2)

384-436: Deposit cancellation tests correctly use batch-limited pendingDeposit

The updates to call vault.pendingDeposit(await orionConfig.maxFulfillBatchSize()) before and after cancellations are consistent with the new ABI and still accurately assert full/partial cancellation behavior for single-request cases.


507-526: Pending amounts tests aligned with maxFulfillBatchSize configuration

Switching pendingDeposit()/pendingRedeem() to accept await orionConfig.maxFulfillBatchSize() keeps the “pending amounts” expectations correct under the batch-limited model and matches how orchestrators now query vaults.

artifacts/contracts/libraries/EventsLib.sol/EventsLib.json (1)

77-89: MaxFulfillBatchSizeUpdated event ABI looks consistent

The added MaxFulfillBatchSizeUpdated(uint256 maxFulfillBatchSize) event (indexed param) matches the new configuration knob and follows the pattern of other EventsLib config events; the corresponding bytecode updates are expected.

Also applies to: 339-340

test/orchestrator/OrchestratorPerformUpkeep.test.ts (1)

1320-1802: performUpkeep flow tests correctly adopt batch-limited pending queues

Using await orionConfig.maxFulfillBatchSize() for pendingRedeem/pendingDeposit in the long upkeep scenario keeps the expectations in sync with the new batch-limited accounting in both orchestrators, without changing the economic meaning of the tests (which never exceed one batch of requests).

contracts/interfaces/IOrionConfig.sol (1)

210-217: IOrionConfig exposes maxFulfillBatchSize used across orchestrators

Adding maxFulfillBatchSize() and setMaxFulfillBatchSize(uint256 size) to the interface matches how InternalStatesOrchestrator, LiquidityOrchestrator, and tests are now consuming this config value.

One thing to double-check in OrionConfig.sol is that setMaxFulfillBatchSize:

  • rejects size == 0, and
  • is gated by isSystemIdle() like other critical config setters,

so that all components see a stable, non-degenerate batch size during an epoch.

contracts/orchestrators/LiquidityOrchestrator.sol (1)

493-511: Vault fulfillment now respects config-driven maxFulfillBatchSize

Fetching uint256 maxFulfillBatchSize = config.maxFulfillBatchSize(); and passing it into vaultContract.pendingRedeem/pendingDeposit aligns fulfillment with the batch-limited accounting done in InternalStatesOrchestrator and removes the old “unbounded pending” assumption, without changing the fulfill logic itself.

test/orchestrator/OrchestratorsZeroState.test.ts (1)

100-131: Zero-state orchestrator tests correctly use batch-size-aware pending queries

Using await orionConfig.maxFulfillBatchSize() in the zero-deposit/zero-intent checks keeps these guardrail tests compatible with the new vault API while still asserting that no spurious work is scheduled when there are no deposits/redeems.

contracts/orchestrators/InternalStatesOrchestrator.sol (2)

298-307: Epoch vault filtering now uses batch-limited pendingDeposit

Caching uint256 maxFulfillBatchSize = config.maxFulfillBatchSize(); and using it in IOrionVault(v).pendingDeposit(maxFulfillBatchSize) keeps the epoch vault selection logic consistent with the new batch-cap semantics, while still correctly including any vault with nonzero TVL or in-batch pending deposits.


364-435: Preprocessing minibatch uses maxFulfillBatchSize consistently for accounting

Reading maxFulfillBatchSize once per minibatch and passing it into both vault.pendingRedeem(maxFulfillBatchSize) (before convertToAssetsWithPITTotalAssets) and vault.pendingDeposit(maxFulfillBatchSize) ensures:

  • Fee and buffer calculations only consider up to the configured number of requests per epoch.
  • The same capped amounts underpin vaultsTotalAssetsForFulfillRedeem/vaultsTotalAssetsForFulfillDeposit, keeping InternalStatesOrchestrator in lockstep with LiquidityOrchestrator and avoiding optimistic double-counting across epochs.

The changes are local and don’t alter existing fee math or ordering logic.

artifacts/contracts/interfaces/IOrionTransparentVault.sol/IOrionTransparentVault.json (1)

837-873: ABI update for batch-sized pending calls looks consistent*

pendingDeposit(uint256 fulfillBatchSize) and pendingRedeem(uint256 fulfillBatchSize) signatures (single uint256 input, uint256 output, view) align with the new batch-size–aware logic used elsewhere; no issues from the artifact side as long as the Solidity interface and implementation match this ABI.

artifacts/contracts/OrionConfig.sol/OrionConfig.json (1)

121-133: OrionConfig ABI correctly exposes maxFulfillBatchSize and its event

The artifact cleanly reflects the new contract surface: MaxFulfillBatchSizeUpdated(uint256 indexed maxFulfillBatchSize), the maxFulfillBatchSize() view, and setMaxFulfillBatchSize(uint256 size) mutator. This ABI matches the Solidity implementation and supports the new batch-size–aware callers.

Also applies to: 632-644, 854-866

test/BatchLimitAccounting.test.ts (1)

28-130: Fixture and core batch-limit tests look correct and aligned with protocol changes

The fixture wires USDC, OrionConfig, both orchestrators, vault factory, vault, and curator whitelist in a minimal yet realistic way, and the core tests for pendingDeposit / pendingRedeem with < maxFulfillBatchSize and 0-request scenarios correctly exercise the new fulfillBatchSize parameter via config.maxFulfillBatchSize(). The pendingRedeem test that mints funding, routes it through the liquidity orchestrator, impersonates its address, and calls fulfillDeposit is a good end-to-end sanity check that pending shares and total supply stay in sync with what pendingRedeem(batchSize) reports.

Also applies to: 132-209

Comment on lines +183 to +192
/// @param fulfillBatchSize The maximum number of requests to process per fulfill call
/// @return Total pending deposits denominated in underlying asset units (e.g., USDC, ETH)
/// @dev This returns asset amounts, not share amounts
function pendingDeposit() external view returns (uint256);
function pendingDeposit(uint256 fulfillBatchSize) external view returns (uint256);

/// @notice Get total pending redemption shares across all users
/// @param fulfillBatchSize The maximum number of requests to process per fulfill call
/// @return Total pending redemptions denominated in vault share units
/// @dev This returns share amounts, not underlying asset amounts
function pendingRedeem() external view returns (uint256);
function pendingRedeem(uint256 fulfillBatchSize) external view returns (uint256);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Breaking API change - verify all callers updated.

The addition of the fulfillBatchSize parameter to pendingDeposit and pendingRedeem is a breaking change. The tests in this PR have been updated, but ensure all external integrations and off-chain components that call these functions are also updated.


🏁 Script executed:

#!/bin/bash
# Verify all calls to pendingDeposit and pendingRedeem pass the batch size parameter

# Search for calls to pendingDeposit without a parameter (potential missed updates)
echo "=== Checking for pendingDeposit calls ==="
rg -n 'pendingDeposit\s*\(\s*\)' --type solidity --type typescript

# Search for calls to pendingRedeem without a parameter (potential missed updates)
echo "=== Checking for pendingRedeem calls ==="
rg -n 'pendingRedeem\s*\(\s*\)' --type solidity --type typescript

Length of output: 148


🏁 Script executed:

#!/bin/bash
# Verify all calls to pendingDeposit and pendingRedeem pass the batch size parameter
# Using proper file patterns instead of invalid --type solidity

echo "=== Checking for pendingDeposit/pendingRedeem calls in .sol files ==="
rg -n 'pendingDeposit|pendingRedeem' -g '*.sol' -C 2

echo ""
echo "=== Checking for pendingDeposit/pendingRedeem calls in TypeScript files ==="
rg -n 'pendingDeposit|pendingRedeem' -g '*.ts' -g '*.tsx' -C 2

Length of output: 33967


Update all callers of pendingDeposit and pendingRedeem to include the fulfillBatchSize parameter.

The breaking API change was not fully applied. Three calls in test/mainnet-fork/multiAssetRobustness.test.ts are missing the required parameter and will fail:

  • Line 517: vault.pendingDeposit() → add parameter
  • Line 528: vault.pendingDeposit() → add parameter
  • Line 552: vault.pendingRedeem() → add parameter

Pass await config.maxFulfillBatchSize() (or equivalent) to each call to match the pattern used elsewhere in the codebase.

🤖 Prompt for AI Agents
In test/mainnet-fork/multiAssetRobustness.test.ts around lines 517, 528 and 552,
three calls to the updated vault API omit the required fulfillBatchSize
parameter; update each call to pass the configured batch size (e.g., replace
vault.pendingDeposit() at lines 517 and 528 with vault.pendingDeposit(await
config.maxFulfillBatchSize()) and replace vault.pendingRedeem() at line 552 with
vault.pendingRedeem(await config.maxFulfillBatchSize()), matching the pattern
used elsewhere in the codebase.

Comment thread test/BatchLimitAccounting.test.ts
@matteoettam09
matteoettam09 merged commit 9b91797 into main Nov 19, 2025
5 checks passed
@matteoettam09
matteoettam09 deleted the issue-20 branch November 19, 2025 14:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants