a80497ef - Read isClosed and principal live from chain in positions refresh - #129
Merged
TaprootFreak merged 4 commits intoAug 31, 2026
Merged
Conversation
The positions refresh already overwrites collateralBalance, virtualPrice and interest with the live contract state but copied closed and principal verbatim from the indexer. The indexer only updates those two fields in its MintingUpdate handler, so a position whose last MintingUpdate never reached the index stayed open with a stale principal in /positions/list and /positions/open although the contract reported it as closed with zero debt. Read isClosed() and principal() per position in the same batched refresh and fall back to the indexed values only when the RPC call fails.
closed is only ever set to true on-chain, so OR the indexed flag with the live isClosed() result instead of replacing it. A stale read from an eventually consistent RPC node can then never reopen a position that the index already knows as closed.
State the motivation once and spell out that principal replaces the indexed value while closed is OR-ed with it.
The indexed closed flag is fetched fresh on every refresh, so a failed live read in a later cycle would have dropped a closure that an earlier cycle had already observed. OR the previously cached flag into the merge so that a position seen as closed stays closed.
Author
|
EN: DE: DetailsReview rounds
Local verification on the final head
Follow-ups outside this PR
|
TaprootFreakAI
marked this pull request as ready for review
August 29, 2026 19:34
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.
EN:
/positions/listand/positions/openkept serving positions as open, with their originalprincipal, although the contracts had long reported them as closed with zero debt. This change readsisClosed()andprincipal()live from each position contract on every refresh, the same waycollateralBalance,virtualPriceandinterestare already read, and falls back to the indexed values only when the RPC call fails;closedis OR-ed with the indexed flag and the last observed value because it is monotonic on-chain. Consumers such as the best-clone selection, the Telegram alerts and the dApp monitoring table therefore no longer depend on the indexer having seen the position's lastMintingUpdateevent.DE:
/positions/listund/positions/openlieferten Positionen weiterhin als offen und mit dem ursprünglichenprincipal, obwohl die Contracts sie längst als geschlossen mit Schuld 0 meldeten. Diese Änderung liestisClosed()undprincipal()bei jedem Refresh live vom jeweiligen Position-Contract – so wiecollateralBalance,virtualPriceundinterestbereits gelesen werden – und fällt nur bei einem fehlgeschlagenen RPC-Call auf die indexierten Werte zurück;closedwird mit dem indexierten Flag und dem zuletzt beobachteten Wert ODER-verknüpft, weil es on-chain monoton ist. Konsumenten wie die Best-Clone-Auswahl, die Telegram-Alerts und die Monitoring-Tabelle der dApp hängen damit nicht mehr davon ab, ob der Indexer das letzteMintingUpdate-Event der Position gesehen hat.Details
Symptom
On 2026-08-29
GET /positions/openreturned 30 addresses. For 11 of them the contracts reportisClosed() == true,principal() == 0,getDebt() == 0and a collateral balance of 0 (examples:0x3fEfD8D571f0d0d7c4B37c1eC6D7E07a3af677dC,0x7f8cb22fA3F0f67Cc65A7709e6994c1540CF07Ed,0x15a91C214e7885C4c38A5A6500EAC68b9b4f8500). The API still servedclosed: falsetogether with the principal from the day the position was opened (40 000, 22 825.38 and 5 000 dEURO). The dApp monitoring table rendered these as rows like "0 cbBTC · 40,000.00 dEURO · -.--%".Cause
The indexer only updates
closedandprincipalin itsMintingUpdatehandler for the position child contract. For 12 of the 13 positions opened since April 2026 not a singleMintingUpdateevent was indexed although the events exist on-chain (both positions above were closed by a regular repay plus full withdrawal in August 2026). The API copied the staleclosed/principalverbatim while it already overwrotecollateralBalancewith the livebalanceOf– producing the inconsistent "0 collateral, 40 000 principal, not closed" records.Change
PositionsService.updatePositonV2s()adds tworeadContractcalls per position (isClosed,principal,PositionV2ABI) next to the existingbalanceOf/virtualPrice/getInterestcalls. They are batched by the existing viem multicall configuration, so the cost is two more calls in the same batch per position.principalreplaces the indexed value with the samePromise.allSettledfallback pattern thatcollateralBalanceuses.closedis merged ascached || indexed || live: the contract only ever sets it to true, so neither a stale read from an eventually consistent RPC node nor a failed read in a later refresh can reopen a position that was already seen as closed. No type or endpoint changes;getPositionsOpen()and the cloneable-parent selection pick up the correctedclosedflag automatically.principalalready denotes the currently outstanding principal everywhere it is consumed (Telegram "outstanding principal" filter, analytics sums, dApp loan display), and the indexer itself populates it fromprincipal()– so reading it live is not a semantic change.Not in this PR (follow-up for the indexer)
mintingUpdateV2s) is complete again; the missing events match Factory child discovery can be falsely marked complete when reusingponder_synccache ponder-sh/ponder#2271 (factory child discovery falsely marked complete when the sync cache is reused across a factory definition change).closedis derived fromcollateral == 0while the contract closes atcollateral < minimumCollateral;PositionDenieddoes not setclosedalthoughdeny()closes the position;ChallengeSucceeded/ForcedSalehandlers do not refresh principal, collateral or closed.Verification
npx eslint "positions/**/*.ts"– the three reported errors are pre-existing Prettier findings on unchanged lines (present ondevelop).npx prettier --check positions/positions.service.tsyarn build