ERC-4626 vault dashboards
Two real mainnet vaults — Morpho Steakhouse USDC and Yearn
yvUSDC v3 — read in a single multicall. For each vault we
batch six fields: symbol, decimals, totalSupply,
totalAssets, convertToAssets(1 share) (share price), and
asset() (underlying token address). Twelve reads across
two contracts, one roundtrip.
Morpho · Steakhouse USDC
Yearn · yvUSDC v3
The code
import "./demo.css"
import { eip155_1 } from "@ethernauta/chain/eip155-1"
import {
AddressSchema,
Uint256Schema,
} from "@ethernauta/core"
import {
decimals,
symbol,
totalSupply,
} from "@ethernauta/erc/20"
import {
asset,
convertToAssets,
totalAssets,
} from "@ethernauta/erc/4626"
import {
contract,
create_multicall,
encode_chain_id,
http,
} from "@ethernauta/transport"
import { useCallback, useEffect, useState } from "react"
import {
bigint,
type InferOutput,
number,
object,
parse,
string,
tuple,
} from "valibot"
import { Button } from "../../components/button"
const MAINNET_CHAIN_ID = encode_chain_id({
namespace: "eip155",
reference: eip155_1.chainId,
})
const VAULTS = [
{
label: "Morpho · Steakhouse USDC",
address: "0xBEEF01735c132Ada46AA9aA4c54623cAA92A64CB",
underlying: "USDC",
underlying_decimals: 6,
},
{
label: "Yearn · yvUSDC v3",
address: "0xBe53A109B494E5c9f97b9Cd39Fe969BE68BF6204",
underlying: "USDC",
underlying_decimals: 6,
},
] as const
const multicall = create_multicall([
{
chainId: MAINNET_CHAIN_ID,
transports: [
http("https://ethereum-rpc.publicnode.com"),
],
},
])
// We request `convertToAssets(1 share)`. The share decimals
// are vault-specific; we read them first via decimals().
// For the "assets per share" display we normalise by share
// decimals on the client.
const ONE_SHARE = parse(
Uint256Schema,
`0x${(10n ** 18n).toString(16)}`,
)
const VaultSnapshotSchema = object({
label: string(),
symbol: string(),
decimals: number(),
total_supply: bigint(),
total_assets: bigint(),
assets_per_share: bigint(),
asset_address: AddressSchema,
underlying: string(),
underlying_decimals: number(),
})
type VaultSnapshot = InferOutput<typeof VaultSnapshotSchema>
// Each multicall slice (one vault, 6 reads). Matches the call
// order built inside `run()` below.
const VaultCallResultsSchema = tuple([
string(), // symbol()
Uint256Schema, // decimals()
Uint256Schema, // totalSupply()
Uint256Schema, // totalAssets()
Uint256Schema, // convertToAssets(1e18)
AddressSchema, // asset()
])
export function VaultsDemo() {
const [snapshots, set_snapshots] = useState<
VaultSnapshot[] | null
>(null)
const [loading, set_loading] = useState(false)
const [error, set_error] = useState<string | null>(null)
const [elapsed_ms, set_elapsed_ms] = useState<
number | null
>(null)
const run = useCallback(async () => {
set_loading(true)
set_error(null)
try {
const calls = VAULTS.flatMap((v) => {
const ctx = contract({
chain_id: MAINNET_CHAIN_ID,
to: parse(AddressSchema, v.address),
})
return [
symbol()(ctx),
decimals()(ctx),
totalSupply()(ctx),
totalAssets()(ctx),
convertToAssets({ shares: ONE_SHARE })(ctx),
asset()(ctx),
]
})
const start = performance.now()
const results = await multicall(calls)
set_elapsed_ms(Math.round(performance.now() - start))
set_snapshots(
VAULTS.map((v, i) => {
const base = i * 6
const [
sym,
dec_hex,
total_supply_hex,
total_assets_hex,
assets_per_share_hex,
asset_addr,
] = parse(
VaultCallResultsSchema,
results.slice(base, base + 6),
)
return parse(VaultSnapshotSchema, {
label: v.label,
symbol: sym,
decimals: Number(BigInt(dec_hex)),
total_supply: BigInt(total_supply_hex),
total_assets: BigInt(total_assets_hex),
assets_per_share: BigInt(assets_per_share_hex),
asset_address: asset_addr,
underlying: v.underlying,
underlying_decimals: v.underlying_decimals,
})
}),
)
} catch (e) {
set_error(
e instanceof Error ? e.message : "Unknown error",
)
} finally {
set_loading(false)
}
}, [])
useEffect(() => {
run()
}, [run])
return (
<div className="vaults-root">
<div className="vaults-grid">
{(snapshots ?? VAULTS).map((v, i) => {
const s = snapshots ? snapshots[i] : null
return (
<div
key={"label" in v ? v.label : i}
className="vaults-card"
>
<div className="vaults-card-label">
{v.label}
</div>
{loading && !s && (
<p className="vaults-loading">Loading…</p>
)}
{s && (
<>
<Row label="Symbol" value={s.symbol} />
<Row
label="Decimals"
value={String(s.decimals)}
/>
<Row
label="Total assets"
value={`${format(s.total_assets, s.underlying_decimals)} ${s.underlying}`}
/>
<Row
label="Total supply"
value={`${format(s.total_supply, s.decimals)} ${s.symbol}`}
/>
<Row
label="Share price"
value={`${format(s.assets_per_share, s.underlying_decimals)} ${s.underlying} / share`}
/>
<Row
label="Asset"
value={s.asset_address}
mono
/>
</>
)}
</div>
)
})}
</div>
{error && <p className="vaults-error">{error}</p>}
{elapsed_ms !== null && (
<p className="vaults-elapsed">
{elapsed_ms} ms · {VAULTS.length * 6} reads · 1
RPC call
</p>
)}
<Button onClick={run} disabled={loading}>
{loading ? "Running…" : "Re-run multicall"}
</Button>
</div>
)
}
function Row({
label,
value,
mono,
}: {
label: string
value: string
mono?: boolean
}) {
return (
<div className="vaults-row">
<span className="vaults-row-label">{label}</span>
<span
className={
mono
? "vaults-row-value is-mono"
: "vaults-row-value"
}
>
{value}
</span>
</div>
)
}
function format(raw: bigint, decimals: number): string {
const base = 10n ** BigInt(decimals)
const whole = raw / base
const fraction = raw % base
const fraction_str = fraction
.toString()
.padStart(decimals, "0")
.slice(0, 4)
return `${whole.toLocaleString()}.${fraction_str}`
}