Skip to content

feat: add MidasFund for Midas mToken integration#201

Open
maximebrugel wants to merge 5 commits into
funds-batch-2from
midas-fund
Open

feat: add MidasFund for Midas mToken integration#201
maximebrugel wants to merge 5 commits into
funds-batch-2from
midas-fund

Conversation

@maximebrugel

@maximebrugel maximebrugel commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds MidasFund, an IFund adapter for the Midas protocol (targeting mGLOBAL/mGLO, generic over any mToken), plus its beacon-proxy factory, integration interfaces, and a full test suite.

Design

  • Asymmetric settlement — deposits and redeems settle through different Midas surfaces:
    • Deposits: async via depositRequest (no holdback)commit() transfers the payment token to Midas and records the mint request id (exposed via activeRequestId() and the requestId param on OrderCommitted). The order stays PROCESSING until a Midas vault admin approves the request, which mints the full mToken amount at the approval NAV directly to the fund → UNLOCKING. A rejected request is not refunded on-chain: state() reports the order recoverable (RECOVERING) once Midas returns the payment token off-band (e.g. via withdrawToken).
    • Redeems: instant via redeemInstant with mandatory holdback — Midas settles ~93% on-chain (±7%-adjusted feeds) and returns the holdback off-band in the payment token. Every redeem stays PROCESSING after the instant settlement until an account with HOLDBACK_ROLE (or the owner) confirms receipt via confirmHoldback(orderId); unlock() then sweeps the fund's full output-token balance (instant settlement + holdback) to the receiver. confirmHoldback reverts (HoldbackNotPending) for deposit orders.
  • RolesOPERATOR_ROLE (resolve/recovering/referrer), DEPOSITOR_ROLE (Facility order lifecycle), HOLDBACK_ROLE (confirm redeem holdback payments; intended for an EOA distinct from the operator), VAULT_MANAGER_ROLE (set deposit/redemption vaults; also intended for a dedicated EOA).
  • Multiple concurrent settlements — one live order per fund (house invariant); deploy additional instances via MidasFundFactory, all sharing the same WrappedAsset (wmGLOBAL) as the share token. Note each order occupies its instance until the Midas admin approves the mint request (deposits) or the holdback is confirmed (redeems), so size the instance count accordingly.
  • Generic over mTokens — the mToken is derived from the deposit vault at initialize and validated against the redemption vault and wrapper underlying; all amounts are converted between the payment token's native decimals and the Midas base-18 API at the boundary.
  • Vault switching with safeguardssetDepositVault(address) / setRedemptionVault(address) (VAULT_MANAGER_ROLE or owner) enforce: no live order, same mToken, payment token registered, vault not paused (globally or for the commit-time function the fund calls), and (when the vault greenlist is enabled) both the fund and the wrapped share greenlisted. Supports routing between the Aave/Swapper redemption vaults or a dedicated instant-redemption vault.
  • Fn-pause preflightcreate(), commit() and the vault setters check Midas' per-function pause (fnPaused) for the two selectors the fund calls (depositRequest(address,uint256,bytes32), redeemInstant(address,uint256,uint256)) in addition to the global pause, so orders fail early instead of at settlement.
  • Recovery model — for off-band refunds (rejected deposit requests, or Midas returning the committed input after an incident), state() auto-reports RECOVERING on a CANCELED mint request once the refund covers the effective input; the operator can also flag recovering() manually, with resolve() / cancelRecovering() escape hatches (USCCFund model).
  • Create-time slippage guard (5% max deviation) priced against the direction-specific Midas data feeds (mint and redemption feeds are intentionally asymmetric, ±7%). For deposits this is a sanity bound only: the actual mint amount is set by the NAV rate at which the Midas admin approves the request.

Tests

  • 131 unit tests + 11 factory tests + 9 fuzz tests + 6 invariants (256 runs × depth 64) with full Midas mocks, including invariants that no redeem ever reaches UNLOCKING without a confirmed holdback and no deposit while its mint request is still PENDING (regardless of token balances)
  • 8 mainnet fork tests against the live mGLOBAL vaults (block 25,472,600): request-deposit approve lifecycle (real depositRequest + admin approveRequest, mint within 0.1% of the deposit-feed expectation), request rejection + off-band-refund recovery, instant redemption holdback lifecycles, off-band holdback payment swept at unlock, Swapper-vault redemption, greenlist gating, totalAssets
  • Full repo suite: 1,880 passed, 0 failed

Operational notes for launch

  • Both the fund instance and the wmGLOBAL WrappedAsset must hold M_GLOBAL_GREENLISTED_ROLE (mGLOBAL is mTokenPermissioned — every transfer checks both parties).
  • Deposit liveness depends on the Midas admin approving mint requests — there is no on-chain claim or deadline; until approval the instance stays occupied. Rejections are refunded off-band only (Midas withdrawToken to the fund contract address).
  • HOLDBACK_ROLE is a liveness dependency for every redeem: the Facility cannot unlock a redemption without a confirmHoldback transaction (owner is the fallback confirmer). Midas must deliver holdback payments to the fund contract address — that is what the unlock sweep captures.
  • As of the fork block, Midas has depositRequest fn-paused on mainnet; only instant redemption is live. Midas must unpause + greenlist before the fund can operate (the fund now pre-checks fnPaused for its own selectors at create/commit time).
  • First deposit per fund instance must mint ≥ 114,000 mGLOBAL (minMTokenAmountForFirstDeposit — applies to depositRequest too).
  • Redemption instant fee is currently 0.5% (per-vault config; Midas can zero it for our address via their waivedFeeRestriction fee waiver — worth requesting).

Wraps Midas issuance and redemption vaults behind the IFund state
machine. Settlement is configurable per direction (INSTANT via
depositInstant/redeemInstant, REQUEST via depositRequest/redeemRequest)
and changeable while no order is live; vault setters atomically update
the paired settlement mode with full safeguards (no live order, same
mToken, payment token registered, vault not paused, fund + wrapped
share greenlisted). Generic over any mToken: the token is derived from
the deposit vault at initialize and all amounts are converted between
native and Midas base-18 decimals at the boundary.

Rejected Midas requests are refunded off-band, handled via the
RECOVERING balance-threshold flow with operator resolve()/recovering()
escape hatches. Shares are WrappedAsset tokens; multiple fund instances
can share one wrapper to run concurrent settlements.

Includes beacon-proxy factory, Midas integration interfaces, unit +
factory + fuzz/invariant tests with full Midas mocks, and mainnet fork
tests against the live mGLOBAL vaults (instant/request deposit and
redemption on both the Aave and Swapper redemption vaults).
@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 79aac405-99f2-4787-978c-cb276d7ab888

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
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch midas-fund

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.

Comment thread src/funds/midas/MidasFund.sol
Comment thread src/funds/midas/MidasFund.sol
Comment thread src/funds/midas/MidasFund.sol
Comment thread src/funds/midas/MidasFund.sol
- remove REQUEST settlement mode entirely: commit() always settles
  synchronously via depositInstant/redeemInstant; SettlementMode,
  request-id tracking and setSettlementMode are gone
- add mandatory per-order holdback gate: every order (both directions)
  stays PROCESSING after the instant settlement until HOLDBACK_ROLE
  confirms the off-band payment via confirmHoldback() (deposits: the
  remaining mTokens airdropped after month-end NAV publication;
  redeems: the holdback returned in the payment token); unlock() then
  sweeps the fund's full output-token balance
- add VAULT_MANAGER_ROLE gating setDepositVault/setRedemptionVault,
  intended for an EOA distinct from the operator, with the existing
  safeguards (no live order, mToken match, payment token, pause and
  greenlist checks)
- trim the Midas integration interfaces to the instant-only surface
- rework the Midas test suite: 131 unit + 11 factory + 8 fuzz tests,
  5 invariants, 7 mainnet fork tests, all green
@maxencerb

maxencerb commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Solidity lines of code (production src/ only)

Correction to my earlier comment: those figures included tests. Restricted to production Solidity under src/, measured against the merge-base with the base branch funds-batch-2 (96a18e3):

Metric Value
src/ files changed 9 (8 new)
Raw diff (git) +1,106 / −0 lines
Solidity code lines (cloc, comments & blanks excluded) +510 added

Counted with cloc --diff <base>/src <head>/src --include-lang=Solidity.

- deposits no longer settle instantly: commit() calls depositRequest and
  records the mint request id (activeRequestId view, requestId in
  OrderCommitted); the order stays PROCESSING until the Midas admin
  approves the request, which mints the full amount at the approval NAV
  directly to the fund — no deposit holdback
- rejected requests are refunded off-band: state() auto-reports
  RECOVERING on a CANCELED mint request once the refund covers the
  effective input
- holdback gate is now redeem-only: create() presets holdbackPaid for
  deposit orders, so holdbackPending() is false and confirmHoldback()
  reverts HoldbackNotPending for deposits
- create()/commit()/setDepositVault/setRedemptionVault preflight
  fnPaused for the commit-time selectors (depositRequest/redeemInstant)
  in addition to the global pause
- restore MidasRequestStatus + depositRequest/mintRequests in the
  integration interfaces; mock deposit vault gets request storage with
  approve/reject helpers
- rework the Midas test suite: 131 unit + 11 factory + 9 fuzz tests,
  6 invariants, 8 mainnet fork tests (real depositRequest approve and
  reject lifecycles), all green
@maximebrugel maximebrugel changed the title feat: add MidasFund for Midas mToken integration (mGLOBAL) feat: add MidasFund for Midas mToken integration Jul 9, 2026
A committed redeem settles irreversibly on-chain via redeemInstant, so
recovering it would strand the instant payment-token settlement in the
fund (recover only returned the re-wrapped mToken). Remove the redeem
recovery path entirely: the only completion path for a committed redeem
is confirmHoldback() — confirming the payment or deliberately waiving
it — followed by unlock().

- recovering() now takes the Order struct (instead of the order id) so
  the mode can be checked, and reverts RecoverNotSupported for redeems
- recover() and the RECOVERING branch of _state() are asset-only now
- confirmHoldback() NatSpec documents the waive-as-write-off procedure
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