ethereum-optimism / ethereum-optimism/actions
Morpho: support native ETH deposits and withdrawals via Bundler3 (parity with Aave WETHGateway)
- 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
Assessment
This issue has not been assessed yet.