Skip to content

Convertible Deposits: Limit Orders (Fork) - #188

Merged
0xJem merged 14 commits into
OlympusDAO:feature/cd-limit-ordersfrom
ohmzeus:cd-limit-orders
Dec 17, 2025
Merged

0xJem merged 14 commits into
OlympusDAO:feature/cd-limit-ordersfrom
ohmzeus:cd-limit-orders

Conversation

@ohmzeus

@ohmzeus ohmzeus commented Dec 8, 2025 •

Copy link
Copy Markdown

Summary

Introduces a limit order system for the Convertible Deposit Auctioneer, enabling users to place persistent buy orders that execute when the auction price reaches their specified maximum. MEV bots are incentivized to fill orders, creating a permissionless execution layer.

Motivation

Currently, users must actively monitor CDAuctioneer prices and manually execute bids. This creates friction and may result in missed opportunities when prices briefly dip to favorable levels. Limit orders allow users to set-and-forget, capturing favorable prices without active management.

Features

Limit Orders

  • Users specify deposit period, deposit budget, max execution price, and minimum fill size
  • Orders remain open until fully filled or cancelled
  • Partial fills supported — orders can be filled incrementally

MEV Incentives

  • Users allocate an incentive budget alongside their deposit budget
  • Incentives paid proportionally to fill size (e.g., filling 20% of order pays 20% of incentive budget)
  • Removes gaming incentive to split fills into minimum-sized chunks

Yield Generation

  • Idle deposits held in sUSDS, generating yield while awaiting execution
  • Yield accrues to a configurable recipient address
  • sweepYield() transfers accrued yield as sUSDS shares

Price Validation

  • Uses previewBid() to check actual execution price including slippage across ticks
  • Orders only fill if effective price ≤ user's max price

User Flow

  1. User approves USDS spending
  2. User calls createOrder() with:
    • depositPeriod: CD term length
    • depositBudget: USDS to spend on bids
    • incentiveBudget: USDS to pay fillers
    • maxPrice: Maximum USDS per OHM
    • minFillSize: Minimum fill per transaction
  3. Contract deposits total into sUSDS
  4. MEV bot monitors prices, calls fillOrder() when profitable
  5. User receives position NFT + receipt tokens per fill
  6. User can cancelOrder() anytime to reclaim remaining funds

Bot Flow

  1. Monitor getFillableOrders() or index OrderCreated events
  2. Check canFillOrder() for fillability and effective price
  3. Call fillOrder() with order ID and fill amount
  4. Receive proportional incentive in USDS

Contract Architecture

┌─────────────────────────────────────────────────────────────┐
│                  CDAuctioneerLimitOrders                    │
├─────────────────────────────────────────────────────────────┤
│  Orders: mapping(orderId => LimitOrder)                     │
│  totalUsdsOwed: tracks principal for yield accounting       │
├─────────────────────────────────────────────────────────────┤
│  createOrder() ──────► USDS ──► sUSDS (yield)               │
│  fillOrder()   ──────► sUSDS ──► USDS ──► CDAuctioneer      │
│  cancelOrder() ──────► sUSDS ──► USDS ──► User              │
│  sweepYield()  ──────► sUSDS ──► Yield Recipient            │
└─────────────────────────────────────────────────────────────┘

Security Considerations

  • Reentrancy: All state-modifying functions use nonReentrant
  • Access Control: Ownable for yield recipient management; order cancellation restricted to owner
  • Price Manipulation: Uses previewBid() for actual execution price, not tick price
  • Yield Accounting: Tracks totalUsdsOwed to isolate user principal from yield

Testing

Comprehensive test suite covering:

  • Order creation (success + all revert cases)
  • Order filling (single, multiple, partial, final fill incentive handling)
  • Order cancellation (full refund, partial refund after fills)
  • Yield accrual and sweep
  • Admin functions
  • Edge cases (zero incentive, exact min fill, multiple users/fillers)

Files

  • src/CDAuctioneerLimitOrders.sol — Main contract
  • test/CDAuctioneerLimitOrders.t.sol — Test suite

Deployment Parameters

Parameter Description
owner_ Admin address for yield recipient management
cdAuctioneer_ CDAuctioneer contract address
usds_ USDS token address
sUsds_ sUSDS vault address
positionNft_ CD position NFT address
yieldRecipient_ Address to receive accrued yield
depositPeriods_ Array of supported deposit periods
receiptTokens_ Array of receipt token addresses (1:1 with periods)

Summary by CodeRabbit

  • New Features

    • Limit-order system for convertible deposits: create, change, fill, and cancel orders with per-order budgets, incentives, price limits, min-fill constraints, receipt-token/NFT handling, and safety checks.
    • On-chain yield accrual and sweep with configurable yield recipient; new view utilities for balances, execution price, incentives, and fillability.
  • Tests

    • Comprehensive end-to-end test suite with mocks covering order lifecycles, change-order flows, incentives, yield sweeping, NFT interactions, admin actions, and edge cases.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai

coderabbitai Bot commented Dec 8, 2025 •

Copy link
Copy Markdown
Contributor

Walkthrough

Adds a new CDAuctioneerLimitOrders contract implementing per-order limit orders, incentive budgets, and sUSDS-based yield accrual/sweep, plus a comprehensive test suite with mocks exercising creation, fills, cancellations, yield, admin flows, and view utilities.

Changes

Cohort / File(s) Summary
Limit-order contract
src/policies/deposits/LimitOrders.sol
New CDAuctioneerLimitOrders contract: order lifecycle (create/change/fill/cancel), per-order/accounting fields, incentive calculations, min-fill/max-price checks, yield accrual & sweep via sUSDS/USDS, receipt-token & position NFT integration, custom errors/events, view helpers, and ERC721 receiver.
Test suite & mocks
src/test/policies/ConvertibleDepositAuctioneer/LimitOrders.t.sol
New tests and mocks: MockUSDS, MockSUSDS, MockReceiptToken, MockPositionNFT, MockCDAuctioneer, and CDAuctioneerLimitOrdersTest covering order creation, change, fills, cancellations, yield handling/sweep, admin actions, view utilities, and many edge-case/error scenarios.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Filler
    participant LimitOrders as CDAuctioneerLimitOrders
    participant USDS
    participant sUSDS
    participant Auctioneer as CD_AUCTIONEER
    participant ReceiptToken
    participant PositionNFT

    User->>LimitOrders: createOrder(period, depositBudget, incentiveBudget, maxPrice, minFill)
    LimitOrders->>USDS: transferFrom(user, deposit+incentive)
    USDS-->>LimitOrders: USDS received
    LimitOrders->>sUSDS: deposit USDS (mint sUSDS)
    sUSDS-->>LimitOrders: sUSDS balance updated
    LimitOrders->>LimitOrders: store order, update totalUsdsOwed

    Filler->>LimitOrders: fillOrder(orderId, fillAmount)
    LimitOrders->>Auctioneer: previewBid(period, fillAmount)
    Auctioneer-->>LimitOrders: price, ohmOut
    LimitOrders->>LimitOrders: validate price, minFill, budgets
    LimitOrders->>Auctioneer: bid(period, fillAmount, incentive, ...)
    Auctioneer-->>LimitOrders: positionId, receiptToken, ohmOut
    LimitOrders->>ReceiptToken: transfer receipt token to order owner
    LimitOrders->>PositionNFT: transfer NFT to filler (or owner)
    LimitOrders->>LimitOrders: update depositSpent, incentiveSpent, totalUsdsOwed

    Admin->>LimitOrders: sweepYield()
    LimitOrders->>sUSDS: calculate accrued shares & transfer to yieldRecipient
    sUSDS-->>LimitOrders: shares transferred (yield swept)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

  • Files/areas to focus:
    • createOrder / changeOrder / fillOrder / cancelOrder state transitions and accounting in LimitOrders.sol
    • Incentive calculation, final-fill handling, enforcement of minFillSize and maxPrice
    • sUSDS exchange-rate math: getAccruedYield, getAccruedYieldShares, sweepYield
    • External interactions: previewBid / bid with CD_AUCTIONEER, receipt-token assumptions, and ERC721 handling
    • Tests in LimitOrders.t.sol for edge-case coverage that validate invariants

