Skip to content

Session close time is taken from the indexer unchecked; a failed read leaves the session "Trading" forever #3

Open

Description

@mani99brar

Summary

The web app takes a session's close time (openingTime) from the indexer and never checks it on chain. If the indexer fails to read it once, it stores null for good, and the session then shows "Trading" forever. Predict stays enabled, and the app's own close checks (assertOpen, the swap deadline cap) are skipped.

Liquidity is withdrawn once a session closes, so in the normal case a run on such a session finds no route and stops before anything is signed. That makes this mainly a wrong-status bug, not a loss of funds. See Impact for what is left.

How it happens

  1. The indexer reads the close time once, when it handles ParentMarketDeployed. If any of the four reads fails, it logs Opening time failed for <market> and returns null (fetchOpeningTime.ts#L44-L47). The read fails if both public Gnosis RPCs fail, or if the node is behind the block that created the market.
  2. The effect is declared cache: true (L11), and the error path does not opt out of the cache. The null is kept, so a re-sync does not fix it.
  3. verifySession reads every other signing field from chain, but passes openingTime through from the indexer row (...row, verifySession.ts#L94-L95).
  4. With closesAt null:

The same applies if an indexer serves a close time later than the real one.

Impact

  • Expected case (liquidity already withdrawn): the session page shows "Trading" after the real close. Predict then fails with "No route was found…" instead of saying the session is closed.
  • If the pools still have liquidity when a run starts after the close, for example in the gap before the withdrawal or if someone else has added liquidity, the app quotes and asks the user to sign trades after the close. The Predict flow moves each pool toward the user's estimate, so if the answer is already known, the user buys at a price above the token's real value. Funds stay in the user's own trade wallet, but the trades can lose value.

Trading the pools directly is possible for anyone at any time. The issue is only that the app tells its users the session is still open.

Suggested fix

Minimal fix, web only: in verifySession, read the close time from chain the same way the indexer does (questionsIds()[0], then realityProxy(), then realitio(), then getOpeningTS(id)), and return it in place of row.openingTime. If the read fails, show the existing "could not be verified" error. The status, assertOpen and the deadline cap then work unchanged. That is four extra cached reads per session page.

Follow-up for the indexer: don't cache a failed read (for example, set context.cache = false in the catch, so the next run retries). That fixes the listing filters as well.

How often it happens in production

This can be checked against the production indexer:

  • Session(where: {openingTime: {_is_null: true}})
  • indexer logs containing Opening time failed for

Found in the review of #1. The verifiers confirmed the path against the code.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions