Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
1d64986
feat: add edb-proxy API endpoint with CORS support and request handling
Timidan Apr 15, 2026
a1ddd99
Refactor and optimize code structure across multiple components
Timidan Apr 15, 2026
5347d19
feat: enhance debug session handling and proxy configuration with API…
Timidan Apr 16, 2026
afc3b49
Merge pull request #12 from Timidan/feat/cleanup
Timidan Apr 16, 2026
7b25870
feat: increase evaluation timeouts and enhance snapshot resolution
Timidan Apr 16, 2026
1fbab74
fix: update type definition for isOpcode in resolveEvalSnapshotId fun…
Timidan Apr 16, 2026
001ed3a
feat: enhance token movements handling and improve accordion behavior…
Timidan Apr 16, 2026
b14421d
chore: track edb submodule on canonical toolkit branch
Timidan Apr 19, 2026
eb78e1c
chore: bump edb submodule with Heimdall bridge endpoints
Timidan Apr 19, 2026
d3b0c08
chore: bump edb submodule for heimdall CLI 0.9.2 fix
Timidan Apr 19, 2026
55d0ee9
feat(tx-analysis): implement LLM-based transaction analysis and verdi…
Timidan Apr 21, 2026
33108ae
Refactor TxAnalysisPanel to remove 'any' type usage and improve error…
Timidan Apr 21, 2026
3e3c1a6
merge: bring fix/debug improvements into feat/tx-captain
Timidan Apr 21, 2026
a676388
fix(tx-analysis): use ValidationResult.valid instead of .success
Timidan Apr 21, 2026
3e832e4
feat(tx-analysis): update transaction analysis to handle null 'to' va…
Timidan Apr 21, 2026
0312388
fix(tx-analysis): null-guard packet.to in hack-analysis rules after s…
Timidan Apr 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ debug-formatter.html
# Submodule / nested repos
edb

# Repo uses npm; prevent accidental pnpm installs at the root from being committed
# (confuses Vercel's package-manager auto-detection)
/pnpm-lock.yaml

# Misc
*.pdf
*test*
Expand Down
4 changes: 4 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[submodule "edb"]
path = edb
url = git@github.com:Timidan/edb-extended.git
branch = toolkit
69 changes: 68 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,47 @@ A full simulation analysis page with six tabs:

Browse past simulations stored in IndexedDB. Re-open any previous result for review or further debugging.

### Transaction Analysis (Tx-Captain)

A forensics sub-tool that turns a completed simulation into a verifiable, evidence-backed verdict. Two entry points:

- Click **Summarize** in the simulation results header (next to Share/Export).
- Open `/builder?mode=analysis` directly — the panel attaches to whichever simulation is currently pinned in the SimulationContext.

What it does:

- **Evidence extraction** -- Walks the V3 trace and emits a structured evidence packet (calls, storage reads/writes, events, transfers, path contracts).
- **Heuristic sieve** -- Flags patterns that routinely ship bugs: `revert_on_path`, `sload_after_sstore` (state contradicting writes), `zero_address_transfer`, `large_delta` (transfers at/over 10 ETH), and `accumulator` (three or more writes to the same contract slot).
- **LLM verdict** -- Sends the sanitized packet to the configured LLM (Gemini / Anthropic / OpenAI / custom, per the LLM config panel) and parses the response against a Zod schema: verdict, confidence, core contradiction, causal chain, gates, risk upper bound, missing evidence.
- **Deep Dive (optional)** -- When enabled, fetches verified Solidity source for each path contract (Sourcify / Etherscan / Blockscout) and falls back to Heimdall decompilation on unverified addresses. Sources are stripped of SPDX/pragma/import lines and vendored libraries (`@openzeppelin/…`, `@chainlink/…`, `node_modules/…`) before being sent to the LLM.

Privacy and export guarantees:

- Analyses are stored in a dedicated IndexedDB store (`web3-toolkit-tx-analysis`) separate from simulation history and are never uploaded.
- Sensitive keys (`rpcUrl`, `apiKey`, `privateKey`, `secret`, …) are stripped recursively before persistence.
- Prompts are SHA-256 hashed alongside the verdict so you can audit exactly what was sent.
- Each verdict can be exported as Markdown or JSON from the panel toolbar.

#### Encrypted Triage (Fhenix Wave 3)

Alongside the classical LLM path, Tx-Captain ships an **encrypted triage** path built on Fhenix CoFHE. The browser encrypts a 12-bit feature vector derived from the EvidencePacket and submits it to a live fhEVM classifier on Ethereum Sepolia. The contract classifies the vector under FHE, storing encrypted `(classBits, severity)` handles that only the submitter can decrypt via `decryptForView` + self-issued permit. A second contract — `RiskThrottle` — consumes the encrypted severity via `FHE.select` and updates per-user throttle status without any party decrypting on-chain.

Live deployments (Ethereum Sepolia, chainId 11155111):

