@anywalletkit/protocol-lending-morpho-evm
Morpho for @anywalletkit/wallet-evm accounts, two
LendingProtocols in one package:
Install
npm install @anywalletkit/protocol-lending-morpho-evm @anywalletkit/core
# or: pnpm add @anywalletkit/protocol-lending-morpho-evm @anywalletkit/core · yarn add @anywalletkit/protocol-lending-morpho-evm @anywalletkit/core
On npm: @anywalletkit/protocol-lending-morpho-evm, published from the release workflow with provenance.
MorphoVaultProtocolEvm— a Morpho Vault (MetaMorpho / Vault V1, or Vault V2):supplydeposits the vault's asset for shares,withdrawredeems them. The simplest Earn product;borrow/repayreject.MorphoMarketProtocolEvm— one Morpho Blue market:supply/withdrawlend the loan token,supplyCollateral/withdrawCollateralpost and take collateral,borrow/repaytake and return the loan token against it.
Written from the protocol's own interfaces (morpho-blue, metamorpho, vault-v2)
with the same shares and interest maths as Morpho Blue's SharesMathLib /
MathLib / Morpho._accrueInterest; one dependency (viem). The Morpho GraphQL API is used
only for discovery and decoration (listing, APY, TVL); every money path reads
the chain.
Chains
Ethereum, Base, Arbitrum, OP Mainnet, Polygon, Unichain, HyperEVM, Katana,
Monad, Robinhood Chain (MORPHO_CHAINS). The Morpho (Blue) addresses come from
docs.morpho.org; the vault factories are the ones behind every vault the Morpho
API lists (the docs carry no factory table). Other chains take morpho in the
config.
Usage
import WalletManagerEvm from '@anywalletkit/wallet-evm'
import { MorphoVaultProtocolEvm, MorphoMarketProtocolEvm, MorphoApi } from '@anywalletkit/protocol-lending-morpho-evm'
const account = await new WalletManagerEvm(seed, { provider: 'https://base-rpc…' }).getAccount(0)
// Discovery (API): the listed USDC vaults on Base, biggest first
const api = new MorphoApi()
const [best] = await api.listVaults({ chainId: 8453, asset: USDC })
// Vault
const vault = new MorphoVaultProtocolEvm(account, { vault: best.address })
await vault.getVault() // verified against Morpho's factories; asset, fees, timelock…
await vault.getApy() // 4.1 (net, percent, from the API)
await vault.supply({ amount: 5_000_000n }) // exact approve → deposit(assets, me)
await vault.getPosition() // { shares, assets, withdrawable }
await vault.withdraw({ amount: 'max' }) // redeem(every share) — refused up front if the vault lacks liquidity
// Market (Blue)
const market = new MorphoMarketProtocolEvm(account, { market: '0x9103…1836' }) // id or MarketParams
const m = await market.getMarket() // loan/collateral tokens, LLTV, price, state accrued to now, APYs, liquidity
await market.supplyCollateral({ amount: 10n ** 7n }) // 0.1 cbBTC
await market.borrow({ amount: 3_000_000_000n }) // 3 000 USDC — refused if above LLTV or market liquidity
await market.getPosition() // debt (rounded up), collateral value, health factor, liquidation price
await market.repay({ amount: 'max' }) // by shares, no dust; allowance = debt + 0.1 %
await market.withdrawCollateral({ amount: 'max' })
Safety before money moves
-
Vault identity.
getVault()asks the chain's Morpho vault factories (isMetaMorpho/isVaultV2) whether they created the address, and a V1 vault'sMORPHO()must be the chain's singleton. An unknown address is refused unlessallowUnverifiedVault: true. -
Vault limits. V1: deposits are checked against
maxDeposit(caps), withdrawals againstmaxWithdraw/maxRedeem(vault liquidity) — the error says how much is possible right now. V2 answers 0 from every max* view, so its transfer gates are read (canReceiveShares/canSendAssetsin,canSendShares/canReceiveAssetsout) and caps and liquidity are left to theeth_callproof that runs before every send. -
Dead deposit. Before a vault deposit or a market supply, the target must hold a dead deposit at
0x…dEaD, the rule Morpho's own SDK applies (morpho-sdk 9.2.0): at leastMIN_DEAD_SHARES(1e9) shares (Blue supply shares for a market, vault shares for a vault); a Vault V2's must also be worthMIN_VAULT_V2_DEAD_ASSETS(1e6 base units), with its share price above 1 / (10 × virtualShares). Without one, a nearly empty target's share price can be inflated against the next depositor. A target that fails is refused withMorphoDeadDepositError(code: 'InsufficientDeadDeposit',observed,required) unless the config passesallowWithoutDeadDeposit: true.getDeadDeposit()reads it without supplying. Withdrawals, collateral, borrows and repayments are never gated by it.Only the target itself is checked. Morpho's SDK also walks the markets a vault can allocate to (every withdraw-queue market of a Vault V1, every capped allocation and the liquidity market of a Vault V2). This module does not, for two reasons. First, the target check guards the deposit itself: the depositor's assets rounding down in the target's own share math. A market the vault allocates to affects the vault's assets, which the curator chooses, caps bound and every holder shares; that is curation risk, which this module does not try to judge. Second, the walk refuses most listed vaults: on 2026-11-04 it would have refused 45 of 69 listed Vault V1s, mostly because of the idle markets (no collateral) curators keep in their queues.
-
Listing vs depositing.
MorphoApi.listVaultsreturns what the API lists, Vault V1 and V2 alike. The API has no dead-deposit field, so a listed vault that fails the rule stays in the list, just like a vault whose caps are full or whose V2 gates refuse the account. Depositability is decided on-chain by the vault protocol:getDeadDeposit()shows the rule, andquoteSupply/supplyrefuse a failing dead deposit (MorphoDeadDepositError), full V1 caps (ExceededMaxDeposit) and V2 gates (CannotReceiveShares,CannotSendAssets) with a typed error before anything is approved. CallquoteSupply(orgetDeadDeposit) before offering a deposit. -
Market identity. An id is resolved with
idToMarketParams, and the RPC's answer must hash back to the id or it is refused; params given in config are hashed to the id and the market must exist (lastUpdate ≠ 0). The golden test derives the live Base cbBTC/USDC id from its params. -
Market state.
market()is accrued to now exactly asaccrueInterestwould (Taylor-compounded borrow rate from the IRM, fee shares), so supplied / borrowed assets, health and liquidity are current, not last-touch. -
Health. Borrows and collateral withdrawals are checked against
collateral × price / 1e36 × LLTV; liquidity against supply − borrow.onBehalfOfborrows needisAuthorized. -
Proof. Every main transaction is
eth_called from the account; Morpho's revert strings (insufficient liquidity,insufficient collateral, …) and the vaults' custom errors (AllCapsReached,ERC4626ExceededMaxWithdraw, V2CannotReceiveShares, …) become readableMorphoErrors. -
Approvals are exact (never unlimited), with the
approve(0)reset Ethereum's USDT demands. Morpho has no native-coin path: wrap first.
Reads
| returns | |
|---|---|
vault.getVault() | version, factory, name, asset, totalAssets, share price, fees, timelock, curator |
vault.getPosition() | shares, assets, withdrawable now |
vault.getApy(), vault.getStats() | net APY %, API record |
vault.getDeadDeposit(), market.getDeadDeposit() | shares (and V2 assets) at 0x…dEaD, whether the dead-deposit rule passes and what failed |
market.getMarket() | params, tokens, LLTV, oracle price (1e36), accrued state, liquidity, utilization, borrow/supply APY |
market.getPosition(user?) | supply/borrow shares and assets, collateral, collateral value, max borrow, health factor, LTV, liquidation price |
MorphoApi.listVaults / listMarkets / vault / marketById | discovery, biggest first, listed: true (V1 and V2; listed is not depositable, see above) |
Tests
pnpm test — 27 offline tests over an in-memory Morpho on Base (Blue
singleton with the real cbBTC/USDC market params, IRM, oracle, a MetaMorpho
vault behind the v1.0 factory, a look-alike vault, Multicall3, reverts) plus
the conformance suite for both protocols. pnpm verify:live reads Base
through a public RPC and the API.
Status
2026-10-10: built, offline tests green, live read-only verification on Base. No funded transaction through the module yet (plan: 5 USDC into a Base vault, withdraw).
Activity
describeActivity(tx, { own }) labels Morpho transactions for a wallet's history, offline and synchronously (shared by both classes through MorphoBase):
- Blue is recognized by address: the called contract is the Morpho singleton of the transaction's chain (
MORPHO_CHAINS, or themorphothis instance was configured with) and the selector issupply/withdraw/borrow/repay/supplyCollateral/withdrawCollateral(→supply/withdraw/borrow/repay) orsetAuthorization(→contract). The principal asset isMarketParams.loanTokenfrom calldata when present. - Vaults (MetaMorpho / Vault V1, Vault V2) cannot be listed offline — they come from factories and the
isMetaMorpho/isVaultV2proof needs a node — so the rule is: the selector is an ERC-4626 entry point (deposit/mint→supply,withdraw/redeem→withdraw) and the called contract is itself the share token the receipt shows minted to (deposit) or burned from (withdraw) one of the wallet's addresses, on a chain with a Morpho deployment. The vault aMorphoVaultProtocolEvminstance is configured with is accepted by address alone. Caveat: any ERC-4626 vault on a Morpho chain satisfies the generic rule; the kit asks modules in registry order, so a module that knows a specific vault should be registered before this one. - Title
"<Verb> <asset>"("Deposited USDC", "Withdrew USDC"; the share token is never the principal), subtitle"Morpho · <chain>"/"Morpho Vault · <chain>",counterpartythe contract called,protocolthe package name;assetsIn/assetsOutare the wallet's net movements (shares included). - Null otherwise.
Fixtures in fixtures/activity/ are public Base transactions (a Moonwell Flagship USDC vault deposit / redeem by one account, a Blue supply / withdraw by another; the test wallet has not run Morpho yet), negatives/ an Aave gateway deposit and a plain USDC transfer.