Map every dependency.
Panorama is a smart contract dependency analyzer that visualizes the entire dependency graph of Ethereum contracts.
Panorama is a smart contract dependency analyzer that visualizes the entire dependency graph in a way that you can clearly see what your vault or pool or strategy depends on. Every node in the graph is a standalone smart contract which plays a specific role (e.g.: multisig owner, oracle, lending market, ...) inside your root contract. The nodes have basic information like number of signers, found risk flags (e.g: upgradeable proxy) which will be useful during the research and analysis.
- π Dependency Graph Visualization - Interactive hierarchical graph showing all contract dependencies
- π³ Dependency Tree View - Hierarchical tree structure showing parent-child relationships
- π Detailed Metadata - Contract tier, source availability
- π― Interactive Nodes - Click any node to view detailed information
- π±οΈ Draggable Graph - Move nodes around to customize your view
- π€ AI Protocol Summaries - Automatic protocol analysis when no node is selected
Panorama/
βββ backend/ # Express + TypeScript API
β βββ src/
β β βββ app.ts # Server entry point
β β βββ clients/ # External service clients
β β β βββ etherscan.client.ts
β β β βββ http.ts
β β β βββ rpc.client.ts
β β β βββ sourcify.client.ts
β β βββ config/
β β β βββ config.ts # Env vars, depth limits, constants
β β βββ controllers/
β β β βββ ai-summary.controller.ts
β β β βββ graph.controller.ts
β β βββ middleware/
β β β βββ error.middleware.ts
β β βββ routes/
β β β βββ ai.router.ts
β β β βββ graph.router.ts
β β βββ services/
β β βββ ai-summary.service.ts
β β βββ cache.service.ts
β β βββ graph.service.ts # BFS dependency-graph builder
β β βββ resolver.service.ts
β β βββ router.service.ts
β β βββ manifests/ # Protocol manifests + executor
β β β βββ executor.ts
β β β βββ index.ts
β β β βββ types.ts
β β β βββ protocols/ # erc20, morpho-*, safe-multisig
β β βββ risk/ # Risk-flag detection (universal + per-profile)
β β βββ index.ts
β β βββ universal.ts
β β βββ types.ts
β β βββ profiles/token.ts
β βββ Dockerfile
β βββ package.json
β
βββ frontend/ # Next.js (App Router) UI
β βββ app/
β β βββ layout.tsx
β β βββ page.tsx # Landing page
β β βββ providers.tsx
β β βββ globals.css
β β βββ dashboard/[address]/page.tsx # Dynamic analysis page
β β βββ src/components/
β β βββ dashboard/ # Graph, node info, metadata, tabs
β β βββ lending/ # Landing hero, scan input, header
β β βββ shared/ # Background glow, intro
β βββ lib/
β β βββ api/ # graph + ai-summary HTTP clients
β β βββ config/api.config.ts
β β βββ context/selected-node.context.tsx
β β βββ hooks/ # useGraphAnalysis, useAiSummary
β β βββ utils/node-display.ts
β β βββ validation/address.validation.ts
β βββ public/
β βββ Dockerfile
β βββ package.json
β
βββ packages/
β βββ shared/src/ # Shared types between FE/BE
β βββ index.ts
β βββ types.ts
β
βββ img/
βββ docker-compose.yml
βββ Makefile
βββ start.sh
βββ README.md
- Next.js 16 - React framework with App Router
- React 19 - Latest React with concurrent features
- TailwindCSS 4 - Utility-first CSS framework
- TanStack Query - Powerful data fetching and caching
- TypeScript - Type-safe development
- Express - Fast, minimalist web framework
- TypeScript - Type-safe backend development
- Viem - Lightweight Ethereum library
- Etherscan API - Contract verification and source code
- Sourcify API - Decentralized contract verification
- Docker - Containerized deployment
- Docker Compose - Multi-container orchestration
- Monorepo - Shared types between frontend and backend
Frontend (.env.local):
NEXT_PUBLIC_API_URL=http://localhost:5000Backend (.env):
SERVER_PORT=5000
ETHERSCAN_API_KEY=your_api_key_here
# Optional: AI-powered protocol summaries (free!)
HUGGINGFACE_API_KEY=your_huggingface_token_hereGet Hugging Face API Key (Free):
- Go to huggingface.co/settings/tokens
- Create a new token (read access is enough)
- Copy and paste into
.envfile
Docker:
make dev # Start development environment (detached)
make up # Start containers in foreground
make build # Rebuild containers
make logs # View logs
make down # Stop containers
make restart # Restart containers
make clean # Remove all containers and volumesDevelopment:
# Backend
cd backend
npm run dev # Start dev server
# Frontend
cd frontend
npm run dev # Start Next.js dev server# Start the entire stack
docker compose up
# Or use the convenience script
./start.shAccess:
- Frontend: http://localhost:3000
- Backend: http://localhost:5000
Backend:
cd backend
npm install
npm run devFrontend:
cd frontend
npm install
npm run devImportant Note. Panorama is a prototype project and currently it is working only on Ethereum Mainnet for Morpho Vault V1.
- Enter a Contract Address - Paste an Ethereum contract address into the input field
- Analyze - Click "Analyze" or press Enter to start the analysis
- Explore the Graph - View the interactive dependency graph
- Inspect Nodes - Click on any node to see detailed information
- Navigate - Use zoom controls and drag nodes to customize your view
Manifests are the mechanism by which Panorama knows how to traverse a specific protocol. Instead of writing a TypeScript code for every protocol, each protocol is described as a JSON file that declares which on-chain functions to call, what to do with the results, and what metadata to surface in the UI. A single generic executor reads any manifest and runs the same pipeline.
When a contract is identified as a known protocol, the executor runs up to three batched RPC rounds:
Round 1 β direct getters + paginated lengths + metadata
All single-call getters are batched into one multicall. This includes addresses returned by direct getters (e.g. owner, curator), the lengths of any lists (e.g. supplyQueueLength), and metadata values like token symbol or multisig threshold.
Round 2 β paginated items
For each list discovered in Round 1, the executor fetches every item up to maxItemsPerSource. Items are either addresses (emitted as edges directly) or bytes32 identifiers that need a further lookup.
Round 3 β cross-contract follow-ups
When items are bytes32 IDs (e.g. Morpho market IDs), the executor calls a second contract to unpack them into concrete addresses. For example, Morpho Vault market IDs are resolved against the Morpho Blue core contract via idToMarketParams, yielding the loan token, collateral token, oracle, and interest rate model β all in one extra multicall.
Every function name in a manifest is validated against the contract's resolved ABI before any call is issued. A function not present in the ABI is silently skipped, so a manifest written for a newer contract version never crashes against an older one.
| File | id |
category |
Fingerprint |
|---|---|---|---|
morpho-vault-v1.json |
morpho |
Vault | asset, MORPHO, supplyQueue |
morpho-vault-v2.json |
morphoV2 |
Vault | asset, MORPHO, supplyQueue, publicAllocator |
morpho-blue.json |
morphoBlue |
Market | idToMarketParams, createMarket, accrueInterest |
safe-multisig.json |
safe |
Multisig | getOwners, getThreshold, isOwner |
erc20.json |
erc20 |
Token | transfer, balanceOf, totalSupply |
Matching is first-match-wins in the order they are registered in manifests/index.ts. More specific protocols (Morpho Vault) are listed before broader ones (ERC-20) because a MetaMorpho vault is also ERC-20-compatible.
1. Create the JSON manifest in backend/src/services/manifests/protocols/:
// protocols/aave-v3-pool.json
{
"id": "aaveV3",
"category": "Lending",
"fingerprint": ["supply", "borrow", "getReserveData"],
"directCalls": [
{ "function": "ADDRESSES_PROVIDER", "role": "addressesProvider" }
],
"metadataCalls": [
{ "function": "MAX_NUMBER_RESERVES", "field": "maxReserves" }
]
}Choose a fingerprint of 2β4 functions that are unique to this protocol. Avoid functions present in ERC-20 (transfer, balanceOf) or other broad interfaces, otherwise the manifest may match unintended contracts.
2. Register the id in AdapterKind (manifests/types.ts):
export type AdapterKind =
| 'morphoBlue' | 'morpho' | 'morphoV2'
| 'erc20' | 'safe'
| 'aaveV3' // add your new id here
| 'fallback';3. Import and register the manifest in manifests/index.ts:
import aaveV3Pool from './protocols/aave-v3-pool.json';
const MANIFESTS: readonly ProtocolManifest[] = [
morphoV1 as ProtocolManifest,
morphoV2 as ProtocolManifest,
morphoBlue as ProtocolManifest,
aaveV3Pool as ProtocolManifest, // add before erc20/safe to avoid false-positive matches
safeMultisig as ProtocolManifest,
erc20 as ProtocolManifest,
];4. Optionally add a risk profile in risk/profiles/ if this protocol has token-level or protocol-specific risk signals worth flagging (e.g. pausing mechanisms, admin key patterns). Register it in risk/index.ts under PROFILE_REGISTRY.
That is everything needed β no changes to the graph builder, executor, resolver, or frontend. The manifest is picked up automatically the next time a contract with the matching fingerprint is resolved.
-
Requires a verified contract. Fingerprint matching runs against the resolved ABI. If a contract has no source on Sourcify or Etherscan, the ABI is
null, no manifest can match, and the node becomes a leaf with no discovered dependencies. Unverified contracts are invisible to the manifest system. -
Fingerprints are heuristics, not guarantees. Two different protocols can expose the same set of function names. A wrong match silently produces incorrect edges. The safest fingerprints are 3β4 functions that are unique to a protocol's interface, but there is no enforcement - a bad fingerprint will not error, it will just graph the wrong thing.
-
No conditional logic. Manifests are declarative JSON. If a protocol's dependency structure is conditional (e.g. "call
Xonly if flagYis set"), that cannot be expressed. The executor always runs all declared calls. Protocols with dynamic or branching dependency graphs need a custom TypeScript adapter instead. -
Only view calls, no event logs. The executor only issues
eth_callreads. Dependencies discovered via emitted events β common in factory patterns where child contracts are created on-chain β are completely invisible. A protocol that registers markets through events rather than exposing them via a length/item getter cannot be covered by a manifest. -
followUpABI is inlined and static. The ABI fragment for a cross-contract lookup must be written directly into the manifest JSON. If the target contract is upgraded and the function signature changes, the manifest silently starts returning nothing rather than erroring. There is no version pinning or ABI re-resolution.
POST /api/graph
{
"address": "0xfff",
"chain_id": 1,
"depth": 3
}address- address of the contract you want to build graph forchain_id- ID of the chain where the contract livesdepth- controls how many levels deep the BFS traversal expands the dependency graph from the root contract (e.g: depth = 3. This means 3 steps out from the root).
Response:
{
"root": "0x...",
"nodes": [...],
"edges": [...],
"graphRiskScore": 0,
"summary": null
}root- the root contract addressnodes- node objects describing each dependency found inside the root contractedges- edge objects describing the relationship between nodes (e.g.node1 --> node2meansnode2was found insidenode1)graphRiskScore- aggregate risk score (currently always0β scoring engine is disabled)summary- alwaysnullhere; the AI summary is fetched separately viaPOST /api/ai/summary
POST /api/ai/summary
Body: the full GraphResponse object returned by /api/graph.
Response:
{
"summary": "..."
}summary- one or two sentences of AI-generated protocol description. Uses Hugging Face Inference ifHUGGINGFACE_API_KEYis set, otherwise falls back to a deterministic template built from the graph data.
Contributions are welcome! Please feel free to submit a Pull Request.
Built with β€οΈ for the Ethereum ecosystem




