feat: add Balancer V3 reclamm pool types#1049
Open
johngrantuk wants to merge 6 commits into
Open
Conversation
Co-authored-by: Cursor <cursoragent@cursor.com>
Contributor
|
Hi! Thanks for the work. This part wasn’t changed by you, but I suspect the logic here might be wrong. I have a rough idea you could use as a reference. You can look at the previous liquidityBuffer integration PR I worked on. The flow would be:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds support for Balancer V3 reClamm pools (rebranded as AutoRange).
Initial test pool can be found here: https://balancer.fi/pools/ethereum/v3/0xda66e8ddf9959e4db759bfd06256730d8a8b2d13
Balancer V3 ReCLAMM integration — issue and fixes
Summary of the
test_reclamm_swap_usdc_wethVM simulation failure and the storage/indexingfixes applied.
Symptom
Test:
test_reclamm_swap_usdc_wethinprotocols/substreams/ethereum-balancer-v3Protocol:
vm:balancer_v3Pool:
0xda66e8ddf9959e4db759bfd06256730d8a8b2d13(ReCLAMM, USDC/WETH)Stop block: 25129786 (index range 25129750–25129786)
During integration testing, VM state decode failed with:
Revert data uses Solidity panic selector
0x4e487b71…0011(Panic(17) — arithmeticoverflow/underflow).
What failed in simulation: A small WETH → USDC probe swap used while building pool state
(spot-price probes via the Tycho VM adapter). The trace showed a plausible Vault swap quote, but
the ReCLAMM pool
onSwapreturn was wrong at USDC scale, thenVault.sendUSDC reverted withpanic
0x11. That pattern indicates desynced Vault vs pool state inrevm, not a bad swap oncanonical mainnet (on-chain
querySwapExactInat block 25129786 succeeded for the same amounts).Misleading pass: The test could still report passed because decode uses
skip_state_decode_failures(true)— failures are logged as warnings,update.statesstays empty,and simulation/execution steps are skipped. A green test only proved RPC component metadata matched
YAML, not that swaps decoded correctly.
Root cause (end-to-end)
Point-in-time reads for the Balancer V3 Vault at block 25129786 did not include
reservesOf[USDC]incontract_storage, while WETH reserves, pool token balances, and poolcontract storage matched mainnet.
Without that slot, the VM snapshot loaded zero/missing USDC reserves. Vault math during decode then
desynced and surfaced as panic
0x11on the USDC send path.Several independent bugs contributed; all had to be addressed for fresh integration-test databases
and historical stop blocks.
reservesOfupdates fromsettle/sendTowere tracked as token balances but not always written toContractChange.slots, so they never reachedcontract_storage.start_value == new_value) could makehas_changed()false and drop the USDC reserve slot from emitted slots.ContractChangemessages per Vault address in one block were not merged; later messages could overwrite earlier ones and drop slots.get_contract_slotsdid not union partitionedcontract_storagewithcontract_storage_default, so point-in-time reads could miss the current row.retention_horizonwas alwaysUtc::now(), so superseded slot versions were discarded instead of archived — nothing to read at an earlier stop block.pg_partmanonly pre-creates future daily partitions. On a fresh DB replaying historical blocks, archive rows landed incontract_storage_default, hitUNIQUE (account_id, slot), and were dropped or aborted the transaction.On-chain,
Vault.settle(USDC)ran in-range (e.g. add-liquidity tx at block 25129771). The gap wasindexer persistence and snapshot assembly, not missing protocol activity.
Fixes (implemented)
Substreams — Balancer V3 (
protocols/substreams/ethereum-balancer-v3)get_vault_reserves+map_protocol_changes: Keep fullStorageChangeper reserve token andwrite each change to both
ContractChange.token_balances(Vault account balances) andContractChange.slots(rawcontract_storage). Why: Reserve slots must follow the same pathas other contract storage deltas.
tycho-substreamsslot merge unification (models.rs): Bothupsert_slotandadd_contract_changesnow use the same slot merge semantics forstart_value/new_value.Why: Reserve-slot persistence must be stable regardless of merge path or event ordering.
Indexer — extract and persist
protobuf_deserialisation.rs: Merge multipleContractChangeentries for the same addressper block with explicit change precedence (
merge_account_delta), not slot-only merging.Why: Avoid losing slots while preserving creation/deletion semantics in multi-message txs.
contract.rs—get_contract_slots:UNIONpartitionedcontract_storage(historicalvalid_from/valid_to) withcontract_storage_default(current row). Why: Point-in-timequeries must see both archived and live slot versions.
contract.rs/protocol.rs— archive inserts: Archive writes are strict inserts again(no
on_conflict_do_nothing()fallback).Why: Silent conflict-ignore can hide historical data loss and break point-in-time correctness.
contract.rs—upsert_slots: Additional handling for superseded rows and default-tableconflicts (supporting correct versioning when slots move between current and archived tables).
Indexer — retention and DB setup
cli.rs/main.rs: Configurableretention_horizonCLI flag (default2024-01-01T00:00:00) instead of hard-codedUtc::now(). Why: Integration tests replayhistorical ranges; archiving must keep versions that are still “in window” relative to the data
being indexed, not relative to today’s clock.
postgres/mod.rs—backfill_historical_partitions: On migration, create daily partitions oncontract_storage,protocol_state, andcomponent_balanceusing a data-aware range:retention_horizon..now(fallbacknow - 3 months..now), with optionalPARTITION_BACKFILL_START/ENDoverrides.Why: Historical replay windows can extend beyond a fixed 3-month lookback.
Follow-up hardening status
add_contract_changes