ethereum-optimism / ethereum-optimism/actions

Morpho: support native ETH deposits and withdrawals via Bundler3 (parity with Aave WETHGateway)

Open
#449 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
32
Forks
25
Avg merge
10h 20m
Merged PRs (30d)
16

Description

The SDK can deposit and withdraw native ETH on Aave (via `WETHGateway`) but not on Morpho. Both Morpho paths (`_openPosition` and `_closePosition`) call `getAssetAddress(asset, ...)` ([MorphoLendProvider.ts:51-55](packages/sdk/src/actions/lend/providers/morpho/MorphoLendProvider.ts#L51-L55), [:101-105](packages/sdk/src/actions/lend/providers/morpho/MorphoLendProvider.ts#L101-L105)) which throws on native assets — fail before any deposit/withdraw logic.

Morpho doesn't ship a single-purpose ETH gateway. Native flows go through their generic multicall, [Bundler3](https://github.com/morpho-org/bundler3) (`0x6566194141eefa99af43bb5aa71460ca2dc90245`):
- **Deposit:** `wrapNative` → `erc4626Deposit`
- **Withdraw:** `erc4626Withdraw` (or `erc4626Redeem`) → `unwrapNative`

Aave already has both sides ([`_buildETHOpenPosition`](packages/sdk/src/actions/lend/providers/aave/AaveLendProvider.ts#L200-L237) + `_closeETHPosition`); Morpho needs parity on both.

## Demo context

The demo's only ETH market is `AaveETH` on Optimism Sepolia ([`packages/demo/backend/src/config/markets.ts:28-34`](packages/demo/backend/src/config/markets.ts#L28-L34)). Both Morpho markets in the demo are USDC. So we haven't tripped this scenario yet — but the moment we wire a Morpho ETH/WETH market in, both `openPosition` and `closePosition` break for native users.

## Two ways to fix

Both add a native branch to `_openPosition` and `_closePosition`. The base `LendProvider` already infers native vs ERC-20 from `params.asset`, so the provider just returns the bundled transaction with `value` set; the base skips approval construction automatically.

**Option A — import `@morpho-org/bundler-sdk-viem`:**

```ts
import { populateBundle, encodeBundle } from '@morpho-org/bundler-sdk-viem'

// open
const { operations: openOps } = populateBundle(
[{ type: 'MetaMorpho_Deposit', address: vault, args: { assets: amountWei, owner: walletAddress } }],
simState,
)
return { assetAddress: wethAddress, transaction: encodeBundle(openOps, simState).tx(), apy }

// close
const { operations: closeOps } = populateBundle(
[{ type: 'MetaMorpho_Withdraw', address: vault, args: { assets: amountWei, receiver: walletAddress, owner: walletAddress } }],
simState,
{ unwrapTokens: new Set([wethAddress]) }, // tells the SDK to append unwrapNative
)
return { ...closeFields, transactionData: { position: encodeBundle(closeOps, simState).tx() } }
```

The SDK detects native asset, prepends `wrapNative` on open, appends `unwrapNative` on close. Both atomic.

**Option B — encode bundles ourselves with viem + Bundler3 ABIs:**

```ts
import { encodeFunctionData } from 'viem'
import { BUNDLER3_ABI, GENERAL_ADAPTER_ABI } from './abis.js'

// open: wrap → deposit
const open = encodeFunctionData({
abi: BUNDLER3_ABI,
functionName: 'multicall',
args: [[
{ to: adapter, data: enc(GENERAL_ADAPTER_ABI, 'wrapNative', [amountWei, bundler3]),
value: amountWei, skipRevert: false, callbackHash: '0x' },
{ to: adapter, data: enc(GENERAL_ADAPTER_ABI, 'erc4626Deposit', [vault, amountWei, 0n, walletAddress]),
value: 0n, skipRevert: false, callbackHash: '0x' },
]],
})
return { assetAddress: wethAddress, transaction: { to: bundler3, data: open, value: amountWei }, apy }

// close: withdraw → unwrap (mirror shape, swap actions)
```

We'd own the ABIs (`BUNDLER3_ABI`, `GENERAL_ADAPTER_ABI`), a per-chain registry for Bundler3 + adapters, and the action-tuple shape (which has shifted across Bundler3 minor versions).

## Dependency footprint

Existing Morpho packages we already pull in (npm unpacked):

| Package | Latest | Unpacked |
|---|---|---|
| `@morpho-org/blue-sdk` | 5.23.1 | 906 KB |
| `@morpho-org/blue-sdk-viem` | 4.6.4 | 1,783 KB |
| `@morpho-org/morpho-ts` | 2.5.1 | 145 KB |
| **Total existing** | | **~2.83 MB** |

Adding Option A would bring:

| Package | Latest | Unpacked |
|---|---|---|
| `@morpho-org/bundler-sdk-viem` | 4.3.3 | 728 KB |
| `@morpho-org/simulation-sdk` (transitive) | 3.4.2 | 277 KB |
| **Net add** | | **~1.0 MB** |

Sizes are install/tarball, not minified+gzipped runtime bundle. Tree-shaking would reduce the actual shipped surface, but install footprint is the metric for SDK consumers' `node_modules`. CONTRIBUTING.md §95 says don't import upstream SDKs by reflex; weigh the ~1 MB and one extra transitive against the ongoing maintenance of hand-encoded bundle actions on both sides.

## Test plan

- Unit (open): native asset → returns `transaction.value === amountWei`, decoded calldata is wrap-then-deposit; base correctly skips approval (no `transactionData.approval`).
- Unit (close): native asset → withdraw-then-unwrap, recipient receives ETH (not WETH).
- Unit: ERC-20 path on both sides unchanged.
- Fork test: deposit 0.01 ETH into a real WETH MetaMorpho vault, then withdraw, verify ETH balance round-trips at the user.

## Out of scope

- A `LendProvider`-level "supported asset types" declaration for friendlier rejection when a provider doesn't support a given asset class — orthogonal.

## See also

- #428 — base-owned lend approval construction (this builds on it)
- Aave reference: `_buildETHOpenPosition` + `_closeETHPosition` ([AaveLendProvider.ts:200-237](packages/sdk/src/actions/lend/providers/aave/AaveLendProvider.ts#L200-L237))
- [Bundler3 docs](https://docs.morpho.org/get-started/resources/contracts/bundlers/) · [bundler3 repo](https://github.com/morpho-org/bundler3) · [@morpho-org/bundler-sdk-viem](https://github.com/morpho-org/sdks/tree/main/packages/bundler-sdk-viem)

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.