Skip to content

Phase A4 slice 2: Balancer V2 cold-start (discover → verify) - #10

Merged
KaiCode2 merged 2 commits into
codex/phase-a4-slice1-cold-startfrom
codex/phase-a4-slice2-balancer
Jun 24, 2026
Merged

Phase A4 slice 2: Balancer V2 cold-start (discover → verify)#10
KaiCode2 merged 2 commits into
codex/phase-a4-slice1-cold-startfrom
codex/phase-a4-slice2-balancer

Conversation

@KaiCode2

Copy link
Copy Markdown
Owner

Stacked on #9 (slice 1: cold-start adoption + V2/V3 planners), which is stacked on #8 (A8 test-matrix hardening). Review/merge #8#9 → this. The base is the slice-1 branch, so the diff below is slice 2 only.

What

Gives Balancer V2 a real cold_start (it previously fell through to the Unsupported default) using the slice-1 AdapterColdStartPlanner machinery. Balancer pool state is not at predictable storage slots, so the planner discovers the vault balance slots by access list rather than naming them, then verifies exactly those slots.

BalancerV2ColdStartPlanner (src/adapters/balancer_v2.rs)

  • Factory cold_start_planner: resolves the vault (BalancerV2Metadata.vault, falling back to the first state address) and the poolId (bytes32 key). No vault → Err(MissingMetadata("Balancer vault")); non-bytes32 key → Err(Custom).
  • Round 1 (discover): accounts = [vault] + a getPoolTokens(poolId) ColdStartCall on the vault with restrict_to = [vault]. A feature-local sol! IBalancerVault::getPoolTokens ABI is added here (not the simulation-gated cache_sync copy), so the path compiles under the balancer-v2 adapter feature alone.
  • on_results decodes the token list from the discover call's return data (ExecutionResult::output()getPoolTokensCall::abi_decode_returns) and continues into a verify round over exactly the captured (vault, slot) pairs; round 2 warms them authoritatively. Robust to failure: no discover result / no output / decode error → DiscoverFailed; decoded-but-empty capture → NoSlotsDiscovered.
  • finish: success → BalancerV2Metadata { vault, pool_address (poolId[..20]), tokens }, status Ready. DiscoverFailedNeedsRepair(ColdStart); NoSlotsDiscoveredNeedsRepair(PurgeStorage) — distinct, actionable repairs.

Tests

Manager-authored acceptance tests in tests/cold_start_adoption.rs, plus a compiled MockBalancerVault stub fixture (tests/fixtures/; getPoolTokens SLOADs fixed slots 0..=4 and returns the (address[2], uint256[2], uint256) tuple), installed offline:

  • happy path — discover → verify reaches Ready with the decoded tokens, the discovered balance slots refreshed by the verify round (proven via a stale-seed-vs-fresh-fetch assertion), and zero RPC (asserter.read_q().is_empty()).
  • missing vault — a vault-less Balancer registration is Unsupported.

Mirrors the upstream two_round_discover_then_verify_offline pattern. Spec: docs/phase-a4-slice2-spec.md.

Test plan

Full CI matrix green locally:

  • cargo fmt --all --check
  • cargo clippy --all-targets -D warnings — default, adapters-only (adapters,uniswap-v2,uniswap-v3,balancer-v2), and --no-default-features
  • cargo test (default; cold_start_adoption 8/8), adapters-only, and --no-default-features
  • RUSTDOCFLAGS="-D warnings" cargo doc --no-deps

🤖 Generated with Claude Code

KaiCode2 and others added 2 commits June 23, 2026 23:51
Give Balancer V2 a real cold_start (it previously used the Unsupported default)
via the slice-1 ColdStartPlanner machinery. Balancer pool state is not at
predictable slots, so the planner discovers the vault balance slots by access
list rather than naming them.

BalancerV2ColdStartPlanner (src/adapters/balancer_v2.rs):
- Factory cold_start_planner: resolves the vault (metadata.vault, falling back to
  the first state address) and the poolId (bytes32 key). No vault ->
  Err(MissingMetadata("Balancer vault")); non-bytes32 key -> Err(Custom).
