Skip to content

Audit 02-2026 Fixes + Fund Batch 01#51

Merged
maximebrugel merged 99 commits into
mainfrom
audit-fixes-02-2026
Apr 8, 2026
Merged

Audit 02-2026 Fixes + Fund Batch 01#51
maximebrugel merged 99 commits into
mainfrom
audit-fixes-02-2026

Conversation

@maximebrugel
Copy link
Copy Markdown
Collaborator

@maximebrugel maximebrugel commented Feb 9, 2026

ChainSecurity Audit Response & Fixes


ChainSecurity Findings (Section 7 — Open Findings)

High Severity (1)

Finding Title PR(s)
CS-GRUNT-001 Stale Morpho market data #70

Medium Severity (14)

Finding Title Status PR(s)
CS-GRUNT-002 / 003 Burn divides by total debt / reverts due to rounding errors Code Change #72
CS-GRUNT-004 Facilitator can set return amount to 0 Code Change #94
CS-GRUNT-005 Facilitator steals from prime brokers Code Change #60, #153
CS-GRUNT-006 Facilitator theft with malicious request Code Change #71, #124
CS-GRUNT-007 Malicious facilitator can drain intent via setFund Code Change #71
CS-GRUNT-008 Missing cooldown on rebalance Code Change #73
CS-GRUNT-009 NAV calculation includes bad debt positions Code Change #68
CS-GRUNT-010 Operator triggers permanent state deadlock Code Change #54
CS-GRUNT-011 / 012 Partial liquidations fail health check / repaid shares exceed borrow shares Code Change #95
CS-GRUNT-013 YT approval resets PT allowance Code Change #50
CS-GRUNT-062 Facilitator steals from other intents via withdrawManager Code Change #109
CS-GRUNT-063 Share deflation bricks Morpho market Risk Accepted

Low Severity (39)

Finding Title Status PR(s)
CS-GRUNT-014 Burning all shares reverts with depleted principal assets Code Change #49, #151, #158
CS-GRUNT-015 Code duplication and inconsistencies in oracle initialization Code Change #65
CS-GRUNT-016 Facility allows payable ERC-6909 transfers Risk Accepted
CS-GRUNT-017 Facility transfers zero-amount tokens when share proportion rounds down Code Change #46
CS-GRUNT-018 Front-running prime broker deposits Code Change #41, #152
CS-GRUNT-019 Incomplete ERC-4626 compliance for ControlledVault Code Change #43, #165
CS-GRUNT-020 Incorrect event emission during rebalancing Code Change #92
CS-GRUNT-021 Incorrect NatSpec comment on resolveStart Code Change #66
CS-GRUNT-022 Management fees incorrectly estimated Risk Accepted
CS-GRUNT-023 Maximum management fee cap of 50% is excessively permissive Code Change #75
CS-GRUNT-024 Missing state checks for setFund and setRequest Code Change #45, #71
CS-GRUNT-025 One fund simultaneously assigned to multiple intents Code Change #48
CS-GRUNT-026 Operator can cause loss of funds for a particular intent Risk Accepted
CS-GRUNT-027 Paused PositionManager allows deposits/withdrawals when totalAssets unchanged Code Change #91, #164
CS-GRUNT-028 Performance fees charged on gross gain instead of net gain Code Change #74
CS-GRUNT-029 PositionManager deposits/withdrawals revert on small asset deltas Code Change #96
CS-GRUNT-030 / 031 Redundant deletions / redundant storage in USCCFund Code Change #63
CS-GRUNT-032 Share inflation via direct Morpho collateral donation Code Change #79, #167
CS-GRUNT-033 Target PM restrictions unchecked when deposit PM is guard key Risk Accepted
CS-GRUNT-034 TokenController reverts on self-transfers Code Change #90
CS-GRUNT-035 USCCFund sends assets to msg.sender instead of order.receiver Code Change #62
CS-GRUNT-036 USCCFund totalAssets reports global wUSCC supply instead of per-fund holdings Risk Accepted
CS-GRUNT-037 USCCFund totalAssets() missing Chainlink price freshness check Risk Accepted
CS-GRUNT-038 Unbacked yield token minting leads to yield theft Risk Accepted #44
CS-GRUNT-039 WrappedAsset initialization allows decimal mismatch Code Change #58
CS-GRUNT-040 _settleShares rounds down shares burned Code Change #69
CS-GRUNT-041 addBorrowModule and removeBorrowModule skip fee accounting Code Change #88
CS-GRUNT-042 repaymentDeadline not validated Code Change #93
CS-GRUNT-043 setRequest can bind the same request to multiple intents Code Change #48
CS-GRUNT-058 Curator can insert duplicate entries in supply/withdrawal queues Code Change #89
CS-GRUNT-059 Missing funding-to-utilization phase boundary in request lifecycle Risk Accepted #44
CS-GRUNT-064 Last shareholder cannot fully exit the PositionManager Risk Accepted
CS-GRUNT-065 Missing token allowlist Risk Accepted
CS-GRUNT-066 Morpho reference can be immutable Code Change #108
CS-GRUNT-067 Off-by-one in offer expiration check Code Change #104
CS-GRUNT-068 Prime broker can front-run authorizeMinting to double-mint Risk Accepted
CS-GRUNT-069 Read-only reentrancy in FacilityLP withdraw and claim Code Change #115
CS-GRUNT-070 Role constants are public in USCCFund and TransferGuard Code Change #105

Open Questions (Section 8)

Question Title Status PR(s)
OQ 8.1 Burn behavior when PositionManager holds bad debt Risk Accepted
OQ 8.2 ERC-6909 content URI extensions Risk Accepted
OQ 8.3 No function to query accurate share price Code Change #113
OQ 8.4 No PositionManager LLTV check enforced during deposits Code Change #126, #147
OQ 8.5 Performance fees ignore previous losses Risk Accepted

Informational Findings (Section 9)

Finding Title Status PR(s)
CS-GRUNT-044 Descriptor change not paused Risk Accepted
CS-GRUNT-045 Inconsistent validation for order's input amount Code Change #128
CS-GRUNT-046 Incorrect return values in USCCFund Todo
CS-GRUNT-047 Initialization not disabled in implementation contracts Code Change #86
CS-GRUNT-048 Misleading error state in USCCFund revert messages Code Change #85
CS-GRUNT-049 / 057 Missing sanity checks on LLTV / addBorrowModule lacks validation Code Change #81
CS-GRUNT-050 No upper bound on setMaxRebalanceLoss Code Change #83
CS-GRUNT-051 Only partial events from FacilityFunds Risk Accepted
CS-GRUNT-052 Outdated NatSpec documentation Code Change #84
CS-GRUNT-053 Solidity version not fixed Code Change #61
CS-GRUNT-054 Typo in _intialPmParameters variable Code Change #82
CS-GRUNT-055 Unbounded token iteration in FacilityLP.claim() Risk Accepted
CS-GRUNT-056 Unnecessary cachedBalance in USCCFund logic Code Change #64
CS-GRUNT-060 Arbitrary borrower address in preLiquidate Code Change #80
CS-GRUNT-061 Arbitrary decimals in PositionManager Code Change #79
CS-GRUNT-071 Beacon proxy can use Yul-optimized deployment Risk Accepted
CS-GRUNT-072 Duplicate logic in IntentDescriptor Risk Accepted
CS-GRUNT-073 Incorrect error referenced in NatSpec Code Change #103
CS-GRUNT-074 Incorrect interface specification of create in IFund Risk Accepted
CS-GRUNT-075 Incorrect safety comment in TokenController._transfer() Code Change #101
CS-GRUNT-076 Missing asset compatibility check in MorphoBorrowPosition.initialize() Code Change #107
CS-GRUNT-077 Missing verification of target order Code Change #112
CS-GRUNT-078 Missing zero-address check for receiver in FacilityLP Code Change #100
CS-GRUNT-079 USCCFund uses potentially stale Superstate allowlist interface Code Change #127
CS-GRUNT-080 Unnecessary free memory pointer update in LibOrder.toId() Code Change #102

Notes (Section 10)

Note Title Status PR(s)
Note 10.1 Achieved leverage will deviate from intended leverage Risk Accepted
Note 10.2 Approve amount does not match allowance amount Spec Change #114
Note 10.3 Facility proxy deployment Process Change
Note 10.4 Liquidator whitelist requirement limits liquidation participation Risk Accepted
Note 10.5 Mixed storage layout from Solady inheritance Code Change #76
Note 10.6 Morpho supplier withdrawals can leave intents unable to repay Risk Accepted
Note 10.7 NAV does not account for liquidation penalty Risk Accepted
Note 10.8 Oracle price jumps Risk Accepted
Note 10.9 Pre-liquidation bonus is large Risk Accepted
Note 10.10 Privileged roles can adjust leverage via bridge loan size Code Change #126, #147
Note 10.11 Security considerations for swap signers Spec Change #42
Note 10.12 Share price deflation after full pre-liquidation Risk Accepted
Note 10.13 Share price inflation possible despite mitigation Risk Accepted
Note 10.14 Supply queue deposits can be manipulated by dominant lenders Risk Accepted

Additional Changes

Changes not directly tied to a ChainSecurity finding, identified during remediation and internal review.

PR Title Description
#122 feat: add compliance role and revertDeposit to Facility Adds COMPLIANCE_ROLE and revertDeposit() to allow reverting deposits from sanctioned wallets without closing the intent
#117 feat: add deployment tracking to all factories Adds deployment tracking arrays and getter functions to all factory contracts for governance and monitoring
#77 feat: emit Rebalanced event from PositionManager.rebalance() Enables off-chain tracking of rebalancing operations
#120 fix: skip signatures when removing fund/request + add DeadlineExpired error Guardian signatures are skipped when removing a fund/request (setting to address(0)) since removal is safe; adds expired signature error
#129 fix: return 0 from maxDeposit/maxRedeem for non-depositors ERC-4626 compliance: maxDeposit() and maxRedeem() return 0 for accounts without depositor role
#130 fix: add allowlist check in USCCFund.commit() Adds Superstate allowlist check at commit time (not just at order creation)
#116 refactor: remove redundant unchecked loop increments, bump pragma to ^0.8.22 Removes unchecked { ++i } patterns (redundant since Solidity 0.8.22)
#132 docs: rename Prime Broker to Bridge Facilitator Renames all "Prime Broker" / "PB" references to "Bridge Facilitator" / "BF" across NatSpec and tests
#67 fix: rename lltv to ltv in PositionManager Renames lltv to ltv throughout PositionManager — it's a target LTV, not a liquidation LTV
#52 fix: block PT/YT transfer to address(0) Prevents supply accounting discrepancies from zero-address transfers
#56 fix: safeApprove vs safeApproveWithRetry inconsistency Replaces safeApprove with safeApproveWithRetry for non-zero amounts (handles USDT-style tokens)
#47 fix: correct FAC-4 deposit cap invariant Fixes incorrect invariant assertion in test suite
#98 fix: correct invariant test assertions to match actual totalAssets logic Fixes 3 invariant tests that didn't match the per-position NAV floor behavior of LibView.totalAssets()
#111 fix(test): false positive in invariant_repayTimelockEnforced Fixes a false positive caused by setRequest short-circuiting on unchanged address
#154 fix(test): use maxPt in setRepaid tests to match updated mint signature Fixes 6 setRepaid tests that called mint(0, 0) — broken after #152 changed the first param from minPt to maxPt

ChainSecurity Re-Audit Findings (03/20)

Open Findings (Section 7)

# Finding Title Status PR(s)
7.1 CS-GRUNT-081 Facilitator overpays request to drain LP deposits Code Change #153
7.2 CS-GRUNT-085 Partial pre-liquidations revert due to rounding mismatch with Morpho Code Change #166
7.3 CS-GRUNT-082 Permanent blocking of setting status to repaid Code Change #152
7.4 CS-GRUNT-083 Burn can increase PM LTV Code Change #156
7.5 CS-GRUNT-084 Behavior of PM deposits, withdrawals and burns with bad debt Risk Accepted
7.6 CS-GRUNT-098 Borrowing after supplying collateralForBorrow can fail the health check Code Change #166
7.7 CS-GRUNT-097 Repaying totalBorrowed() can revert due to rounding mismatch Code Change #166
7.8 CS-GRUNT-086 Reverting a deposit might be ineffective Risk Accepted
7.9 CS-GRUNT-099 Withdrawing availableCollateral can fail the health check Code Change #166
7.10 CS-GRUNT-087 maxBorrow returns an unborrowable amount Code Change #166
7.11 CS-GRUNT-014 Burning all shares reverts with depleted principal assets Code Change #158
7.12 CS-GRUNT-019 Incomplete ERC-4626 compliance for ControlledVault Code Change #165
7.13 CS-GRUNT-027 Paused PositionManager allows deposits/withdrawals when totalAssets unchanged (partially corrected) Code Change #164
7.14 CS-GRUNT-032 Share inflation via direct Morpho collateral donation Code Change #167
7.15 CS-GRUNT-038 Unbacked yield token minting leads to yield theft (partially corrected) Risk Accepted
7.16 CS-GRUNT-105 Malicious consumer bypasses mintToRepaidDelay Code Change #173

Informational Findings (Section 9)

# Finding Title Status PR(s)
9.1 CS-GRUNT-100 Balanced proportional withdrawals revert when a position exceeds PM LTV Risk Accepted
9.2 CS-GRUNT-088 Gas inefficient implementation of setSupplyQueue Risk Accepted
9.3 CS-GRUNT-089 Incorrect comment about fee shares Code Change #160
9.4 CS-GRUNT-090 Misnamed custom error Code Change #157
9.5 CS-GRUNT-091 Pause duration is one second longer than specified Code Change #162
9.6 CS-GRUNT-092 Redundant calls to accrue interest Code Change #163
9.7 CS-GRUNT-093 Unchecked return value in removeBorrowModule Code Change #161
9.8 CS-GRUNT-101 Unreachable ZeroShares revert in _settleShares Code Change #159

#40)

* fix: add repay timelock to prevent facilitator deposit theft (CS-I-11)

Enforce a configurable delay between setRequest() and the first
pull()/repay() call so that guardians and depositors can review the
request before funds are moved. The timelock is a global parameter set
at initialization and only settable by the owner.

- Add uint40 repayTimelock to FacilityStorageData (packs with pausedUntil)
- Add uint40 requestSetAt to Intent struct (packs with resolved)
- Revert with RepayTimelockActive if pull/repay is called too early
- Add setRepayTimelock (onlyOwner) with RepayTimelockSet event
- Add repayAvailableAt view for off-chain monitoring
- Add PoC test demonstrating the attack is blocked
- Add FAC-11 and FAC-12 invariants for timelock enforcement

* fix: restrict repay timelock to repay only, add act_setTimelock invariant action

The timelock should only prevent repay(), not pull(). Move the timelock
enforcement from the shared _initialRequestParameters() into the repay()
function. Add act_setTimelock handler action to fuzz the timelock
duration during invariant runs.

* fix: add checkNotZero to setRepayTimelock and align NatSpec

- Validate repayTimelock is non-zero via LibChecks.checkNotZero
- Fix NatSpec: timelock only gates repay(), not pull()
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 9, 2026

Important

Review skipped

Too many files!

This PR contains 185 files, which is 35 over the limit of 150. Please upgrade to a paid plan to get higher limits.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Free

Run ID: 4b2ea8ec-b56c-4662-9a84-1dbcb992092e

📥 Commits

Reviewing files that changed from the base of the PR and between 762c7dd and 12bd551.

📒 Files selected for processing (185)
  • .github/workflows/test.yml
  • README.md
  • foundry.toml
  • src/borrow/MorphoBorrowPosition.sol
  • src/borrow/MorphoBorrowPositionFactory.sol
  • src/facility/Facility.sol
  • src/facility/IntentDescriptor.sol
  • src/facility/base/FacilityFunds.sol
  • src/facility/base/FacilityIntents.sol
  • src/facility/base/FacilityLP.sol
  • src/facility/base/FacilityPositionManager.sol
  • src/facility/base/FacilityRequests.sol
  • src/facility/base/FacilityRoles.sol
  • src/facility/base/FacilitySwap.sol
  • src/funds/USCC/USCCFund.sol
  • src/funds/USCC/USCCFundFactory.sol
  • src/funds/WrappedAsset.sol
  • src/funds/centrifuge/CentrifugeFund.sol
  • src/funds/centrifuge/CentrifugeFundFactory.sol
  • src/funds/pareto/ParetoFund.sol
  • src/funds/pareto/ParetoFundFactory.sol
  • src/guard/TransferGuard.sol
  • src/guard/TransferGuardFactory.sol
  • src/interfaces/borrow/IBorrowPosition.sol
  • src/interfaces/facility/IFacility.sol
  • src/interfaces/facility/IIntentDescriptor.sol
  • src/interfaces/facility/base/IFacilityFunds.sol
  • src/interfaces/facility/base/IFacilityIntents.sol
  • src/interfaces/facility/base/IFacilityLP.sol
  • src/interfaces/facility/base/IFacilityPositionManager.sol
  • src/interfaces/facility/base/IFacilityRequests.sol
  • src/interfaces/facility/base/IFacilitySwap.sol
  • src/interfaces/funds/IFund.sol
  • src/interfaces/funds/IWrappedAsset.sol
  • src/interfaces/funds/USCC/IUSCCFund.sol
  • src/interfaces/funds/centrifuge/ICentrifugeFund.sol
  • src/interfaces/funds/pareto/IParetoFund.sol
  • src/interfaces/guard/ITransferGuard.sol
  • src/interfaces/integrations/AggregatorV3Interface.sol
  • src/interfaces/integrations/IERC5267.sol
  • src/interfaces/integrations/centrifuge/ICentrifugeVault.sol
  • src/interfaces/integrations/pareto/IIdleCDOEpochVariant.sol
  • src/interfaces/integrations/pareto/IIdleCreditVault.sol
  • src/interfaces/integrations/superstate/IAllowlist.sol
  • src/interfaces/integrations/superstate/ISuperstateToken.sol
  • src/interfaces/manager/IPositionManager.sol
  • src/interfaces/manager/base/IPositionManagerAdmin.sol
  • src/interfaces/manager/base/IPositionManagerLP.sol
  • src/interfaces/manager/base/IPositionManagerRebalancing.sol
  • src/interfaces/request/IHasAsset.sol
  • src/interfaces/request/IOfferReceiver.sol
  • src/interfaces/request/IRequest.sol
  • src/interfaces/request/IRequestCallback.sol
  • src/interfaces/request/IRequestInteractions.sol
  • src/interfaces/request/IRequestInteractionsCallback.sol
  • src/interfaces/request/ITokenController.sol
  • src/interfaces/request/IVaultController.sol
  • src/libs/Constants.sol
  • src/libs/borrow/LibBorrowErrors.sol
  • src/libs/borrow/MorphoBalancesLib.sol
  • src/libs/borrow/SharesMathLib.sol
  • src/libs/common/LibChecks.sol
  • src/libs/common/LibCommonErrors.sol
  • src/libs/common/LibPause.sol
  • src/libs/facility/LibAddress.sol
  • src/libs/facility/LibConstants.sol
  • src/libs/facility/LibFacilityErrors.sol
  • src/libs/facility/LibIntent.sol
  • src/libs/facility/LibStorage.sol
  • src/libs/facility/LibTokenBalances.sol
  • src/libs/funds/LibFundsErrors.sol
  • src/libs/funds/Order.sol
  • src/libs/manager/LibConstants.sol
  • src/libs/manager/LibExecutor.sol
  • src/libs/manager/LibManagerErrors.sol
  • src/libs/manager/LibOperations.sol
  • src/libs/manager/LibStorage.sol
  • src/libs/manager/LibView.sol
  • src/libs/request/Lib128Fields.sol
  • src/libs/request/LibAllowance.sol
  • src/libs/request/LibMintAuth.sol
  • src/libs/request/LibRequestErrors.sol
  • src/libs/request/LibTokenController.sol
  • src/manager/PositionManager.sol
  • src/manager/PositionManagerFactory.sol
  • src/manager/base/PositionManagerAdmin.sol
  • src/manager/base/PositionManagerBase.sol
  • src/manager/base/PositionManagerLP.sol
  • src/manager/base/PositionManagerRebalancing.sol
  • src/manager/rebalancer/MorphoRebalancer.sol
  • src/request/MorphoFlashLoanRequest.sol
  • src/request/MorphoFlashLoanRequestFactory.sol
  • src/request/Request.sol
  • src/request/RequestFactory.sol
  • src/request/Vault.sol
  • src/request/abstract/OfferReceiver.sol
  • src/request/abstract/tokens/ControlledToken.sol
  • src/request/abstract/tokens/TokenController.sol
  • src/request/abstract/vault/ControlledVault.sol
  • src/request/abstract/vault/VaultController.sol
  • src/request/scripts/SyncDeposit.sol
  • src/request/scripts/SyncWithdrawal.sol
  • test/borrow/MorphoBorrowPosition.t.sol
  • test/facility/Facility.invariant.t.sol
  • test/facility/FacilityBase.t.sol
  • test/facility/FacilityCentrifugeFunds.t.sol
  • test/facility/FacilityFunds.t.sol
  • test/facility/FacilityInit.t.sol
  • test/facility/FacilityIntents.t.sol
  • test/facility/FacilityLP.t.sol
  • test/facility/FacilityPause.t.sol
  • test/facility/FacilityPositionManager.t.sol
  • test/facility/FacilityReentrancy.t.sol
  • test/facility/FacilityRequests.t.sol
  • test/facility/FacilityRevertDeposit.t.sol
  • test/facility/FacilitySwap.t.sol
  • test/funds/USCC/USCCFund.fuzz.t.sol
  • test/funds/USCC/USCCFund.t.sol
  • test/funds/USCC/USCCFundFactory.t.sol
  • test/funds/WrappedAsset.invariant.t.sol
  • test/funds/WrappedAsset.t.sol
  • test/funds/centrifuge/CentrifugeFund.fork.t.sol
  • test/funds/centrifuge/CentrifugeFund.fuzz.t.sol
  • test/funds/centrifuge/CentrifugeFund.t.sol
  • test/funds/centrifuge/CentrifugeFundFactory.t.sol
  • test/funds/pareto/ParetoFund.fork.t.sol
  • test/funds/pareto/ParetoFund.fuzz.t.sol
  • test/funds/pareto/ParetoFund.t.sol
  • test/funds/pareto/ParetoFundFactory.t.sol
  • test/guard/TransferGuard.invariant.t.sol
  • test/guard/TransferGuard.t.sol
  • test/guard/TransferGuardFactory.t.sol
  • test/guard/TransferGuardReentrancy.t.sol
  • test/libs/borrow/MorphoBalancesLib.t.sol
  • test/libs/common/LibChecks.t.sol
  • test/libs/common/LibPause.t.sol
  • test/libs/facility/LibStorage.t.sol
  • test/libs/manager/LibOperations.t.sol
  • test/libs/manager/LibStorage.t.sol
  • test/libs/manager/LibView.t.sol
  • test/manager/PositionManager.invariant.t.sol
  • test/manager/PositionManagerBase.t.sol
  • test/manager/PositionManagerBurn.t.sol
  • test/manager/PositionManagerDeposit.t.sol
  • test/manager/PositionManagerEdgeCases.t.sol
  • test/manager/PositionManagerFactory.t.sol
  • test/manager/PositionManagerFee.t.sol
  • test/manager/PositionManagerInit.t.sol
  • test/manager/PositionManagerLiquidation.t.sol
  • test/manager/PositionManagerRebalance.t.sol
  • test/manager/PositionManagerReentrancy.t.sol
  • test/manager/PositionManagerRoles.t.sol
  • test/manager/PositionManagerTransferGuard.t.sol
  • test/manager/PositionManagerWithdraw.t.sol
  • test/manager/rebalancer/MorphoRebalancer.t.sol
  • test/mock/borrow/MockBorrowPosition.sol
  • test/mock/facility/CallbackToken.sol
  • test/mock/facility/FacilityHandler.sol
  • test/mock/funds/MockSuperstateToken.sol
  • test/mock/funds/USCC/USCCFundHandler.sol
  • test/mock/funds/centrifuge/CentrifugeFundHandler.sol
  • test/mock/funds/centrifuge/MockCentrifugeVault.sol
  • test/mock/funds/pareto/MockIdleCDOEpochVariant.sol
  • test/mock/funds/pareto/MockIdleCreditVault.sol
  • test/mock/funds/pareto/ParetoFundHandler.sol
  • test/mock/guard/MaliciousBorrowModule.sol
  • test/mock/guard/ReentrantBorrowModule.sol
  • test/mock/libs/LibChecksHarness.sol
  • test/mock/libs/LibFacilityStorageHarness.sol
  • test/mock/libs/LibManagerStorageHarness.sol
  • test/mock/libs/LibOperationsHarness.sol
  • test/mock/manager/MockMaliciousPositionManager.sol
  • test/mock/manager/PositionManagerHandler.sol
  • test/mock/manager/ReentrantMinter.sol
  • test/mock/request/MaliciousRequestCallback.sol
  • test/request/BurnAllDepletedAssets.t.sol
  • test/request/ControlledToken.t.sol
  • test/request/ControlledVault.t.sol
  • test/request/MorphoFlashLoanRequest.t.sol
  • test/request/OfferReceiver.t.sol
  • test/request/Request.t.sol
  • test/request/RequestConsume.t.sol
  • test/request/RequestMintSlippagePoC.t.sol
  • test/request/RequestMintTimelockPoC.t.sol
  • test/request/RequestReentrancy.t.sol

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Note