{ "id": "morpho", // AdapterKind β used as GraphNode.type and by risk profiles "category": "Vault", // UI chip label ("Vault", "Market", "Token", "Multisig", β¦) "fingerprint": [ // ALL of these function names must exist in the ABI to match "asset", "MORPHO", "supplyQueue" ], "directCalls": [ // Single getters that return one address each --> one edge each { "function": "owner", "role": "owner" }, { "function": "asset", "role": "loanToken" } ], "paginatedCalls": [ // Length-prefixed list getters --> one edge per item { "sources": [ { "lengthFunction": "supplyQueueLength", "itemFunction": "supplyQueue" } ], "itemType": "bytes32", // "address" --> direct edges; "bytes32" --> needs followUp "maxItemsPerSource": 5, "followUp": { // Only needed when itemType is "bytes32" "addressFromRole": "protocolCore", // Role of a directCall whose result is the target "function": { ... }, // Inline ABI of the function to call on that target "extract": [ // Which tuple fields to pull out and with what role { "index": 0, "role": "loanToken" }, { "index": 2, "role": "oracle" } ], "anchorFromTarget": true // Attribute edges to the target contract, not the vault } } ], "metadataCalls": [ // Read-only facts surfaced in the UI; never produce edges { "function": "symbol", "field": "symbol" }, { "function": "getOwners", "field": "signerCount", "project": "length" }, { "function": "getThreshold","field": "signerThreshold" } ] }