| Contract | Address | Etherscan |
|---|---|---|
| `HackTriage` | `0xBe02be322c7733759ee068067BD620791e9e73D4` | [verified](https://sepolia.etherscan.io/address/0xBe02be322c7733759ee068067BD620791e9e73D4#code) |
| `RiskThrottle` | `0x1FbB01D8e3FE4E99D6742eeBf1A4e276050ecD05` | [verified](https://sepolia.etherscan.io/address/0x1FbB01D8e3FE4E99D6742eeBf1A4e276050ecD05#code) |

Reproduction:

1. Set `VITE_HACK_TRIAGE_ADDRESS` in `.env` (defaults to the live address above).
2. Connect a Sepolia-funded wallet via RainbowKit, run a simulation, and open the Analysis panel.
3. Flip the **Cleartext / Encrypted** toggle in `TxAnalysisPanel` to Encrypted, then click **Run Encrypted Triage**.
4. The hook walks `encrypting → writing → waiting-fhe → decrypting → ready`, revealing class labels and severity locally without ever decrypting on-chain.

Architecture, threat model, leakage table, and open questions live in [docs/superpowers/plans/fhenix-wave3-architecture.md](docs/superpowers/plans/fhenix-wave3-architecture.md). Solidity sources are in [fhe/contracts/](fhe/contracts/); client helpers in [src/utils/hack-analysis/triage/](src/utils/hack-analysis/triage/) and [src/components/tx-analysis/useHackTriage.ts](src/components/tx-analysis/useHackTriage.ts).

### Integrations

The `/integrations` route hosts protocol-specific modules that extend HexKit beyond debugging into active DeFi operations.
Expand Down Expand Up @@ -175,6 +216,31 @@ For the LI.FI Earn integration and AI concierge, set the following in `.env`:
| `GEMINI_FALLBACK_MODEL` | Fallback on 429/503 (default: `gemini-2.5-flash`) |
| `PROXY_SECRET` | Shared secret for API proxy authentication (production) |
| `ALLOWED_ORIGINS` | Comma-separated allowed CORS origins (production) |
| `VITE_HACK_TRIAGE_ADDRESS` | Ethereum Sepolia `HackTriage` contract (default: `0xBe02...73D4`) |

### Heimdall endpoints (bridge)

The bridge exposes three POST endpoints that shell out to the [heimdall-rs](https://github.com/Jon-Becker/heimdall-rs) CLI:

- `POST /heimdall/version` → `{ available: boolean, version?: string }`
- `POST /heimdall/decompile` → `{ source, abi, bytecodeHash, ... }`
- Body: `{ bytecode: "0x..." }` **or** `{ address, chainId }`
- `POST /heimdall/dump` → `{ slots: [...], address, chainId, blockNumber, ... }`
- Body: `{ address, chainId, blockNumber? | blockTag? }`

**Security:** Clients must not send `rpcUrl` — the bridge resolves it per-chain from a server-side allowlist (default: mainnet / optimism / base / arbitrum) and rejects any request that carries `rpcUrl` with HTTP 400. Override the allowlist with `HEIMDALL_RPC_BY_CHAIN` (see `.env.example`). The bridge also computes `bytecodeHash` itself from `eth_getCode` — the client never supplies it — to prevent cache-key poisoning.

Results are cached in-memory on the bridge (LRU + TTL). To install the binary: run `bash edb/scripts/install.sh` — it pulls heimdall via bifrost automatically unless `INSTALL_HEIMDALL=0`. Front end consumers use [`src/utils/heimdall/hooks.ts`](src/utils/heimdall/hooks.ts).

#### Unverified contract support (heuristic tier)

When Sourcify has no source for a contract, the Storage Layout Viewer falls back to Heimdall decompilation if the bridge has it installed. Results are shown with an amber **Heuristic** badge and a banner explaining the caveats:

- Slot labels derived from decompiled function signatures (ERC20-like, Ownable, Pausable) and Heimdall's `modifiers` hint per slot.
- Struct packing is **not** resolved — every populated slot is shown as a single 32-byte value.
- Mapping keys must be discovered manually (use the existing mapping probe).

To enable: install Heimdall on the bridge (see install step above). To disable globally, set `HEIMDALL_BIN_PATH` to a path that does not exist — the availability check will fall back to "unavailable" and the heuristic tier is skipped.

## Project Structure

Expand Down Expand Up @@ -229,7 +295,8 @@ src/
lib/ Shared libraries

api/
llm-recommend.ts Gemini LLM proxy with model fallback
llm-invoke.ts Provider-agnostic LLM proxy (allowlist-enforced BYOK transport)
llm-recommend.ts Gemini LLM proxy with model fallback (legacy; kept for rollback)
lifi-composer.ts LI.FI Composer quote/execute proxy
edb/ EDB simulation API routes

Expand Down
42 changes: 42 additions & 0 deletions api/_llm/allowlist.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { LlmProvider } from "../../src/utils/llm/types";

const BASE_URLS: Record<Exclude<LlmProvider, "custom">, string> = {
anthropic: (process.env.ANTHROPIC_BASE_URL ?? "https://api.anthropic.com").replace(/\/$/, ""),
openai: (process.env.OPENAI_BASE_URL ?? "https://api.openai.com").replace(/\/$/, ""),
gemini: (process.env.GEMINI_BASE_URL ?? "https://generativelanguage.googleapis.com").replace(/\/$/, ""),
};

const ALLOWED_PATHS: Record<Exclude<LlmProvider, "custom">, RegExp[]> = {
anthropic: [/^\/v1\/messages$/],
openai: [/^\/v1\/chat\/completions$/, /^\/v1\/responses$/],
gemini: [
/^\/v1beta\/models\/[A-Za-z0-9._-]+:generateContent$/,
/^\/v1beta\/models\/[A-Za-z0-9._-]+:streamGenerateContent(\?alt=sse)?$/,
],
};

export function isAllowedProviderUrl(
provider: LlmProvider,
path: string,
): boolean {
if (provider === "custom") return false;
if (!(provider in BASE_URLS)) return false;
if (path.startsWith("http://") || path.startsWith("https://")) return false;
if (path.includes("..")) return false;
const rules = ALLOWED_PATHS[provider];
return rules.some((rx) => rx.test(path));
}

export function resolveProviderUrl(
provider: LlmProvider,
path: string,
): string {
if (!isAllowedProviderUrl(provider, path)) {
throw new Error(`Provider URL not allowed: ${provider}${path}`);
}
return `${BASE_URLS[provider as Exclude<LlmProvider, "custom">]}${path}`;
}

export function listAllowedProviders(): LlmProvider[] {
return Object.keys(BASE_URLS) as LlmProvider[];
}
71 changes: 71 additions & 0 deletions api/_llm/guardHeaders.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import * as crypto from "crypto";

export interface GuardConfig {
allowedOrigins: string[];
proxySecret: string | undefined;
}

export interface GuardResult {
ok: boolean;
status?: number;
reason?: string;
}

function timingSafeEqualStr(a: string, b: string): boolean {
const ab = Buffer.from(a);
const bb = Buffer.from(b);
if (ab.length !== bb.length) return false;
return crypto.timingSafeEqual(ab, bb);
}

function isSameOriginOrLocalhost(origin: string, host: string | undefined): boolean {
if (origin.startsWith("http://localhost:") || origin === "http://localhost") return true;
if (origin.startsWith("http://127.0.0.1:") || origin === "http://127.0.0.1") return true;
if (!host) return false;
return origin === `https://${host}` || origin === `http://${host}`;
}

/**
* Fail-closed request guard. Allow order:
* 1. If proxySecret configured, require x-proxy-secret match (timing-safe).
* 2. Else if Origin absent, allow (same-origin server call / curl).
* 3. Else allow when origin is in allowedOrigins, is localhost/127.0.0.1,
* or equals same-host (http(s)://${host}).
* 4. Anything else: reject 403. Empty allowedOrigins + external Origin
* does NOT open the proxy — previously this was a fail-open bug.
*/
export function checkRequestGuards(
req: { headers: Record<string, string | string[] | undefined> },
cfg: GuardConfig,
): GuardResult {
const h = (name: string): string | undefined => {
const v = req.headers[name.toLowerCase()];
return Array.isArray(v) ? v[0] : v;
};

if (cfg.proxySecret) {
const sent = h("x-proxy-secret") ?? "";
return timingSafeEqualStr(sent, cfg.proxySecret)
? { ok: true }
: { ok: false, status: 403, reason: "bad_proxy_secret" };
}

const origin = h("origin");
const host = h("host");

if (!origin) return { ok: true };

if (cfg.allowedOrigins.includes(origin)) return { ok: true };
if (isSameOriginOrLocalhost(origin, host)) return { ok: true };

return { ok: false, status: 403, reason: "origin_not_allowed" };
}

export function readGuardConfigFromEnv(): GuardConfig {
const allowedOrigins = (process.env.ALLOWED_ORIGINS ?? "")
.split(",")
.map((s) => s.trim())
.filter(Boolean);
const proxySecret = process.env.PROXY_SECRET || undefined;
return { allowedOrigins, proxySecret };
}
16 changes: 12 additions & 4 deletions api/edb/[...path].ts → api/edb-proxy.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { VercelRequest, VercelResponse } from "@vercel/node";
import { maybeInjectDefaultEtherscanKey } from "../edbShared.js";
import { maybeInjectDefaultEtherscanKey } from "./edbShared.js";

export const config = {
api: { bodyParser: false },
Expand Down Expand Up @@ -104,9 +104,17 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
return res.status(405).json({ error: "method_not_allowed" });
}

// Extract sub-path from URL — more reliable than req.query.path across Vercel runtimes
const urlPath = (req.url || "").split("?")[0];
const subPath = urlPath.replace(/^\/api\/edb\/?/, "");
// Extract sub-path from the `path` query param populated by the Vercel
// rewrite rule in vercel.json. Vercel's file-based catch-all routing
// (`api/edb/[...path].ts`) does not reliably match multi-segment requests
// under `/api/edb/*` on this project, so we route via an explicit rewrite
// that mirrors the lifi-composer pattern.
const pathParam = req.query?.path;
const subPath = Array.isArray(pathParam)
? pathParam.join("/")
: typeof pathParam === "string"
? pathParam
: "";

// Validate each path segment
const parts = subPath ? subPath.split("/") : [];
Expand Down
Loading