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
| @@ -0,0 +1,163 @@ | |||
| // Package blend implements the BLEND v2 lending protocol's on-chain | |||
There was a problem hiding this comment.
these helpers all seem pretty general, is there any reason not to have them in utils so that they can be used by other packages?
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