Skip to content

ohMySol/panorama

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

48 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Panorama Logo

Panorama

Map every dependency.

Panorama is a smart contract dependency analyzer that visualizes the entire dependency graph of Ethereum contracts.

Panorama Home Page

🎯 What is Panorama?

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.

✨ Features

  • πŸ”— 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 Dashboard

πŸ—οΈ Architecture

Workflow

Workflow

Project Structure

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

Frontend

  • 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

Backend

  • 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

Infrastructure

  • Docker - Containerized deployment
  • Docker Compose - Multi-container orchestration
  • Monorepo - Shared types between frontend and backend

πŸš€ Quick Start

Environment Variables

Frontend (.env.local):

NEXT_PUBLIC_API_URL=http://localhost:5000

Backend (.env):

SERVER_PORT=5000
ETHERSCAN_API_KEY=your_api_key_here
# Optional: AI-powered protocol summaries (free!)
HUGGINGFACE_API_KEY=your_huggingface_token_here

Get Hugging Face API Key (Free):

  1. Go to huggingface.co/settings/tokens
  2. Create a new token (read access is enough)
  3. Copy and paste into .env file

Available Commands

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 volumes

Development:

# Backend
cd backend
npm run dev    # Start dev server

# Frontend
cd frontend
npm run dev    # Start Next.js dev server

Using Docker (Recommended)

# Start the entire stack
docker compose up

# Or use the convenience script
./start.sh

Access:

Manual Setup

Backend:

cd backend
npm install
npm run dev

Frontend:

cd frontend
npm install
npm run dev

πŸ“– Usage

Important Note. Panorama is a prototype project and currently it is working only on Ethereum Mainnet for Morpho Vault V1.

  1. Enter a Contract Address - Paste an Ethereum contract address into the input field
  2. Analyze - Click "Analyze" or press Enter to start the analysis
  3. Explore the Graph - View the interactive dependency graph
  4. Inspect Nodes - Click on any node to see detailed information
  5. Navigate - Use zoom controls and drag nodes to customize your view

πŸ—‚οΈ Protocol Manifests

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.

How the executor works

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.

Manifest file structure

{
  "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" }
  ]
}

Existing manifests

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.

Adding a new protocol

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.

Limitations

  • 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 X only if flag Y is 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_call reads. 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.

  • followUp ABI 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.

πŸ“š API Documentation

Endpoints

POST /api/graph

{
  "address": "0xfff",
  "chain_id": 1,
  "depth": 3
}
  • address - address of the contract you want to build graph for
  • chain_id - ID of the chain where the contract lives
  • depth - 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 address
  • nodes - node objects describing each dependency found inside the root contract
  • edges - edge objects describing the relationship between nodes (e.g. node1 --> node2 means node2 was found inside node1)
  • graphRiskScore - aggregate risk score (currently always 0 β€” scoring engine is disabled)
  • summary - always null here; the AI summary is fetched separately via POST /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 if HUGGINGFACE_API_KEY is set, otherwise falls back to a deterministic template built from the graph data.

🀝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.


Built with ❀️ for the Ethereum ecosystem

About

A system that helps to explore DeFi protocol smart contract dependencies in a visualised graph way.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages