From 56d31f6faefea8e392a2c17ba79949658e12fae6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn=20Kyne?= Date: Thu, 4 Jun 2026 23:35:32 +0100 Subject: [PATCH 1/6] Pass on the stack docs --- SUMMARY.md | 3 +- obol-stack/README.md | 7 + obol-stack/build-a-profitable-stack.md | 153 ++++++ obol-stack/faq.md | 2 +- obol-stack/installing-networks.md | 39 ++ obol-stack/quickstart.md | 10 +- obol-stack/selling-services.md | 186 +++++++ .../selling-training-from-google-colab.md | 517 ------------------ 8 files changed, 395 insertions(+), 522 deletions(-) create mode 100644 obol-stack/build-a-profitable-stack.md create mode 100644 obol-stack/selling-services.md delete mode 100644 obol-stack/selling-training-from-google-colab.md diff --git a/SUMMARY.md b/SUMMARY.md index bd646ca..33176a8 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -109,9 +109,10 @@ * [Introduction](obol-stack/README.md) * [Quickstart](obol-stack/quickstart.md) +* [Build a Profitable Obol Stack](obol-stack/build-a-profitable-stack.md) +* [Selling Agent Services](obol-stack/selling-services.md) * [Installing Networks](obol-stack/installing-networks.md) * [Installing Apps](obol-stack/installing-apps.md) -* [Selling training from Google Colab](obol-stack/selling-training-from-google-colab.md) * [FAQ](obol-stack/faq.md) ## Walkthrough Guides diff --git a/obol-stack/README.md b/obol-stack/README.md index be42fdf..97b8fa4 100644 --- a/obol-stack/README.md +++ b/obol-stack/README.md @@ -124,6 +124,13 @@ Running full Ethereum nodes requires significant disk space. Mainnet execution c +---------------------------------------------------------+ ``` +## Where next + +- [Quickstart](quickstart.md) — install the stack, talk to your agent, and run `obol sell demo`. +- [Build a profitable Obol Stack](build-a-profitable-stack.md) — end-to-end: bounded archive node → index → paid service → specialized agent → listed on marketplaces. +- [Selling agent services](selling-services.md) — full orientation on the three `sell` shapes, x402 economics, ERC-8004 registration, and Telegram notifications. +- [Installing Networks](installing-networks.md) — sync Ethereum / Aztec, including bounded archives via `--since`. + ## Need assistance? If you have questions or encounter issues with the Obol Stack, head over to our [Discord](https://discord.gg/n6ebKsX46w) where a member of our team or the community will be happy to assist. diff --git a/obol-stack/build-a-profitable-stack.md b/obol-stack/build-a-profitable-stack.md new file mode 100644 index 0000000..9cb75c3 --- /dev/null +++ b/obol-stack/build-a-profitable-stack.md @@ -0,0 +1,153 @@ +--- +description: An end-to-end walkthrough — sync a bounded archive node, build a specialized quant agent on top, expose it as a paid service, and get it discoverable on agent marketplaces. +--- + +# Build a profitable Obol Stack + +This is the long-form narrative for an Obol Stack operator who wants to actually make money. It assumes you've finished the [Quickstart](quickstart.md) — you have `obol stack up` running, you can talk to your default Hermes agent, and you've watched `obol sell demo` settle a payment. + +The journey: + +1. Sync a **bounded archive node** of the chain your target application lives on, starting from the block it was deployed. +2. Build a **purpose-built index or feed** over that archive. +3. Wrap the index as a **paid HTTP service** (`obol sell http`). +4. Build a **specialized agent** on top that uses the index and a tight skill set to answer buyer questions better than any general-purpose agent could. +5. Sell the agent's replies via `obol sell agent` and **get listed** on the marketplaces where buyer-agents look. +6. Set up out-of-band notifications so your agent can ping you on **Telegram** when the work is done — you don't need to babysit it. +7. **Iterate**. + +You can use Claude Code as your operator throughout — the [`run-obol-stack` skill](https://github.com/ObolNetwork/skills/blob/main/skills/run-obol-stack/SKILL.md) drives all of this and knows the right next command at every step. + +## Step 1: pick the right archive scope + +The single most important decision is **what history you actually need**. A full mainnet archive from genesis is ~4 TB and weeks of sync — overkill for almost every realistic agent business. A bounded archive is the right default. + +If your target application is a DeFi protocol deployed at block `N`, install with `--mode=archive --since=`. You'll carry the state trie from `N` forward and nothing earlier: + +```shell +obol network install ethereum \ + --network=mainnet \ + --mode=archive \ + --since=22500000 # the block your app was deployed + +obol network sync ethereum +``` + +Pick `--since` based on what your agent needs to answer: + +- **A specific app deployed at block N** → `--since=N`. +- **Anything Cancun-or-later** → `--since=cancun` (~800 GB). +- **Anything from the last year** → `--since=365d` (~600 GB). +- **Recent execution-layer history for a quick indexer** → `--since=prague` (~0.4 TB). + +See [Installing Networks](installing-networks.md#archive-nodes-and-bounded-history-since) for the full `--since` reference. + +While the node syncs (hours to days depending on scope), keep moving. The other steps don't require the archive to be complete. + +## Step 2: build the index your agent will sell + +A raw archive node is not yet a product — it's storage. The product is **a query surface that returns useful, application-specific answers in milliseconds**. + +Two reliable shapes: + +- **An events index.** Subscribe to your app's contract logs from `--since` forward, decode them, and write them to a small Postgres or DuckDB. Buyers query "all positions opened between X and Y", "all liquidations involving address Z", etc. +- **A state snapshot service.** Run historical `eth_call` against the archive at the block ranges that matter, cache the results, and expose a stable HTTP endpoint that returns "what did this contract look like at this block?" without paying the archive node's full cost per query. + +Deploy whatever you build as a regular Kubernetes Service in your cluster — the `obol app install` command takes any Helm chart, and the `obol-app` chart in [`ObolNetwork/helm-charts`](https://github.com/ObolNetwork/helm-charts) packages an arbitrary Dockerfile for you. + +## Step 3: sell the index + +Once the index is healthy, wrap it as a paid HTTP service: + +```shell +obol sell http my-index \ + --upstream my-index-svc \ + --port 8080 \ + --namespace my-ns \ + --per-request 0.001 \ + --chain base \ + --token USDC \ + --wallet 0x...your-wallet... +``` + +Confirm it reconciles to `Ready`: + +```shell +obol sell status my-index --namespace my-ns +``` + +You now have a billable data feed. Some buyers — particularly other agents writing custom analyses — will pay for this directly. + +## Step 4: build the specialized quant agent + +The bigger margin lives one layer up. Buyers who don't want to write their own analysis will happily pay 10–50× more for an **agent that already knows what to do with the index**. + +Inside the Obol Agent, the levers that compound are: + +- **`SOUL.md`** — the agent's system prompt. The difference between "an LLM that knows about Ethereum" and "a quant agent that answers a specific class of questions reliably" is two well-edited paragraphs of `SOUL.md`. +- **Custom skills.** Add a skill that knows how to query your index, decode your app's events, and present the result in the format your buyers actually want. +- **Reference data.** Give the agent direct access to the index (and any other curated data you have) so it doesn't have to guess. + +The `run-obol-stack` skill calls this the **margin-bearing path**: the LLM is fungible, but a tight skill plus proprietary data is not. Iterate on `SOUL.md` and the skill until paid buyers consistently give thumbs-up answers. + +Once the agent is reliable, expose it as a paid agent service: + +```shell +obol sell agent my-quant \ + --instance my-quant \ + --per-request 0.05 \ + --token USDC \ + --chain base +``` + +Buyers pay per reply, not per token — and pay for the answer, not the inference. + +## Step 5: get listed + +Register your agent on [ERC-8004](https://eips.ethereum.org/EIPS/eip-8004) so buyers can find it: + +```shell +obol sell register \ + --chain mainnet \ + --name my-quant \ + --private-key-file ~/.config/obol/agents/my-quant/wallet.json +``` + +Registration writes to the on-chain Identity Registry. From there, multiple marketplaces and explorers index your offer: + +- **[x402scan.com](https://x402scan.com)** — the broadest cross-chain x402 registry. +- **[Coinbase Bazaar](https://www.coinbase.com/)** — Coinbase's USDC-on-Base agent marketplace. +- **The ERC-8004 contracts directly** — for buyer-agents written against the raw registry. + +You don't need to pick one marketplace; registering once gets you indexed by all of them. See [Selling agent services](selling-services.md#get-discoverable-erc-8004-and-marketplaces) for the longer marketplace orientation. + +## Step 6: tell your agent to ping you on Telegram + +By the time you have an archive node syncing in the background, an index updating, and a paid agent live on a tunnel — you probably don't want to sit and watch a terminal. `obol hermes setup` wires Hermes to a Telegram bot (or Discord, Slack, etc.) so it can message you when things change. + +The Telegram bot flow involves talking to two other Telegram bots: + +1. **Talk to `@BotFather`** in Telegram (`https://t.me/BotFather`). Send `/newbot`, follow the prompts to pick a name and a username for your bot, and you'll receive an **HTTP API token** (something like `1234567890:ABCdefGhIJKlmNoPQRstUvWXYZabcdefghi`). Save it — this is how Hermes will send messages **as** the bot. +2. **Talk to `@userinfobot`** in Telegram (`https://t.me/userinfobot`). It will reply with your numeric **Telegram user ID** (something like `123456789`). Save this — this is the chat the bot will message. +3. **Start a chat with your new bot** by clicking through the link `@BotFather` gave you, and send it `/start`. Telegram won't let a bot DM you until you initiate the conversation. +4. **Run `obol hermes setup`** and paste both the bot token and your numeric user ID when prompted. + +Test it: ask the agent to ping you when the next sync milestone hits. If you get a Telegram message, you're done — you can close the laptop and let the agent work. + +## Step 7: iterate + +The compounding loop: + +- Watch which buyer questions your agent answers poorly — that's the prompt for the next `SOUL.md` edit or skill improvement. +- Watch which queries against your index are popular — that's the prompt for the next index expansion (more contracts decoded, deeper historical coverage, etc.). +- Watch your revenue per buyer. Specialized work is not price-sensitive in the way raw inference is — once you're reliably better than the alternatives, raise the price. +- When you stop an offer, **drain it gracefully** (`obol sell stop --grace 1h`). On-chain buyers' reputation systems penalise abrupt teardown. + +A profitable Obol Stack is one where the operator is *bored*. The agent syncs the data, builds the index, sells the queries, sells the replies, pings the operator on Telegram when something interesting happens, and otherwise gets on with it. + +## Where to go from here + +- [Selling agent services](selling-services.md) — the depth on `obol sell`, x402 economics, and marketplaces. +- [Installing Networks](installing-networks.md) — full archive `--since` reference and multi-network setup. +- [Installing Apps](installing-apps.md) — deploying any Helm chart (your index, your custom services) into the stack. +- [`run-obol-stack` Claude Code skill](https://github.com/ObolNetwork/skills/blob/main/skills/run-obol-stack/SKILL.md) — drive the entire flow from your terminal, with a Claude that already knows the right next command. diff --git a/obol-stack/faq.md b/obol-stack/faq.md index f51bef5..84f5d52 100644 --- a/obol-stack/faq.md +++ b/obol-stack/faq.md @@ -164,7 +164,7 @@ USDC and other tokens settle on the rail their issuer supports (EIP-3009 for USD obol sell register --chain mainnet --name my-service --private-key-file ``` -This publishes the agent's wallet + service catalogue to the [ERC-8004](https://eips.ethereum.org/EIPS/eip-8004) Identity Registry on the chain you specify. Note that this requires ETH on the registering wallet for gas. +This publishes the agent's wallet + service catalog to the [ERC-8004](https://eips.ethereum.org/EIPS/eip-8004) Identity Registry on the chain you specify. Note that this requires ETH on the registering wallet for gas. `obol sell demo` deliberately skips registration by default — run `obol sell register` later when you want on-chain discovery. diff --git a/obol-stack/installing-networks.md b/obol-stack/installing-networks.md index 865958a..5cd3450 100644 --- a/obol-stack/installing-networks.md +++ b/obol-stack/installing-networks.md @@ -86,6 +86,8 @@ Deploy a full Ethereum node with configurable execution and consensus clients. | `--network` | Ethereum network | mainnet, sepolia, hoodi | mainnet | | `--execution-client` | Execution layer client | reth, geth, nethermind, besu, erigon, ethereumjs | reth | | `--consensus-client` | Consensus layer client | lighthouse, prysm, teku, nimbus, lodestar, grandine | lighthouse | +| `--mode` | Pruning mode | full, archive | full | +| `--since` | Lower bound for archive history (requires `--mode=archive`) | fork name, duration, block number, `genesis`/`all` | (interactive picker on TTY; `all` on non-TTY) | ### Examples @@ -133,6 +135,43 @@ obol network sync ethereum/hoodi Full Ethereum nodes require significant resources. Mainnet execution clients need 1+ TB of storage and can take days to sync. Consider using testnets for development. {% endhint %} +### Archive nodes and bounded history (`--since`) + +Pruned full nodes are sufficient for everyday RPC use, but they cannot serve historical state. If you plan to **index events, run historical `eth_call`, build a block explorer, or back a query service with a specific app's history**, you need an archive node — and you almost certainly do not need it all the way back to genesis. + +`--mode=archive` switches reth to archive mode. `--since` bounds the archive at a known starting point, so you only carry the history you actually need. + +```shell +# Archive back to the Cancun hardfork (~800 GB on mainnet) +obol network install ethereum --network=mainnet --mode=archive --since=cancun + +# Archive of the last 365 days (~600 GB) +obol network install ethereum --network=mainnet --mode=archive --since=365d + +# Archive from a specific block forward (e.g. the block your DeFi app was deployed) +obol network install ethereum --network=mainnet --mode=archive --since=22500000 + +# Full archive from genesis (~4 TB+ on mainnet) +obol network install ethereum --network=mainnet --mode=archive --since=all +``` + +Accepted `--since` values: + +| Form | Example | Meaning | +| --- | --- | --- | +| EL fork name | `merge`, `shanghai`, `cancun`, `prague`, `osaka` | Prune state before that mainnet hardfork. | +| Duration | `365d`, `1y`, `6mo` | Keep approximately the last N blocks (~12s slot rate). | +| Block number | `22500000` | Prune state before that block. | +| `genesis` / `all` | `all` | Full archive from genesis. | + +{% hint style="info" %} +Fork-name presets reference **mainnet** block numbers. On testnets, use a raw block number or a duration. +{% endhint %} + +When `--mode=archive` is set without `--since` on a TTY, the installer shows an interactive picker. On non-TTY (scripts, CI), the default is `all`. `--since` is currently fine-tuned for **reth**; other execution clients fall back to their chart-default pruning behavior with a warning. + +This pairs naturally with [Selling agent services](selling-services.md) — once your archive node has synced from the block your target application was deployed, you have a defensible data set that no public RPC will serve, and you can wrap it as a paid endpoint or a specialized agent. + ### Check sync status ```shell diff --git a/obol-stack/quickstart.md b/obol-stack/quickstart.md index 29c5063..822e8d0 100644 --- a/obol-stack/quickstart.md +++ b/obol-stack/quickstart.md @@ -101,7 +101,7 @@ obol hermes --help # full Hermes CLI surface ``` {% hint style="success" %} -Want the agent to message you on Telegram, Discord, or Slack? Run `obol hermes setup` and follow the prompts to wire up a chat-app integration. Hermes will then notify you when long-running work finishes. +Want the agent to message you on Telegram, Discord, or Slack? Run `obol hermes setup` and follow the prompts to wire up a chat-app integration. Hermes will then notify you when long-running work finishes. The full Telegram bot flow (which involves talking to `@BotFather` and `@userinfobot` first) is covered in [Build a profitable Obol Stack](build-a-profitable-stack.md#step-6-tell-your-agent-to-ping-you-on-telegram). {% endhint %} The agent has its own Ethereum wallet — back it up before you put anything on it: @@ -142,6 +142,8 @@ obol sell http my-api --upstream my-svc --port 8080 --namespace my-ns \ The mental model is: **anything in your cluster that exposes a Service can be wrapped in a `ServiceOffer` and gated behind x402**. The goal of v0.9 is to make that loop short enough that you can actually iterate on what's worth selling. +See [Selling agent services](selling-services.md) for the full orientation on the three `sell` shapes (`http`, `inference`, `agent`), x402 economics, ERC-8004 registration, and marketplaces. + ### Why $OBOL on mainnet? Buyers paying in `$OBOL` on Ethereum mainnet sign an [EIP-2612](https://eips.ethereum.org/EIPS/eip-2612) permit off-chain, and the Obol-operated facilitator batches that permit with the on-chain transfer at settlement time. **Buyers never need ETH for gas**, and they skip the one-time `approve` step that most ERC-20 payment flows require. @@ -156,7 +158,7 @@ Sellers receive `$OBOL` directly into their agent wallet. Read more about the [O obol sell register --chain mainnet --name my-service --private-key-file ``` -This publishes the agent's wallet + service catalogue to the [ERC-8004](https://eips.ethereum.org/EIPS/eip-8004) Identity Registry on the chain you specify. +This publishes the agent's wallet + service catalog to the [ERC-8004](https://eips.ethereum.org/EIPS/eip-8004) Identity Registry on the chain you specify. ## Step 5: Drive the stack from Claude Code (optional) @@ -207,6 +209,8 @@ obol stack purge -f # remove everything, including data ## Next steps -* [Installing Networks](installing-networks.md) — sync local Ethereum / Aztec nodes. +* [Build a profitable Obol Stack](build-a-profitable-stack.md) — the end-to-end narrative: sync a bounded archive node, build an index, wrap it as a paid service, and turn it into a specialized agent business. +* [Selling agent services](selling-services.md) — depth on the three `sell` shapes, x402 economics, and getting listed on marketplaces. +* [Installing Networks](installing-networks.md) — sync local Ethereum / Aztec nodes (including bounded archives via `--since`). * [Installing Apps](installing-apps.md) — deploy any Helm chart. * [FAQ](faq.md) — common questions and troubleshooting. diff --git a/obol-stack/selling-services.md b/obol-stack/selling-services.md new file mode 100644 index 0000000..4b850f5 --- /dev/null +++ b/obol-stack/selling-services.md @@ -0,0 +1,186 @@ +--- +description: Sell agent services from your Obol Stack — payment-gated HTTP endpoints, inference, and full agent businesses paid in OBOL or USDC over x402. +--- + +# Selling agent services + +The Obol Stack turns any pod, model, or sub-agent in your local cluster into a payment-gated HTTP service that buyers — humans or other agents — can pay for over [x402](https://www.x402.org/) micropayments. This page is the orientation for sellers: how to ship your first paid service, which `obol sell` shape fits which kind of agent business, and how to get listed on the marketplaces buyers actually use. + +The mental model is: **anything in your cluster that exposes a Kubernetes Service can be wrapped in a `ServiceOffer` and gated behind x402**. Run `obol sell demo` to see the full loop settle end-to-end, then wrap your own services. + +{% hint style="info" %} +The selling subsystem is alpha software. Run smoke tests on `base-sepolia` before quoting a buyer real prices on mainnet. +{% endhint %} + +## Start here: `obol sell demo` + +`obol sell demo` is the "Hello World" of selling on the Obol Stack. It deploys a tiny HTTP service behind an x402 payment gate, exposes it through a Cloudflare quick tunnel, and prints copy-paste instructions for paying it as a buyer. + +```shell +obol sell demo # default: 1 OBOL/req on Ethereum mainnet (gas-sponsored) +obol sell demo blocks # 0.0001 USDC/req on base-sepolia (live chain data via eRPC) +obol sell demo quant # 0.01 USDC/req on base-sepolia (agent-driven analysis report) +``` + +The output walks you through: + +1. The public URL where the gated endpoint lives (`https://.trycloudflare.com/services/demo-hello/...`). +2. A `curl` snippet that hits the endpoint and gets back `HTTP 402 Payment Required` with the price. +3. A `python` snippet using the [x402 SDK](https://github.com/coinbase/x402) to pay and consume the response. + +Watch the offer reconcile: + +```shell +obol sell list +obol sell status demo-hello +``` + +Once a single paid request settles end-to-end, you understand the loop. Everything below is variations on this same machinery. + +## Pick the right `sell` shape + +Three shapes, three different agent businesses: + +### `obol sell http` — sell access to indexed data, an API, or a dashboard + +Wrap any HTTP endpoint in your cluster — an index built from your archive node, a custom analytics API, a curated data feed — and charge per request. + +```shell +obol sell http my-index \ + --upstream my-svc \ + --port 8080 \ + --namespace my-ns \ + --per-request 0.001 \ + --chain base \ + --token USDC \ + --wallet 0x...your-wallet... +``` + +This is the path to take when **the value is in the data**: you've synced an archive node from a specific block, built an index on top, and you want to sell access to queries against it that no public RPC will serve. + +### `obol sell inference` — sell raw LLM completions + +Wrap your cluster's LiteLLM (which already routes to local Ollama and/or your configured cloud providers) and charge per million tokens or per request. + +```shell +obol sell inference my-model \ + --model qwen3.5:9b \ + --per-mtok 1.25 \ + --token USDC \ + --chain base +``` + +This is the path when **the value is in the compute**: you've got a GPU, or a model under license, or a low-latency setup, and you want to sell raw token completions to anyone who needs them. + +### `obol sell agent` — sell an agent business + +Wrap a running agent — with its skills, its reference data, and its memory — as a paid OpenAI-compatible endpoint. Buyers don't pay for tokens; they pay for **specialized work**. + +```shell +obol sell agent my-quant \ + --instance my-quant \ + --per-request 0.05 \ + --token USDC \ + --chain base +``` + +This is the **margin-bearing path**. The moat is not the LLM — buyers can rent any model cheaply. The moat is: + +- **Proprietary skills** you've written for this agent (the analysis methodology, the parsing tricks, the report format). +- **Reference data** the agent has access to — your archive-node index, your curated knowledge base, your historical results. +- **Iteration**: tighter `SOUL.md`, better tool selection, better failure handling, all guided by what real paying buyers ask for. + +A specialized quant agent that answers questions about your archive-node data set will out-earn a generic `qwen3.5:9b` endpoint by an order of magnitude on the same hardware — because the buyer is paying for the answer, not the inference. + +## Why pay in `$OBOL` on mainnet + +Buyers paying in `$OBOL` on Ethereum mainnet sign an [EIP-2612](https://eips.ethereum.org/EIPS/eip-2612) permit off-chain. The Obol-operated x402 facilitator batches that permit with the on-chain transfer at settlement time, so: + +- **Buyers never spend ETH on gas** for an OBOL payment. +- Buyers skip the one-time `approve(Permit2, max)` step that most ERC-20 payment flows require. +- Sellers receive `$OBOL` directly to their agent's wallet. + +USDC and other tokens settle on the rails their issuers support (EIP-3009 for USDC on Base, etc.). For everyday smoke tests, use **USDC on `base-sepolia`** — it's free and fast. For live mainnet sales, quote in **OBOL on Ethereum mainnet** (headline gasless UX) or **USDC on Base** (cheap real money). + +{% hint style="info" %} +When quoting prices, name the unit explicitly — `0.01 OBOL / MTok`, `0.001 USDC / request`. Never write `$0.01`; the payment rail matters and the conversion depends on it. +{% endhint %} + +## Get discoverable: ERC-8004 and marketplaces + +Buyers need to find you. The Obol Stack publishes an [ERC-8004](https://eips.ethereum.org/EIPS/eip-8004) agent registration document at `/.well-known/agent-registration.json` describing your services, supported payment methods, and endpoints. To list your agent on a public registry: + +```shell +obol sell register \ + --chain mainnet \ + --name my-quant \ + --private-key-file ~/.config/obol/agents/my-quant/wallet.json +``` + +This publishes your agent's wallet and service catalog to the ERC-8004 Identity Registry on the chain you specify. **Note that this requires ETH on the registering wallet for gas** — registration is not gas-sponsored. + +`obol sell demo` deliberately skips registration by default (to avoid double-register reverts and the need for ETH on the agent wallet). Run `obol sell register` later when you want on-chain discovery. + +### Where buyers actually look + +Once registered, several marketplaces and explorers index ERC-8004 registries and x402-gated endpoints: + +- **[x402scan.com](https://x402scan.com)** — the broadest registry of x402-gated services across chains. Indexes the on-chain registry automatically once you've registered. +- **[Coinbase Bazaar](https://www.coinbase.com/)** — Coinbase's agent marketplace, focused on Base. Surfaces USDC-on-Base agent services to its buyer-side audience. +- **The ERC-8004 contracts directly** — for buyer-agents written against the raw on-chain registry, `obol sell register` is all you need. The registry is the source of truth; marketplaces are just frontends. + +We do not recommend a single "official" marketplace — the agent-registry ecosystem is evolving fast, and the best strategy is to register on-chain and let multiple indexers pick you up. + +## Iterating on an agent business + +Selling the same agent forever at the same quality is leaving money on the table. The loop that compounds: + +1. **Watch what buyers ask.** Log every paid request body and response. Pay attention to questions your agent gives a mediocre answer to. +2. **Improve the skill, not the model.** Most quality gains come from a sharper `SOUL.md`, better tool selection in the agent's skill, and richer reference data — not from upgrading the LLM behind it. +3. **Expand the reference data.** If your quant agent is good because of an archive-node index, add more indexes. If it's good at parsing a specific contract's events, parse more of them. +4. **Tighten the price.** Once the agent is reliably better than the alternatives, raise the per-request price. The buyers paying for specialized work are not price-sensitive in the way buyers paying for raw inference are. +5. **Drain old offers gracefully.** Use `obol sell stop --grace 1h` so in-flight buyers can wind down; only `--force` when you need to reclaim a path immediately. Abrupt teardown is a worse reputation signal on-chain than a drain. + +## Lifecycle: drain, delete, replace + +```shell +# Drain gracefully — advertises wind-down via discovery, then tears down the route +obol sell stop my-quant --namespace my-ns --grace 1h + +# Force-stop immediately (worse reputation signal) +obol sell stop my-quant --namespace my-ns --force + +# Delete the offer and clean up +obol sell delete my-quant --namespace my-ns +``` + +Deletion removes the ServiceOffer CR, cascades the underlying Middleware and HTTPRoute via owner references, and deactivates the ERC-8004 registration (sets `active=false`). The agent's wallet and accumulated revenue are untouched. + +## Verifying your paths + +When you've shipped your first real (non-demo) offer, walk these checks: + +```shell +export TUNNEL_URL=$(obol tunnel status | grep -oE 'https://[a-z0-9-]+\.trycloudflare\.com') + +# Public frontend (should be 200 — but cluster-admin UI; do not expose deeper routes) +curl -s -o /dev/null -w "%{http_code}\n" "$TUNNEL_URL/" + +# Your paid route (should be 402 with payment requirements) +curl -s -w "\nHTTP %{http_code}\n" -X POST \ + "$TUNNEL_URL/services/my-quant/v1/chat/completions" \ + -H "Content-Type: application/json" \ + -d '{"model":"qwen3.5:9b","messages":[{"role":"user","content":"ping"}]}' + +# Your ERC-8004 registration document +curl -s "$TUNNEL_URL/.well-known/agent-registration.json" | jq . +``` + +A `402` with a JSON body containing `accepts[].asset`, `accepts[].amount`, and `accepts[].payTo` confirms the gate is live. + +## Going deeper + +- [Build a profitable Obol Stack](build-a-profitable-stack.md) — the end-to-end narrative from archive-node sync to a discoverable, billable specialized agent. +- [Installing Networks](installing-networks.md) — sync local Ethereum / Aztec / L2 nodes, including bounded archive nodes via `--since`. +- The source-repo guide [`obol-stack/docs/guides/monetize-inference.md`](https://github.com/ObolNetwork/obol-stack/blob/main/docs/guides/monetize-inference.md) covers the ServiceOffer condition state machine, the self-hosted x402 facilitator path, and the buyer-side `blockrun-llm` SDK in depth. +- The Claude Code skill [`run-obol-stack`](https://github.com/ObolNetwork/skills/blob/main/skills/run-obol-stack/SKILL.md) drives all of this from your terminal end-to-end. diff --git a/obol-stack/selling-training-from-google-colab.md b/obol-stack/selling-training-from-google-colab.md deleted file mode 100644 index 13e0c8f..0000000 --- a/obol-stack/selling-training-from-google-colab.md +++ /dev/null @@ -1,517 +0,0 @@ ---- -description: Attempted walkthrough for selling autoresearch training from a Google Colab T4 GPU through Obol Stack ---- - -# Selling training from Google Colab (T4) through Obol Stack - -This guide walks through the current best-effort path for selling GPU-backed autoresearch experiment execution from a Google Colab T4 runtime through Obol Stack. - -It is written from an actual implementation attempt around the current `feat/autoresearch-integration` work in `obol-stack`. - -## What this guide is - -This is a practical guide for the **GPU contributor / seller** persona: - -- you have access to a Google Colab T4 GPU -- you want to run the `autoresearch-worker` there -- you want Obol Stack to publish a paid x402-gated endpoint for that worker -- you want the worker to be discoverable through ERC-8004 registration metadata - -## What was validated while preparing this guide - -Validated in the `obol-stack` PR branch behind this guide: - -- `autoresearch-worker` exists as a real worker API -- the worker runs one experiment at a time and stores results on disk -- `obol sell http` can attach richer registration metadata -- canonical provenance support is wired through the stack -- the autoresearch coordinator now works against the current 8004scan and remote-signer APIs -- local persona tests cover: - - GPU seller / worker flow - - researcher / coordinator flow - - service-builder + consumer flow using a resume/CV enhancer example -- the x402 BDD integration harness was extended toward richer registry-backed discovery coverage - -## What was NOT fully executed from the autonomous environment - -The following still require an interactive Google session or real GPU runtime access: - -- signing into Google Colab and attaching a T4 runtime -- running a public tunnel from the Colab notebook for a long-lived seller session -- keeping that Colab runtime alive long enough to act as a stable marketplace worker -- a full end-to-end paid training sale from a live Colab notebook through the production stack - -So this guide is: -- **implementation-backed**, -- **honest about current limitations**, -- and the best current path to try this on a real machine. - ---- - -## Architecture - -At the moment, the most reliable setup is: - -```text -Google Colab T4 - └── autoresearch-worker API on :8080 - └── exposed with ngrok - └── public Colab worker URL - -Local machine running Obol Stack - └── small in-cluster relay service - └── proxies HTTP inside the cluster to the Colab HTTPS tunnel URL - └── monetized with `obol sell http` - └── x402 gate + route + ERC-8004 registration -``` - -### Why use an in-cluster relay? - -Today, `obol sell http` expects an in-cluster Kubernetes Service and the built-in upstream health check uses: - -```text -http://..svc.cluster.local: -``` - -That means the cleanest current path is not pointing Obol directly at an arbitrary external HTTPS URL. -Instead, you run a tiny relay in the cluster and let Obol monetize that relay. - -This is a current implementation constraint, not a fundamental product requirement. - ---- - -## Prerequisites - -### On your local machine - -- Obol Stack running -- OpenClaw agent initialized -- Docker available -- a wallet you want to receive USDC to -- ngrok account and authtoken - -### In Google Colab - -- a notebook with GPU runtime enabled -- a T4 runtime (or equivalent Colab GPU) -- enough credits to keep the notebook alive during the test - ---- - -## Step 1: Prepare your local Obol Stack machine - -Start the stack and the agent: - -```bash -obol stack init -obol stack up -obol agent init -``` - -Confirm the cluster is healthy: - -```bash -obol kubectl get pods -A -obol tunnel status -``` - -If you plan to register the worker publicly, make sure the stack has a reachable public base URL or tunnel. - ---- - -## Step 2: Start the worker in Google Colab - -Open a Colab notebook, switch to a GPU runtime, and use a T4 if available. - -### 2.1 Install the worker dependencies - -In a Colab cell: - -```python -!apt-get update -qq -!apt-get install -y git curl python3-venv > /dev/null -!curl -LsSf https://astral.sh/uv/install.sh | sh -!pip -q install pyngrok -``` - -### 2.2 Clone `obol-stack` - -```python -!git clone https://github.com/ObolNetwork/obol-stack.git -%cd obol-stack -``` - -Use a version that includes the `autoresearch-worker` files. - -### 2.3 Prepare or copy your autoresearch workspace - -The worker expects a repo/workdir mounted at `/data/autoresearch` in the containerized form, but in Colab you can simply pick a working directory. - -For example: - -```python -!mkdir -p /content/data/autoresearch -``` - -If you already have an autoresearch workspace, sync it into that directory. - -### 2.4 Start the worker API - -```python -import subprocess - -proc = subprocess.Popen([ - "python3", - "internal/embed/skills/autoresearch-worker/scripts/worker_api.py", - "serve", - "--repo", "/content/data/autoresearch", - "--data-dir", "/content/data", - "--host", "0.0.0.0", - "--port", "8080", - "--timeout", "300", - "--train-command", "python3 train.py", -]) -print("worker started", proc.pid) -``` - -### 2.5 Verify the worker in the notebook - -```python -import json, urllib.request -print(json.loads(urllib.request.urlopen("http://127.0.0.1:8080/health").read())) -``` - -You should see an `ok` health payload. - ---- - -## Step 3: Expose the Colab worker with ngrok - -ngrok has a documented Google Colab flow and is the easiest current option here. - -### 3.1 Add your ngrok authtoken - -```python -from pyngrok import ngrok -ngrok.set_auth_token("YOUR_NGROK_AUTHTOKEN") -``` - -### 3.2 Start a tunnel to the worker - -```python -from pyngrok import ngrok - -tunnel = ngrok.connect(8080) -print(tunnel.public_url) -``` - -Record the URL. Example: - -```text -https://example.ngrok-free.app -``` - -### 3.3 Probe the worker through ngrok - -```python -import json, urllib.request -req = urllib.request.Request( - tunnel.public_url + "/experiment", - data=json.dumps({"probe": True}).encode(), - headers={"Content-Type": "application/json"}, - method="POST", -) -print(json.loads(urllib.request.urlopen(req).read())) -``` - -If that works, the Colab worker is reachable publicly. - ---- - -## Step 4: Create an in-cluster relay to the Colab worker - -Because `obol sell http` expects an in-cluster Service, create a tiny relay in the cluster that forwards to the ngrok URL. - -Replace `example.ngrok-free.app` below with your actual ngrok hostname. - -### 4.1 Create the relay ConfigMap - -```bash -cat <<'EOF' | obol kubectl apply -f - -apiVersion: v1 -kind: ConfigMap -metadata: - name: colab-worker-relay - namespace: llm -data: - app.py: | - import os - from http.server import BaseHTTPRequestHandler, HTTPServer - from urllib.request import Request, urlopen - from urllib.error import HTTPError - - TARGET = os.environ["TARGET_BASE"] - - class Handler(BaseHTTPRequestHandler): - def do_GET(self): - target = TARGET + self.path - req = Request(target, method="GET") - try: - with urlopen(req, timeout=30) as resp: - body = resp.read() - self.send_response(resp.status) - for k, v in resp.headers.items(): - if k.lower() != "transfer-encoding": - self.send_header(k, v) - self.end_headers() - self.wfile.write(body) - except HTTPError as e: - body = e.read() - self.send_response(e.code) - self.end_headers() - self.wfile.write(body) - - def do_POST(self): - length = int(self.headers.get("Content-Length", "0") or "0") - body = self.rfile.read(length) - target = TARGET + self.path - headers = {k: v for k, v in self.headers.items() if k.lower() != "host"} - req = Request(target, data=body, headers=headers, method="POST") - try: - with urlopen(req, timeout=300) as resp: - out = resp.read() - self.send_response(resp.status) - for k, v in resp.headers.items(): - if k.lower() != "transfer-encoding": - self.send_header(k, v) - self.end_headers() - self.wfile.write(out) - except HTTPError as e: - out = e.read() - self.send_response(e.code) - self.end_headers() - self.wfile.write(out) - - HTTPServer(("0.0.0.0", 8080), Handler).serve_forever() -EOF -``` - -### 4.2 Create the relay Deployment + Service - -```bash -cat <<'EOF' | obol kubectl apply -f - -apiVersion: apps/v1 -kind: Deployment -metadata: - name: colab-worker-relay - namespace: llm -spec: - replicas: 1 - selector: - matchLabels: - app: colab-worker-relay - template: - metadata: - labels: - app: colab-worker-relay - spec: - containers: - - name: relay - image: python:3.12-slim - command: ["python", "/app/app.py"] - env: - - name: TARGET_BASE - value: "https://example.ngrok-free.app" - ports: - - containerPort: 8080 - volumeMounts: - - name: app - mountPath: /app - volumes: - - name: app - configMap: - name: colab-worker-relay ---- -apiVersion: v1 -kind: Service -metadata: - name: colab-worker-relay - namespace: llm -spec: - selector: - app: colab-worker-relay - ports: - - name: http - port: 8080 - targetPort: 8080 -EOF -``` - -### 4.3 Verify the relay - -```bash -obol kubectl get pods -n llm -l app=colab-worker-relay -obol kubectl exec -n llm deploy/colab-worker-relay -- python - <<'PY' -import json, urllib.request -print(json.loads(urllib.request.urlopen('http://127.0.0.1:8080/health').read())) -PY -``` - -If this works, the cluster can reach the Colab worker. - ---- - -## Step 5: Monetize the training worker - -Once the relay is healthy, sell it through Obol Stack: - -```bash -obol sell http autoresearch-worker \ - --namespace llm \ - --upstream colab-worker-relay \ - --port 8080 \ - --health-path /health \ - --wallet 0xYOUR_WALLET \ - --chain base-sepolia \ - --per-hour 0.50 \ - --path /services/autoresearch-worker \ - --register \ - --register-name "Colab T4 Worker" \ - --register-description "Google Colab T4 worker for paid autoresearch experiments" \ - --register-skills machine_learning/model_optimization \ - --register-domains technology/artificial_intelligence/research \ - --register-metadata gpu=T4 \ - --register-metadata framework=autoresearch \ - --register-metadata runtime=google-colab -``` - -Then check status: - -```bash -obol sell status autoresearch-worker -n llm -obol kubectl get serviceoffers.obol.org autoresearch-worker -n llm -o yaml -``` - -You want the conditions to progress to `Ready=True`. - ---- - -## Step 6: Probe the paid endpoint - -Once the offer is ready, probe it from the local machine or from inside the agent: - -```bash -curl -i -X POST http://obol.stack:8080/services/autoresearch-worker/experiment \ - -H 'Content-Type: application/json' \ - -d '{"probe": true}' -``` - -Expected: -- `402 Payment Required` -- x402 pricing fields in the response body/headers - -That confirms the seller path is live. - ---- - -## Step 7: Submit a real experiment - -Once the coordinator or a buyer is wired to the endpoint, the worker expects a payload like: - -```json -{ - "train_py": "print('training script here')", - "config": { - "batch_size": 64, - "learning_rate": 0.001 - } -} -``` - -In a full researcher flow, the autoresearch coordinator does this for you and attaches payment. - ---- - -## Operational caveats - -### 1. Colab sessions are not stable seller infrastructure - -Google Colab runtimes can: -- stop unexpectedly -- time out -- disconnect tunnels -- rotate public URLs - -This makes Colab good for: -- prototyping -- validating the worker UX -- testing the marketplace path - -It is not ideal for long-lived sellers. - -### 2. The relay is a current workaround - -Today, the relay exists because `obol sell http` expects an in-cluster Service. -A future improvement would be first-class support for external HTTPS upstreams. - -### 3. Pricing is approximate - -Current `--per-hour` pricing is converted into a request price using the 5-minute experiment budget assumption. -That is good enough for the current autoresearch worker model, but it is still an approximation. - ---- - -## Recommended next step after the seller works - -Once the Colab worker is sellable, the next downstream path is: - -1. run remote autoresearch experiments through the paid worker -2. publish the best result with canonical provenance -3. expose optimized inference -4. build an x402-gated application on top (for example the resume/CV enhancer flow) - -This is where the resume example becomes useful as the **service-builder + consumer** persona path. - ---- - -## Troubleshooting - -### The relay health check fails - -Check: - -```bash -obol kubectl logs -n llm deploy/colab-worker-relay -``` - -Most common causes: -- the ngrok URL changed -- the Colab notebook went idle -- the worker process crashed in Colab - -### `obol sell http` never becomes Ready - -Inspect: - -```bash -obol kubectl get serviceoffers.obol.org autoresearch-worker -n llm -o yaml -obol kubectl get events -n llm -``` - -### The Colab worker is reachable directly but not through Obol - -Verify each hop separately: - -1. Colab localhost → worker health -2. ngrok URL → worker health -3. relay pod → ngrok health -4. `obol stack` route → x402 402 response - ---- - -## Final note - -This guide reflects the current state of the implementation work. -It is the best practical path today, but not yet the final product experience. - -The core seller pieces are now in place. The remaining work is mostly around packaging, smoother external-upstream support, and more end-to-end production validation. From c244a59a1d0ec2774814b74f84b59c2359aab9a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn=20Kyne?= Date: Thu, 4 Jun 2026 23:40:13 +0100 Subject: [PATCH 2/6] Update security sections --- .../security/bug-bounty.md | 218 ++++++++---------- .../security/contact.md | 2 +- .../security/ev-assessment.md | 4 +- .../security/overview.md | 2 +- .../security/smart-contract-audit.md | 4 + .../security/threat_model.md | 2 +- 6 files changed, 107 insertions(+), 125 deletions(-) diff --git a/advanced-and-troubleshooting/security/bug-bounty.md b/advanced-and-troubleshooting/security/bug-bounty.md index accdc2d..8dd1798 100644 --- a/advanced-and-troubleshooting/security/bug-bounty.md +++ b/advanced-and-troubleshooting/security/bug-bounty.md @@ -1,17 +1,19 @@ --- sidebar_position: 3 -description: Bug Bounty Policy +description: Obol's bug bounty program, scope, and reward structure for security researchers reporting vulnerabilities in our distributed validator software. --- # Obol Bug Bounty Program ## Overview -At Obol Labs, we prioritize the security of our distributed validator software and related services. Our Bug Bounty Program is designed to encourage and reward security researchers for identifying and reporting potential vulnerabilities. This initiative supports our commitment to the security and integrity of our products. +At Obol, we prioritize the security of our distributed validator software and the staking capital that depends on it. Our Bug Bounty Program rewards security researchers who find and responsibly disclose vulnerabilities in the software, smart contracts, and supporting services that operators and delegators rely on. + +Because an Obol Distributed Validator (DV) is an independent BFT cluster — not a shared network with global liveness or a pooled balance sheet — our threat model and reward structure are different from those of an L1 chain or a defi protocol. We reward findings that map to the way DVs actually fail. ## Participant Eligibility -Participants must meet the following criteria to be eligible for the Bug Bounty Program: +Participants must: - Not reside in countries where participation in such programs is prohibited. - Be at least 14 years of age and possess the legal capacity to participate. @@ -20,28 +22,40 @@ Participants must meet the following criteria to be eligible for the Bug Bounty ## Scope of the Program -Eligible submissions must involve software and services developed by Obol, specifically under the domains of: +Eligible submissions must concern software and services developed by Obol, specifically: -- Charon the DV Middleware Client -- Obol DV Launchpad and Public API -- Obol Splits Contracts -- Obol Labs hosted Public Relay Infrastructure +- [Charon](https://github.com/ObolNetwork/charon), the DV middleware client. +- The [Obol DV Launchpad](https://launchpad.obol.org) and the [Obol public API](https://api.obol.tech). +- The [Obol Splits](https://github.com/ObolNetwork/obol-splits) contracts (including the Obol Validator Manager). +- Obol-operated public relay infrastructure. +- Charon's cluster lifecycle commands, including the experimental `charon alpha edit` family. Submissions related to the following are considered out of scope: -- Social engineering -- Rate Limiting (Non-critical issues) -- Physical security breaches -- Non-security related UX/UI issues -- Third-party application vulnerabilities -- The [Obol](https://obol.org) static website or the Obol infrastructure -- The operational security of node operators running or using Obol software +- Social engineering of Obol staff or community members. +- Rate limiting and similar non-security UX issues. +- Physical security breaches. +- Non-security UX/UI issues. +- Third-party application or library vulnerabilities (please report upstream). +- The [obol.org](https://obol.org) static website and Obol's internal corporate infrastructure. +- The operational security of node operators running Obol software (operators are responsible for their own host security). +- The Obol Stack and its in-cluster components (Hermes, OpenClaw, x402 facilitator, Cloudflared, eRPC, etc.). The Obol Stack is alpha software and is not yet in scope for this bounty. + +## How we prioritize + +The Obol threat model treats **external attackers harming live DV clusters** as the highest priority — these are the failures that destroy delegator capital or take validators offline at scale. A cluster member acting irrationally against the cluster they themselves operate is a real but lower-priority risk: the cluster's BFT thresholds and slashing economics already make this self-defeating, and we deliberately don't over-reward findings that depend on a member being maliciously incompetent against their own stake. + +In practice, this means: + +- A vulnerability that lets an **outside attacker** slash, partition, or destabilize a DV is rewarded above a comparable-impact finding that requires a **threshold of operators to collude** against themselves. +- Direct vulnerabilities in Obol smart contracts (where capital lives) are rewarded above operational vulnerabilities of equivalent impact severity. +- Findings that survive careful review by a security-aware operator (e.g. a malicious cluster invite that passes inspection) are rewarded above findings that require the victim to opt into known-unsafe behavior (e.g. running with `--no-verify`). ## Program Rules - Submitted bugs must not have been previously disclosed publicly. - Only first reports of vulnerabilities will be considered for rewards; previously reported or known vulnerabilities are ineligible. -- The severity of the vulnerability, as assessed by our team, will determine the reward amount. See the "Rewards" section for details. +- The severity of the vulnerability, as assessed by our team, determines the reward amount. - Submissions must include a reproducible proof of concept. - The Obol security team reserves the right to determine the eligibility and reward for each submission. - Program terms may be updated at Obol's discretion. @@ -49,120 +63,84 @@ Submissions related to the following are considered out of scope: ## Rewards Structure -Rewards are issued based on the severity and impact of the disclosed vulnerability, determined at the discretion of Obol Labs. - -### Critical Vulnerabilities: Up to $100,000 - -A Critical-level vulnerability is one that has a severe impact on the security of the in-production system from an unauthenticated external attacker, and requires immediate attention to fix. Highly likely to have a material impact on validator private key security, and/or loss of funds. - -- High impact, high likelihood - -Impacts: - -- Attacker that is not a member of the cluster can successfully exfiltrate BLS (not K1) private key material from a threshold number of operators in the cluster. -- Attacker that is not a member of the cluster can achieve the production of arbitrary BLS signatures from a threshold number of operators in the cluster. -- Attacker can craft a malicious cluster invite capable of subverting even careful review of all data to steal funds during a deposit. -- Direct theft of any user funds, whether at-rest or in-motion, other than unclaimed yield -- Direct loss of funds -- Permanent freezing of funds (fix requires hard fork) -- Network not being able to confirm new transactions (Total network shutdown) -- Protocol insolvency - -### High Vulnerabilities: Up to $10,000 - -For significant security risks that impact the system from a position of low-trust and require a significant effort to fix. - -- High impact, medium likelihood -- Medium impact, high likelihood - -Impacts: - -- Attacker that is not a member of the cluster can successfully partition the cluster and keep the cluster offline indefinitely. -- Attacker that is not a member of the cluster can exfiltrate Charon ENR private keys. -- Attacker that is not a member of the cluster can destroy funds but cannot steal them. -- Unintended chain split (Network partition) -- Temporary freezing of network transactions by delaying one block by 500% or more of the average block time of the preceding 24 hours beyond standard difficulty adjustments -- RPC API crash affecting projects with greater than or equal to 25% of the market capitalization on top of the respective layer -- Theft of unclaimed yield -- Theft of unclaimed royalties -- Permanent freezing of unclaimed yield -- Permanent freezing of unclaimed royalties -- Temporary freezing of funds -- Retrieve sensitive data/files from a running server: - - blockchain keys - - database passwords - - (this does not include non-sensitive environment variables, open source code, or usernames) -- Taking state-modifying authenticated actions (with or without blockchain state interaction) on behalf of other users without any interaction by that user, such as: - - Changing cluster information - - Withdrawals - - Making trades - -### Medium Vulnerabilities: Up to $2,500 - -For vulnerabilities with a moderate impact, affecting system availability or integrity. - -- High impact, low likelihood -- Medium impact, medium likelihood -- Low impact, high likelihood - -Impacts: - -- Attacker that is a member of a cluster can exfiltrate K1 key material from another member. -- Attacker that is a member of the cluster can denial of service attack enough peers in the cluster to prevent operation of the validator(s) -- Attacker that is a member of the cluster can bias the protocol in a manner to control the majority of block proposal opportunities. -- Attacker can get a DV Launchpad user to inadvertently interact with a smart contract that is not a part of normal operation of the launchpad. -- Increasing network processing node resource consumption by at least 30% without brute force actions, compared to the preceding 24 hours -- Shutdown of greater than or equal to 30% of network processing nodes without brute force actions, but does not shut down the network -- Charon cluster identity private key theft -- Rogue node operator to penetrate and compromise other nodes to disturb the cluster’s lifecycle -- Charon public relay node is compromised and leads to cluster topologies getting discovered and disrupted -- Smart contract unable to operate due to lack of token funds -- Block stuffing -- Griefing (e.g. no profit motive for an attacker, but damage to the users or the protocol) -- Theft of gas -- Unbounded gas consumption -- Redirecting users to malicious websites (Open Redirect) - -### Low Vulnerabilities: Up to $500 - -For vulnerabilities with minimal impact, unlikely to significantly affect system operations. - -- Low impact, medium likelihood -- Medium impact, low likelihood - -Impacts: - -- Attacker can sometimes put a Charon node in a state that causes it to drop one out of every one hundred attestations made by a validator -- Attacker can display bad data on a non-interactive part of the launchpad. -- Contract fails to deliver promised returns, but doesn't lose value -- Shutdown of greater than 10% or equal to but less than 30% of network processing nodes without brute force actions, but does not shut down the network -- Changing details of other users (including modifying browser local storage) without already-connected wallet interaction and with significant user interaction such as: - - Iframing leading to modifying the backend/browser state (must demonstrate impact with PoC) -- Taking over broken or expired outgoing links such as: - - Social media handles, etc. -- Temporarily disabling user to access target site, such as: - - Locking up the victim from login - - Cookie bombing, etc. +Rewards are issued based on the severity and impact of the disclosed vulnerability, determined at the discretion of Obol Labs. Reward ceilings are guidelines; the exact payout reflects exploitability, blast radius, and the quality of the report. + +### Critical Vulnerabilities: Up to $50,000 + +A Critical finding gives an **external attacker** a path to large-scale loss of delegator funds or systemic harm across many clusters. High impact, high likelihood. + +Eligible impacts: + +- An external attacker (not a cluster member) can cause a Charon cluster to produce a slashable signature, or otherwise trigger a slashing event, without colluding with cluster operators. +- An external attacker can exfiltrate enough BLS validator key shares from a threshold of operators in a cluster to reconstruct a validator's full private key. +- A vulnerability in Obol Splits or the Obol Validator Manager allows direct theft of delegator or operator funds at rest or in flight. +- A vulnerability in the DV Launchpad or its dependencies allows an attacker to craft a cluster invite that passes careful operator review while diverting deposits or substituting withdrawal credentials. +- Remote code execution in Charon, exploitable from a peer or relay connection, without prior compromise of the host. + +### High Vulnerabilities: Up to $5,000 + +A High finding lets an **external attacker** take down a cluster's liveness, exfiltrate operator key material, or compromise infrastructure that many clusters share. High impact, medium likelihood; or medium impact, high likelihood. + +Eligible impacts: + +- An external attacker can partition a cluster and keep it offline indefinitely, even after operators take reasonable recovery steps. +- An external attacker can exfiltrate Charon ENR (SECP256K1 identity) private keys from a node without compromising the host operating system. +- An external attacker can destroy validator funds (e.g. force exit-with-loss) but cannot steal them. +- An external attacker compromises Obol-operated public relay infrastructure in a way that reveals cluster topologies, disrupts peer discovery across many clusters, or facilitates partitioning attacks against the clusters relying on it. +- An attacker exfiltrates pre-signed exit messages held by the Obol API and uses them to forcibly exit validators against the delegator's wishes (see [Centralization Risks](risks.md)). +- An attacker subverts a `charon alpha edit` ceremony (add/remove operators, add validators, recreate keys) such that the post-ceremony cluster is no longer controlled by its legitimate operators. +- Retrieval of sensitive operational secrets from a running Obol-operated service: BLS or ENR keys, database credentials, signing keys for the public API, etc. +- Authenticated, state-modifying actions on the DV Launchpad performed on behalf of another user without their interaction (changing cluster definitions, redirecting withdrawals, etc.). + +### Medium Vulnerabilities: Up to $1,000 + +A Medium finding requires either an **insider** (a malicious cluster member) or a constrained external position to cause cluster-level damage. These are real, but the BFT and slashing economics of a DV mean the attacker is usually harming themselves alongside their victims. High impact, low likelihood; medium impact, medium likelihood; low impact, high likelihood. + +Eligible impacts: + +- A cluster member can exfiltrate K1 (identity) or BLS key material from another member of the same cluster. +- A cluster member can DoS enough peers in their own cluster to take the validator offline, beyond what is possible by simply going offline themselves. +- A cluster member can bias the protocol to control a disproportionate share of block proposal opportunities or other duty assignment. +- A DV Launchpad user can be steered into interacting with a smart contract that is not part of the normal launchpad flow. +- A vulnerability in Obol Splits or the Obol Validator Manager prevents the contract from operating normally without permanent loss of funds (e.g. temporary freeze, denial of withdrawal under specific state). +- Block-stuffing-style or unbounded-gas vulnerabilities in Obol Splits. +- Charon cluster lock-file tampering accepted by a victim operator who is *not* running with `--no-verify`. +- An open-redirect or similar phishing aid on Obol-operated domains. + +### Low Vulnerabilities: Up to $250 + +Low-severity findings have minimal impact on the integrity of a DV cluster or the security of Obol's smart contracts. Low impact, medium likelihood; medium impact, low likelihood. + +Eligible impacts: + +- An attacker can occasionally put a Charon node into a state that causes it to drop a small fraction of attestations (e.g. one in a hundred). +- An attacker can display incorrect data on a non-interactive part of the DV Launchpad. +- An Obol Splits contract behaves suboptimally but does not lose value or block legitimate operations. +- Temporary lockout from a wallet-connected session on Obol-operated services that does not persist past a normal session refresh. +- Takeover of broken or expired outgoing links from Obol-operated content (e.g. abandoned social handles linked from official channels). +- Minor griefing or local-storage tampering that requires significant social interaction to land and does not modify server-side state. Rewards may be issued as cash, merchandise, or other forms of recognition, at Obol's discretion. Only one reward will be granted per unique vulnerability. -## The following activities are prohibited by this bug bounty program +## Prohibited testing + +The following activities are not authorized under this program: -- Any testing on mainnet or public testnet deployed code; all testing should be done on local-forks of either public testnet or mainnet -- Any testing with pricing oracles or third-party smart contracts -- Attempting phishing or other social engineering attacks against our employees and/or customers -- Any testing with third-party systems and applications (e.g. browser extensions) as well as websites (e.g. SSO providers, advertising networks) -- Any denial of service attacks that are executed against project assets -- Automated testing of services that generate significant amounts of traffic -- Public disclosure of an unpatched vulnerability in an embargoed bounty +- Any testing on mainnet or public testnet deployed code; all testing should be done on local forks. +- Any testing involving pricing oracles or third-party smart contracts. +- Phishing or other social engineering attacks against Obol employees, contractors, partners, or community members. +- Any testing against third-party systems, applications, browser extensions, or websites (including SSO providers and advertising networks). +- Denial-of-service attacks executed against Obol-operated infrastructure or the deployed contracts. +- Automated testing of services that generates significant traffic. +- Public disclosure of an unpatched vulnerability under an embargoed bounty. ## Submission process -To report a vulnerability, please contact us at security@obol.tech with: +To report a vulnerability, please contact us at [security@obol.tech](mailto:security@obol.tech) with: - A detailed description of the vulnerability and its potential impact. - Steps to reproduce the issue. -- Any relevant proof of concept code, screenshots, or documentation. +- Any relevant proof-of-concept code, screenshots, or documentation. - Your contact information. Incomplete reports may not be eligible for rewards. diff --git a/advanced-and-troubleshooting/security/contact.md b/advanced-and-troubleshooting/security/contact.md index 29a07ec..5bea72b 100644 --- a/advanced-and-troubleshooting/security/contact.md +++ b/advanced-and-troubleshooting/security/contact.md @@ -1,6 +1,6 @@ --- sidebar_position: 7 -description: Security details for the Obol Network +description: How to report a security incident or vulnerability to Obol, and where to find Obol's public security disclosures. --- # Contacts diff --git a/advanced-and-troubleshooting/security/ev-assessment.md b/advanced-and-troubleshooting/security/ev-assessment.md index d48ce41..fd6b692 100644 --- a/advanced-and-troubleshooting/security/ev-assessment.md +++ b/advanced-and-troubleshooting/security/ev-assessment.md @@ -1,6 +1,6 @@ --- sidebar_position: 5 -description: Software Development Security Assessment +description: Ethereal Ventures' assessment of Obol's software development lifecycle and operational security practices, with the team's responses to each recommendation. --- # Software Development at Obol @@ -65,7 +65,7 @@ Obol’s product consists of three main components, each run by its own team: a * [DV Launchpad](../../learn/intro/launchpad.md): A webapp to create and manage distributed validators. * [Charon](../../learn/charon/intro.md): A middleware client that enables operators to run distributed validators. -* [Solidity](../../learn/intro/obol-splits.mdx): Withdrawal and fee recipient contracts for use with distributed validators. +* [Solidity](../../learn/intro/obol-splits.md): Withdrawal and fee recipient contracts for use with distributed validators. ## Analysis - Cluster Setup and DKG diff --git a/advanced-and-troubleshooting/security/overview.md b/advanced-and-troubleshooting/security/overview.md index 927493f..a8904d8 100644 --- a/advanced-and-troubleshooting/security/overview.md +++ b/advanced-and-troubleshooting/security/overview.md @@ -1,6 +1,6 @@ --- sidebar_position: 1 -description: Security Overview +description: An overview of Obol's security posture — official domains, completed audits, threat model references, and the bug bounty program. --- # Overview diff --git a/advanced-and-troubleshooting/security/smart-contract-audit.md b/advanced-and-troubleshooting/security/smart-contract-audit.md index c9def31..fdbbf3b 100644 --- a/advanced-and-troubleshooting/security/smart-contract-audit.md +++ b/advanced-and-troubleshooting/security/smart-contract-audit.md @@ -1,3 +1,7 @@ +--- +description: Public audit reports for the Obol Splits and Obol Validator Manager smart contracts. +--- + # Smart Contract Audit The Obol Splits smart contracts have undergone multiple security audits to ensure the safety and reliability of the protocol. All audit reports are available in the [obol-splits audit directory](https://github.com/ObolNetwork/obol-splits/tree/main/audit). diff --git a/advanced-and-troubleshooting/security/threat_model.md b/advanced-and-troubleshooting/security/threat_model.md index 89f8360..f90756d 100644 --- a/advanced-and-troubleshooting/security/threat_model.md +++ b/advanced-and-troubleshooting/security/threat_model.md @@ -1,6 +1,6 @@ --- sidebar_position: 5 -description: Threat model for a Distributed Validator +description: A threat model for Charon as a distributed validator middleware — actors, attack surfaces, and the cryptographic and BFT properties that bound each risk. --- # Charon Threat Model From 809c22d60f1a7d944b13834a7eedba156cdb61f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn=20Kyne?= Date: Thu, 4 Jun 2026 23:44:52 +0100 Subject: [PATCH 3/6] Usability, spelling, agent friendliness pass --- CLAUDE.md | 3 +- README.md | 2 + SUMMARY.md | 1 + adv/_category_.json | 7 - adv/advanced/_category_.json | 5 - adv/advanced/quickstart-builder-api.mdx | 168 ------ adv/advanced/quickstart-sdk.mdx | 129 ----- adv/security/_category_.json | 5 - adv/security/smart_contract_audit.mdx | 495 ------------------ adv/troubleshooting/_category_.json | 5 - .../advanced/assign-ovm-roles.md | 8 +- .../advanced/ovm-predeploy.md | 4 +- .../community/staking-masters.md | 2 +- community-and-governance/community/techne.md | 2 +- .../token-distribution-and-liquidity.md | 6 +- gov/_category_.json | 7 - gov/community/_category_.json | 5 - gov/contribution/_category_.json | 5 - gov/governance/_category_.json | 5 - guides/_category_.json | 7 - guides/walkthroughs/_category_.json | 5 - learn/charon/charon-cli-reference.md | 2 +- learn/charon/charon-networking.md | 6 +- learn/charon/intro.md | 4 +- learn/charon/networking.mdx | 2 +- learn/further-reading/ethereum_and_dvt.md | 6 +- learn/further-reading/resources.md | 2 +- learn/intro/faq.mdx | 8 +- learn/intro/key-concepts.md | 16 +- learn/intro/launchpad.md | 8 +- learn/intro/obol-collective.md | 6 +- learn/intro/obol-incentives.md | 2 +- learn/intro/obol-splits.md | 2 +- learn/intro/obol-vs-others.md | 16 +- run-a-dv/integrations/dappnode.md | 4 +- run-a-dv/integrations/lido-csm.md | 44 +- .../lido-v3-stvault-for-node-operator.md | 6 +- run-a-dv/prepare/deployment-best-practices.md | 2 +- run-a-dv/running/activate-a-dv.md | 8 +- run-a-dv/running/claim-rewards.md | 4 +- run-a-dv/running/distribute-rewards.md | 4 +- run-a-dv/running/exit-a-dv.md | 154 +++--- run-a-dv/running/monitoring.md | 2 +- run-a-dv/running/request-withdrawal.md | 8 +- run-a-dv/start/create-a-dv-with-a-group.md | 66 +-- run-a-dv/start/obol-monitoring.md | 4 +- run-a-dv/start/quickstart_overview.md | 2 +- 47 files changed, 210 insertions(+), 1054 deletions(-) delete mode 100644 adv/_category_.json delete mode 100644 adv/advanced/_category_.json delete mode 100644 adv/advanced/quickstart-builder-api.mdx delete mode 100644 adv/advanced/quickstart-sdk.mdx delete mode 100644 adv/security/_category_.json delete mode 100644 adv/security/smart_contract_audit.mdx delete mode 100644 adv/troubleshooting/_category_.json delete mode 100644 gov/_category_.json delete mode 100644 gov/community/_category_.json delete mode 100644 gov/contribution/_category_.json delete mode 100644 gov/governance/_category_.json delete mode 100644 guides/_category_.json delete mode 100644 guides/walkthroughs/_category_.json diff --git a/CLAUDE.md b/CLAUDE.md index 6cc4c28..85a9ec6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -76,7 +76,8 @@ Two distinct content types used throughout the documentation: - **Conceptual articles** - Explain _why_ something exists or works. No steps. Confident and friendly tone. Structure: (1) intro, (2) what it is, (3) why it's essential, (4) related topics, (5) summary. ### Formatting Rules -- **Titles** follow sentence structure — only names and places are capitalized, e.g., `## Distributed key generation` not `## Distributed Key Generation` +- **Headings** use title case (including entries in `SUMMARY.md`), e.g., `## Distributed Key Generation`. Lowercase short function words inside multi-word headings (a, an, the, of, to, with, in, on, for, at, by, vs, and, or, but) unless they are the first word. Proper nouns and product names (e.g. *Obol*, *Charon*, *OBOL Token*, *DV Launchpad*) are capitalized everywhere. +- **Inline prose** uses sentence case — don't capitalize concept nouns mid-sentence ("distributed validator", "key share", "cluster lock file"); only proper nouns and product names. - **Bold** (`**text**`) for UI elements the reader interacts with (buttons, fields, window names) - **Italics** (`_text_`) for names of things (products, documents, concepts) - **Lists** use `-` for unordered items; each item ends with a period `.` diff --git a/README.md b/README.md index 36b1b87..6b29052 100644 --- a/README.md +++ b/README.md @@ -62,3 +62,5 @@ Whether you’re here to learn about DVT, integrate it into your staking stack, + +> **Browsing as an AI agent?** Start with [obol.org/llms.txt](https://obol.org/llms.txt) for a terse index of the ecosystem, or [obol.org/llms-full.txt](https://obol.org/llms-full.txt) for a self-contained briefing. The [`ObolNetwork/skills`](https://github.com/ObolNetwork/skills) repo publishes Claude Code skills for running DVs and the Obol Stack. diff --git a/SUMMARY.md b/SUMMARY.md index 33176a8..4088ffa 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -76,6 +76,7 @@ * [Self-Host a Relay](advanced-and-troubleshooting/advanced/self-relay.md) * [Advanced Docker Configs](advanced-and-troubleshooting/advanced/adv-docker-configs.md) * [Assign OVM Roles](advanced-and-troubleshooting/advanced/assign-ovm-roles.md) + * [NAT Hole Punching](advanced-and-troubleshooting/advanced/hole-punching.md) * [Troubleshooting](advanced-and-troubleshooting/troubleshooting/README.md) * [Errors & Resolutions](advanced-and-troubleshooting/troubleshooting/errors.md) * [Handling DKG Failure](advanced-and-troubleshooting/troubleshooting/dkg_failure.md) diff --git a/adv/_category_.json b/adv/_category_.json deleted file mode 100644 index 55aa807..0000000 --- a/adv/_category_.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "label": "ADVANCED & TROUBLESHOOTING", - "position": 3, - "collapsed": false, - "collapsible": false, - "className": "menuSection" -} \ No newline at end of file diff --git a/adv/advanced/_category_.json b/adv/advanced/_category_.json deleted file mode 100644 index 6547998..0000000 --- a/adv/advanced/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "Advanced Guides", - "position": 1, - "collapsed": true -} \ No newline at end of file diff --git a/adv/advanced/quickstart-builder-api.mdx b/adv/advanced/quickstart-builder-api.mdx deleted file mode 100644 index b77ccb7..0000000 --- a/adv/advanced/quickstart-builder-api.mdx +++ /dev/null @@ -1,168 +0,0 @@ ---- -sidebar_position: 4 -description: Run a distributed validator cluster with the builder API (MEV-Boost) ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -# Enable MEV - -This quickstart guide focuses on configuring the builder API for Charon and supported validator and consensus clients. - -## Getting started with Charon & the Builder API - -Running a distributed validator cluster with the builder API enabled will give the validators in the cluster access to the builder network. This builder network is a network of "Block Builders" -who work with MEV searchers to produce the most valuable blocks a validator can propose. - -[MEV-Boost](https://boost.flashbots.net/) is one such product from Flashbots that enables you to ask multiple -block relays (who communicate with the "Block Builders") for blocks to propose. The block that pays the largest reward to the validator will be signed and returned to the relay for broadcasting to the wider -network. The end result for the validator is generally an increased APR as they receive some share of the MEV. - -:::info -Before completing this guide, please check your cluster version, which can be found inside the `cluster-lock.json` file. If you are using cluster-lock version `1.7.0` or higher, Charon seamlessly accommodates all validator client implementations within a MEV-enabled distributed validator cluster. - -For clusters with a `cluster-lock.json` version `1.6.0` and below, Charon is compatible only with [Teku](https://github.com/ConsenSys/teku). Use the version history feature of this documentation to see the instructions for configuring a cluster in that manner (`v0.16.0`). -::: - -## Client configuration - -:::note -You need to add CLI flags to your consensus client, Charon client, and validator client, to enable the builder API. - -You need all operators in the cluster to have their nodes properly configured to use the builder API, or you risk missing a proposal. -::: - -### Charon - -Charon supports builder API with the `--builder-api` flag. To use builder API, one simply needs to add this flag to the `charon run` command: - -```shell -charon run --builder-api -``` - -### Consensus Clients - -The following flags need to be configured on your chosen consensus client. A Flashbots relay URL is provided for example purposes, you should use the [charon test mev command](../../run/prepare/test-command.mdx#test-mev-relay) and select the two or three relays with the lowest latency to your node that also conform to your block building preferences. A public list of MEV relays is available [here](https://github.com/eth-educators/ethstaker-guides/blob/main/MEV-relay-list.md#mev-relay-list-for-mainnet). - - - - Teku can communicate with a single relay directly: -
-      
-    {String.raw`teku --builder-endpoint="https://0xac6e77dfe25ecd6110b8e780608cce0dab71fdd5ebea22a16c0205200f2f8e2e3ad3b71d3499c54ad14d6c21b41a37ae@boost-relay.flashbots.net"`}
-      
-    
- Or you can configure it to communicate with a local MEV-boost sidecar to configure multiple relays: -
-      
-    {String.raw`teku --builder-endpoint=http://mev-boost:18550`}
-      
-    
-
- - Lighthouse can communicate with a single relay directly: -
-      
-    {String.raw`lighthouse bn --builder="https://0xac6e77dfe25ecd6110b8e780608cce0dab71fdd5ebea22a16c0205200f2f8e2e3ad3b71d3499c54ad14d6c21b41a37ae@boost-relay.flashbots.net"`}
-      
-    
- Or you can configure it to communicate with a local MEV-boost sidecar to configure multiple relays: -
-      
-    {String.raw`lighthouse bn --builder="http://mev-boost:18550"`}
-      
-    
-
- - Prysm can communicate with a single relay directly: -
-      
-    {String.raw`prysm beacon-chain --http-mev-relay="https://0xac6e77dfe25ecd6110b8e780608cce0dab71fdd5ebea22a16c0205200f2f8e2e3ad3b71d3499c54ad14d6c21b41a37ae@boost-relay.flashbots.net"`}
-      
-    
-
- - Nimbus can communicate with a single relay directly: -
-      
-        {String.raw`nimbus_beacon_node \
-        --payload-builder=true \
-        --payload-builder-url="https://0xac6e77dfe25ecd6110b8e780608cce0dab71fdd5ebea22a16c0205200f2f8e2e3ad3b71d3499c54ad14d6c21b41a37ae@boost-relay.flashbots.net"`}
-      
-    
-
- - Lodestar can communicate with a single relay directly: -
-      
-    {String.raw`node ./lodestar --builder --builder.urls="https://0xac6e77dfe25ecd6110b8e780608cce0dab71fdd5ebea22a16c0205200f2f8e2e3ad3b71d3499c54ad14d6c21b41a37ae@boost-relay.flashbots.net"`}
-      
-    
-
-
- -### Validator Clients - -The following flags need to be configured on your chosen validator client - - - -
-      
-    {String.raw`teku validator-client --validators-builder-registration-default-enabled=true`}
-      
-    
- -
- -
-      
-    {String.raw`lighthouse vc --builder-proposals`}
-      
-    
-
- -
-      
-    {String.raw`prysm validator --enable-builder`}
-      
-    
-
- -
-      
-    {String.raw`nimbus_validator_client --payload-builder=true`}
-      
-    
-
- -
-      
-    {String.raw`node ./lodestar validator --builder="true" --builder.selection="builderalways"`}
-      
-    
-
-
- -:::info -For at-scale deployments, additional flags should be configured on the consensus or validator client to ensure builder bids are preferred over locally-built blocks whenever available. See [Builder Block Selection](../../run/prepare/deployment-best-practices.mdx#builder-block-selection) in the deployment best practices for the recommended flag per client. -::: - -## Verify your cluster is correctly configured - -It can be difficult to confirm everything is configured correctly with your cluster until a proposal opportunity arrives, but here are some things you can check. - -When your cluster is running, you should see if Charon is logging something like this each epoch: - -```log -13:10:47.094 INFO bcast Successfully submitted validator registration to beacon node {"delay": "24913h10m12.094667699s", "pubkey": "84b_713", "duty": "1/builder_registration"} -``` - -This indicates that your Charon node is successfully registering with the relay for a blinded block when the time comes. - -If you are using the [ultrasound relay](https://relay.ultrasound.money), you can enter your cluster's distributed validator public key(s) into their website, to confirm they also see the validator as correctly registered. - -You should check that your validator client's logs look healthy, and ensure that you haven't added a `fee-recipient` address that conflicts with what has been selected by your cluster in your `cluster-lock.json` file, as that may prevent your validator from producing a signature for the block when the opportunity arises. You should also confirm the same for all of the other peers in your cluster. - -Once a proposal has been made, you should look at the `Block Extra Data` field under `Execution Payload` for the block on [Beaconcha.in](https://beaconcha.in/block/18450364), and confirm there is text present, this generally suggests the block came from a builder, and was not a locally constructed block. diff --git a/adv/advanced/quickstart-sdk.mdx b/adv/advanced/quickstart-sdk.mdx deleted file mode 100644 index 6e6395d..0000000 --- a/adv/advanced/quickstart-sdk.mdx +++ /dev/null @@ -1,129 +0,0 @@ ---- -sidebar_position: 2 -description: Create a DV cluster using the Obol Typescript SDK ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -# Create a DV Using the SDK - -This is a walkthrough of using the [Obol-SDK](https://www.npmjs.com/package/@obolnetwork/obol-sdk) to propose a four-node distributed validator cluster for creation using the [DV Launchpad](../../learn/intro/launchpad.md). - -## Pre-requisites - -- You have [node.js](https://nodejs.org/en) installed. - -## Install the package - -Install the Obol-SDK package into your development environment - - - -
-      npm install --save @obolnetwork/obol-sdk
-    
-
- -
-      yarn add @obolnetwork/obol-sdk
-    
-
-
- -## Instantiate the client - -The first thing you need to do is create an instance of the Obol SDK client. The client takes two constructor parameters: - -- The `chainID` for the chain you intend to use. -- An ethers.js [signer](https://docs.ethers.org/v6/api/providers/#Signer-signTypedData) object. - -```ts -import { Client } from "@obolnetwork/obol-sdk"; -import { ethers } from "ethers"; - -// Create a dummy ethers signer object with a throwaway private key -const mnemonic = ethers.Wallet.createRandom().mnemonic?.phrase || ""; -const privateKey = ethers.Wallet.fromPhrase(mnemonic).privateKey; -const wallet = new ethers.Wallet(privateKey); -const signer = wallet.connect(null); - -// Instantiate the Obol Client for Hoodi -const obol = new Client({ chainId: 560048 }, signer); -``` - -## Propose the cluster - -List the Ethereum addresses of participating operators, along with withdrawal and fee recipient address data for each validator you intend for the operators to create. - -```ts -// A config hash is a deterministic hash of the proposed DV cluster configuration -const configHash = await obol.createClusterDefinition({ - name: "SDK Demo Cluster", - operators: [ - { address: "0xC35CfCd67b9C27345a54EDEcC1033F2284148c81" }, - { address: "0x33807D6F1DCe44b9C599fFE03640762A6F08C496" }, - { address: "0xc6e76F72Ea672FAe05C357157CfC37720F0aF26f" }, - { address: "0x86B8145c98e5BD25BA722645b15eD65f024a87EC" }, - ], - validators: [ - { - fee_recipient_address: "0x3CD4958e76C317abcEA19faDd076348808424F99", - withdrawal_address: "0xE0C5ceA4D3869F156717C66E188Ae81C80914a6e", - }, - ], -}); - -console.log( - `Direct the operators to https://hoodi.launchpad.obol.org/dv?configHash=${configHash} to complete the key generation process` -); -``` - -## Invite the Operators to complete the DKG - -Once the Obol-API returns a `configHash` string from the `createClusterDefinition` method, you can use this identifier to invite the operators to the [Launchpad](../../learn/intro/launchpad.md) to complete the process - -1. Operators navigate to `https://.launchpad.obol.org/dv?configHash=` and complete the [run a DV with others](../../run-a-dv/start/create-a-dv-with-a-group.md) flow. -1. Once the DKG is complete, and operators are using the `--publish` flag, the created cluster details will be posted to the Obol API. -1. The creator will be able to retrieve this data with `obol.getClusterLock(configHash)`, to use for activating the newly created validator. - -## Retrieve the created Distributed Validators using the SDK - -Once the DKG is complete, the proposer of the cluster can retrieve key data such as the validator public keys and their associated deposit data messages. - -```js -const clusterLock = await obol.getClusterLock(configHash); -``` - -Reference lock files can be found [here](https://github.com/ObolNetwork/charon/tree/main/cluster/testdata). - -## Activate the DVs using the deposit contract - -In order to activate the distributed validators, the cluster operator can retrieve the validators' associated deposit data from the lock file and use it to craft transactions to the `deposit()` method on the deposit contract. - -```js -const validatorDepositData = - clusterLock.distributed_validators[validatorIndex].deposit_data; - -const depositContract = new ethers.Contract( - DEPOSIT_CONTRACT_ADDRESS, // 0x00000000219ab540356cBB839Cbe05303d7705Fa for Mainnet, 0xff50ed3d0ec03aC01D4C79aAd74928BFF48a7b2b for Goerli - depositContractABI, // https://etherscan.io/address/0x00000000219ab540356cBB839Cbe05303d7705Fa#code for Mainnet, and replace the address for Goerli - signer -); - -const TX_VALUE = ethers.parseEther("32"); - -const tx = await depositContract.deposit( - validatorDepositData.pubkey, - validatorDepositData.withdrawal_credentials, - validatorDepositData.signature, - validatorDepositData.deposit_data_root, - { value: TX_VALUE } -); - -const txResult = await tx.wait(); -``` - -## Usage Examples - -Examples of how our SDK can be used are found [here](https://github.com/ObolNetwork/obol-sdk-examples). diff --git a/adv/security/_category_.json b/adv/security/_category_.json deleted file mode 100644 index f445b9f..0000000 --- a/adv/security/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "Security", - "position": 8, - "collapsed": true -} diff --git a/adv/security/smart_contract_audit.mdx b/adv/security/smart_contract_audit.mdx deleted file mode 100644 index 15d4528..0000000 --- a/adv/security/smart_contract_audit.mdx +++ /dev/null @@ -1,495 +0,0 @@ ---- -sidebar_position: 4 -description: Smart Contract Audit ---- - -# Smart Contract Audit - - - - - - - -
-

Obol Audit Report

-

Obol Manager Contracts

-

Prepared by: Zach Obront, Independent Security Researcher

-

Date: Sept 18 to 22, 2023

-

PDF Version

-
- -## About **Obol** - -The Obol Network is an ecosystem for trust minimized staking that enables people to create, test, run & co-ordinate distributed validators. - -The Obol Manager contracts are responsible for distributing validator rewards and withdrawals among the validator and node operators involved in a distributed validator. - -## About **zachobront** - -Zach Obront is an independent smart contract security researcher. He serves as a Lead Senior Watson at Sherlock, a Security Researcher at Spearbit, and has identified multiple critical severity bugs in the wild, including in a Top 5 Protocol on Immunefi. You can say hi on Twitter at [@zachobront](http://x.com/zachobront). - -## Summary & Scope - -The [ObolNetwork/obol-manager-contracts](https://github.com/ObolNetwork/obol-manager-contracts/) repository was audited at commit [50ce277919723c80b96f6353fa8d1f8facda6e0e](https://github.com/ObolNetwork/obol-manager-contracts/tree/50ce277919723c80b96f6353fa8d1f8facda6e0e). - -The following contracts were in scope: - -- src/controllers/ImmutableSplitController.sol -- src/controllers/ImmutableSplitControllerFactory.sol -- src/lido/LidoSplit.sol -- src/lido/LidoSplitFactory.sol -- src/owr/OptimisticWithdrawalReceiver.sol -- src/owr/OptimisticWithdrawalReceiverFactory.sol - -After completion of the fixes, the [2f4f059bfd145f5f05d794948c918d65d222c3a9](https://github.com/ObolNetwork/obol-manager-contracts/tree/2f4f059bfd145f5f05d794948c918d65d222c3a9) commit was reviewed. After this review, the updated Lido fee share system in [PR #96](https://github.com/ObolNetwork/obol-manager-contracts/pull/96/files) (at commit [fd244a05f964617707b0a40ebb11b523bbd683b8](https://github.com/ObolNetwork/obol-splits/pull/96/commits/fd244a05f964617707b0a40ebb11b523bbd683b8)) was reviewed. - -## Summary of Findings - -| Identifier | Title | Severity | Fixed | -| :------: | ---------------------------- | :-------------: | :-----: | -| [M-01](#m-01-future-fees-may-be-skirted-by-setting-a-non-eth-reward-token) | Future fees may be skirted by setting a non-ETH reward token | Medium | ✓ | -| [M-02](#m-02-splits-with-256-or-more-node-operators-will-not-be-able-to-switch-on-fees) | Splits with 256 or more node operators will not be able to switch on fees | Medium | ✓ | -| [M-03](#m-03-in-a-mass-slashing-event-node-operators-are-incentivized-to-get-slashed) | In a mass slashing event, node operators are incentivized to get slashed | Medium | | -| [L-01](#l-01-obol-fees-will-be-applied-retroactively-to-all-non-distributed-funds-in-the-splitter) | Obol fees will be applied retroactively to all non-distributed funds in the Splitter | Low | ✓ | -| [L-02](#l-02-if-owr-is-used-with-rebase-tokens-and-theres-a-negative-rebase-principal-can-be-lost) | If OWR is used with rebase tokens and there's a negative rebase, principal can be lost | Low | ✓ | -| [L-03](#l-03-lidosplit-can-receive-eth-which-will-be-locked-in-contract) | LidoSplit can receive ETH, which will be locked in contract | Low | ✓ | -| [L-04](#l-04-upgrade-to-latest-version-of-solady-to-fix-libclone-bug) | Upgrade to latest version of Solady to fix LibClone bug | Low | ✓ | -| [G-01](#g-01-steth-and-wsteth-addresses-can-be-saved-on-implementation-to-save-gas) | stETH and wstETH addresses can be saved on implementation to save gas | Gas | ✓ | -| [G-02](#g-02-owr-can-be-simplified-and-save-gas-by-not-tracking-distributedfunds) | OWR can be simplified and save gas by not tracking distributedFunds | Gas | ✓ | -| [I-01](#i-01-strong-trust-assumptions-between-validators-and-node-operators) | Strong trust assumptions between validators and node operators | Informational | | -| [I-02](#i-02-provide-node-operator-checklist-to-validate-setup) | Provide node operator checklist to validate setup | Informational | | - -## Detailed Findings - -### [M-01] Future fees may be skirted by setting a non-ETH reward token - -Fees are planned to be implemented on the `rewardRecipient` splitter by updating to a new fee structure using the `ImmutableSplitController`. - -It is assumed that all rewards will flow through the splitter, because (a) all distributed rewards less than 16 ETH are sent to the `rewardRecipient`, and (b) even if a team waited for rewards to be greater than 16 ETH, rewards sent to the `principalRecipient` are capped at the `amountOfPrincipalStake`. - -This creates a fairly strong guarantee that reward funds will flow to the `rewardRecipient`. Even if a user were to set their `amountOfPrincipalStake` high enough that the `principalRecipient` could receive unlimited funds, the Obol team could call `distributeFunds()` when the balance got near 16 ETH to ensure fees were paid. - -However, if the user selects a non-ETH token, all ETH will be withdrawable only thorugh the `recoverFunds()` function. If they set up a split with their node operators as their `recoveryAddress`, all funds will be withdrawable via `recoverFunds()` without ever touching the `rewardRecipient` or paying a fee. - -#### Recommendation - -I would recommend removing the ability to use a non-ETH token from the `OptimisticWithdrawalRecipient`. Alternatively, if it feels like it may be a use case that is needed, it may make sense to always include ETH as a valid token, in addition to any `OWRToken` set. - -#### Review - -Fixed in [PR 85](https://github.com/ObolNetwork/obol-manager-contracts/pull/85) by removing the ability to use non-ETH tokens. - -### [M-02] Splits with 256 or more node operators will not be able to switch on fees - -0xSplits is used to distribute rewards across node operators. All Splits are deployed with an ImmutableSplitController, which is given permissions to update the split one time to add a fee for Obol at a future date. - -The Factory deploys these controllers as Clones with Immutable Args, hard coding the `owner`, `accounts`, `percentAllocations`, and `distributorFee` for the future update. This data is packed as follows: - -```solidity - function _packSplitControllerData( - address owner, - address[] calldata accounts, - uint32[] calldata percentAllocations, - uint32 distributorFee - ) internal view returns (bytes memory data) { - uint256 recipientsSize = accounts.length; - uint256[] memory recipients = new uint[](recipientsSize); - - uint256 i = 0; - for (; i < recipientsSize;) { - recipients[i] = (uint256(percentAllocations[i]) << ADDRESS_BITS) | uint256(uint160(accounts[i])); - - unchecked { - i++; - } - } - - data = abi.encodePacked(splitMain, distributorFee, owner, uint8(recipientsSize), recipients); - } -``` - -In the process, `recipientsSize` is unsafely downcasted into a `uint8`, which has a maximum value of `256`. As a result, any values greater than 256 will overflow and result in a lower value of `recipients.length % 256` being passed as `recipientsSize`. - -When the Controller is deployed, the full list of `percentAllocations` is passed to the `validSplit` check, which will pass as expected. However, later, when `updateSplit()` is called, the `getNewSplitConfiguation()` function will only return the first `recipientsSize` accounts, ignoring the rest. - -```solidity - function getNewSplitConfiguration() - public - pure - returns (address[] memory accounts, uint32[] memory percentAllocations) - { - // fetch the size first - // then parse the data gradually - uint256 size = _recipientsSize(); - accounts = new address[](size); - percentAllocations = new uint32[](size); - - uint256 i = 0; - for (; i < size;) { - uint256 recipient = _getRecipient(i); - accounts[i] = address(uint160(recipient)); - percentAllocations[i] = uint32(recipient >> ADDRESS_BITS); - unchecked { - i++; - } - } - } -``` - -When `updateSplit()` is eventually called on `splitsMain` to turn on fees, the `validSplit()` check on that contract will revert because the sum of the percent allocations will no longer sum to `1e6`, and the update will not be possible. - -#### Proof of Concept - -The following test can be dropped into a file in `src/test` to demonstrate that passing 400 accounts will result in a `recipientSize` of `400 - 256 = 144`: - -```solidity -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -import { Test } from "forge-std/Test.sol"; -import { console } from "forge-std/console.sol"; -import { ImmutableSplitControllerFactory } from "src/controllers/ImmutableSplitControllerFactory.sol"; -import { ImmutableSplitController } from "src/controllers/ImmutableSplitController.sol"; - -interface ISplitsMain { - function createSplit(address[] calldata accounts, uint32[] calldata percentAllocations, uint32 distributorFee, address controller) external returns (address); -} - -contract ZachTest is Test { - function testZach_RecipientSizeCappedAt256Accounts() public { - vm.createSelectFork("https://mainnet.infura.io/v3/fb419f740b7e401bad5bec77d0d285a5"); - - ImmutableSplitControllerFactory factory = new ImmutableSplitControllerFactory(address(9999)); - bytes32 deploymentSalt = keccak256(abi.encodePacked(uint256(1102))); - address owner = address(this); - - address[] memory bigAccounts = new address[](400); - uint32[] memory bigPercentAllocations = new uint32[](400); - - for (uint i = 0; i < 400; i++) { - bigAccounts[i] = address(uint160(i)); - bigPercentAllocations[i] = 2500; - } - - // confirmation that 0xSplits will allow creating a split with this many accounts - // dummy acct passed as controller, but doesn't matter for these purposes - address split = ISplitsMain(0x2ed6c4B5dA6378c7897AC67Ba9e43102Feb694EE).createSplit(bigAccounts, bigPercentAllocations, 0, address(8888)); - - ImmutableSplitController controller = factory.createController(split, owner, bigAccounts, bigPercentAllocations, 0, deploymentSalt); - - // added a public function to controller to read recipient size directly - uint savedRecipientSize = controller.ZachTest__recipientSize(); - assert(savedRecipientSize < 400); - console.log(savedRecipientSize); // 144 - } -} -``` - -#### Recommendation - -When packing the data in `_packSplitControllerData()`, check `recipientsSize` before downcasting to a uint8: - -```diff -function _packSplitControllerData( - address owner, - address[] calldata accounts, - uint32[] calldata percentAllocations, - uint32 distributorFee -) internal view returns (bytes memory data) { - uint256 recipientsSize = accounts.length; -+ if (recipientsSize > 256) revert InvalidSplit__TooManyAccounts(recipientSize); - ... -} -``` - -#### Review - -Fixed as recommended in [PR 86](https://github.com/ObolNetwork/obol-manager-contracts/pull/86). - -### [M-03] In a mass slashing event, node operators are incentivized to get slashed - -When the `OptimisticWithdrawalRecipient` receives funds from the beacon chain, it uses the following rule to determine the allocation: - -> If the amount of funds to be distributed is greater than or equal to 16 ether, it is assumed that it is a withdrawal (to be returned to the principal, with a cap on principal withdrawals of the total amount they deposited). - -> Otherwise, it is assumed that the funds are rewards. - -This value being as low as 16 ether protects against any predictable attack the node operator could perform. For example, due to the effect of hysteresis in updating effective balances, it does not seem to be possible for node operators to predictably bleed a withdrawal down to be below 16 ether (even if they timed a slashing perfectly). - -However, in the event of a mass slashing event, slashing punishments can be much more severe than they otherwise would be. To calculate the size of a slash, we: - -- take the total percentage of validator stake slashed in the 18 days preceding and following a user's slash -- multiply this percentage by 3 (capped at 100%) -- the full slashing penalty for a given validator equals 1/32 of their stake, plus the resulting percentage above applied to the remaining 31/32 of their stake - -In order for such penalties to bring the withdrawal balance below 16 ether (assuming a full 32 ether to start), we would need the percentage taken to be greater than `15 / 31 = 48.3%`, which implies that `48.3 / 3 = 16.1%` of validators would need to be slashed. - -Because the measurement is taken from the 18 days before and after the incident, node operators would have the opportunity to see a mass slashing event unfold, and later decide that they would like to be slashed along with it. - -In the event that they observed that greater than 16.1% of validators were slashed, Obol node operators would be able to get themselves slashed, be exited with a withdrawal of less than 16 ether, and claim that withdrawal as rewards, effectively stealing from the principal recipient. - -#### Recommendations - -Find a solution that provides a higher level of guarantee that the funds withdrawn are actually rewards, and not a withdrawal. - -#### Review - -Acknowledged. We believe this is a black swan event. It would require a major ETH client to be compromised, and would be a betrayal of trust, so likely not EV+ for doxxed operators. Users of this contract with unknown operators should be wary of such a risk. - -### [L-01] Obol fees will be applied retroactively to all non-distributed funds in the Splitter - -When Obol decides to turn on fees, a call will be made to `ImmutableSplitController::updateSplit()`, which will take the predefined split parameters (the original user specified split with Obol's fees added in) and call `updateSplit()` to implement the change. - -```solidity -function updateSplit() external payable { - if (msg.sender != owner()) revert Unauthorized(); - - (address[] memory accounts, uint32[] memory percentAllocations) = getNewSplitConfiguration(); - - ISplitMain(splitMain()).updateSplit(split, accounts, percentAllocations, uint32(distributorFee())); -} -``` - -If we look at the code on `SplitsMain`, we can see that this `updateSplit()` function is applied retroactively to all funds that are already in the split, because it updates the parameters without performing a distribution first: - -```solidity -function updateSplit( - address split, - address[] calldata accounts, - uint32[] calldata percentAllocations, - uint32 distributorFee -) - external - override - onlySplitController(split) - validSplit(accounts, percentAllocations, distributorFee) -{ - _updateSplit(split, accounts, percentAllocations, distributorFee); -} -``` - -This means that any funds that have been sent to the split but have not yet be distributed will be subject to the Obol fee. Since these splitters will be accumulating all execution layer fees, it is possible that some of them may have received large MEV bribes, where this after-the-fact fee could be quite expensive. - -#### Recommendation - -The most strict solution would be for the `ImmutableSplitController` to store both the old split parameters and the new parameters. The old parameters could first be used to call `distributeETH()` on the split, and then `updateSplit()` could be called with the new parameters. - -If storing both sets of values seems too complex, the alternative would be to require that `split.balance <= 1` to update the split. Then the Obol team could simply store the old parameters off chain to call `distributeETH()` on each split to "unlock" it to update the fees. - -(Note that for the second solution, the ETH balance should be less than or equal to 1, not 0, because 0xSplits stores empty balances as `1` for gas savings.) - -#### Review - -Fixed as recommended in [PR 86](https://github.com/ObolNetwork/obol-manager-contracts/pull/86). - -### [L-02] If OWR is used with rebase tokens and there's a negative rebase, principal can be lost - -The `OptimisticWithdrawalRecipient` is deployed with a specific token immutably set on the clone. It is presumed that that token will usually be ETH, but it can also be an ERC20 to account for future integrations with tokenized versions of ETH. - -In the event that one of these integrations used a rebasing version of ETH (like `stETH`), the architecture would need to be set up as follows: - -`OptimisticWithdrawalRecipient => rewards to something like LidoSplit.sol => Split Wallet` - -In this case, the OWR would need to be able to handle rebasing tokens. - -In the event that rebasing tokens are used, there is the risk that slashing or inactivity leads to a period with a negative rebase. In this case, the following chain of events could happen: - -- `distribute(PULL)` is called, setting `fundsPendingWithdrawal == balance` -- rebasing causes the balance to decrease slightly -- `distribute(PULL)` is called again, so when `fundsToBeDistributed = balance - fundsPendingWithdrawal` is calculated in an unchecked block, it ends up being near `type(uint256).max` -- since this is more than `16 ether`, the first `amountOfPrincipalStake - _claimedPrincipalFunds` will be allocated to the principal recipient, and the rest to the reward recipient -- we check that `endingDistributedFunds <= type(uint128).max`, but unfortunately this check misses the issue, because only `fundsToBeDistributed` underflows, not `endingDistributedFunds` -- `_claimedPrincipalFunds` is set to `amountOfPrincipalStake`, so all future claims will go to the reward recipient -- the `pullBalances` for both recipients will be set higher than the balance of the contract, and so will be unusable - -In this situation, the only way for the principal to get their funds back would be for the full `amountOfPrincipalStake` to hit the contract at once, and for them to call `withdraw()` before anyone called `distribute(PUSH)`. If anyone was to be able to call `distribute(PUSH)` before them, all principal would be sent to the reward recipient instead. - -#### Recommendation - -Similar to #74, I would recommend removing the ability for the `OptimisticWithdrawalRecipient` to accept non-ETH tokens. - -Otherwise, I would recommend two changes for redundant safety: - -1) Do not allow the OWR to be used with rebasing tokens. - -2) Move the `_fundsToBeDistributed = _endingDistributedFunds - _startingDistributedFunds;` out of the unchecked block. The case where `_endingDistributedFunds` underflows is already handled by a later check, so this one change should be sufficient to prevent any risk of this issue. - -#### Review - -Fixed in [PR 85](https://github.com/ObolNetwork/obol-manager-contracts/pull/85) by removing the ability to use non-ETH tokens. - -### [L-03] LidoSplit can receive ETH, which will be locked in contract - -Each new `LidoSplit` is deployed as a clone, which comes with a `receive()` function for receiving ETH. - -However, the only function on `LidoSplit` is `distribute()`, which converts `stETH` to `wstETH` and transfers it to the `splitWallet`. - -While this contract should only be used for Lido to pay out rewards (which will come in `stETH`), it seems possible that users may accidentally use the same contract to receive other validator rewards (in ETH), or that Lido governance may introduce ETH payments in the future, which would cause the funds to be locked. - -#### Proof of Concept - -The following test can be dropped into `LidoSplit.t.sol` to confirm that the clones can currently receive ETH: - -```solidity -function testZach_CanReceiveEth() public { - uint before = address(lidoSplit).balance; - payable(address(lidoSplit)).transfer(1 ether); - assertEq(address(lidoSplit).balance, before + 1 ether); -} -``` - -#### Recommendation - -Introduce an additional function to `LidoSplit.sol` which wraps ETH into stETH before calling `distribute()`, in order to rescue any ETH accidentally sent to the contract. - -#### Review - -Fixed in [PR 87](https://github.com/ObolNetwork/obol-manager-contracts/pull/87/files) by adding a `rescueFunds()` function that can send ETH or any ERC20 (except `stETH` or `wstETH`) to the `splitWallet`. - -### [L-04] Upgrade to latest version of Solady to fix LibClone bug - -In the recent [Solady audit](https://github.com/Vectorized/solady/blob/main/audits/cantina-solady-report.pdf), an issue was found the affects LibClone. - -In short, LibClone assumes that the length of the immutable arguments on the clone will fit in 2 bytes. If it's larger, it overlaps other op codes and can lead to strange behaviors, including causing the deployment to fail or causing the deployment to succeed with no resulting bytecode. - -Because the `ImmutableSplitControllerFactory` allows the user to input arrays of any length that will be encoded as immutable arguments on the Clone, we can manipulate the length to accomplish these goals. - -Fortunately, failed deployments or empty bytecode (which causes a revert when `init()` is called) are not problems in this case, as the transactions will fail, and it can only happen with unrealistically long arrays that would only be used by malicious users. - -However, it is difficult to be sure how else this risk might be exploited by using the overflow to jump to later op codes, and it is recommended to update to a newer version of Solady where the issue has been resolved. - -#### Proof of Concept - -If we comment out the `init()` call in the `createController()` call, we can see that the following test "successfully" deploys the controller, but the result is that there is no bytecode: - -```solidity -function testZach__CreateControllerSoladyBug() public { - ImmutableSplitControllerFactory factory = new ImmutableSplitControllerFactory(address(9999)); - bytes32 deploymentSalt = keccak256(abi.encodePacked(uint256(1102))); - address owner = address(this); - - address[] memory bigAccounts = new address[](28672); - uint32[] memory bigPercentAllocations = new uint32[](28672); - - for (uint i = 0; i < 28672; i++) { - bigAccounts[i] = address(uint160(i)); - if (i < 32) bigPercentAllocations[i] = 820; - else bigPercentAllocations[i] = 34; - } - - ImmutableSplitController controller = factory.createController(address(8888), owner, bigAccounts, bigPercentAllocations, 0, deploymentSalt); - assert(address(controller) != address(0)); - assert(address(controller).code.length == 0); -} -``` - -#### Recommendation - -Delete Solady and clone it from the most recent commit, or any commit after the fixes from [PR #548](https://github.com/Vectorized/solady/pull/548/files#diff-27a3ba4730de4b778ecba4697ab7dfb9b4f30f9e3666d1e5665b194fe6c9ae45) were merged. - -#### Review - -Solady has been updated to v.0.0.123 in [PR 88](https://github.com/ObolNetwork/obol-manager-contracts/pull/88). - -### [G-01] stETH and wstETH addresses can be saved on implementation to save gas - -The `LidoSplitFactory` contract holds two immutable values for the addresses of the `stETH` and `wstETH` tokens. - -When new clones are deployed, these values are encoded as immutable args. This adds the values to the contract code of the clone, so that each time a call is made, they are passed as calldata along to the implementation, which reads the values from the calldata for use. - -Since these values will be consistent across all clones on the same chain, it would be more gas efficient to store them in the implementation directly, which can be done with `immutable` storage values, set in the constructor. - -This would save 40 bytes of calldata on each call to the clone, which leads to a savings of approximately 640 gas on each call. - -#### Recommendation - -1) Add the following to `LidoSplit.sol`: - -```solidity -address immutable public stETH; -address immutable public wstETH; -``` - -2) Add a constructor to `LidoSplit.sol` which sets these immutable values. Solidity treats immutable values as constants and stores them directly in the contract bytecode, so they will be accessible from the clones. - -3) Remove `stETH` and `wstETH` from `LidoSplitFactory.sol`, both as storage values, arguments to the constructor, and arguments to `clone()`. - -4) Adjust the `distribute()` function in `LidoSplit.sol` to read the storage values for these two addresses, and remove the helper functions to read the clone's immutable arguments for these two values. - -#### Review - -Fixed as recommended in [PR 87](https://github.com/ObolNetwork/obol-manager-contracts/pull/87). - -### [G-02] OWR can be simplified and save gas by not tracking distributedFunds - -Currently, the `OptimisticWithdrawalRecipient` contract tracks four variables: - -- distributedFunds: total amount of the token distributed via push or pull -- fundsPendingWithdrawal: total balance distributed via pull that haven't been claimed yet -- claimedPrincipalFunds: total amount of funds claimed by the principal recipient -- pullBalances: individual pull balances that haven't been claimed yet - -When `_distributeFunds()` is called, we perform the following math (simplified to only include relevant updates): - -```solidity -endingDistributedFunds = distributedFunds - fundsPendingWithdrawal + currentBalance; -fundsToBeDistributed = endingDistributedFunds - distributedFunds; -distributedFunds = endingDistributedFunds; -``` - -As we can see, `distributedFunds` is added to the `endingDistributedFunds` variable and then removed when calculating `fundsToBeDistributed`, having no impact on the resulting `fundsToBeDistributed` value. - -The `distributedFunds` variable is not read or used anywhere else on the contract. - -#### Recommendation - -We can simplify the math and save substantial gas (a storage write plus additional operations) by not tracking this value at all. - -This would allow us to calculate `fundsToBeDistributed` directly, as follows: - -```solidity -fundsToBeDistributed = currentBalance - fundsPendingWithdrawal; -``` - -#### Review - -Fixed as recommended in [PR 85](https://github.com/ObolNetwork/obol-manager-contracts/pull/85). - -### [I-01] Strong trust assumptions between validators and node operators - -It is assumed that validators and node operators will always act in the best interest of the group, rather than in their selfish best interest. - -It is important to make clear to users that there are strong trust assumptions between the various parties involved in the DVT. - -Here are a select few examples of attacks that a malicious set of node operators could perform: - -1) Since there is currently no mechanism for withdrawals besides the consensus of the node operators, a minority of them sufficient to withhold consensus could blackmail the principal for a payment of up to 16 ether in order to allow them to withdraw. Otherwise, they could turn off their node operators and force the principal to bleed down to a final withdrawn balance of just over 16 ether. - -2) Node operators are all able to propose blocks within the P2P network, which are then propogated out to the rest of the network. Node software is accustomed to signing for blocks built by block builders based on the metadata including quantity of fees and the address they'll be sent to. This is enforced by social consensus, with block builders not wanting to harm validators in order to have their blocks accepted in the future. However, node operators in a DVT are not concerned with the social consensus of the network, and could therefore build blocks that include large MEV payments to their personal address (instead of the DVT's 0xSplit), add fictious metadata to the block header, have their fellow node operators accept the block, and take the MEV for themselves. - -3) While the withdrawal address is immutably set on the beacon chain to the OWR, the fee address is added by the nodes to each block. Any majority of node operators sufficient to reach consensus could create a new 0xSplit with only themselves on it, and use that for all execution layer fees. The principal (and other node operators) would not be able to stop them or withdraw their principal, and would be stuck with staked funds paying fees to the malicious node operators. - -Note that there are likely many other possible attacks that malicious node operators could perform. This report is intended to demonstrate some examples of the trust level that is needed between validators and node operators, and to emphasize the importance of making these assumptions clear to users. - -#### Review - -Acknowledged. We believe EIP 7002 will reduce this trust assumption as it would enable the validator exit via the execution layer withdrawal key. - -### [I-02] Provide node operator checklist to validate setup - -There are a number of ways that the user setting up the DVT could plant backdoors to harm the other users involved in the DVT. - -Each of these risks is possible to check before signing off on the setup, but some are rather hidden, so it would be useful for the protocol to provide a list of checks that node operators should do before signing off on the setup parameters (or, even better, provide these checks for them through the front end). - -1) Confirm that `SplitsMain.getHash(split)` matches the hash of the parameters that the user is expecting to be used. - -2) Confirm that the controller clone delegates to the correct implementation. If not, it could be pointed to delegate to `SplitMain` and then called to `transferControl()` to a user's own address, allowing them to update the split arbitrarily. - -3) `OptimisticWithdrawalRecipient.getTranches()` should be called to check that `amountOfPrincipalStake` is equal to the amount that they will actually be providing. - -4) The controller's `owner` and future split including Obol fees should be provided to the user. They should be able to check that `ImmutableSplitControllerFactory.predictSplitControllerAddress()`, with those parameters inputted, results in the controller that is actually listed on `SplitsMain.getController(split)`. - -#### Review - -Acknowledged. We do some of these already (will add the remainder) automatically in the launchpad UI during the cluster confirmation phase by the node operator. We will also add it in markdown to the repo. diff --git a/adv/troubleshooting/_category_.json b/adv/troubleshooting/_category_.json deleted file mode 100644 index 9b64570..0000000 --- a/adv/troubleshooting/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "Troubleshooting", - "position": 2, - "collapsed": true -} diff --git a/advanced-and-troubleshooting/advanced/assign-ovm-roles.md b/advanced-and-troubleshooting/advanced/assign-ovm-roles.md index a9adbd6..fd5bbc6 100644 --- a/advanced-and-troubleshooting/advanced/assign-ovm-roles.md +++ b/advanced-and-troubleshooting/advanced/assign-ovm-roles.md @@ -50,17 +50,17 @@ Roles are assigned using the **`grantRoles`** function on the OVM smart contract - **`roles (uint256)`:** Input the **Decimal Value** (e.g., `17`) or **Hex Value** (e.g., `0x11`) copied from the calculator. - Click **"Write"** and approve the transaction. -
+
Screenshot of the OVM contract on a block explorer with the role-assignment transaction prepared.
## 3. Review the Roles 1. Assigned roles will show up in the Launchpad to the designated address. -
+
Screenshot of the DV Launchpad showing newly assigned OVM roles.
2. Sometimes the Launchpad may take a short time to reflect role updates due to RPC issues.. Try refreshing if this occurs. You can also use Etherscan directly to confirm roles. -
+
Screenshot of the DV Launchpad reflecting updated OVM role assignments.
## 4. Security and Recommendations 🔒 @@ -79,7 +79,7 @@ The security of the cluster relies entirely on the assignment and control of the - **Cluster Creation Timing:** It is recommended to **grant final roles before sharing cluster invites** with external invitees. This ensures the security model is locked down before the cluster scales. - **Renounce Ownership (Conditional):** If the cluster's roles are intended to be fixed forever (e.g., in a fully immutable system), you can **renounce ownership** after setting the final roles. However, if any role needs to be modifiable later (like changing the fee recipient), the owner must retain the ability to execute `grantRoles`. -
+
Screenshot of the DV Launchpad showing the OVM ownership and role-renouncement options.
## 5. Miscellaneous: How does Bitwise Logic Work? diff --git a/advanced-and-troubleshooting/advanced/ovm-predeploy.md b/advanced-and-troubleshooting/advanced/ovm-predeploy.md index 4b4be0c..25283fa 100644 --- a/advanced-and-troubleshooting/advanced/ovm-predeploy.md +++ b/advanced-and-troubleshooting/advanced/ovm-predeploy.md @@ -16,7 +16,7 @@ This guide will demonstrate the key steps in preparing a DV cluster for this typ The Hoodi testnet will be used for all examples. -
+
Diagram of the on-demand Obol Validator Manager pre-deploy workflow.
{% hint style="warning" %} The following code snippets are minimal examples for the purpose of achieving the desired functionality. These should not be run in production without thorough testing and review. @@ -1075,7 +1075,7 @@ console.log("Funds distributed to beneficiary and reward recipient");