Possibly related PRs

  • olympus-v3#119 — Changes to auction preview/bid behavior used by CDAuctioneerLimitOrders (price/preview semantics).
  • olympus-v3#114 — Changes to deposit-period enablement and related auction APIs (isDepositPeriodEnabled, minimum bid) referenced by the new contract.

Suggested labels

audit-convertible-deposits-guardian

Poem

🐇 I placed my orders under moonbeam code,
USDS tucked into sUSDS mode.
Fillers hop by, positions take flight,
Yield swept to carrots in soft night.
A tiny rabbit cheers the deploy delight.

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'CDAuctioneer Limit Orders' directly summarizes the main change: a new limit order system for the CDAuctioneer, which is the primary feature added in this PR.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 08df4e9 and 9dbbf1d.

📒 Files selected for processing (2)
  • src/policies/deposits/LimitOrders.sol (1 hunks)
  • src/test/policies/ConvertibleDepositAuctioneer/LimitOrders.t.sol (1 hunks)
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: 0xJem
Repo: OlympusDAO/olympus-v3 PR: 29
File: src/policies/deposits/BaseDepositFacility.sol:631-639
Timestamp: 2025-11-07T11:09:53.808Z
Learning: OlympusDAO/olympus-v3: BaseDepositFacility.setAssetPeriodReclaimRate now uses a dedicated reclaim-rate error (not InvalidAddress) when reclaimRate_ > ONE_HUNDRED_PERCENT, fixed in a follow-up PR (#180). Avoid re-flagging this in earlier PRs.
📚 Learning: 2025-08-08T07:33:13.575Z
Learnt from: 0xJem
Repo: OlympusDAO/olympus-v3 PR: 86
File: src/policies/deposits/DepositManager.sol:271-320
Timestamp: 2025-08-08T07:33:13.575Z
Learning: DepositManager (src/policies/deposits/DepositManager.sol) is deployed as non-upgradeable; when releasing a new version, existing receipt tokens and underlying deposits remain with the previous DepositManager instance. No migration path is expected or required when introducing facility scoping.

Applied to files:

  • src/policies/deposits/LimitOrders.sol
📚 Learning: 2025-08-08T11:14:54.317Z
Learnt from: 0xJem
Repo: OlympusDAO/olympus-v3 PR: 88
File: src/test/policies/ConvertibleDepositAuctioneer/ConvertibleDepositAuctioneerTest.sol:413-416
Timestamp: 2025-08-08T11:14:54.317Z
Learning: In ConvertibleDepositAuctioneer (src/policies/deposits/ConvertibleDepositAuctioneer.sol), bid() reverts with ConvertibleDepositAuctioneer_ConvertedAmountZero when ohmOut == 0, before evaluating the minOhmOut slippage check. Therefore, setting minOhmOut=0 does not guarantee acceptance in zero-output scenarios; the zero-output revert triggers first.

Applied to files:

  • src/policies/deposits/LimitOrders.sol
📚 Learning: 2025-08-04T11:03:24.007Z
Learnt from: 0xJem
Repo: OlympusDAO/olympus-v3 PR: 84
File: src/test/policies/YieldDepositFacility/execute.t.sol:41-43
Timestamp: 2025-08-04T11:03:24.007Z
Learning: In YieldDepositFacility, 0xJem intentionally allows snapshots to be overwritten/updated within the same block to ensure they reflect the latest vault conversion rates. This is not a gaming vulnerability but desired behavior - the gaming fix was removing timestamp hints, not preventing snapshot updates within blocks.

Applied to files:

  • src/policies/deposits/LimitOrders.sol
📚 Learning: 2025-11-07T11:09:53.808Z
Learnt from: 0xJem
Repo: OlympusDAO/olympus-v3 PR: 29
File: src/policies/deposits/BaseDepositFacility.sol:631-639
Timestamp: 2025-11-07T11:09:53.808Z
Learning: OlympusDAO/olympus-v3: BaseDepositFacility.setAssetPeriodReclaimRate now uses a dedicated reclaim-rate error (not InvalidAddress) when reclaimRate_ > ONE_HUNDRED_PERCENT, fixed in a follow-up PR (#180). Avoid re-flagging this in earlier PRs.

Applied to files:

  • src/policies/deposits/LimitOrders.sol
🔇 Additional comments (7)
src/test/policies/ConvertibleDepositAuctioneer/LimitOrders.t.sol (4)

301-452: LGTM - Comprehensive fill order tests.

The fill order tests thoroughly cover single fills, multiple fills, final fill incentive distribution, capping to remaining deposits, minimum fill validation, and error conditions.


456-510: LGTM - Cancel order tests are well-structured.

Cancel order tests properly verify refunds, partial fill scenarios, ownership checks, and double-cancel protection.


514-571: LGTM - Yield tests validate core mechanics.

Yield tests effectively validate accrual via exchange rate adjustments, sweeping, edge cases (no yield), and calculations after partial fills.


575-971: LGTM - Excellent test coverage for admin, views, and edge cases.

The remaining test suite comprehensively covers:

  • Admin functions with access control
  • View utilities (canFillOrder, execution price, fillable orders)
  • Zero-incentive scenarios
  • ERC721 receiver
  • Multi-user interactions
  • Change order functionality with budget adjustments and state resets

The change order tests are particularly thorough in validating spent amount resets and edge cases.

src/policies/deposits/LimitOrders.sol (3)

114-143: LGTM - Constructor validation is thorough.

Constructor properly validates all addresses, checks array lengths, configures receipt tokens, and approves USDS for sUSDS deposits.


388-417: LGTM - Yield accounting is conservative and safe.

The yield functions use previewWithdraw() which rounds UP, providing a conservative estimate of shares needed to cover obligations. The early exit on zero yield is appropriate.


420-541: LGTM - View functions provide comprehensive order inspection.

The view functions offer robust querying capabilities with appropriate gas warnings for intensive operations. The price validation logic consistently uses previewBid() to compute effective execution prices.


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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
src/test/policies/ConvertibleDepositAuctioneer/LimitOrders.t.sol (2)

557-571: Consider adding a more precise yield assertion.

The test correctly validates that yield is greater than zero after fills, but a more precise assertion (e.g., approximate expected yield based on remaining balance and exchange rate) would strengthen confidence in the yield calculation logic.


689-701: Test coverage gap: consider testing PERIOD_6 fillability.

getFillableOrders test creates a PERIOD_6 order but only queries PERIOD_3. Consider adding a test that verifies PERIOD_6 orders are correctly returned when querying that period.

+    function test_getFillableOrders_differentPeriod() public {
+        vm.prank(alice);
+        limitOrders.createOrder(PERIOD_6, 10_000e18, 50e18, 35e18, 1_000e18);
+
+        uint256[] memory fillable = limitOrders.getFillableOrders(PERIOD_6);
+
+        assertEq(fillable.length, 1);
+        assertEq(fillable[0], 0);
+    }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ea65ec5 and 9a54312.

📒 Files selected for processing (2)
  • src/policies/deposits/LimitOrders.sol (1 hunks)
  • src/test/policies/ConvertibleDepositAuctioneer/LimitOrders.t.sol (1 hunks)
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: 0xJem
Repo: OlympusDAO/olympus-v3 PR: 29
File: src/policies/deposits/BaseDepositFacility.sol:631-639
Timestamp: 2025-11-07T11:09:53.808Z
Learning: OlympusDAO/olympus-v3: BaseDepositFacility.setAssetPeriodReclaimRate now uses a dedicated reclaim-rate error (not InvalidAddress) when reclaimRate_ > ONE_HUNDRED_PERCENT, fixed in a follow-up PR (#180). Avoid re-flagging this in earlier PRs.
Learnt from: 0xJem
Repo: OlympusDAO/olympus-v3 PR: 88
File: src/test/policies/ConvertibleDepositAuctioneer/ConvertibleDepositAuctioneerTest.sol:413-416
Timestamp: 2025-08-08T11:14:54.317Z
Learning: In ConvertibleDepositAuctioneer (src/policies/deposits/ConvertibleDepositAuctioneer.sol), bid() reverts with ConvertibleDepositAuctioneer_ConvertedAmountZero when ohmOut == 0, before evaluating the minOhmOut slippage check. Therefore, setting minOhmOut=0 does not guarantee acceptance in zero-output scenarios; the zero-output revert triggers first.
📚 Learning: 2025-08-08T07:33:13.575Z
Learnt from: 0xJem
Repo: OlympusDAO/olympus-v3 PR: 86
File: src/policies/deposits/DepositManager.sol:271-320
Timestamp: 2025-08-08T07:33:13.575Z
Learning: DepositManager (src/policies/deposits/DepositManager.sol) is deployed as non-upgradeable; when releasing a new version, existing receipt tokens and underlying deposits remain with the previous DepositManager instance. No migration path is expected or required when introducing facility scoping.

Applied to files:

  • src/policies/deposits/LimitOrders.sol
📚 Learning: 2025-08-08T11:14:54.317Z
Learnt from: 0xJem
Repo: OlympusDAO/olympus-v3 PR: 88
File: src/test/policies/ConvertibleDepositAuctioneer/ConvertibleDepositAuctioneerTest.sol:413-416
Timestamp: 2025-08-08T11:14:54.317Z
Learning: In ConvertibleDepositAuctioneer (src/policies/deposits/ConvertibleDepositAuctioneer.sol), bid() reverts with ConvertibleDepositAuctioneer_ConvertedAmountZero when ohmOut == 0, before evaluating the minOhmOut slippage check. Therefore, setting minOhmOut=0 does not guarantee acceptance in zero-output scenarios; the zero-output revert triggers first.

Applied to files:

  • src/policies/deposits/LimitOrders.sol
🔇 Additional comments (19)
src/policies/deposits/LimitOrders.sol (11)

1-57: LGTM!

The imports, interface definition, and error declarations are well-structured. Custom errors provide gas-efficient and descriptive revert reasons.


59-88: LGTM!

Events are appropriately indexed for efficient filtering, and the OHM_SCALE constant correctly reflects OHM's 9-decimal precision.


90-127: LGTM!

State variables and the LimitOrder struct are well-organized. The totalUsdsOwed tracking is a sound approach for isolating principal from yield.


130-159: LGTM!

Constructor validation is thorough with zero-address checks for all critical addresses and array length validation. The max approval for SUSDS deposits is a standard pattern.


180-232: LGTM!

Comprehensive input validation including deposit period enablement check, minimum bid validation against auctioneer, and proper use of safeTransferFrom. The order creation flow is secure with reentrancy protection.


296-300: Receipt token transfer relies on full balance assumption.

Transferring balanceOf(address(this)) works under the assumption that the contract never holds residual receipt tokens. This is acceptable given nonReentrant, but consider tracking the expected receipt amount from the bid() call for more robust accounting if the auctioneer's behavior changes.


319-337: LGTM!

Cancel order logic is correct with proper ownership validation and direct withdrawal to the order owner.


362-373: sweepYield is callable by anyone.

This is likely intentional to allow permissionless yield distribution to yieldRecipient. Just confirming this is the desired behavior and not an access control oversight.


450-469: LGTM with appropriate warning.

The gas-intensive warning for getFillableOrders is appropriate. The O(2n) iteration pattern is acceptable for off-chain queries.


508-515: LGTM!

Standard onERC721Received implementation required for the contract to receive position NFTs from the auctioneer.


282-290: Consider using safeIncreaseAllowance and validating actualAmount.

  1. For consistency with SafeERC20 usage elsewhere, consider USDS.safeIncreaseAllowance() instead of direct approve().
  2. The actualAmount return value from bid() is ignored. Verify that the auctioneer always returns exactly fillAmount_ as actualAmount; if it can differ, the depositSpent accounting would be incorrect.
src/test/policies/ConvertibleDepositAuctioneer/LimitOrders.t.sol (8)

15-25: LGTM!

Simple and correct ERC20 mock for USDS.


27-47: LGTM!

Well-designed ERC4626 mock with configurable exchange rate for yield simulation testing.


49-67: LGTM!

Simple and correct mocks for receipt tokens and position NFTs.


69-144: LGTM!

MockCDAuctioneer correctly implements the interface with configurable parameters for various test scenarios. The previewBid returning 0 below minimum bid correctly exercises the ZeroOhmOut error path.


148-210: LGTM!

Test setup is comprehensive with all necessary mock deployments, user funding, and approvals.


212-297: LGTM!

Comprehensive test coverage for order creation including validation of all order fields and proper error revert cases.


299-452: LGTM!

Excellent test coverage for fill order functionality including partial fills, final fill incentive handling, cap logic, and all error conditions. The test for test_fillOrder_zeroOhmOut correctly validates the scenario where minimum bid is raised after order creation.


1-779: Comprehensive test suite.

The test file provides excellent coverage of the CDAuctioneerLimitOrders contract functionality including order lifecycle, yield management, admin functions, and edge cases. The mock implementations are well-designed for testing various scenarios.

Comment thread src/policies/deposits/LimitOrders.sol Outdated
Comment thread src/policies/deposits/LimitOrders.sol
Comment thread src/policies/deposits/LimitOrders.sol
Comment thread src/policies/deposits/LimitOrders.sol Outdated
Comment thread src/policies/deposits/LimitOrders.sol
Comment thread src/policies/deposits/LimitOrders.sol
Comment thread src/test/policies/ConvertibleDepositAuctioneer/LimitOrders.t.sol
Comment thread src/test/policies/ConvertibleDepositAuctioneer/LimitOrders.t.sol
Comment thread src/test/policies/ConvertibleDepositAuctioneer/LimitOrders.t.sol
Comment thread src/test/policies/ConvertibleDepositAuctioneer/LimitOrders.t.sol

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
src/policies/deposits/LimitOrders.sol (2)

292-292: Major: Use safeTransferFrom for ERC721 position NFT.

Line 292 uses transferFrom() which doesn't verify that order.owner can receive ERC721 tokens. If the owner is a contract without onERC721Received(), the position NFT becomes permanently stuck in that contract.

Apply this diff:

-        POSITION_NFT.transferFrom(address(this), order.owner, positionId);
+        POSITION_NFT.safeTransferFrom(address(this), order.owner, positionId);

Additionally, consider validating at order creation (line 185) that msg.sender can receive ERC721 tokens to fail fast, though this requires reproducing ERC721's _checkOnERC721Received() logic.


201-204: Critical: ERC4626 rounding mismatch in accounting.

Lines 203-204 create an accounting discrepancy: deposit() returns shares minted, but line 204 adds the input totalDeposit (asset amount) to totalUsdsOwed. Due to ERC4626 rounding, the shares may not redeem to exactly totalDeposit. This mismatch compounds across orders and can cause totalUsdsOwed to drift from actual redeemable assets, breaking yield calculations in getAccruedYieldShares() (line 356) and potentially preventing withdrawals.

Apply this diff to fix the accounting:

-        USDS.safeTransferFrom(msg.sender, address(this), totalDeposit);
-        SUSDS.deposit(totalDeposit, address(this));
-        totalUsdsOwed += totalDeposit;
+        USDS.safeTransferFrom(msg.sender, address(this), totalDeposit);
+        uint256 sharesMinted = SUSDS.deposit(totalDeposit, address(this));
+        uint256 redeemableAmount = SUSDS.previewRedeem(sharesMinted);
+        totalUsdsOwed += redeemableAmount;

Note: You'll also need to handle the small difference (totalDeposit - redeemableAmount) when splitting between depositBudget and incentiveBudget in the order struct, or document that users effectively deposit slightly less due to rounding.

🧹 Nitpick comments (3)
src/policies/deposits/LimitOrders.sol (3)

329-330: Consider replacing ternaries with checked subtraction.

Lines 329-330 use ternaries to prevent underflow, but if depositSpent > depositBudget or incentiveSpent > incentiveBudget (which violates contract invariants), this silently returns 0 instead of reverting. This masks potential accounting bugs in fillOrder().

Apply this diff to enforce invariants:

-        uint256 remainingDeposit = order.depositBudget > order.depositSpent ? order.depositBudget - order.depositSpent : 0;
-        uint256 remainingIncentive = order.incentiveBudget > order.incentiveSpent ? order.incentiveBudget - order.incentiveSpent : 0;
+        uint256 remainingDeposit = order.depositBudget - order.depositSpent;
+        uint256 remainingIncentive = order.incentiveBudget - order.incentiveSpent;

Alternatively, add explicit checks:

+        if (order.depositSpent > order.depositBudget || order.incentiveSpent > order.incentiveBudget) {
+            revert InvalidParam("accounting invariant violated");
+        }
         uint256 remainingDeposit = order.depositBudget - order.depositSpent;
         uint256 remainingIncentive = order.incentiveBudget - order.incentiveSpent;

502-509: Consider restricting ERC721 acceptance to CD_AUCTIONEER.

The current implementation accepts ERC721 tokens from any source. While not a security risk (the contract immediately forwards position NFTs to order owners in fillOrder()), restricting acceptance to CD_AUCTIONEER provides defense in depth against unexpected NFT transfers.

Optional enhancement:

 function onERC721Received(
-    address,
+    address operator,
     address,
     uint256,
     bytes calldata
 ) external pure returns (bytes4) {
+    if (operator != address(CD_AUCTIONEER)) revert InvalidParam("operator");
     return this.onERC721Received.selector;
 }

179-185: Add emergency pause mechanism for order creation.

The createOrder() function lacks an emergency stop capability. Consider adding a pausable pattern (similar to OpenZeppelin's Pausable) with an onlyOwner modifier to allow the contract owner to disable new order creation during security incidents or maintenance, without affecting existing active orders.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 84d277a and cd0f10b.

📒 Files selected for processing (1)
  • src/policies/deposits/LimitOrders.sol (1 hunks)
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: 0xJem
Repo: OlympusDAO/olympus-v3 PR: 29
File: src/policies/deposits/BaseDepositFacility.sol:631-639
Timestamp: 2025-11-07T11:09:53.808Z
Learning: OlympusDAO/olympus-v3: BaseDepositFacility.setAssetPeriodReclaimRate now uses a dedicated reclaim-rate error (not InvalidAddress) when reclaimRate_ > ONE_HUNDRED_PERCENT, fixed in a follow-up PR (#180). Avoid re-flagging this in earlier PRs.
📚 Learning: 2025-08-08T07:33:13.575Z
Learnt from: 0xJem
Repo: OlympusDAO/olympus-v3 PR: 86
File: src/policies/deposits/DepositManager.sol:271-320
Timestamp: 2025-08-08T07:33:13.575Z
Learning: DepositManager (src/policies/deposits/DepositManager.sol) is deployed as non-upgradeable; when releasing a new version, existing receipt tokens and underlying deposits remain with the previous DepositManager instance. No migration path is expected or required when introducing facility scoping.

Applied to files:

  • src/policies/deposits/LimitOrders.sol
📚 Learning: 2025-08-08T11:14:54.317Z
Learnt from: 0xJem
Repo: OlympusDAO/olympus-v3 PR: 88
File: src/test/policies/ConvertibleDepositAuctioneer/ConvertibleDepositAuctioneerTest.sol:413-416
Timestamp: 2025-08-08T11:14:54.317Z
Learning: In ConvertibleDepositAuctioneer (src/policies/deposits/ConvertibleDepositAuctioneer.sol), bid() reverts with ConvertibleDepositAuctioneer_ConvertedAmountZero when ohmOut == 0, before evaluating the minOhmOut slippage check. Therefore, setting minOhmOut=0 does not guarantee acceptance in zero-output scenarios; the zero-output revert triggers first.

Applied to files:

  • src/policies/deposits/LimitOrders.sol
📚 Learning: 2025-08-04T11:03:24.007Z
Learnt from: 0xJem
Repo: OlympusDAO/olympus-v3 PR: 84
File: src/test/policies/YieldDepositFacility/execute.t.sol:41-43
Timestamp: 2025-08-04T11:03:24.007Z
Learning: In YieldDepositFacility, 0xJem intentionally allows snapshots to be overwritten/updated within the same block to ensure they reflect the latest vault conversion rates. This is not a gaming vulnerability but desired behavior - the gaming fix was removing timestamp hints, not preventing snapshot updates within blocks.

Applied to files:

  • src/policies/deposits/LimitOrders.sol
🔇 Additional comments (4)
src/policies/deposits/LimitOrders.sol (4)

129-158: LGTM: Constructor validation is comprehensive.

The constructor properly validates all address parameters, ensures array length consistency, and initializes the USDS→sUSDS approval. The max approval on line 157 is appropriate for ERC4626 vault deposits.


343-374: LGTM: Yield accounting is sound.

The yield calculation (lines 352-361) correctly uses previewWithdraw() with upward rounding (line 356) to ensure principal protection. This guarantees totalUsdsOwed can always be withdrawn even if yield is swept. The early return on line 367 for zero yield is efficient.


376-498: LGTM: View functions are comprehensive.

The view functions provide thorough order inspection capabilities for MEV bots and users:

  • previewFillOrder(), canFillOrder(), and _isOrderFillable() validate fill conditions accurately
  • getFillableOrders() with both overloads allows chunked scanning to avoid gas limits (lines 443-454)
  • The gas warning comments (lines 444, 450) appropriately flag off-chain-only usage

4-9: No action needed. The OpenZeppelin version usage is intentional and correct: ReentrancyGuardTransient only exists in v5.3.0 (explicitly imported via @openzeppelin-5.3.0/), while the other imports from lines 5-9 use the default mapping to v4.8.0 (both versions are available through remappings.txt). This aligns with the project's dual-version strategy where v4.8.0 is standard and v5.3.0 is available through explicit opt-in paths.

Likely an incorrect or invalid review comment.

Comment on lines +272 to +278
uint256 usdsNeeded = fillAmount_ + incentive;
SUSDS.withdraw(usdsNeeded, address(this), address(this));

// Update accounting
order.depositSpent += fillAmount_;
order.incentiveSpent += incentive;
totalUsdsOwed -= usdsNeeded;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Capture withdraw() return value for precise accounting.

Line 273 withdraws usdsNeeded but doesn't capture the actual assets returned. ERC4626 withdraw() can return slightly different amounts due to rounding. While the re-wrap logic (lines 305-308) mitigates excess USDS, totalUsdsOwed (line 278) is decremented by the requested amount, not the actual, which could cause minor accounting drift over many fills.

Apply this diff for precision:

-        SUSDS.withdraw(usdsNeeded, address(this), address(this));
+        uint256 actualWithdrawn = SUSDS.withdraw(usdsNeeded, address(this), address(this));
 
         // Update accounting
         order.depositSpent += fillAmount_;
         order.incentiveSpent += incentive;
-        totalUsdsOwed -= usdsNeeded;
+        totalUsdsOwed -= actualWithdrawn;

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/policies/deposits/LimitOrders.sol around lines 272-278, capture the
uint256 returned from SUSDS.withdraw(...) instead of assuming the requested
usdsNeeded was actually returned; use that actualWithdrawn value when updating
accounting: compute usedForFill = min(fillAmount_, actualWithdrawn) and
usedIncentive = actualWithdrawn - usedForFill, increment order.depositSpent by
usedForFill and order.incentiveSpent by usedIncentive, and decrement
totalUsdsOwed by actualWithdrawn so the ledger reflects the real amount
withdrawn (the existing re-wrap logic for any excess remains unchanged).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
src/policies/deposits/LimitOrders.sol (2)

265-266: Use safeTransferFrom for ERC721 transfer.

transferFrom doesn't verify the recipient can receive ERC721 tokens. If order.owner is a contract without onERC721Received, the NFT will be transferred but potentially locked.

This was flagged in previous review and marked as a remaining to-do.

-        POSITION_NFT.transferFrom(address(this), order.owner, positionId);
+        POSITION_NFT.safeTransferFrom(address(this), order.owner, positionId);

245-252: Consider capturing withdraw return value for precise accounting.

ERC4626 withdraw() returns the actual shares burned, which may differ slightly from the requested assets due to rounding. While the re-wrap logic at lines 279-282 handles excess USDS, using the actual withdrawn amount for totalUsdsOwed would be more precise.

This was flagged in previous review. The current implementation works correctly due to the re-wrap safety net, but capturing the return value would be more precise.

🧹 Nitpick comments (3)
src/policies/deposits/LimitOrders.sol (3)

2-9: Pragma version may be incompatible with transient storage.

ReentrancyGuardTransient uses EIP-1153 transient storage which requires Solidity ≥0.8.24 and the Cancun hardfork. The pragma >=0.8.20 allows compilation with versions that don't support transient opcodes, which would fail at deployment.

Additionally, the imports mix versioned (@openzeppelin-5.3.0) and unversioned (@openzeppelin) paths. Consider standardizing on one approach for consistency.

-pragma solidity >=0.8.20;
+pragma solidity >=0.8.24;

103-132: Consider validating sUSDS underlying asset matches USDS.

The constructor doesn't verify that sUsds_ is actually a vault for usds_. A misconfiguration would cause silent failures when depositing.

 if (sUsds_ == address(0)) revert InvalidParam("sUsds");
+if (ERC4626(sUsds_).asset() != usds_) revert InvalidParam("sUsds asset mismatch");

400-405: getRemaining lacks underflow protection unlike cancelOrder.

cancelOrder uses defensive ternaries (lines 303-304) to prevent underflow, but getRemaining directly subtracts without guards. While this should never occur in normal operation, adding consistency would make the code more defensive.

 function getRemaining(uint256 orderId_) external view returns (uint256 deposit, uint256 incentive) {
     LimitOrder memory order = orders[orderId_];
-    deposit = order.depositBudget - order.depositSpent;
-    incentive = order.incentiveBudget - order.incentiveSpent;
+    deposit = order.depositBudget > order.depositSpent ? order.depositBudget - order.depositSpent : 0;
+    incentive = order.incentiveBudget > order.incentiveSpent ? order.incentiveBudget - order.incentiveSpent : 0;
 }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cd0f10b and 3defe90.

📒 Files selected for processing (1)
  • src/policies/deposits/LimitOrders.sol (1 hunks)
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: 0xJem
Repo: OlympusDAO/olympus-v3 PR: 29
File: src/policies/deposits/BaseDepositFacility.sol:631-639
Timestamp: 2025-11-07T11:09:53.808Z
Learning: OlympusDAO/olympus-v3: BaseDepositFacility.setAssetPeriodReclaimRate now uses a dedicated reclaim-rate error (not InvalidAddress) when reclaimRate_ > ONE_HUNDRED_PERCENT, fixed in a follow-up PR (#180). Avoid re-flagging this in earlier PRs.
📚 Learning: 2025-08-08T11:14:54.317Z
Learnt from: 0xJem
Repo: OlympusDAO/olympus-v3 PR: 88
File: src/test/policies/ConvertibleDepositAuctioneer/ConvertibleDepositAuctioneerTest.sol:413-416
Timestamp: 2025-08-08T11:14:54.317Z
Learning: In ConvertibleDepositAuctioneer (src/policies/deposits/ConvertibleDepositAuctioneer.sol), bid() reverts with ConvertibleDepositAuctioneer_ConvertedAmountZero when ohmOut == 0, before evaluating the minOhmOut slippage check. Therefore, setting minOhmOut=0 does not guarantee acceptance in zero-output scenarios; the zero-output revert triggers first.

Applied to files:

  • src/policies/deposits/LimitOrders.sol
📚 Learning: 2025-08-08T07:33:13.575Z
Learnt from: 0xJem
Repo: OlympusDAO/olympus-v3 PR: 86
File: src/policies/deposits/DepositManager.sol:271-320
Timestamp: 2025-08-08T07:33:13.575Z
Learning: DepositManager (src/policies/deposits/DepositManager.sol) is deployed as non-upgradeable; when releasing a new version, existing receipt tokens and underlying deposits remain with the previous DepositManager instance. No migration path is expected or required when introducing facility scoping.

Applied to files:

  • src/policies/deposits/LimitOrders.sol
📚 Learning: 2025-08-04T11:03:24.007Z
Learnt from: 0xJem
Repo: OlympusDAO/olympus-v3 PR: 84
File: src/test/policies/YieldDepositFacility/execute.t.sol:41-43
Timestamp: 2025-08-04T11:03:24.007Z
Learning: In YieldDepositFacility, 0xJem intentionally allows snapshots to be overwritten/updated within the same block to ensure they reflect the latest vault conversion rates. This is not a gaming vulnerability but desired behavior - the gaming fix was removing timestamp hints, not preventing snapshot updates within blocks.

Applied to files:

  • src/policies/deposits/LimitOrders.sol
📚 Learning: 2025-11-07T11:09:53.808Z
Learnt from: 0xJem
Repo: OlympusDAO/olympus-v3 PR: 29
File: src/policies/deposits/BaseDepositFacility.sol:631-639
Timestamp: 2025-11-07T11:09:53.808Z
Learning: OlympusDAO/olympus-v3: BaseDepositFacility.setAssetPeriodReclaimRate now uses a dedicated reclaim-rate error (not InvalidAddress) when reclaimRate_ > ONE_HUNDRED_PERCENT, fixed in a follow-up PR (#180). Avoid re-flagging this in earlier PRs.

Applied to files:

  • src/policies/deposits/LimitOrders.sol
🔇 Additional comments (9)
src/policies/deposits/LimitOrders.sol (9)

19-69: LGTM!

Error definitions, events, and constants are well-defined. OHM_SCALE = 1e9 correctly matches OHM's 9 decimal places.


70-100: LGTM!

State variables and struct design are appropriate. totalUsdsOwed correctly tracks principal for yield calculation.


134-143: LGTM!

Admin function properly validates input and emits event. Access control via onlyOwner is appropriate.


153-205: LGTM!

Order creation validates all critical parameters including the auctioneer's minimum bid requirement. The totalUsdsOwed tracking correctly records the nominal USDS deposited.

Note: Orders with incentiveBudget_ = 0 are allowed, meaning fillers receive no incentive. This is valid behavior if the owner wants fills executed at favorable prices without paying for them, but may result in orders never being filled.


207-292: LGTM with noted duplicates above.

The fill logic is sound: proper validation of fill constraints, proportional incentive calculation with final-fill handling for dust, price protection via previewBid, and leftover USDS re-wrapping. The slippage protection by passing expectedOhmOut as minOhmOut to bid() is a good safeguard.


294-315: LGTM!

Cancel logic correctly follows CEI pattern. The defensive ternaries at lines 303-304 properly guard against underflow, and state is updated before the external withdraw call.


317-348: LGTM!

Yield calculation correctly uses previewWithdraw which rounds up, ensuring sufficient shares are retained to cover all obligations. Sweeping sUSDS shares directly (vs. redeeming to USDS) is more gas efficient and avoids rounding losses.


417-447: LGTM!

getFillableOrders appropriately warns about gas intensity and provides an indexed variant for pagination. The dual-loop pattern (count then populate) is a common Solidity pattern for dynamic array building.


476-483: LGTM!

Correct implementation of IERC721Receiver to accept position NFTs via safeTransferFrom.

Comment on lines +360 to +369
/// @notice Calculate incentive and incentive rate for a given fill amount
function calculateIncentive(
uint256 orderId_,
uint256 fillAmount_
) public view returns (uint256 incentive, uint256 incentiveRate) {
LimitOrder memory order = orders[orderId_];
if (order.depositBudget == 0) return (0, 0);
incentive = (fillAmount_ * order.incentiveBudget) / order.depositBudget;
incentiveRate = (order.incentiveBudget * 10_000) / order.depositBudget;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

calculateIncentive doesn't match final-fill logic in fillOrder.

In fillOrder (lines 230-236), the final fill receives all remaining incentive to avoid dust. However, calculateIncentive always uses proportional calculation. This causes previewFillOrder to return incorrect incentive amounts for final fills.

 function calculateIncentive(
     uint256 orderId_,
     uint256 fillAmount_
 ) public view returns (uint256 incentive, uint256 incentiveRate) {
     LimitOrder memory order = orders[orderId_];
     if (order.depositBudget == 0) return (0, 0);
-    incentive = (fillAmount_ * order.incentiveBudget) / order.depositBudget;
+    
+    uint256 remainingDeposit = order.depositBudget - order.depositSpent;
+    uint256 remainingIncentive = order.incentiveBudget - order.incentiveSpent;
+    
+    // Cap fillAmount to remaining deposit
+    uint256 actualFill = fillAmount_ > remainingDeposit ? remainingDeposit : fillAmount_;
+    
+    // Final fill gets all remaining incentive (matches fillOrder logic)
+    if (actualFill == remainingDeposit) {
+        incentive = remainingIncentive;
+    } else {
+        incentive = (actualFill * order.incentiveBudget) / order.depositBudget;
+    }
+    
     incentiveRate = (order.incentiveBudget * 10_000) / order.depositBudget;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// @notice Calculate incentive and incentive rate for a given fill amount
function calculateIncentive(
uint256 orderId_,
uint256 fillAmount_
) public view returns (uint256 incentive, uint256 incentiveRate) {
LimitOrder memory order = orders[orderId_];
if (order.depositBudget == 0) return (0, 0);
incentive = (fillAmount_ * order.incentiveBudget) / order.depositBudget;
incentiveRate = (order.incentiveBudget * 10_000) / order.depositBudget;
}
/// @notice Calculate incentive and incentive rate for a given fill amount
function calculateIncentive(
uint256 orderId_,
uint256 fillAmount_
) public view returns (uint256 incentive, uint256 incentiveRate) {
LimitOrder memory order = orders[orderId_];
if (order.depositBudget == 0) return (0, 0);
uint256 remainingDeposit = order.depositBudget - order.depositSpent;
uint256 remainingIncentive = order.incentiveBudget - order.incentiveSpent;
// Cap fillAmount to remaining deposit
uint256 actualFill = fillAmount_ > remainingDeposit ? remainingDeposit : fillAmount_;
// Final fill gets all remaining incentive (matches fillOrder logic)
if (actualFill == remainingDeposit) {
incentive = remainingIncentive;
} else {
incentive = (actualFill * order.incentiveBudget) / order.depositBudget;
}
incentiveRate = (order.incentiveBudget * 10_000) / order.depositBudget;
}
🤖 Prompt for AI Agents
In src/policies/deposits/LimitOrders.sol around lines 360 to 369,
calculateIncentive currently always pro-rates incentive by fillAmount but does
not mirror fillOrder's final-fill behavior where the last fill receives the
remaining incentive; update the function so that if order.depositBudget is zero
return (0,0) as before, and if fillAmount_ equals the order.depositBudget (i.e.,
this is the final/remaining fill) return the entire order.incentiveBudget as
incentive and compute incentiveRate appropriately, otherwise keep the existing
proportional calculation.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (4)
src/policies/deposits/LimitOrders.sol (4)

252-257: Capture withdraw() return value for precise accounting.

Line 252 withdraws usdsNeeded but doesn't capture the actual assets returned. ERC4626's withdraw() can return slightly different amounts due to rounding. Line 257 decrements totalUsdsOwed by usdsNeeded (the requested amount) rather than the actual withdrawn amount, which could cause accounting drift over many fills.

This is a duplicate concern from the previous bot review.

Apply this diff for precision:

-        SUSDS.withdraw(usdsNeeded, address(this), address(this));
+        uint256 actualWithdrawn = SUSDS.withdraw(usdsNeeded, address(this), address(this));
 
         // Update accounting
         order.depositSpent += fillAmount_;
         order.incentiveSpent += incentive;
-        totalUsdsOwed -= usdsNeeded;
+        totalUsdsOwed -= actualWithdrawn;

366-374: calculateIncentive doesn't match final-fill logic in fillOrder.

In fillOrder (lines 237-241), the final fill receives all remaining incentive to avoid dust. However, calculateIncentive always uses proportional calculation (fillAmount_ * order.incentiveBudget) / order.depositBudget. This causes previewFillOrder to return incorrect incentive amounts for final fills.

This is a duplicate of the previous bot review comment.

 function calculateIncentive(
     uint256 orderId_,
     uint256 fillAmount_
 ) public view returns (uint256 incentive, uint256 incentiveRate) {
     LimitOrder memory order = orders[orderId_];
     if (order.depositBudget == 0) return (0, 0);
-    incentive = (fillAmount_ * order.incentiveBudget) / order.depositBudget;
+    
+    uint256 remainingDeposit = order.depositBudget - order.depositSpent;
+    uint256 remainingIncentive = order.incentiveBudget - order.incentiveSpent;
+    
+    // Cap fillAmount to remaining deposit
+    uint256 actualFill = fillAmount_ > remainingDeposit ? remainingDeposit : fillAmount_;
+    
+    // Final fill gets all remaining incentive (matches fillOrder logic)
+    if (actualFill == remainingDeposit) {
+        incentive = remainingIncentive;
+    } else {
+        incentive = (actualFill * order.incentiveBudget) / order.depositBudget;
+    }
+    
     incentiveRate = (order.incentiveBudget * 10_000) / order.depositBudget;
 }

271-271: Use safeTransferFrom for ERC721 transfers.

Replace unsafe transferFrom with OpenZeppelin's safeTransferFrom to ensure the receiver can handle ERC721 tokens. Without this check, NFTs may become permanently stuck if sent to contracts that don't implement IERC721Receiver.

-        POSITION_NFT.transferFrom(address(this), order.owner, positionId);
+        POSITION_NFT.safeTransferFrom(address(this), order.owner, positionId);

178-181: Use previewWithdraw() or capture actual shares to ensure accounting precision with ERC4626 vault.

Line 180 deposits totalDeposit USDS to sUSDS but ignores the returned share amount, while line 181 tracks only the asset amount in totalUsdsOwed. When depositing tokens, ERC4626 rounds down the shares issued, which can create accounting drift between tracked assets and redeemable shares. sUSDS has no fees, and share to asset conversions are real-time, which mitigates but doesn't eliminate rounding loss with virtual shares in OpenZeppelin's ERC4626 implementation.

To ensure precise accounting across multiple deposits and partial withdrawals, either capture and track actual shares received from deposit, or consistently use previewWithdraw(totalUsdsOwed) before each withdrawal to verify sufficient shares exist (as done at line 335 in the cancel function).

🧹 Nitpick comments (2)
src/policies/deposits/LimitOrders.sol (2)

16-16: Consider inheriting from PeripheryEnabler for emergency stop capability.

Past review comment suggested inheriting from PeripheryEnabler and adding an onlyEnabled modifier to createOrder. This would allow the owner to disable the contract and prevent any further orders in case of emergency.

Based on learnings, this pattern is used elsewhere in the OlympusDAO codebase for administrative control.

-contract CDAuctioneerLimitOrders is ReentrancyGuardTransient, Ownable {
+contract CDAuctioneerLimitOrders is ReentrancyGuardTransient, Ownable, PeripheryEnabler {

Then add onlyEnabled to createOrder:

-    ) external nonReentrant returns (uint256 orderId) {
+    ) external nonReentrant onlyEnabled returns (uint256 orderId) {

129-132: Consider automating receipt token configuration.

Past review comment suggested calling ReceiptTokenManager.getWrappableTokens() to automatically retrieve the list of token IDs and wrapped tokens, rather than requiring manual configuration via constructor parameters.

This would reduce deployment complexity and ensure consistency with the ReceiptTokenManager's configuration. However, the current approach provides explicit control over which tokens are supported.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3defe90 and 08df4e9.

📒 Files selected for processing (1)
  • src/policies/deposits/LimitOrders.sol (1 hunks)
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: 0xJem
Repo: OlympusDAO/olympus-v3 PR: 29
File: src/policies/deposits/BaseDepositFacility.sol:631-639
Timestamp: 2025-11-07T11:09:53.808Z
Learning: OlympusDAO/olympus-v3: BaseDepositFacility.setAssetPeriodReclaimRate now uses a dedicated reclaim-rate error (not InvalidAddress) when reclaimRate_ > ONE_HUNDRED_PERCENT, fixed in a follow-up PR (#180). Avoid re-flagging this in earlier PRs.
📚 Learning: 2025-08-08T11:14:54.317Z
Learnt from: 0xJem
Repo: OlympusDAO/olympus-v3 PR: 88
File: src/test/policies/ConvertibleDepositAuctioneer/ConvertibleDepositAuctioneerTest.sol:413-416
Timestamp: 2025-08-08T11:14:54.317Z
Learning: In ConvertibleDepositAuctioneer (src/policies/deposits/ConvertibleDepositAuctioneer.sol), bid() reverts with ConvertibleDepositAuctioneer_ConvertedAmountZero when ohmOut == 0, before evaluating the minOhmOut slippage check. Therefore, setting minOhmOut=0 does not guarantee acceptance in zero-output scenarios; the zero-output revert triggers first.

Applied to files:

  • src/policies/deposits/LimitOrders.sol
📚 Learning: 2025-08-08T07:33:13.575Z
Learnt from: 0xJem
Repo: OlympusDAO/olympus-v3 PR: 86
File: src/policies/deposits/DepositManager.sol:271-320
Timestamp: 2025-08-08T07:33:13.575Z
Learning: DepositManager (src/policies/deposits/DepositManager.sol) is deployed as non-upgradeable; when releasing a new version, existing receipt tokens and underlying deposits remain with the previous DepositManager instance. No migration path is expected or required when introducing facility scoping.

Applied to files:

  • src/policies/deposits/LimitOrders.sol
📚 Learning: 2025-08-04T11:03:24.007Z
Learnt from: 0xJem
Repo: OlympusDAO/olympus-v3 PR: 84
File: src/test/policies/YieldDepositFacility/execute.t.sol:41-43
Timestamp: 2025-08-04T11:03:24.007Z
Learning: In YieldDepositFacility, 0xJem intentionally allows snapshots to be overwritten/updated within the same block to ensure they reflect the latest vault conversion rates. This is not a gaming vulnerability but desired behavior - the gaming fix was removing timestamp hints, not preventing snapshot updates within blocks.

Applied to files:

  • src/policies/deposits/LimitOrders.sol
📚 Learning: 2025-11-07T11:09:53.808Z
Learnt from: 0xJem
Repo: OlympusDAO/olympus-v3 PR: 29
File: src/policies/deposits/BaseDepositFacility.sol:631-639
Timestamp: 2025-11-07T11:09:53.808Z
Learning: OlympusDAO/olympus-v3: BaseDepositFacility.setAssetPeriodReclaimRate now uses a dedicated reclaim-rate error (not InvalidAddress) when reclaimRate_ > ONE_HUNDRED_PERCENT, fixed in a follow-up PR (#180). Avoid re-flagging this in earlier PRs.

Applied to files:

  • src/policies/deposits/LimitOrders.sol
🔇 Additional comments (9)
src/policies/deposits/LimitOrders.sol (9)

1-11: LGTM: Imports are appropriate.

The imports include necessary OpenZeppelin contracts and the CDAuctioneer interface. The use of ReentrancyGuardTransient is correct for gas-efficient reentrancy protection.


19-58: LGTM: Errors and events are well-defined.

Comprehensive error coverage for validation, order state, and execution conditions. Events are appropriately indexed for efficient filtering.


59-102: LGTM: State variables and struct are well-organized.

The totalUsdsOwed variable provides critical accounting for yield separation. The LimitOrder struct comprehensively tracks order state.


106-135: LGTM: Constructor validation is thorough.

All address parameters are validated, array lengths are checked, and infinite USDS approval for SUSDS is appropriately set for gas efficiency.


137-145: LGTM: Admin function is properly secured.

The setYieldRecipient function includes appropriate validation and access control.


284-287: Re-wrap logic doesn't update totalUsdsOwed accounting.

Lines 284-287 re-wrap any remaining USDS balance back into sUSDS. However, this doesn't update totalUsdsOwed. If there's excess USDS (e.g., from rounding in the bid), wrapping it increases the sUSDS balance but totalUsdsOwed remains unchanged, which means the excess would be counted as yield.

While this might be intentional (excess becomes yield), it's worth clarifying the design intent. If the bid returns less than fillAmount_ in actualAmount, the difference becomes untracked.

Consider whether this behavior is intentional or if accounting should be adjusted:

         uint256 remainingBalance = USDS.balanceOf(address(this));
         if (remainingBalance > 0) {
-            SUSDS.deposit(remainingBalance, address(this));
+            uint256 shares = SUSDS.deposit(remainingBalance, address(this));
+            // Note: remainingBalance is now treated as yield (not added to totalUsdsOwed)
         }

322-353: LGTM: Yield accounting is sound.

The use of previewWithdraw (which rounds up) ensures sufficient shares are reserved for user obligations. The logic correctly identifies excess as yield and transfers it to the configured recipient.


479-488: LGTM: ERC721 receiver is correctly implemented.

The onERC721Received function properly implements the IERC721Receiver interface, allowing the contract to receive position NFTs from the auctioneer.


262-268: Verify bid parameters handle zero OHM output correctly.

Lines 244-245 check that expectedOhmOut is non-zero and revert if it is. Line 265 then passes expectedOhmOut as minOhmOut to the bid function. However, based on learnings, ConvertibleDepositAuctioneer.bid() reverts with ConvertibleDepositAuctioneer_ConvertedAmountZero when ohmOut == 0, before evaluating the minOhmOut slippage check.

The current logic should work correctly since the zero check at line 245 prevents calling bid with problematic parameters.

Based on learnings, the zero-output check is correctly placed before the bid call.

Comment on lines +315 to +317
if (totalRemaining > 0) {
SUSDS.withdraw(totalRemaining, order.owner, address(this));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Capture withdraw() return value in cancellation.

Similar to fillOrder, line 316 withdraws totalRemaining but doesn't capture the actual assets returned. If the withdrawal returns less due to ERC4626 rounding, the user receives less than expected while totalUsdsOwed is decremented by the full totalRemaining.

Apply this diff:

         if (totalRemaining > 0) {
-            SUSDS.withdraw(totalRemaining, order.owner, address(this));
+            uint256 actualWithdrawn = SUSDS.withdraw(totalRemaining, order.owner, address(this));
+            // Note: If actualWithdrawn < totalRemaining, the difference remains as yield
         }
🤖 Prompt for AI Agents
In src/policies/deposits/LimitOrders.sol around lines 315-317, the cancellation
calls SUSDS.withdraw(totalRemaining, order.owner, address(this)) but does not
capture the actual amount returned; change the call to capture the returned
amount (e.g., uint256 withdrawn = SUSDS.withdraw(...)), use that withdrawn value
when updating totalUsdsOwed (decrement by withdrawn instead of totalRemaining),
and propagate the withdrawn amount into any subsequent state updates/emit events
or checks so ERC4626 rounding shortfalls are handled correctly and accounting
remains consistent.

Allows user to change details of an open order. Functionally equivalent to canceling an existing order and opening a new one, with orderID retained and only one transaction required.
Difference between existing and new order details is transferred from or to user.
@0xJem
0xJem changed the base branch from master to feature/cd-limit-orders December 17, 2025 09:12
@0xJem

0xJem commented Dec 17, 2025

Copy link
Copy Markdown
Member

Merging into branch in main repo to allow for changes

@0xJem
0xJem merged commit dcc4905 into OlympusDAO:feature/cd-limit-orders Dec 17, 2025
1 check passed
@0xJem 0xJem changed the title CDAuctioneer Limit Orders Convertible Deposits: Limit Orders (Fork) Dec 17, 2025
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.

3 participants