🎁 Summarized by CodeRabbit Free

Your organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Pro by visiting https://app.coderabbit.ai/login.

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

maxencerb and others added 28 commits February 9, 2026 17:03
…-12) (#41)

Add minPt and minYt parameters to Request.mint() so the minter can
specify minimum expected PT/YT amounts. Reverts with SlippageExceeded
if authorizeMinting has been overwritten below the caller's minimums.

- Add SlippageExceeded error to LibRequestErrors
- Update IRequest.mint(uint128 minPt, uint128 minYt) signature
- Add slippage check in Request.mint() before clearing authorization
- Add PoC test demonstrating the front-run attack is blocked
- Add unit and fuzz tests for slippage protection
- Update existing mint() call sites to use new signature
* docs: document swap digest signer-address replay risk (CS-I-9)

Add NatSpec security notes to FacilitySwap and IFacilitySwap documenting
that the EIP-712 swap digest does not hash the signer address. SC wallet
guardians must use implementations that bind their own address into
EIP-1271 isValidSignature (e.g., Safe >= 1.3.0).

* docs: remove audit reference from NatSpec
… (CS-I-6) (#43)

* fix: return 0 from previewDeposit/previewMint for ERC-4626 compliance (CS-I-6)

previewDeposit() and previewMint() returned calculated amounts even
though deposit() and mint() always revert. Per ERC-4626, preview
functions must reflect the actual outcome of the corresponding
operation. Return 0 since no deposits/mints are possible.

- Update previewDeposit() and previewMint() to return 0
- Update existing tests to assert 0 return values
- Add PoC tests verifying full deposit/mint compliance surface

* chore: remove unnecessary PoC test
…(CS-I-13) (#44)

Enforce a configurable delay between the last mint/consume and setRepaid()
to prevent a colluding facilitator from inflating YT supply and atomically
marking the request as repaid, stealing yield from legitimate holders.
Update README state diagrams and access control table to reflect that
setFund() and setRequest() are callable in any intent phase (DEPOSITING,
RESOLVING, RESOLVED), not just RESOLVING as previously documented.
When mulDiv rounds a token's proportional share to zero, skip the
safeTransfer call. Some ERC-20 implementations revert on zero-amount
transfers which would brick claim() for small shareholders.
The deposit cap invariant incorrectly asserted totalSupply <= depositCap
globally. Since the cap can be reduced below current supply via
setDepositCap, the invariant should only verify that deposits cannot
push supply above the cap.

- Convert FAC-4 from global assertion to per-action ghost flag
- Add act_updateDepositCap handler action to exercise cap reduction
- Add depositExceededCap ghost checked after each successful deposit
Calling setFund or setRequest a second time with the same address
deleted the reverse mapping (fundsIntent/requestsIntent) via
abandonFund/abandonRequest while the intent still referenced it.
This broke the uniqueness check, allowing the same fund/request
to be assigned to another intent.

Add early return when newFund/newRequest matches the current value.
* fix: match getIntent return values in test destructuring

* build: update optimizer runs
When totalYAssets = 0 (e.g. after full liquidation), convertToShares
uses the initial conversion ratio of type(uint256).max for non-zero YT
amounts, causing yt.withdraw to revert instead of gracefully handling
the edge case. Add an early return for zero-amount withdrawals.
_approve() previously zeroed the sibling token allowance because it
passed 0 for the other token. Now reads existing allowances and only
overwrites the targeted token, preserving the sibling's allowance.
_transfer did not validate the recipient, allowing tokens to be sent to
address(0) without reducing totalSupply. Add checkNotZero(to) using
LibChecks so transfers to the zero address revert with AddressZero().
…#54)

* fix: add cancelRecovering() to prevent permanent fund lock (CS-I-4)

If recovering() is called by mistake while PROCESSING and Superstate
delivers the output tokens, funds become permanently stuck. Add
cancelRecovering() to revert internalState back to PROCESSING.

* fix: forge fmt

* refactor: USCCFund implements IUSCCFund

* test: register act_cancelRecovering and remove early returns
* fix(wrapped-asset): centralize InvalidDecimals in LibFundsErrors

* refactor(wrapped-asset): derive decimals from underlying token
Lock the compiler to a specific version instead of relying on
auto-detect with floating pragma `^0.8.20`.
Update the resolveStart comment to accurately describe it as the timestamp
at which the deposit phase ends and the resolving phase begins.

Fixes 3F-71
In recover() and unlock(), transfers and events incorrectly used
msg.sender as the recipient. The IFund interface specifies that
output assets and shares must go to order.receiver.
The cachedBalance storage variable was a defensive snapshot of the USCC
balance before Superstate processing. Since commit() always leaves the
fund with zero USCC (deposit sends USDC; redeem burns USCC via
offchainRedeem), the cached value was always zero and the
zeroFloorSub was a no-op. Removing it simplifies storage, gas, and
the _state() logic.
* fix: use _setOracle helper in USCCFund.initialize

Extract oracle validation (contract check, decimals check, storage set)
into a private _setOracle() helper to eliminate duplication between
initialize() and setOracle().

* fix: move event in internal function

* fix: rollback cancelRecovering
* fix: add minBalance param to setRepaid() to prevent frontrun (Fixes 3F-79)

The facilitator could frontrun setRepaid() by calling pullFunds() to
drain the contract before repayment is finalized. Adding a minBalance
parameter lets the owner specify the minimum expected asset balance,
reverting if the balance has been drained below that threshold.

* docs: update setRepaid() references to setRepaid(uint256)
The PositionManager's LTV is not a liquidation loan-to-value but a simple
loan-to-value threshold — a small buffer above the target that determines
how much collateral can be withdrawn from borrow positions based on debt.

Renames across 31 files:
- Storage field: lltv → ltv
- Functions: setLltv → setLtv, checkValidLltv → checkValidLtv
- Error: InvalidLltv → InvalidLtv
- Event: LLTVSet → LTVSet
- IBorrowPosition interface params: lltv → ltv
- All NatSpec and test references updated accordingly

Morpho's own marketParams.lltv and LiquidationLtvExceedsMarketLltv are
kept as-is since those refer to Morpho Blue's actual liquidation LTV.

Fixes 3F-80
- Remove unused `currentOrder` from USCCFund storage (stored but never read)
- Replace `resolvedOrder` struct (5 slots) with minimal fields (bool + 2 uint256)
- Remove redundant `delete` operations in cancel/recover/unlock (cleared by next create)
- Use direct `_values` mapping access in LibTokenBalances to skip unnecessary contains() check

Fixes 3F-75
… (3F-84) (#69)

convertToShares used mulDiv (round down) unconditionally. When burning
shares on asset decrease, rounding down means the caller surrenders
fewer shares than the value extracted, leaking fractional value from LPs.

Added a roundUp boolean param to convertToShares. The burn path in
_settleShares now passes roundUp=true (mulDivUp), ensuring the caller
surrenders at least as many shares as the value removed.
…F-85) (#68)

Previously totalAssets() summed all collateral and all debt globally,
then applied zeroFloorSub once. A position with bad debt (debt > collateral)
would reduce the overall NAV, understating the value for healthy positions.

Now each position's NAV is computed individually using zeroFloorSub,
so bad-debt positions contribute 0 instead of a negative value.
* fix: accrue interest before reading Morpho market data (3F-83)

Stale Morpho market data from IMorpho.market() caused debt to be
systematically understated, affecting health checks, borrowing capacity,
fee computation, and share settlement.

For state-changing functions (withdrawCollateral, borrow, preLiquidate):
call morpho.accrueInterest() before reading market data. Critically,
preLiquidate now accrues interest BEFORE the health check instead of
after, preventing stale data from blocking legitimate liquidations.

For view functions (totalBorrowed, isHealthy, maxBorrow,
availableLiquidity, availableCollateral): introduce MorphoExpectedLib,
a Solady-based library that computes expected interest-accrued market
balances without mutating state, mirroring Morpho Blue's _accrueInterest
logic exactly.

MorphoExpectedLib is fuzz-tested against actual Morpho accrueInterest
output across arbitrary supply/borrow/time/fee combinations.

* fix: use Market struct with SafeCastLib in MorphoBalancesLib

Operate directly on the Market memory struct (uint128 fields) and use
Solady SafeCastLib.toUint128() for safe casts, mirroring the original
Morpho Blue MorphoBalancesLib pattern. This ensures overflow behavior
matches Morpho exactly instead of silently allowing >uint128 values.
* feat(facility): share digest replay checks across swap and intent updates

* fix: forge fmt

* fix: apply pr comments

* fix: keep _digest in caller function

* fix: move digest replay check into base _checkSignatures
maximebrugel and others added 22 commits March 6, 2026 17:45
maxDeposit() always returned type(uint256).max and maxRedeem() returned
the full wUSCC balance regardless of caller permissions. Now both
functions check _DEPOSITOR_ROLE and return 0 for unauthorized accounts.
Replace all "Prime Broker" / "PB" references with "Bridge Facilitator" / "BF"
across NatSpec comments, test variable names, and test comments.
Replace proportional collateral allocation with LTV-aware deposit logic.
Each position now uses collateralForBorrow/borrowForCollateral to determine
exact collateral needs at the target LTV, preventing deposits from exceeding
the owner-configured LTV threshold.

Changes:
- Add collateralForBorrow() and borrowForCollateral() to IBorrowPosition
- Implement both in MorphoBorrowPosition with full natspec
- Add _requiredCollateral() and _maxBorrow() internal helpers
- Update processDeposit to use LTV-bounded collateral allocation
- Update MockBorrowPosition with realistic LTV-aware math
- Add comprehensive tests for new view functions and LTV constraints
- Update README to reflect LTV-enforced deposit flow

Fixes 3F-114
Fixes 3F-171
Proportional withdrawals did not verify that each position's collateral
withdrawal respects the storage LTV, unlike sequential withdrawals which
use availableCollateral(ltv). This could leave individual positions in an
unhealthy state during withdraw(). Burns are unaffected since amounts are
proportional to total debt/collateral.

Add checkLtv boolean to processWithdrawal — true for withdraw(), false
for burn(). When true, each position's proportional collateral withdrawal
is checked against availableCollateral(ltv) after repayment.
Proportional withdrawals did not verify that each position's collateral
withdrawal respects the storage LTV, unlike sequential withdrawals which
use availableCollateral(ltv). This could leave individual positions in an
unhealthy state during withdraw(). Burns are unaffected since amounts are
proportional to total debt/collateral.

Add checkLtv boolean to processWithdrawal — true for withdraw(), false
for burn(). When true, each position's proportional collateral withdrawal
is checked against availableCollateral(ltv) after repayment.
…(CS-GRUNT-014) (#151)

When a vault was repaid but had zero principal assets remaining, convertToAssets
incorrectly returned ptShares (1:1 initial conversion) instead of 0, causing
burnAll to revert on the subsequent zero-balance safeTransfer.

The fix distinguishes pre-repayment from post-repayment state: initial conversion
(1:1 for PT) is now only used when supply is zero (div-by-zero guard) or when
not yet repaid. After repayment, mulDiv correctly yields 0 for depleted vaults.
* fix: change mint slippage from minPt to maxPt to prevent PT inflation attack (CS-GRUNT-018)

The mint function's first parameter was minPt (floor on deposit amount) but
should be maxPt (cap on deposit amount). A consumer could front-run by increasing
the PT authorization, forcing the broker to deposit more than expected for the
same yield. The maxPt parameter now caps the deposit so ptMintAuth > maxPt reverts.

Callers that previously passed mint(0, 0) for no protection should now pass
mint(type(uint128).max, 0).

* fix: bound fuzz delay to < 90 days to satisfy initialize constraint

The testFuzz_setRepaid_respectsTimelock test bounded delay to [1, 90 days]
but initialize requires repaymentDeadline > block.timestamp + mintToRepaidDelay.
When delay == 90 days the strict inequality fails. Bound to 90 days - 1.

* fix: early return on zero-authorized mint to prevent setRepaid griefing

A user with zero PT/YT minting authority could repeatedly call mint() to
bump lastMintTimestamp, permanently delaying setRepaid(). Now mint()
returns early when both ptMintAuth and ytMintAuth are zero.
…-GRUNT-005) (#153)

A malicious facilitator who is also a significant YT holder could over-repay
assets just before setRepaid is called, inflating their YT redemption value at
the expense of intent shareholders. The new maxBalance parameter caps the
allowed balance, reverting with ExcessiveBalance if exceeded.

Pass type(uint256).max for maxBalance to skip the upper bound check.
Also fixes the pre-existing fuzz bound bug in testFuzz_setRepaid_respectsTimelock.
…mint signature (#154)

The setRepaid tests from #153 used mint(0, 0), which fails after #152
changed the first parameter from minPt to maxPt (maxPt=0 triggers
SlippageExceeded when ptMintAuth > 0).
…partial (3F-200) (#156)

When burn passes checkLtv=false to processWithdrawal, it assumes proportional
amounts are safe because they're derived from total debt/collateral. This only
holds when the withdrawal queue covers ALL whitelisted positions. If the queue
is a strict subset, queue positions bear a disproportionate share of the
withdrawal, potentially violating individual position LTV limits.

Now checkLtv is only skipped when withdrawalQueue.length == borrowModules.length().
…014) (#158)

Guard _withdrawalOperation against zero-amount transfers when vault is
depleted. Complements the convertToAssets fix (e60f5ad) with defense-in-depth
for non-standard ERC20 tokens that may revert on zero-amount transfers.
…quidity (CS-GRUNT-089) (#160)

Fee shares only redistribute ownership among suppliers; they do not
change totalSupplyAssets and have no effect on available liquidity.
…UNT-019) (#165)

maxWithdraw now returns convertToAssets(balanceOf(owner)) and maxRedeem
returns balanceOf(owner) instead of type(uint256).max, satisfying the
ERC-4626 requirement that these functions reflect the actual amount
accepted for the specific owner.
…-GRUNT-085/087/097/098/099) (#166)

* fix: use two sequential ceilings in _computeSeizedAssets to match Morpho's two-floor health check (CS-GRUNT-085)

Morpho's _isHealthy applies two sequential floors: floor(collateral * price / SCALE)
then floor(result * lltv / WAD). The old code combined them into a single
floor(price * lltv), which could be 1 unit too optimistic, causing partial
pre-liquidations to revert on Morpho's withdrawCollateral health check.

* fix: use two sequential ceilings in _requiredCollateral to match _isHealthy's two-floor check (CS-GRUNT-099)

_requiredCollateral used a single combined ceiling to invert _isHealthy's
two sequential floors, causing withdraw(availableCollateral(ltv)) to
intermittently revert. Split into two mulDivUp calls matching the pattern
already applied in _computeSeizedAssets.

* fix: simulate share round-trip in collateralForBorrow to prevent post-borrow health check revert (CS-GRUNT-098)

collateralForBorrow predicted post-borrow debt as currentDebt + borrowAmount,
but Morpho's borrow() converts assets to shares via toSharesUp then _isHealthy
reconverts via toAssetsUp. This double rounding can inflate actual debt beyond
the prediction. Now simulates the exact share round-trip to match what
_isHealthy will see.

* fix: compute _maxBorrow in share space to match Morpho's rounding (CS-GRUNT-087)

_maxBorrow computed capacity in asset space, but Morpho's borrow() converts
assets to shares via toSharesUp then _isHealthy reconverts via toAssetsUp.
The compounded rounding could make borrow(maxBorrow(ltv)) revert.

Now computes in share space using Morpho's own SharesMathLib: find max
shares via toSharesDown, convert to assets via toAssetsDown. This directly
matches Morpho's rounding with no post-hoc adjustment needed. Same fix
propagates to borrowForCollateral which also delegates to _maxBorrow.

* fix: use toAssetsDown in totalBorrowed to prevent repay revert (CS-GRUNT-097)

totalBorrowed() used toAssetsUp which rounds up. When passed to repay(),
Morpho converts back via toSharesDown — the round-trip
toSharesDown(toAssetsUp(bs)) can exceed bs, causing underflow.

Switching to toAssetsDown ensures toSharesDown(toAssetsDown(bs)) <= bs,
so repay(totalBorrowed()) never reverts. May leave at most 1 dust share
whose asset value rounds to 0.
@maximebrugel maximebrugel marked this pull request as ready for review March 24, 2026 19:55
maximebrugel and others added 2 commits March 25, 2026 11:06
…revent share inflation via direct Morpho collateral donation (#167)
* feat: Centrifuge Fund Integration (JAAA & ACREDX) (#78)

* refactor: reorganize USCC fund files into funds/USCC/ subfolder

Move USCC-specific files into dedicated USCC/ subfolders to prepare for
additional fund types alongside USCCFund.

* feat: vendor Centrifuge IVaultRouter interface (3F-94)

Add a flattened, self-contained IVaultRouter interface with all
contract-type parameters replaced by plain address for ABI
compatibility. PoolId and ShareClassId UDVTs are defined inline
and IBatchedMulticall is flattened in. This removes the need for
the centrifuge_export/ vendor directory.

* feat: CentrifugeFund

* refactor: remove dead resolve(), validate output at creation, clean up _state

Remove resolve() and its storage fields (hasResolvedAmounts, resolvedInput,
resolvedOutput) which are unused — CentrifugeFund queries the vault directly
unlike USCCFund. Validate order.output against the vault's conversion rate at
creation time. Inline RECOVERING branch in _state() to avoid duplicate
claimableCancel* external calls. Collapse recovering/cancelRecovering/cancelOrder
into a single cancelRequest().

* fix: forge fmt

* refactor: add NatSpec, use before/after pattern, simplify RECOVERING state

Add NatSpec to storage struct fields, _stateHasPendingRequest, and
_stateHasPendingRecover. Document requestId = 0 convention. Use
before/after balance delta in unlock() and recover() to exclude dust.
Simplify _state() RECOVERING branch to only check claimable amount,
mirroring the PROCESSING/UNLOCKING pattern.

* doc: missing comments

* doc: add CentrifugeFund to README

* refactor: move archived order check from _state() to state()

* feat: add MAX_OUTPUT_DEVIATION slippage floor to CentrifugeFund

Enforce a minimum output relative to the current exchange rate in
create(), preventing callers from setting output to 0 or unreasonably
low values. Move BPS constant from LibConstants to global Constants.sol.

* fix: reset vault approval to 0 after commit requests

* fix: report dynamic state in unlock() revert

* fix: report dynamic state in recover() revert

* feat: use vault's maxDeposit/maxRedeem limits in CentrifugeFund

* refactor: cache redundant storage reads in CentrifugeFund

* refactor: cache computed orderId instead of storage read in cancelRequest

* refactor: simplify PROCESSING branch in _state() to match RECOVERING style

* refactor: move unknown-order guard from _state() to state()

* refactor: add constructor section and natspec, remove unused _orderId in _state()

* refactor: extract repeated owner check into _checkOwner()

* refactor: rename _checkOwner to _checkOrderOwner

* fix: return archived order first in state

* test: add CentrifugeFund test suite with 100% coverage

Unit tests (73), fuzz tests (8), invariant tests (4), and factory tests (8)
covering all CentrifugeFund operations including partial fills, slippage
guard, cancel request, and state machine transitions.

* fix: remove upper bound check on slippage guard in create()

Allow order.output > expectedOutput (optimistic pricing). Only reject
when output deviates too far below the current rate.

* fix: forge fmt

* fix: guard cancelRequest() against partial fills

Revert with PendingClaimableAssets when claimable partial-fill assets
exist (maxMint for deposits, maxWithdraw for redeems), forcing the
operator to unlock() before cancelling to prevent stuck shares.

* fix: add isPermissioned guard to commit()

Prevents opaque vault reverts if permission is revoked between
create() and commit(), matching the existing check in create().

* test: add integration tests

* build: link secret

* test: expand CentrifugeFund coverage with fork and fuzz tests

Fork: add redeem cancel lifecycle, partial redeem fill, and slippage
guard tests with real on-chain conversion rates. Fuzz: add non-1:1
rate tests for slippage guard and totalAssets. Fix invariant to use
vault.convertToAssets instead of hardcoded 1:1 assumption.

* fix: address PR review comments on CentrifugeFund

- Reset share token allowance after wrappedShare.mint in unlock() and
  recover() for safety
- Add _PENDING_REQUEST constant for readability instead of raw 0
- Change MAX_OUTPUT_DEVIATION from 1% to 5% for consistency with
  USCCFund
- Update all test thresholds to match 5% slippage guard

* feat: Pareto Fund Integration (FalconX Credit Vault) (#110)

* feat: ParetoFund

* feat: add resolve() to ParetoFund for operator output override

Allow operator/owner to override order input/output after commit
via resolve(), following the USCCFund pattern. The deposit state
transition check in _state() uses the resolved output when available.

* fix: use InvalidUnderlyingAsset error for wrappedShare mismatch

Replace misused InvalidReceiver with a dedicated InvalidUnderlyingAsset
error when wrappedShare.underlying() does not match the expected share
token during initialize(). Applies to CentrifugeFund and ParetoFund.

* refactor: rename NotAllowedToOperateWithVault to NotAllowedByFund

* style: align IParetoFund and ICentrifugeFund interfaces with IUSCCFund conventions

Add full NatSpec, section banners, and OPERATOR_ROLE/DEPOSITOR_ROLE
view getters to IParetoFund and ICentrifugeFund for consistency.

* test: add unit/invariant tests

* fix: forge fmt

* test: integration tests

* docs: add ParetoFund to README

* fix: correct totalAssets NatSpec and remove unused resolvedInput storage

- Fix virtualPrice decimal description (underlying decimals, not 18)
- Remove resolvedInput from ParetoFundStorage (no RECOVERING state)

* fix: use before/after balance pattern in unlock() for REDEEM

Use actual received balance instead of trusting withdrawsRequests()
amount, which may differ due to fees, rounding, or partial fills.

* feat: add isWalletAllowed check in commit() and test for revoked allowance

* fix: store deposit-received amount in commit() and use it in unlock()

Prevents unlock() from wrapping the entire AA tranche balance (which
could include airdrops or accidental transfers) by capturing only the
tokens actually received from depositAA via a before/after balance
pattern, storing the result in depositReceived, and using it in both
_state() and unlock().

* feat: add slippage guard on create() in ParetoFund

Reject orders whose output deviates more than 5% below the current
virtualPrice rate, matching the existing CentrifugeFund behaviour.

* fix: forge fmt

* test: add missing coverage for ParetoFund unlock and resolve edge cases

Add 6 tests covering PR review feedback:
- claimWithdrawRequest returning a different amount than withdrawsRequests
- resolve() with input=0 and output=0
- accidental tokens sent to the fund not being swept on unlock

* style: run forge fmt

* fix(test): pin fork tests to block 24570780 to prevent mainnet state drift

* fix: use `depositDuringEpoch` (#119)

* fix: Fund Batch [01] (#125)

* fix: make OPERATOR_ROLE and DEPOSITOR_ROLE internal in CentrifugeFund and ParetoFund (#123)

* feat: add `MorphoFlashLoanRequest` facilitator contract (#118)

* feat: add MorphoFlashLoanRequest facilitator contract

Introduces a facilitator contract that uses Morpho flash loans to
temporarily act as a request on a facility intent. It atomically sets
itself as the request, pulls the flash-loaned amount into the facility,
executes caller-specified operations, repays back, unsets itself, and
returns the flash loan to Morpho.

* style: run forge fmt on MorphoFlashLoanRequest

* fix: set rawDebt to full balance to prevent dust griefing

Set rawDebt to the contract's full token balance at callback start
(flash loan amount + any pre-existing dust) instead of just the flash
loan amount. This prevents an attacker from sending 1 wei of dust to
revert the entire flash loan cycle via BalanceExceedsDebt().

* test: cover remaining branch paths in MorphoFlashLoanRequest

Add 3 tests exercising revert branches that require non-zero rawDebt
in transient storage, bringing branch coverage from 50% to 100%.

* feat: add MorphoFlashLoanRequestFactory contract

* style: rename storage variable from `s` to `$`

---------

Co-authored-by: maximebrugel <maxime.brugel@gmail.com>

* fix: handle fulfilled shares stuck after `cancelRequest` races with `approveDeposits` (#133)

* fix: handle fulfilled shares stuck after cancelRequest races with approveDeposits

When cancelRequest() passes its maxMint==0 guard between Centrifuge's
multi-step fulfillment (approveDeposits → issueShares → notifyDeposit),
the deposit can be partially or fully fulfilled despite the cancel. The
RECOVERING state previously only checked claimableCancelDepositRequest,
making fulfilled shares permanently stuck.

Update _state() to check maxMint/maxWithdraw first when in RECOVERING,
returning UNLOCKING so fulfilled shares can be claimed. Update unlock()
to transition back to RECOVERING (not PROCESSING) when cancel assets
still need claiming. Update invariant model to match new state logic.

* test: add race condition handler actions and redeem partial fulfillment test

Add fulfillDepositAndCancelDeposit / fulfillRedeemAndCancelRedeem handler
actions so the fuzzer can reach the race condition path where the Centrifuge
pool fulfills both the request and its cancellation in the same epoch. Also
add test_Unlock_RedeemPartialFulfilledDuringRecovery mirroring the existing
deposit-side test.

* fix: pack `CentrifugeFundStorage` struct to save one storage slot (#134)

Move `internalState` (1-byte enum) next to `vault` (20-byte address) so
they share slot 0, reducing the struct from 6 slots to 5 and saving
~2,100 gas on cold SLOAD when both fields are accessed together.

* fix: revert on reused order ID to prevent ambiguous state (#135)

Add `OrderAlreadyExists` guard in `create()` for CentrifugeFund,
ParetoFund, and USCCFund to reject orders whose computed ID already
exists in `endedOrders`, preventing `state()` from returning a stale
ENDED result for a newly created order.

* fix: check WrappedShare is permissioned in `create()` and `commit()` (#137)

Without this check, orders proceed through ACCEPTED → PROCESSING before
reverting at `unlock()` when WrappedAsset lacks share token permissions,
locking assets recoverable only via async `cancelRequest → recover`.

* fix: add `forceEnd` to CentrifugeFund to unblock griefed orders (#136)

* feat: add `forceEnd` to CentrifugeFund to unblock griefed orders

An attacker can call `vault.requestDeposit(1, fundAddress, attacker)`
to inflate `pendingDepositRequest(0, fundAddress)`, preventing the fund
from transitioning out of PROCESSING after a legitimate fill is claimed.
`forceEnd` lets the operator or owner force-end a stuck order from
PROCESSING or RECOVERING state, bypassing the vault's pending counters.

* fix: guard `forceEnd` against stranding claimable assets

* fix: return 0 from `maxDeposit` and `maxRedeem` for non-depositors (#138)

* fix: use `lastWithdrawRequest` in ParetoFund to unblock REDEEM on apr0 vaults (#139)

When IdleCreditVault has unscaledApr == 0, requestWithdraw routes amounts
through _requestWithdrawApr0 and never increments withdrawsRequests[user].
ParetoFund._state() relied on withdrawsRequests > 0 to detect claimable
withdrawals, causing REDEEM orders to be permanently stuck in PROCESSING.

Switch to lastWithdrawRequest > 0 as the pending-withdrawal signal, which
is set on both the normal and apr0 paths and reset to 0 after claim.

* fix: remove unnecessary tranche token approvals in `ParetoFund.commit()` (#140)

IdleCDO's `requestWithdraw` burns tranche tokens directly via
`IdleCDOTranche.burn()`, which does not use `transferFrom` and
therefore requires no approval. Remove the redundant approve/reset
calls and fix the mock to match the real CDO behavior.

* fix: allow `ParetoFund.cancel()` in PENDING state per IFund spec (#141)

* fix: remove misleading `newOrderId` from `OrderResolved` event (#142)

`resolve()` emitted a `newOrderId` computed from in-memory order
mutations, but `currentOrderId` was never updated — so the emitted ID
didn't correspond to any on-chain state. Remove the parameter and the
dead `order.input`/`order.output` mutations that only existed to
compute it.

* fix: use `mulDiv` consistently in ParetoFund slippage guard (#143)

* fix: replace literal `0` with `_PENDING_REQUEST` constant in CentrifugeFund (#144)

* fix: pass `account` instead of `address(this)` in `maxDeposit` and `maxRedeem` (#145)

* fix: pass `account` instead of `address(this)` in `maxDeposit` and `maxRedeem`

* docs: add @dev comments to `maxDeposit` and `maxRedeem` explaining vault permissioning

* fix: CentrifugeFund.recover() and unlock() return PROCESSING per IFund spec (#146)

* fix: return `PROCESSING` instead of `RECOVERING` from `recover()` and `unlock()` per IFund spec

`recover()` and `unlock()` returned `RECOVERING` for partial operations,
violating the IFund interface which specifies `PROCESSING`. Decoupled the
internal state (stays `RECOVERING` for correct state-machine transitions)
from the return value (now `PROCESSING` per spec).

* fix: update stale assertion messages from "still recovering" to "still processing"

* fix: revert on instant withdrawals in ParetoFund to prevent stuck orders (#148)

When the CDO silently routes a withdrawal to the instant path,
`withdrawsRequests` doesn't increase and the order gets permanently
stuck in PROCESSING. Check before/after values and revert with
`InstantWithdrawDetected` if the request wasn't registered.

* refactor: extract `_revertIfUnclaimedFills` helper in CentrifugeFund (#149)

Deduplicate the identical claimable-fills guard from `cancelRequest()`
and `forceEnd()` into a single internal helper that also encapsulates
the `_stateHasPendingRequest` bypass for polluted claimable amounts.

* feat: `MorphoFlashLoanRequest` script (#150)

* feat: replace Operation[] loop with whitelisted script delegatecall

Replace the generic Operation[] loop in MorphoFlashLoanRequest with a
delegatecall to owner-whitelisted scripts, enabling intermediate return
values (e.g. variable share amounts from unlock) to flow between chained
facility calls.

- Add `scripts` mapping to ERC-7201 storage, `setScript`/`isScript` management
- Change `execute()` signature to accept `(script, scriptPayload)` instead of `Operation[]`
- Delegatecall whitelisted script in callback (preserves FACILITATOR_ROLE via msg.sender)
- Add SyncDeposit script: create → commit → unlock → depositManager with computed share amounts
- Update all tests to new signature; add script management and SyncDeposit integration tests

* feat: add in-script balance-diff tracking to SyncWithdrawal

Derive redeemAmount on-chain via shareToken balance diff before/after
withdrawManager, mirroring SyncDeposit's pattern. This couples the
withdrawal and redeem steps so the script provides value over separate
facilitator calls.

* feat: separate owner and executor roles in MorphoFlashLoanRequest (#155)

Migrate from Ownable to OwnableRoles so the address that whitelists
scripts (owner) is distinct from the address that executes flash loans
(EXECUTOR_ROLE). Update factory to accept executor param and add tests
for the new access control boundaries.

* fix: CentrifugeFund `forceEnd()` deadlock and claimable fills bypass [CS-GRUNTFUND-17/18] (#168)

* fix: add syncEndedOrder() to unblock intents after CentrifugeFund.forceEnd()

When forceEnd() is called directly on the fund, the Facility's Intent
retains a stale order (order.owner != address(0)), permanently blocking
resolve(), setFund(), and create(). Add syncEndedOrder() in LibIntent
that queries IFund.state(order) and clears the stale order+fund binding
when the fund reports ENDED. Auto-sync in resolve() and setFund()
(before the same-fund early return) so recovery requires no extra steps.

* fix: check claimable fills unconditionally in forceEnd()

forceEnd() delegated to _revertIfUnclaimedFills() which skips the
maxMint/maxWithdraw check when a pending request exists. An attacker
could submit a dust requestDeposit via the vault to create a pending
request, bypassing the guard and force-ending an order with real
claimable assets still present. Replace with direct unconditional checks.

* fix: use `lastWithdrawRequest` in ParetoFund instant-withdrawal guard to unblock apr0 (#169)

The instant-withdrawal guard in commit() compared withdrawsRequests before/after
requestWithdraw(), but on the apr0 path withdrawsRequests is never set — only
lastWithdrawRequest is. This caused commit() to revert with InstantWithdrawDetected
for all apr0 vaults. Switch the guard to lastWithdrawRequest, which is written on
both normal and apr0 paths but unchanged on the instant path.

Also hardens test coverage: apr0 lifecycle and transition tests pin the bug surface,
instant-withdraw test asserts clean rollback invariants, consecutive-redeem test
verifies the guard across multiple cycles, and fuzz accounting uses totalClaimable.

* fix: optimize ParetoFundStorage struct packing (#170)

Pack `internalState` and `hasResolvedAmounts` into the same storage
slot as `vault`, saving one full 32-byte slot and ~2,100 gas per
cold access in functions that read these fields together.

* fix: clear rawDebt at end of onMorphoFlashLoan [CS-GRUNTFUND-021] (#171)

* fix: clear rawDebt at end of onMorphoFlashLoan so isRepaid() is unambiguous

* test: restore repay BalanceExceedsDebt test using mock script during callback

* test: restore isRepaid BalanceExceedsDebt test using mock script during callback

* fix: mint enough tokens in isRepaid BalanceExceedsDebt test to actually hit _debt() guard

* fix: replace Solady `SafeTransferLib.balanceOf` with `IERC20.balanceOf` (#172)

* fix: replace Solady SafeTransferLib.balanceOf with IERC20.balanceOf

SafeTransferLib.balanceOf silently returns 0 on call failure, masking
bugs. IERC20.balanceOf properly reverts, matching the pattern already
used in CentrifugeFund and ParetoFund.

* fix: replace Solady SafeTransferLib.balanceOf with IERC20.balanceOf in MorphoFlashLoanRequest and Request

---------

Co-authored-by: Maxence Raballand <dev@maxencerb.com>

---------

Co-authored-by: maxencerb <dev@maxencerb.com>
@maximebrugel maximebrugel changed the title fix: Audit 02-2026 Audit 02-2026 Fixes + Fund Batch 01 Apr 1, 2026
@maximebrugel maximebrugel requested a review from maxencerb April 1, 2026 15:40
@maximebrugel maximebrugel merged commit 0106577 into main Apr 8, 2026
3 checks passed
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