- Round 1 (discover): accounts=[vault] + a getPoolTokens(poolId) ColdStartCall on
  the vault with restrict_to=[vault]. A local sol! IBalancerVault::getPoolTokens
  ABI is added here (not the simulation-gated cache_sync copy).
- on_results decodes the token list from the discover call's return data
  (ExecutionResult::output() -> getPoolTokensCall::abi_decode_returns) and Continues
  into a verify round over exactly the captured (vault, slot) pairs; round 2 warms
  them authoritatively. Robust to failure: no discover result / no output / decode
  error -> DiscoverFailed; decoded-but-empty capture -> NoSlotsDiscovered.
- finish: success -> BalancerV2Metadata { vault, pool_address (poolId[..20]),
  tokens }, status Ready. DiscoverFailed -> NeedsRepair(ColdStart);
  NoSlotsDiscovered -> NeedsRepair(PurgeStorage) — distinct repairs.

Tests (manager-authored, tests/cold_start_adoption.rs): a compiled MockBalancerVault
stub fixture (tests/fixtures/, getPoolTokens SLOADs fixed slots 0..=4 and returns
the (address[2], uint256[2], uint256) tuple) installed offline. New acceptance
tests: discover->verify reaches Ready with decoded tokens + verify-refreshed
balance slots and zero RPC; a vault-less pool is Unsupported. Mirrors the upstream
two_round_discover_then_verify_offline pattern (install Address::ZERO beneficiary +
the stub, discover -> verify, assert read_q().is_empty()). Spec at
docs/phase-a4-slice2-spec.md.

Full CI matrix green: fmt; clippy default + adapters-only + no-default (-D warnings);
tests default (cold_start_adoption 8/8) + adapters-only + no-default; cargo doc
-D warnings. The cold-start path compiles in the adapters-only build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A 23-agent adversarial audit of the slice-2 planner (15 confirmed findings, no
high-severity bugs; happy path verified sound) drove these fixes:

- Verify round now honors per-slot fetch outcomes (the one behavioral fix). The
  BalancerPhase::Verify arm returned Done unconditionally, so an archive miss /
  FetchFailed on a discovered balance slot still yielded Ready with unwarmed
  balances. It now inspects results.fetched like the V2/V3 planners and, on an
  unfetchable/never-attempted discovered slot, sets a new
  BalancerRepair::BalancesUnfetched -> NeedsRepair(VerifySlots(discovered)) /
  Degraded; a genuine Zero stays acceptable. This is the per-slot-outcome
  surfacing A4 exists for.
- Empty-capture (NoSlotsDiscovered) now repairs via ColdStart (re-discover)
  instead of PurgeStorage(vault) — the Balancer vault is a shared singleton, so
  a wholesale purge would have wiped every co-tenant pool's warmed state.
- Discover arm branches on call.result.is_success() before decoding and uses
  abi_decode_returns_validate, instead of relying on the decoder to reject a
  revert/halt payload.

Tests (tests/cold_start_adoption.rs, manager-authored): new acceptance tests for
revert->repair, empty-capture->ColdStart, verify-slot-fetch-failure->repair, and
N=3 tokens, with new MockBalancerVault3 / MockBalancerVaultNoSlot / revert
fixtures. pool_address = poolId[..20] is now asserted via a distinct
leading-20/trailing-12 poolId; the missing-vault test pins
MissingMetadata("Balancer vault"). Doc comments corrected from address[2]/
uint256[2] to the real dynamic ABI. cold_start_adoption: 8 -> 12 tests.

Full matrix green: fmt; clippy default + adapters-only + no-default (-D warnings);
tests default + adapters-only + no-default; cargo doc -D warnings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@KaiCode2
KaiCode2 merged commit 3d4f875 into codex/phase-a4-slice1-cold-start Jun 24, 2026
1 check passed
KaiCode2 added a commit that referenced this pull request Jun 24, 2026
Deploy adapters pipeline + hardening to main (PRs #10, #13, #14, #15, #16)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant