Skip to content

test(morpho-sdk): deploy VaultExitBundlesV1 onto a fork - #907

Closed
Foulks-Plb wants to merge 7 commits into
mainfrom
deploy-vault-exit-bundles-in-tests
Closed

test(morpho-sdk): deploy VaultExitBundlesV1 onto a fork#907
Foulks-Plb wants to merge 7 commits into
mainfrom
deploy-vault-exit-bundles-in-tests

Conversation

@Foulks-Plb

@Foulks-Plb Foulks-Plb commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

VaultExitBundlesV1 is not deployed on any chain yet. This puts it on an Anvil fork so vault-exit flows can be exercised now, ahead of the real deployment.

What lands

Two test-only files in morpho-sdk — the package that will grow the vault-exit actions:

file
test/fixtures/vaultExitBundlesV1.ts compiled artifact (ABI + creation bytecode) with a provenance header
test/vaultExitBundlesV1.test.ts the fork test; the deploy is a local, not a shared helper
const deployVaultExitBundlesV1 = async (client: AnvilTestClient) =>
  (
    await client.deployContractWait({
      abi,
      bytecode: code,
      args: [getChainAddress(client.chain.id, "blue")],
    })
  ).contractAddress;

deployContractWait is the generic deploy path already on AnvilTestClient: it deploys from the test account, waits for the receipt, and throws when no contract address comes back. It had zero call sites before this PR.

Nothing published changes

morpho-sdk ships only lib, and its build excludes **/*.test.ts. So there is no changeset — root §7 excludes tests-only and fixture-only changes — and no vitest.config.ts change either: the morpho-sdk project already globs test/** at a 120s timeout.

What the test proves

On the mainnet fork the package already pins (block 24,593,903):

The contract is deployed. It has code, and BLUE() returns the live Morpho Blue address — which only holds if the runtime actually executes and the constructor immutable was set. It cannot pass against an empty or half-deployed account.

The right functions are on it. All six ABI selectors are asserted present in the deployed bytecode, and the signature list itself is pinned, so an artifact that drifts from the contract it claims to be fails the test. The selectors sit in the dispatcher jump table (8063<selector>14), not as incidental byte matches:

function selector
BLUE() 0xc011ff64
onMorphoFlashLoan(uint256,bytes) 0x31f57072
onMorphoSupply(uint256,bytes) 0x2075be03
vaultExitBundlesV1ForceWithdrawVaultV2(…) 0x902118db
vaultExitBundlesV1InKindRedemptionVaultV1(…) 0xac581a83
vaultExitBundlesV1InKindRedemptionVaultV2(…) 0x3debf2f0

The artifact

The contract pulls in 37 Solidity files across six foundry submodules (morpho-blue, metamorpho, vault-v2, midnight, permit2, openzeppelin). Vendoring those to compile in-repo via scripts/compile-solidity.js would mean checking ~150 KB of upstream contracts into this repo, so the artifact is compiled out-of-band and committed with its provenance in the file header:

  • morpho-org/bundles @ d5dc4c6ae805231cc37a48ee85dcb460d690bb96
  • solc 0.8.34+commit.80d5c536, viaIR, optimizer on (4294967295 runs), metadata.bytecodeHash: "none" — the upstream foundry profile
  • evmVersion: "cancun" instead of the upstream "osaka", because the fork blocks pinned across this repo predate Osaka. Both targets emit byte-identical output for this contract, so this is a compatibility guarantee, not a divergence.

Regeneration steps are in the header, along with a note to delete the fixture once the contract ships on a live chain.

Verification

✓ VaultExitBundlesV1 > default
✓ VaultExitBundlesV1 > behavior: the deployed code exposes every declared function

Observed on the fork: 12,623 runtime bytes deployed (matching the compiled runtime exactly), 2,763,201 gas, BLUE() == 0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb.

Also green: pnpm lint (biome + JSDoc coverage + checksum-address), tsc for morpho-sdk, and 741 tests across morpho-test / morpho-ts / blue-sdk.

🤖 Generated with Claude Code


Open in Devin Review

Let tests exercise VaultExitBundlesV1 before it is deployed on any live
chain.

`deployVaultExitBundlesV1` deploys the contract onto an Anvil fork and
returns its address. It goes through the canonical CREATE2 deterministic
deployment proxy rather than plain CREATE, so the address is a pure
function of the bound Morpho Blue address and the salt instead of the
deployer's nonce. That matters because `registerCustomAddresses` is
additive-only and throws on a conflicting re-registration: a
nonce-dependent address would break any test file that deploys twice.
`getVaultExitBundlesV1Address` exposes the same address without spending
a transaction, and the deploy is idempotent.

`ChainAddresses` gains the optional `vaultExitBundles` key so the
deployed address can be registered and resolved through
`getChainAddress`. No chain declares it yet.

The artifact is compiled from morpho-org/bundles at a pinned commit with
the upstream foundry settings, retargeted from the "osaka" EVM to
"cancun" so the fork blocks pinned across this repo can execute it; both
targets emit byte-identical output for this contract.

Co-Authored-By: Claude <noreply@anthropic.com>
@Foulks-Plb
Foulks-Plb requested a review from a team as a code owner July 26, 2026 17:25

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review for a one-time review, or @claude review always to subscribe this PR to a review on every future push.

Tip: disable this comment in your organization's Code Review settings.

@devin-ai-integration devin-ai-integration 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.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Open in Devin Review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0a66e39dfd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .changeset/vault-exit-bundles-fork-deployment.md Outdated
Comment thread packages/morpho-test/src/contracts/deployVaultExitBundlesV1.ts Outdated
Comment thread packages/morpho-test/src/contracts/deployVaultExitBundlesV1.ts Outdated
Foulks-Plb and others added 5 commits July 26, 2026 20:29
Drop the `salt` option. Nothing needed a non-zero salt: `blue` already
varies the CREATE2 address, and a configurable salt only widened the
surface that has to be documented and kept in sync with the computed
address.

With `salt` gone the options bag holds a single optional field, so take
`blue` positionally instead. That removes the exported
`VaultExitBundlesV1Parameters` interface and, with it, the
`VaultExitBundlesV1Parameters & { readonly blue: Address }` intersection
that `getVaultExitBundlesV1Address` needed to re-require the field the
interface had made optional. The shape now matches the existing
`deployVaultV2(client, loanToken)` deploy helper.

Co-Authored-By: Claude <noreply@anthropic.com>
Registering the deployed address is the point of `deployVaultExitBundlesV1`,
but morpho-test's `@morpho-org/morpho-ts` peer floor still admitted 2.7 and
2.8, where `ChainAddresses` and `AddressLabel` have no `vaultExitBundles`
key. A consumer on that valid graph would find the documented
`registerCustomAddresses` and `getChainAddress` calls fail to type-check.
Raise the floor to the `^2.9.0` this changeset produces, matching how
blue-sdk-viem's blue-sdk floor was raised ahead of the release in 6c14469.
morpho-test is only ever a dev dependency, so the narrowing cascades nowhere.

Also document that the helper requires automine. `sendTransaction` in
`@morpho-org/test` only awaits the receipt when `getAutomine()` is true, so
under `noMining: true` the deploy stays pending and the code check throws.
Nothing in the repo disables automine, and mining a block on the caller's
behalf would be a surprising side effect in a helper that was only asked to
deploy — so state the precondition and name it in the error message rather
than paper over it.

Co-Authored-By: Claude <noreply@anthropic.com>
Nothing read the key. No action, entity or fetcher resolved it, and no
chain declared it, so the only consumer was the test that registered it
and read it back — which exercises `registerCustomAddresses` and
`getChainAddress`, already covered by morpho-ts's own suite, rather than
the deploy helper. The deployment is proven by the assertions that
remain: code at the address, and `BLUE()` returning the live Morpho Blue
address.

That slot was the sole reason this PR reached beyond morpho-test: it
forced a morpho-ts minor, a morpho-sdk patch, and the peer floor
narrowing Codex flagged. All three are gone; the changeset is now a
single morpho-test minor, and the diff touches one package.

`deployVaultExitBundlesV1` already returns the address, so a test uses it
directly. When an action eventually needs registry resolution, adding an
optional field stays a non-breaking minor, made at the point it has a
real consumer.

CREATE2 stays: the address must still be stable across runs and knowable
before the contract exists, which is what setup needs when something has
to reference it up front, such as a Vault V2 receiveAssetsGate that must
permit the bundler. The rationale in AGENTS.md and the JSDoc no longer
leans on the registry argument.

Co-Authored-By: Claude <noreply@anthropic.com>
Replace the CREATE2 machinery with `client.deployContractWait`, the
generic deploy path already on `AnvilTestClient` — it deploys from the
test account, waits for the receipt and throws when no contract address
comes back. It had zero call sites in the repo until now.

The helper is a three-line binding of the constructor argument, so this
drops `DETERMINISTIC_DEPLOYER_ADDRESS`, `getVaultExitBundlesV1Address`,
both error classes, the idempotent early return and the automine
precondition — 145 lines down to 44, and two public symbols instead of
six. The automine note goes because `deployContractWait` waits for the
receipt unconditionally rather than only under automine.

What is given up is determinism: the address now follows the test
account's nonce, so it cannot be known before deploying and changes
between calls. Tests read it from the return value. The new
"each call deploys a distinct instance" case pins that behaviour.

Co-Authored-By: Claude <noreply@anthropic.com>
…o tests

Drop the published helper entirely. `morpho-test` goes back to what it was
on main: no `src/contracts`, no new exports, no barrel change. The deploy
is now a six-line local in the test that needs it, and the compiled
artifact sits in `test/fixtures` next to the other morpho-sdk fixtures —
the package that will grow the vault-exit actions.

Nothing published changes any more: morpho-sdk ships only `lib`, and its
build excludes `**/*.test.ts`. So the changeset goes too, per root §7,
which excludes tests-only and fixture-only changes. The `vitest.config.ts`
edit goes as well — the morpho-sdk project already globs `test/**` and
already runs at a 120s timeout.

Two tests, on the mainnet fork the package already pins:
- the contract is deployed — it has code, and `BLUE()` returns the live
  Morpho Blue address, which only holds if the runtime executes and the
  constructor immutable was set;
- the deployed dispatcher routes every function the ABI declares. Each of
  the six selectors is asserted present in the runtime code, and the
  signature list is pinned, so the artifact drifting from the contract it
  claims to be fails the test.

Co-Authored-By: Claude <noreply@anthropic.com>
@Foulks-Plb Foulks-Plb changed the title feat(morpho-test): deploy VaultExitBundlesV1 onto forks in tests test(morpho-sdk): deploy VaultExitBundlesV1 onto a fork Jul 27, 2026
@Foulks-Plb Foulks-Plb self-assigned this Jul 27, 2026
0xbulma
0xbulma previously approved these changes Jul 27, 2026
Foulks-Plb added a commit that referenced this pull request Jul 28, 2026
…demption TIB

Integrate review feedback grounded in the vvrm-app consumer and the
VaultExitBundlesV1 contract:

- Frame in-kind redemption as a complementary illiquid-vault exit path
  alongside the existing liquid withdraw / force-deallocate flows, with
  vvrm-app named as the first consumer.
- Add a first-class adapter-type check: the sole adapter must be a
  MorphoMarketV1AdapterV2 (reject legacy MorphoMarketV1Adapter /
  MorphoVaultV1Adapter) via UnsupportedInKindAdapterError.
- Sequence the work to parallelise on PR #907's fork so the integration
  ships the moment the contract deploys; note the post-audit HEAD drift.
- Resolve permit-vs-approve on supportSignature only, no getCode probe
  (would misclassify EIP-7702 EOAs).
- Acknowledge penalty/adapter-position snapshot drift as an unclosable,
  undefended SDK-side blind spot.
- Resolve and remove the Open Questions section: deploy to every
  Morpho-supported chain; fork address wiring reuses PR #907;
  supportSignature-driven permit path; timelocked markets are not
  considered by design.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ress registry

Add a `bundles` group to `ChainAddresses` with `vaultExitBundlesV1`, so the
label resolves through `getChainAddress`/`registerCustomAddresses`. The fork
test registers the deployed address at runtime under Ethereum mainnet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@linear-code

linear-code Bot commented Aug 10, 2026

Copy link
Copy Markdown

VRM-5350

Rubilmax added a commit that referenced this pull request Aug 13, 2026
* docs: add TIB for VaultExitBundlesV1 in-kind redemption

Design record for integrating the two in-kind redemption entry points of
morpho-org/bundles' VaultExitBundlesV1 into morpho-sdk, letting a depositor
exit an illiquid vault into Morpho Blue supply positions without curator
cooperation.

Co-Authored-By: Claude <noreply@anthropic.com>

* docs(tib): fold vvrm context and resolve open questions in in-kind redemption TIB

Integrate review feedback grounded in the vvrm-app consumer and the
VaultExitBundlesV1 contract:

- Frame in-kind redemption as a complementary illiquid-vault exit path
  alongside the existing liquid withdraw / force-deallocate flows, with
  vvrm-app named as the first consumer.
- Add a first-class adapter-type check: the sole adapter must be a
  MorphoMarketV1AdapterV2 (reject legacy MorphoMarketV1Adapter /
  MorphoVaultV1Adapter) via UnsupportedInKindAdapterError.
- Sequence the work to parallelise on PR #907's fork so the integration
  ships the moment the contract deploys; note the post-audit HEAD drift.
- Resolve permit-vs-approve on supportSignature only, no getCode probe
  (would misclassify EIP-7702 EOAs).
- Acknowledge penalty/adapter-position snapshot drift as an unclosable,
  undefended SDK-side blind spot.
- Resolve and remove the Open Questions section: deploy to every
  Morpho-supported chain; fork address wiring reuses PR #907;
  supportSignature-driven permit path; timelocked markets are not
  considered by design.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(tib): integrate Codex review feedback on in-kind redemption TIB

Addresses all 7 Codex findings on PR #909:

- allowlist `vaultExitBundlesV1` in `encodeErc20Approval` too, so the default
  `supportSignature: false` approve path (and smart-contract wallets) does not
  throw `UnsupportedErc20ApprovalSpenderError`
- bind `vaultData` to `this.vault` (`VaultAddressMismatchError`) as the first
  synchronous matrix row, since every row trusts the snapshot
- bind the supplied permit to this user and kind in `buildTx`
  (`action.type === "permit"`, `args.owner === userAddress`, spender), not just
  asset/amount/deadline
- state the Vault V2 output is net of the force-deallocation penalty
  (`floor(exitAssets·WAD/(WAD+penalty))`), not the full `exitAssets`
- clarify `buildTx` is synchronous and RPC-free; only `getRequirements()` is
  async, per the SDK layering rule
- scope out share-sufficiency validation by decision (V1 + V2): a max allowance
  settles authorization, not balance; sizing `amount` against the user's shares
  is the caller's job. Documented as a Non-Goal and a Security residual; the
  `deleteShares` balance row is removed from the async matrix

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(tib): integrate second Codex review pass on in-kind redemption TIB

Addresses the actionable findings from PR #909 review 4798450831:

- Fork test must exercise the supportSignature:true V2 permit path
  end-to-end so getVaultV2PermitTypedData's two-field EIP-712 domain is
  verified against the live VaultV2.DOMAIN_SEPARATOR() via ecrecover;
  approve-based exits and self-consistent unit round-trips cannot prove it.
- V1 coverage must NOT deduplicate: unlike V2 (drained mid-loop via
  forceDeallocate+withdraw), V1 reads the undrained vault position every
  iteration, so duplicates legitimately re-contribute; V1 sums the raw
  ordered list. Dedup stays V2-only.
- Add ChainIdMismatchError as the first synchronous matrix row, matching
  the guard every existing Vault V1/V2 entity method applies.
- Fix permit binding: spender lives on action.args.spender (PermitArgs has
  no spender field); checking args.spender would reject every valid
  ERC-2612 signature and break the supportSignature:true path.
- Unify action naming: vaultV{1,2}InKindRedemption are the frozen SDK
  names; the long vaultExitBundlesV1InKindRedemptionVaultV{1,2} are the
  contract entry points they encode.
- Share-sufficiency guidance in asset terms: amount <= previewRedeem(shares),
  not the dimensionally-wrong amount <= sharesHeld; note V2 penalty-leg shares.
- Do not surface the net-of-penalty V2 figure in the action args/output;
  it stays caller-derived math (deferred preview helper), resolving the
  contradiction with the entity handle's {buildTx, getRequirements} surface.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
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