feat(blend): BLEND validator, entry/event decoders, and processor - #659
feat(blend): BLEND validator, entry/event decoders, and processor#659aditya1702 wants to merge 12 commits into
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7fa1c21f24
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
7fa1c21 to
8036e49
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8036e49c83
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
9330867 to
edaf72c
Compare
edaf72c to
e00ce3f
Compare
e00ce3f to
23307c6
Compare
8b68326 to
d6d740f
Compare
There was a problem hiding this comment.
Pull request overview
Adds Blend Capital v2 ledger ingestion, including contract classification, state/event decoding, persistence, and registration.
Changes:
- Introduces shared WASM signature matching and Blend pool/backstop validation.
- Adds Blend entry/event decoders and a staged processor with canonical-backstop protection.
- Adds comprehensive fixtures, mocks, tests, and command/ingestion registration.
Reviewed changes
Copilot reviewed 22 out of 24 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
internal/services/wasmspec/wasmspec.go |
Shared WASM interface matcher. |
internal/services/wasmspec/wasmspec_test.go |
Matcher tests. |
internal/services/sep41/validator.go |
Uses shared matcher. |
internal/services/sep41/validator_test.go |
Removes relocated tests. |
internal/services/blend/validator.go |
Blend classification and enrichment. |
internal/services/blend/validator_test.go |
Validator and WASM tests. |
internal/services/blend/scval.go |
Soroban value helpers. |
internal/services/blend/scval_test.go |
Value-decoder tests. |
internal/services/blend/entries.go |
Contract-data decoders. |
internal/services/blend/entries_test.go |
Entry-decoder tests. |
internal/services/blend/events.go |
Event and fold decoders. |
internal/services/blend/events_test.go |
Event-decoder tests. |
internal/services/blend/processor.go |
Staging and persistence pipeline. |
internal/services/blend/processor_test.go |
Processor behavior tests. |
internal/services/blend/register.go |
Registers Blend services. |
internal/services/blend/register_test.go |
Registration tests. |
internal/services/blend/testdata/blend_pool_v2.wasm |
Real pool fixture. |
internal/services/blend/testdata/blend_backstop_v2.wasm |
Real backstop fixture. |
internal/data/blend/mocks.go |
Blend model mocks. |
internal/data/blend/pools.go |
Documents monotonic ledger updates. |
internal/data/blend/pools_test.go |
Tests ledger non-regression. |
internal/ingest/ingest.go |
Enables Blend ingestion. |
cmd/protocol_setup.go |
Enables Blend setup. |
cmd/protocol_migrate.go |
Enables Blend migration. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
73c2e72 to
841c130
Compare
…claim token Verified against blend-contracts-v2 @ ba22b487: fill_auction (pool/src/auctions/*.rs): fill_bad_debt_auction moves the bid dTokens from the backstop's Positions to the FILLER's — the filler assumes the debt — while the lot (backstop LP tokens) is drawn straight to the filler's wallet and never touches pool Positions. fill_interest_auction settles entirely outside pool Positions (bid donated to the backstop, lot paid from the reserves' backstop_credit, captured by the ResData entry snapshot). The decoder previously folded the user side of every asset for all auction types and mirrored to the filler only for type 0: a type-1 filler's net_borrowed missed the assumed debt, and type-2 fills fabricated lot adjustments against the backstop-address row (in the wrong units — underlying, valued as bTokens). Folds now mirror the on-chain Positions moves exactly: type 0 both sides, type 1 bid-only both sides, type 2 none. backstop claim (backstop/src/contract.rs -> emissions/claim.rs): the event's amount is execute_claim's return — the Comet LP tokens minted and auto-deposited (per-pool deposit events are emitted alongside) — never raw BLND. The row's token_id is now NULL with units backstop_lp in key_value, matching every other backstop-LP-denominated row, instead of mislabeling LP amounts as BLND.
Decode a ClaimFold from each pool/backstop claim event and accumulate it into the staged pool/backstop claimed-total maps during current-state indexing, persisting via PoolClaimed/BackstopClaimed.BatchApplyDeltas. History mode is unchanged — it still records the CLAIM feed rows but folds no totals.
… swap caveats Audit of the decoders/processor against blend-contracts-v2 @ ba22b487 found no behavioral gaps; this records the three residual findings: - test: a backstop claim op also emits one genuine deposit event per claimed pool (auto-restake) — 1 CLAIM + N BACKSTOP_DEPOSIT rows, a single account-wide claimed-total fold, no double count - godoc: TTL-evicted temporary Auction entries are invisible to ingestion (tx-meta-only reads), so an unfilled expired auction leaves a stale blend_auctions row - godoc: an emitter backstop swap requires updating the canonical backstop pin and migrating backstop-derived state
841c130 to
1339841
Compare
BLEND validator, entry/event decoders, and processor
Third of 5 stacked PRs adding Blend Capital v2 lending support (stacked on #658). Adds the ingestion layer between the ledger stream and #658's writer models: a validator (classifies pools/backstop by WASM interface), entry decoders (ContractData changes → current-state snapshots), event decoders (contract events → Blend state-change history rows + additive folds), and the processor that stages both and persists in dependency order. Every shape was verified against the deployed v2 WASMs (mainnet and testnet are byte-identical) and
blend-capital/blend-contracts-v2 @ ba22b487.Also: the SEP-41 validator's WASM-spec matcher is extracted verbatim into shared
internal/services/wasmspec(mechanical; SEP-41 unchanged), and everything registers viainit()+ blank imports inprotocol-setup/protocol-migrate/ingest.mocks.go(deferred from PR2's deadcode gate) lands here with its consumer, the processor tests.Validator
Classification is purely interface-based (all required functions must match), exercised by real-WASM fixture tests:
get_config,get_reserve,get_positions,submit,flash_loan,bad_debtdeposit,queue_withdrawal,dequeue_withdrawal,withdraw,claim,draw,donatePool matches are enriched best-effort via RPC
get_config(a failed call never fails validation). Pool names have no on-chain getter — the processor fills them from instance storage.Canonical-backstop pin. Backstop-derived tables key rows by pool/user with no backstop contract id, so an impostor deployed from the real backstop WASM could overwrite genuine rows. Backstop-shaped entries (
UserBalance/PoolBalance/BEmisData/UEmisData/RZ) and events (deposit/queue_withdrawal/dequeue_withdrawal/withdraw/backstopclaim) are therefore folded only from the canonical backstop per network (addresses from docs.blend.capital + blend-utils, WASM hashes verified on-chain; skips get one debug log per contract per window). Junk pools stay tolerated — they key rows under their own contract ids.CAQQR5SWBXKIGZKPBZDH3KM5GQ5GUTPKB7JAFCINLZBC5WXPJKRG3IM7CD25MNVTZDL4Y3XBCPCJXGXATV5WUHHOWMYFF4YBEGU5FCPGMYTVG5JYCBDVWXT433PRVTUNM56C3JREF3HIZHRBA64NB2C3B2UNCKIS65ZYCLZACB22KRA3YZVCNCQI64JQ5WE7UY2VAV7WFLK6A2JN3HEX56T2EDAFO7QFEntry decoders
Twelve ContractData kinds, routed by disjoint key shape; anything else decodes to
KindIgnored(never an error). Removal andOption::None(ScvVoid) are explicit nil-payload cases.Config)blend_pools— config +Admin/Name(best-effort)Positions(user)blend_positions— supplied / collateral / borrowedResConfig(asset)/ResData(asset)blend_reserves— curve config / rates, supplies, backstop creditEmisData(u32)/UserEmis(user, reserve_id)blend_reserve_emissions/blend_emissions(token_id >= 0)UserBalance(pool, user)/PoolBalance(pool)blend_backstop_positions/blend_backstop_poolsBEmisData(pool)/UEmisData(pool, user),Option<…>blend_backstop_poolsemission state /blend_emissions(token_id = -1)Auction(auct_type, user)blend_auctions"RZ"blend_pools.in_reward_zoneAuctionencodings verified against a live mainnet auction; theAuctionKeyScMap sortsauct_typebeforeuser(symbol-byte order, reverse of Rust field order). An emptyRZvec is a valid live value. An instance entry withoutConfigis the backstop's own → ignored.ResList/PoolEmis/PropAdmin/ResInit; backstopLastDist/DropList/BackfillEmis/Backfill/RzEmis/PoolUSDC.Event decoders
Each row carries a (category, reason) pair under the schema-wide convention — category names the on-chain object (
BLEND_SUPPLY,BLEND_DEBT, …), reason the action — pluskey_value.poolId(where the event names a pool) andtoken_id= reserve asset, BLND, or NULL. Unrecognized symbols decode to nothing; a malformed recognized event errors → logged at Warn, counted inwallet_ingestion_protocol_decode_failures_total{protocol_id, kind}(probable contract drift), and skipped, never aborting the ledger. Decode validation rejectsauct_typeabove int32 range andfill_percentoutside 1..100 (the contract enforces ≤ 100 on-chain).supply/supply_collateralnet_supplied +=withdraw_collateralnet_supplied -=borrow/repaynet_borrowed +=/-=flash_loannet_borrowed +=(a same-tx repay nets it out)bad_debtnet_borrowedreset to 0defaulted_debtfill_auctionwithdraw(ambiguous)net_supplied -=(pool side only)claim(ambiguous)deposit/queue_withdrawal/dequeue_withdrawalwithdraw: pool and backstop shapes are identical; disambiguated by topic[1] — a backstopwithdraw's is a tracked Blend pool, a poolwithdraw's is a reserve asset (never Blend-tracked).claim: pool = tuple data, backstop = barei128; the category alone now encodes the source. A backstop claim pays Comet LP (BLND auto-swapped + re-deposited), so its row istoken_idNULL +units:"backstop_lp"with no pool address.fill_auctionfolds mirror on-chain Positions moves: type 0 (UserLiquidation) lot+bid both sides; type 1 (BadDebt) bid both sides; type 2 (Interest) none.Processor
RequiresContractData()= true.StateChangeOrdinalBase()returns the reserved Blendstate_change_idnamespace base, andPersistHistoryassigns deterministic ordinals in emission order viatypes.AssignStateChangeOrdinals— Blend rows can never collide with the main indexer's or SEP-41's IDs for the same operation. Event groups are folded in on-chain(TxIdx, OpIdx)order (not map order): the cost-basis folds accumulate across groups into shared staged state andbad_debt's reset makes them order-sensitive, so a borrow and a bad_debt for one key in different groups of the same ledger must fold in chain order — pinned by a probabilistic regression test. Events stage history rows + additive folds; entries stage last-write-wins snapshots; aPersistwithout a followingReset()refuses further folds (no double-count). Persist order: pools → reserves (fold SQL resolves asset →reserve_indexviablend_reserves; a window that decoded a reserve's config without its data goes through a config-only partial update —BatchUpdateConfig— so aset_reserveinvariant violation degrades to a stale data half, never zeroed live rates) → positions (delete removed → zero absent → upsert snapshots → net-deltas → auction adjustments) → backstop/emission/claimed/auction sets (independently keyed) → reward zone last (flipsin_reward_zoneon rows the pools upsert just created). Pool claims fold per emitting pool; backstop claims fold account-wide. Auctions stage LWW per(pool, user, type)— created-then-filled in one window nets to a delete. Batch equivalence (one- vs two-window folds, removal+recreate, same-ledger cross-group ordering) is covered by processor tests.Notes for reviewers
token_id/amountNULL (multi-asset); per-asset detail inkey_value.lot/bid(exposed as first-class lists in feat(graphql): Blend positions, pools, and earn discovery #661); non-reserve assets no-op via the reserves join.defaulted_debt: backstop-side basis not folded (not user-facing).b_rate/d_rate1e12,ir_mod1e7, backstop emissions 1e14; the pool reserve-emission scalar is per-reserve (10^decimals) — PR5 math must joinReserveConfig.decimals.depositevent per pool, so one claim op yields 1 backstop-emissions CLAIM + N backstop CREDIT rows (Σ deposits == claim amount); no double-count — covered by a dedicated processor test.token_idon pool-claim rows rather than a misattributed token.interestEarned/interestPaid, and is one-time (live ingestion runs window=1, exact).Auctionentry evicted by TTL leaves a staleblend_auctionsrow. Accepted: auction economics make an unfilled expiry near-impossible.🤖 Generated with Claude Code