Morpho Blue / MetaMorpho vault APY computation, deposit/withdrawal impact simulation, and max-amount binary search.
# From npm
pnpm add origin-morpho-utils
# From GitHub
pnpm add github:OriginProtocol/morpho-utilsNo viem imports needed — just pass an RPC URL:
import {
fetchVaultApy,
computeDepositImpactRpc,
computeWithdrawalImpactRpc,
findMaxDepositRpc,
} from 'origin-morpho-utils'
const RPC_URL = 'https://eth.llamarpc.com'
const vault = '0x5b8b...'
// Current APY
const result = await fetchVaultApy(RPC_URL, 1, vault)
console.log(result.apy) // e.g. 0.0423 (4.23%)
// With per-market breakdown
const detailed = await fetchVaultApy(RPC_URL, 1, vault, { includeMarkets: true })
detailed.marketDetails.forEach(m => {
console.log(m.marketId, m.supplyApy, m.utilization, m.allocationPct)
})
// Deposit impact
const deposit = await computeDepositImpactRpc(RPC_URL, 1, vault, 1000000000000000000n)
console.log(deposit.impactBps) // e.g. -12 (APY drops 0.12%)
// With per-market details
const depositDetailed = await computeDepositImpactRpc(RPC_URL, 1, vault, 1000000000000000000n, { includeMarkets: true })
depositDetailed.markets.forEach(m => {
console.log(m.marketId, m.current.supplyApy, '→', m.simulated.supplyApy)
})
// Withdrawal impact
const withdrawal = await computeWithdrawalImpactRpc(RPC_URL, 1, vault, 1000000000000000000n)
console.log(withdrawal.impactBps)
console.log(withdrawal.isPartial) // true if insufficient liquidity
// Find max deposit within 50 bps APY impact
const maxDeposit = await findMaxDepositRpc(RPC_URL, 1, vault, 100000n * 10n**18n, 50)
console.log(maxDeposit.amount) // largest deposit within threshold
console.log(maxDeposit.isCapped) // true if limited by vault capacityIf you already have a viem client:
import { createPublicClient, http } from 'viem'
import { mainnet } from 'viem/chains'
import { computeDepositImpact, fetchVaultApyViem } from 'origin-morpho-utils'
const client = createPublicClient({ chain: mainnet, transport: http(RPC_URL) })
const result = await fetchVaultApyViem(client, vault, '0xBBBB...EFFCb')For non-TypeScript projects using require():
const { fetchVaultApy, computeDepositImpactRpc } = require('origin-morpho-utils')
const result = await fetchVaultApy('https://eth.llamarpc.com', 1, '0x5b8b...')
console.log(result.apy)For offline computation or validation without viem:
import { estimateMarketApy, weightedVaultApyDetailed, simulateDeposit } from 'origin-morpho-utils/math'
const { supplyApy, borrowApy } = estimateMarketApy(0, 1000, 800, 0, 3_170_979_198)# Current APY
npx origin-morpho-utils apy \
--rpc-url https://eth.llamarpc.com --chain-id 1 --vault 0x5b8b...
# APY with per-market breakdown
npx origin-morpho-utils apy \
--rpc-url https://eth.llamarpc.com --chain-id 1 --vault 0x5b8b... --markets
# Deposit impact
npx origin-morpho-utils deposit-impact \
--rpc-url https://eth.llamarpc.com --chain-id 1 --vault 0x5b8b... \
--amount 1000000000000000000
# Find max deposit within 50 bps impact
npx origin-morpho-utils find-max-deposit \
--rpc-url https://eth.llamarpc.com --chain-id 1 --vault 0x5b8b... \
--amount 100000000000000000000 --max-impact 50
# Offline compute from JSON stdin
echo '{"markets": [...], "idleAssets": "0"}' | \
npx origin-morpho-utils compute --deposit 1000000CLI outputs JSON to stdout. BigInt fields are string-encoded. Use --markets on any command to include per-market details.
All RPC and viem functions accept an options object as the last parameter:
| Option | Type | Default | Description |
|---|---|---|---|
morphoAddress |
string | Auto-detected from chainId | Morpho Blue singleton address |
includeMarkets |
boolean | false |
Include per-market breakdown in results |
precision |
bigint | 1 whole token | Search granularity for find-max functions |
constraint |
(state) => boolean | Promise<boolean> |
— | Custom constraint for find-max functions (see below) |
fetchVaultApy(rpcUrl, chainId, vaultAddress, options?)
Returns VaultApyResult | null — { apy, markets, idleAssets, marketDetails }.
fetchVaultMarkets(rpcUrl, chainId, vaultAddress, morphoAddress)
Returns { markets, idleAssets } — raw market data for offline use.
computeDepositImpactRpc(rpcUrl, chainId, vaultAddress, depositAmount, options?)
Returns { currentApy, newApy, impact, impactBps, markets }.
computeWithdrawalImpactRpc(rpcUrl, chainId, vaultAddress, withdrawAmount, options?)
Returns { currentApy, newApy, impact, impactBps, isPartial, withdrawableAmount, markets }.
findMaxDepositRpc(rpcUrl, chainId, vaultAddress, maxAmount, maxImpactBps, options?)
Binary search for largest deposit within APY impact threshold.
Returns { amount, maxPossibleAmount, isMaxAmount, isCapped, impact }.
findMaxWithdrawalRpc(rpcUrl, chainId, vaultAddress, maxAmount, maxImpactBps, options?)
Binary search for largest withdrawal within APY impact threshold.
Same as RPC functions but take a viem PublicClient instead of rpcUrl/chainId:
fetchVaultApyViem(client, vaultAddress, morphoAddress, options?)computeDepositImpact(client, chainId, vaultAddress, amount, options?)computeWithdrawalImpact(client, chainId, vaultAddress, amount, options?)findMaxDeposit(client, chainId, vaultAddress, maxAmount, maxImpactBps, options?)findMaxWithdrawal(client, chainId, vaultAddress, maxAmount, maxImpactBps, options?)
estimateMarketApy(depositAmt, totalSupply, totalBorrows, fee, rateAtTarget)
Compute supply and borrow APY for a single Morpho Blue market.
weightedVaultApy(markets, depositSim?, withdrawalSim?)
Position-weighted vault APY. Returns a single number.
weightedVaultApyDetailed(markets, depositSim?, withdrawalSim?)
Same but returns { apy, markets: MarketApyDetail[] } with per-market details.
simulateDeposit(markets, depositAmount) / simulateWithdrawal(markets, withdrawAmount, idleAssets)
Simulate how deposits/withdrawals flow through the queue.
findMaxDepositAmount(markets, maxAmount, maxImpactBps, options?) / findMaxWithdrawalAmount(markets, idleAssets, maxAmount, maxImpactBps, options?)
Pure math binary search (no RPC). Options: { precision?, includeMarkets? }.
MORPHO_BLUE_ADDRESSES / getMorphoBlueAddress(chainId)
When includeMarkets: true, impact results include a markets array of MarketImpactDetail:
{
marketId: string
name: string // "WETH/USDC" (collateral/loan symbols)
current: {
supplyApy: number // 0-8.0
borrowApy: number // 0-8.0
utilization: number // 0-1
allocationPct: number // 0-1
vaultSupplyAssets: bigint
}
simulated: { /* same shape */ }
}For fetchVaultApy, the marketDetails array contains MarketApyDetail (same fields as above, without the current/simulated nesting).
Impact values are signed as:
impact = newApy - currentApy
impactBps = round(impact * 10000)- negative impact means APY decreased
- positive impact means APY increased
This library converts on-chain bigint balances to JavaScript number for APY math.
Because IEEE-754 doubles have 53 bits of integer precision, precision loss starts when raw base units exceed 2^53:
- 18 decimals:
2^53 / 1e18 = 0.009007199254740993tokens - 6 decimals:
2^53 / 1e6 = 9,007,199,254.740992tokens
This does not automatically imply large APY error; it usually starts as tiny base-unit rounding.
Approximate 18-decimal token magnitudes where floating spacing reaches:
1e-12tokens at ~4.72e3tokens1e-9tokens at ~4.84e6tokens1e-6tokens at ~4.95e9tokens1e-3tokens at ~5.07e12tokens0.1tokens at ~6.49e14tokens1token at ~5.19e15tokens
At runtime, the math layer emits a one-time warning per market when estimated token spacing reaches 1e-6 tokens or higher.
The findMaxDeposit* and findMaxWithdrawal* functions accept an optional constraint callback that adds custom criteria beyond APY impact. The binary search finds the largest amount satisfying both the APY impact threshold and the custom constraint.
const result = await findMaxDepositRpc(RPC_URL, 1, vault, 100000n * 10n**18n, 50, {
includeMarkets: true,
constraint: (state) => {
// Keep target market utilization under 80%
const market = state.markets.find(m => m.marketId === targetId)
return !market || market.simulated.utilization <= 0.8
},
})The callback receives FindMaxConstraintState:
{
amount: bigint // candidate amount being tested
currentApy: number // vault APY before the action
simulatedApy: number // vault APY after the action
impactBps: number // APY change in basis points
markets: MarketImpactDetail[] // per-market current vs simulated
isPartial?: boolean // withdrawal only: liquidity-constrained
withdrawableAmount?: bigint // withdrawal only: actual removable amount
}The constraint can be sync or async (return boolean or Promise<boolean>).
Important: The constraint must be monotonic — if amount X fails, all amounts > X must also fail. Non-monotonic constraints (e.g., "utilization between 70% and 85%") will produce incorrect results. For constraint-only mode (no APY threshold), set maxImpactBps to 10000 (100%).
See docs/algorithm.md for detailed math, pseudocode, and on-chain data requirements.
pnpm install
pnpm build # ESM + CJS + types
pnpm test # Unit tests
pnpm dev -- apy --rpc-url $RPC --chain-id 1 --vault 0x5b8b... # Run CLI from sourcepnpm build
npm publish