Standalone static website for withdrawing legacy Balancer positions. Currently enabled: Balancer v1 on Ethereum, plus Balancer v2/v3 on every production network in the registry (Ethereum, Polygon, Arbitrum, Gnosis, Optimism, Avalanche, Base, Polygon zkEVM, Mode, Fraxtal, HyperEVM, Plasma, X Layer, Monad).
- Connect an injected wallet (MetaMask, Rabby, ...) or watch any address read-only
- Optionally configure a custom RPC URL per chain (persisted in localStorage)
- Scan for pool (BPT) and gauge (staked) positions via Multicall3 against a bundled pool list
- Unstake from gauges (
withdraw(amount, claim_rewards=true)) and claim rewards - Proportionally exit v1 core and smart pools (
BPool.exitPool/CRP.exitPool) - Proportionally exit v2 pools (
Vault.exitPool) and v3 pools (Router.removeLiquidityProportional) - Exit Xave FX and Element pools, which use their own
userDataand have noqueryExitsupport: FX takes(bptIn, assets[])and is previewed with the pool'sviewWithdraw; Element takes the amounts out and burns the matching BPT from the recipient (the holder). Both exits are simulated from the user's address. Matured Element principal tokens (ePyv…) are found by the scan and can be redeemed for the underlying (Tranche.withdrawPrincipal). - Exits are simulated first; minimum amounts = expected − slippage (default 1%). An opt-in emergency mode submits with zero minimums.
- Recovery-mode pools (incl. paused and linear pools) exit via the recovery path automatically;
expected amounts are computed client-side (
cashBalance × bptIn / virtualSupply) becauseBalancerQueries.queryExitreverts on paused pools while the real recovery exit succeeds.
The site is fully static: no backend, no subgraph — it only talks to the RPC you configure.
No UI? MANUAL_EXIT.md documents every exit as raw contract calls (explorer, cast,
Solidity) and @balancer/sdk snippets, plus how to identify what you hold.
npm install
npm run dev # http://localhost:3001
npm run build # static output in dist/Dependencies: react, react-dom, viem. That's all.
Positions are scanned against per-chain pool/gauge lists committed in src/config/data/<chain>.json.
These lists are generated by:
npm run discover -- --chain mode [--rpc <url>] [--to-block <n>]eth_getLogs requests cover at most 10k blocks: some RPCs accept larger ranges but silently drop
logs from the answer (seen on dRPC for 100k–1M-block ranges). Each range is also requested twice
and the results merged, since a load-balanced RPC can answer from a backend that misses recent logs.
A daily GitHub Action (.github/workflows/discover.yml) keeps them current. It runs
--incremental, which resumes from meta.scannedAtBlock, sweeps only newer blocks (up to 1000
blocks behind head), merges new pools/gauges in and pushes the result to main. It covers every chain marked
"deprecated": true in the registry. Halted chains (head older than a day, e.g. zkEVM) are
swept to their final block once and skipped afterwards; an unreachable RPC is a warning, not a
failure. RPCs can be overridden with a DISCOVERY_RPC_URLS repository secret
({"mode": "https://..."}).
The script enumerates:
- v1 pools:
LOG_NEW_POOLevents from the EthereumBFactory. Like the official V1 subgraph, it classifiesevent.callerwithCRPFactory.isCrp: a recognized caller is the user-facing smart-pool share token and the emitted pool is its backing BPool. - v2 pools:
PoolCreatedevents from every pool factory (fromsrc/config/registry.json) - v3 pools:
PoolRegisteredevents from the v3 Vault (covers all v3 factories) - gauges: gauge-creation events from the gauge factories (
GaugeCreated, plus the legacyGaugeCreated(gauge, pool)andRewardsOnlyGaugeCreatedforms of the older factories)
and enriches everything (poolId, tokens, symbols, decimals, phantom-BPT detection) via Multicall3.
eth_getLogs is chunked adaptively and respects strict public RPCs (zkEVM caps at 1k blocks).
Ethereum V1 discovery can be run independently with an archive-capable RPC:
npm run discover -- --chain mainnet --protocol v1 --rpc <archive-rpc-url>Pools missing from a list can always be added in the UI by pasting the pool address.
src/config/registry.json holds config for all Balancer chains (Ethereum v1 factories,
v2 vault + pool factories +
gauge factories + start blocks, v3 vault + router, BalancerQueries, Multicall3, default RPC).
It is machine-generated from sibling repos — never hand-edit it; regenerate instead:
node tools/extract-registry.mjs --repos <dir containing the 4 repos below>Sources: the archived Balancer V1 address docs and V1 subgraph,
balancer-subgraph-v2/networks.json, balancer-subgraph-v3/networks.json,
gauges-subgraph/subgraph.<chain>.yaml, backend/config/<chain>.ts.
- Regenerate the registry if factory lists changed:
node tools/extract-registry.mjs - Set
"deprecated": truefor the chain insrc/config/registry.json(or mark it intools/extract-registry.mjsand regenerate) - Run discovery:
npm run discover -- --chain <key>(the daily workflow picks it up from then on) - Smoke test the exits:
node tools/smoke-test.mjs --chain <key> - Build and deploy
Chains that are not deprecated (currently only Sepolia) are hidden in the UI but visible in dev
mode (npm run dev or append ?dev=1 to the URL).
node tools/smoke-test.mjs --chain modeHeadless test against the live chain (no funds needed): uses each gauge contract as a pseudo-user
(gauges hold the staked BPT), simulates the exact exit calls the UI would send, and for
recovery-mode pools verifies the client-side math by simulating the real exitPool with
0.5%-tight minimums.
For end-to-end testing with real transactions, fork a chain with
anvil --fork-url <rpc>, set the UI's RPC to http://localhost:8545, and impersonate a holder.
- Exits always pay out wrapped native tokens (no auto-unwrap), matching vault registration.
- V1 was deployed only on Ethereum mainnet. V1 has no gauge discovery or unstaking path here.
- V1 discovery intentionally watches only
BFactory, matching the V1 subgraph;CRPFactoryis used to classify factory-event callers and recover the smart-pool share-token address. - No USD pricing — amounts are token quantities.
- FX pools convert amounts through price-oracle assimilators: a dead oracle feed would block exits.
- Pools pasted into the manual entry are checked for FX (
derivatives) and Element (bond) getters, so they never get the standard(exitKind, bptIn)userData. On FX that encoding would burn 1 wei. - Linear pools can only exit while in recovery mode; the UI blocks them otherwise instead of implementing batch swaps.
- Nested/boosted pools (e.g. bb-o-USD on zkEVM) pay out the inner BPTs (linear pool tokens). Rescan after exiting — the inner BPTs show up as new positions and exit via the recovery path.
- v3 exits require a one-time BPT approval to the v3 Router (plain ERC20 approve, no permit2).
- Child-chain BAL rewards are distributed as regular gauge reward tokens and are included in
claim_rewards.