|
| 1 | +--- |
| 2 | +name: obol-ovm |
| 3 | +description: | |
| 4 | + Manage Obol Validator Manager (OVM) smart contracts on Ethereum. Use this skill for any OVM operation: querying contract state, deploying new OVMs, managing roles (grant/revoke), distributing funds, setting beneficiaries or reward recipients, and requesting validator withdrawals. Trigger this skill whenever the user mentions OVM, Obol Validator Manager, validator management, distributed validators, or wants to interact with OVM contracts on mainnet/hoodi/sepolia. |
| 5 | +--- |
| 6 | + |
| 7 | +# Obol Validator Manager (OVM) Skill |
| 8 | + |
| 9 | +This skill provides scripts and knowledge to manage OVM contracts on Ethereum. OVM contracts manage distributed validators — handling ETH deposits, withdrawals (EIP-7002), consolidations (EIP-7251), and fund distribution to principal and reward recipients. |
| 10 | + |
| 11 | +## Prerequisites |
| 12 | + |
| 13 | +- **Foundry (`cast`)** must be installed and on PATH |
| 14 | +- **Read operations** work without any keys — they use public RPCs |
| 15 | +- **Write operations** require `PRIVATE_KEY` env var to be set by the user before running |
| 16 | + |
| 17 | +### Environment Variables |
| 18 | + |
| 19 | +The user sets these in their shell before running scripts: |
| 20 | + |
| 21 | +```bash |
| 22 | +# Required for write operations (the scripts pass this to cast, never read or log it) |
| 23 | +export PRIVATE_KEY=0x<private_key> |
| 24 | + |
| 25 | +# Optional: override default RPC for any network |
| 26 | +export RPC_URL=https://your-preferred-rpc.com |
| 27 | +``` |
| 28 | + |
| 29 | +For write operations, always confirm with the user that `PRIVATE_KEY` is set. Never attempt to read, echo, or log the key — the scripts pass it directly to `cast` as `$PRIVATE_KEY`. |
| 30 | + |
| 31 | +## Scripts |
| 32 | + |
| 33 | +All scripts are in `.claude/skills/obol-ovm/scripts/`. Every script accepts an optional network argument (defaults to `mainnet`). Supported networks: `mainnet`, `hoodi`, `sepolia`. |
| 34 | + |
| 35 | +Override the default RPC by setting `RPC_URL` env var. |
| 36 | + |
| 37 | +### Verify an Address is an OVM |
| 38 | + |
| 39 | +Before performing write operations on an address, verify it was deployed by the factory: |
| 40 | +```bash |
| 41 | +.claude/skills/obol-ovm/scripts/check-is-ovm.sh <address> [network] |
| 42 | +``` |
| 43 | +Queries `CreateObolValidatorManager` event logs from the factory. Exits 0 if the address is an OVM, exits 1 if not. Run this before grant-roles, revoke-roles, distribute, set-beneficiary, set-reward-recipient, or withdraw to catch mistakes early. |
| 44 | + |
| 45 | +### Read Operations (no key needed) |
| 46 | + |
| 47 | +**Query OVM state:** |
| 48 | +```bash |
| 49 | +.claude/skills/obol-ovm/scripts/query-ovm.sh <ovm_address> [network] |
| 50 | +``` |
| 51 | +Returns owner, principal/reward recipients, threshold, balances, version. |
| 52 | + |
| 53 | +**Query roles for an address:** |
| 54 | +```bash |
| 55 | +.claude/skills/obol-ovm/scripts/query-roles.sh <ovm_address> <target_address> [network] |
| 56 | +``` |
| 57 | +Returns the decoded role bitmask showing which roles the target has. |
| 58 | + |
| 59 | +**Query EIP-7002/7251 system contract fees:** |
| 60 | +```bash |
| 61 | +.claude/skills/obol-ovm/scripts/query-fees.sh [network] |
| 62 | +``` |
| 63 | +Returns current withdrawal fee (EIP-7002) and consolidation fee (EIP-7251) in wei. Useful before calling withdraw or consolidate to know how much ETH to send. |
| 64 | + |
| 65 | +You can also query OVM state directly with `cast call` for individual fields: |
| 66 | +```bash |
| 67 | +cast call <ovm> "owner()(address)" --rpc-url <rpc> |
| 68 | +cast call <ovm> "principalRecipient()(address)" --rpc-url <rpc> |
| 69 | +cast call <ovm> "rolesOf(address)(uint256)" <addr> --rpc-url <rpc> |
| 70 | +cast balance <ovm> --rpc-url <rpc> |
| 71 | +``` |
| 72 | + |
| 73 | +### Write Operations (require PRIVATE_KEY) |
| 74 | + |
| 75 | +Each write script checks that `PRIVATE_KEY` is set, prints what it's about to do, then executes via `cast send`. |
| 76 | + |
| 77 | +**Deploy a new OVM:** |
| 78 | +```bash |
| 79 | +.claude/skills/obol-ovm/scripts/deploy-ovm.sh <owner> <beneficiary> <reward_recipient> [threshold_gwei] [network] |
| 80 | +``` |
| 81 | +Default threshold is 16 gwei. Deploys via the network's factory contract. |
| 82 | + |
| 83 | +**Grant roles:** |
| 84 | +```bash |
| 85 | +.claude/skills/obol-ovm/scripts/grant-roles.sh <ovm_address> <target_address> <roles_value> [network] |
| 86 | +``` |
| 87 | + |
| 88 | +**Revoke roles:** |
| 89 | +```bash |
| 90 | +.claude/skills/obol-ovm/scripts/revoke-roles.sh <ovm_address> <target_address> <roles_value> [network] |
| 91 | +``` |
| 92 | + |
| 93 | +**Distribute funds:** |
| 94 | +```bash |
| 95 | +.claude/skills/obol-ovm/scripts/distribute-funds.sh <ovm_address> [network] |
| 96 | +``` |
| 97 | +Anyone can call this — no special role required. |
| 98 | + |
| 99 | +**Set beneficiary:** |
| 100 | +```bash |
| 101 | +.claude/skills/obol-ovm/scripts/set-beneficiary.sh <ovm_address> <new_beneficiary> [network] |
| 102 | +``` |
| 103 | +Requires SET_BENEFICIARY_ROLE (4). |
| 104 | + |
| 105 | +**Set reward recipient:** |
| 106 | +```bash |
| 107 | +.claude/skills/obol-ovm/scripts/set-reward-recipient.sh <ovm_address> <new_reward_recipient> [network] |
| 108 | +``` |
| 109 | +Requires SET_REWARD_ROLE (16). |
| 110 | + |
| 111 | +**Request validator withdrawal (EIP-7002):** |
| 112 | +```bash |
| 113 | +.claude/skills/obol-ovm/scripts/withdraw.sh <ovm_address> <pubkeys_csv> <amounts_csv> <max_fee_wei> <excess_fee_recipient> [network] |
| 114 | +``` |
| 115 | +Requires WITHDRAWAL_ROLE (1). Sends ETH = max_fee * num_validators for fees. |
| 116 | + |
| 117 | +**Consolidate validators (EIP-7251):** |
| 118 | +```bash |
| 119 | +.claude/skills/obol-ovm/scripts/consolidate.sh <ovm_address> <source_pubkey> <dest_pubkey> <max_fee_wei> <excess_fee_recipient> [network] |
| 120 | +``` |
| 121 | +Requires CONSOLIDATION_ROLE (2). Consolidates stake from source validator into destination. Sends max_fee as ETH. Query current fees with `query-fees.sh` first. |
| 122 | + |
| 123 | +**Deposit for validator(s):** |
| 124 | +```bash |
| 125 | +.claude/skills/obol-ovm/scripts/deposit.sh <ovm_address> <deposit_json_path> [network] |
| 126 | +``` |
| 127 | +Requires DEPOSIT_ROLE (32). Reads a deposit data JSON file (standard format from deposit CLI) and executes deposits via `forge script`. Each deposit sends 32 ETH. |
| 128 | + |
| 129 | +**Set principal stake amount:** |
| 130 | +```bash |
| 131 | +.claude/skills/obol-ovm/scripts/set-principal-stake.sh <ovm_address> <new_amount_wei> [network] |
| 132 | +``` |
| 133 | +Requires owner. Sets `amountOfPrincipalStake` which controls how much of distributed funds goes to the principal recipient. Queries and prints current value before changing. |
| 134 | + |
| 135 | +**Sweep pull balance:** |
| 136 | +```bash |
| 137 | +.claude/skills/obol-ovm/scripts/sweep.sh <ovm_address> <beneficiary> <amount_wei> [network] |
| 138 | +``` |
| 139 | +Extracts funds from `pullBalances[principalRecipient]`. Pass `0x0000000000000000000000000000000000000000` as beneficiary to sweep to principal recipient (anyone can call). Pass a custom address to sweep there (owner only). Amount=0 sweeps all. |
| 140 | + |
| 141 | +## Role System |
| 142 | + |
| 143 | +OVM uses bitwise role flags. Add values together for multiple roles: |
| 144 | + |
| 145 | +| Role | Value | Purpose | |
| 146 | +|------|-------|---------| |
| 147 | +| WITHDRAWAL_ROLE | 1 | Request validator withdrawals (EIP-7002) | |
| 148 | +| CONSOLIDATION_ROLE | 2 | Consolidate validator stakes (EIP-7251) | |
| 149 | +| SET_BENEFICIARY_ROLE | 4 | Change principal recipient | |
| 150 | +| RECOVER_FUNDS_ROLE | 8 | Recover stuck funds | |
| 151 | +| SET_REWARD_ROLE | 16 | Change reward recipient | |
| 152 | +| DEPOSIT_ROLE | 32 | Make validator deposits | |
| 153 | +| ALL ROLES | 63 | All of the above combined | |
| 154 | + |
| 155 | +Example: grant WITHDRAWAL + DEPOSIT = pass `33` as the roles value. |
| 156 | + |
| 157 | +## Factory Addresses |
| 158 | + |
| 159 | +| Network | Factory Address | |
| 160 | +|---------|----------------| |
| 161 | +| mainnet | `0x2c26B5A373294CaccBd3DE817D9B7C6aea7De584` | |
| 162 | +| hoodi | `0x5754C8665B7e7BF15E83fCdF6d9636684B782b12` | |
| 163 | +| sepolia | `0xF32F8B563d8369d40C45D5d667C2B26937F2A3d3` | |
| 164 | + |
| 165 | +## Default RPCs |
| 166 | + |
| 167 | +| Network | RPC URL | |
| 168 | +|---------|---------| |
| 169 | +| mainnet | `https://ethereum-rpc.publicnode.com` | |
| 170 | +| hoodi | `https://ethereum-hoodi-rpc.publicnode.com` | |
| 171 | +| sepolia | `https://sepolia.drpc.org` | |
| 172 | + |
| 173 | +Override any default by setting `RPC_URL` env var before running a script. |
| 174 | + |
| 175 | +## Fund Distribution Logic |
| 176 | + |
| 177 | +When `distributeFunds()` is called: |
| 178 | +- If `balance - fundsPendingWithdrawal >= principalThreshold * 1e9` AND `amountOfPrincipalStake > 0`: principal gets paid first (up to `amountOfPrincipalStake`), overflow goes to reward recipient |
| 179 | +- Otherwise: everything goes to reward recipient |
| 180 | +- `amountOfPrincipalStake` decrements after each principal payout |
| 181 | + |
| 182 | +## Workflow Examples |
| 183 | + |
| 184 | +### Deploy and configure a new OVM |
| 185 | +``` |
| 186 | +1. User sets PRIVATE_KEY env var |
| 187 | +2. Deploy: .claude/skills/obol-ovm/scripts/deploy-ovm.sh <owner> <beneficiary> <reward> 16 hoodi |
| 188 | +3. Query tx receipt to get the new OVM address from logs |
| 189 | +4. Grant roles: .claude/skills/obol-ovm/scripts/grant-roles.sh <new_ovm> <operator_addr> 33 hoodi |
| 190 | +5. Verify: .claude/skills/obol-ovm/scripts/query-roles.sh <new_ovm> <operator_addr> hoodi |
| 191 | +``` |
| 192 | + |
| 193 | +### Check OVM state and distribute |
| 194 | +``` |
| 195 | +1. Query: .claude/skills/obol-ovm/scripts/query-ovm.sh <ovm_address> mainnet |
| 196 | +2. If balance > 0, distribute: .claude/skills/obol-ovm/scripts/distribute-funds.sh <ovm_address> mainnet |
| 197 | +``` |
| 198 | + |
| 199 | +### Listing OVMs on a network |
| 200 | +Use `cast logs` against the factory to find all deployed OVMs: |
| 201 | +```bash |
| 202 | +cast logs --from-block <deploy_block> --to-block latest \ |
| 203 | + --address <factory_address> \ |
| 204 | + "CreateObolValidatorManager(address indexed,address indexed,address,address,uint64)" \ |
| 205 | + --rpc-url <rpc> |
| 206 | +``` |
| 207 | +Deploy blocks: mainnet=23919948, hoodi=1735335, sepolia=9159573. |
| 208 | + |
| 209 | +## RPC Retry Rule |
| 210 | + |
| 211 | +Public RPCs can be unreliable (timeouts, rate limits, incomplete responses). **Never treat an RPC failure as a definitive answer.** All scripts that query the chain should: |
| 212 | + |
| 213 | +1. Retry up to **3 times** with the default public RPC before giving up |
| 214 | +2. If all 3 attempts fail, **ask the user to provide a custom RPC** via `export RPC_URL=...` — do NOT report a false negative (e.g. saying an address is not an OVM when the RPC simply failed) |
| 215 | +3. Exit with code `2` to distinguish RPC failures from genuine "not found" results (exit code `1`) |
| 216 | + |
| 217 | +This rule applies to `check-is-ovm.sh` and any future scripts that depend on RPC queries for verification. |
| 218 | + |
| 219 | +## Troubleshooting |
| 220 | + |
| 221 | +**"PRIVATE_KEY env var must be set"** — The user needs to export their private key before write operations. Remind them: `export PRIVATE_KEY=0x...` |
| 222 | + |
| 223 | +**RPC timeouts or errors** — Scripts retry 3 times automatically. If they still fail, ask the user for a custom RPC and set `RPC_URL` env var. Free-tier RPCs may have block range limits on log queries. |
| 224 | + |
| 225 | +**"execution reverted"** — Check that the signer has the required role for the operation. Use `query-roles.sh` to verify permissions. |
| 226 | + |
| 227 | +## Security |
| 228 | + |
| 229 | +- Scripts never read, echo, or log `PRIVATE_KEY` — it's only passed as `$PRIVATE_KEY` to `cast send` |
| 230 | +- Read operations use public RPCs with no credentials |
| 231 | +- Always show the user what transaction will execute before running a write script |
| 232 | +- Warn about gas costs on mainnet |
0 commit comments