WarBow: indexer refresh candidates + protocol page refresh button (EcoStrategy M-04) #160
Labels
No labels
agent:fix_bugfix
agent:fix_conflicts
agent:fix_security
agent:gap_analysis
agent:implement
agent:open_issues
agents
agent:security_audit
agent:verify
area
timecurve
arena
arena-v2
audit
block:feature_review
block:security
bug
contracts
design
devops
docs
documentation
enhancement
epic
feature
frontend
indexer
low priority
megaeth
operations
ops
performance
QA
qa checklist
referrals
removal
security
smart-contracts
tech-debt
testing
time-arena
timecurve
visual-qa
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
code/yieldomega#160
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Context
EcoStrategy audit M-04:
finalizeWarbowPodium(candidates)needs a complete superset;refreshWarbowPodium(candidates)is permissionless butrequire(!ended)(GitLab #149) blocks post-end refresh.Ask: allow anyone to call
refreshWarbowPodiumany time; indexer feeds candidates;/timecurve/protocolbutton loads candidates and submits the tx.Design / conflict
GitLab #149 prevents post-end permissionless refresh from unlatching owner finalize. Implementation must not regress #129 / #133 / #149 invariants.
Resolution options (pick in MR):
finalizeWarbowPodiumonly.refreshWarbowPodiumafterendedonly if!warbowPodiumFinalizedand/or beforedistributePrizes—requires state-machine review + Forge.Indexer
New or extended route, e.g.
GET /v1/timecurve/warbow/refresh-candidates→address[](deduped), derived from WarBow events + BP mutators + current podium hints; document max length / pagination.Frontend
frontend/src/pages/TimeCurveProtocolPage.tsx: button Load candidates → Refresh WarBow podium callingrefreshWarbowPodium(candidates).Reuse patterns from
timeCurveWarbowSnapshotClaim.ts/ arena model where appropriate.Acceptance criteria
distributePrizeslatch.References
contracts/src/TimeCurve.sol—refreshWarbowPodium,finalizeWarbowPodiumaudits/audit_ecostrategy_1777969776.md— M-04mentioned in commit
4ebbdb9086mentioned in commit
a6482837c9Implemented (#160 / EcoStrategy M-04 — Option A)
Merged to
main(4ebbdb9): sale-live indexer-assistedrefreshWarbowPodiumcandidates only — no contract change; post-end behavior unchanged (refreshWarbowPodiumreverts whenendedper #149); operators still use ownerfinalizeWarbowPodiumbeforedistributePrizeswhen required.What changed
GET /v1/timecurve/warbow/refresh-candidates(schema 1.15.0): deduped addresses from WarBowidx_*tables + buyers withbattle_points_after > 0, prepends head WarBowpodium(3)hints whenchain_timerhas a snapshot;limit(1–500, default 200),offset,total,next_offset,distinct_sql_cap_hit,podium_warbow_hint_count./timecurve/protocol: Load candidates from indexer → merge with connected wallet + onchainwarbowLadderPodiumvia existingwarBowRefreshCandidateAddresses, then Refresh WarBow podium (wrong-chain barrier + sale-ended guard).INV-INDEXER-160-WARBOW-REFRESH-CANDIDATES, manual QA, indexer design, timecurve-views, play skill +skills/README.md, guardrails cross-link.Verification checklist for @brouie
curl -sS "$INDEXER/v1/timecurve/warbow/refresh-candidates?limit=10" | jq .candidates,.total,.notex-schema-version: 1.15.0on indexer routes.VITE_INDEXER_URLset) and an EOA can submitrefreshWarbowPodium;warbowPodiumFinalizedmay flip false until owner re-finalizes (expected #129 behavior).finalizeWarbowPodium(#149).cargo clippy --all-targets -- -D warnings+ indexer tests; frontendtsc -b/ Vitest green on CI.Issue left open for your sign-off.
@PlasticDigits — verification PASS on
a648283(merge4ebbdb9).checklist walked:
curl -sS "http://127.0.0.1:3100/v1/timecurve/warbow/refresh-candidates?limit=10"→ returns 21 total candidates, deduped, podium hints prepended (top 3 = 0x02484c, 0x5e661b, 0x8626f6 matching the live WarBow leaderboard at 22000 BP). note field correctly explains sale-live-only behavior + post-end fallback to owner finalize + candidate derivation logic (DISTINCT WarBow participants + buyers with battle_points_after > 0). pagination/cap fields all present.response header:
x-schema-version: 1.15.0confirmed viacurl -sI.live walk on /timecurve/protocol → "Indexer + Wallet → WarBow Podium Refresh" panel:
warbowPodiumFinalized (read): false, candidates:—Indexer candidates loaded (checksummed): 21,Indexer merged total (API): 21,Head WarBow hints merged: 3,SQL DISTINCT cap hit: false0x4ed7c70F96B99c776995fB64377f0d4aB3B0e1C1with method selector0x5e68e4c2(refreshWarbowPodium(address[]))0xf39fd6e5...b92266(deployer EOA), status0x1SUCCESSsale-ended path not walked separately — sale is live (deadline still ~2 days out), can't end it without breaking the rest of the verification matrix. source side coverage:
refreshWarbowPodiumreverts withTimeCurve: endedper #149 baseline; UI disables and points tofinalizeWarbowPodiumper the panel description.tooling clean:
cargo clippy --all-targets -- -D warnings(indexer): cleancargo test(indexer): 42/42 PASS at HEAD390f9betsc -b: cleannpm test: 286/286 PASS across 47 filesscope honored: option A (sale-live indexer-assisted refresh only, no contract change) — #149 invariant preserved (post-end refresh still reverts), #129 / #133 expectations intact (warbowPodiumFinalized may flip false until owner re-finalizes, expected).
ready for close on your side.
@PlasticDigits — follow-up observations from a deeper read of
TimeCurveProtocolWarbowRefreshSection.tsxafter the live-walk PASS posted earlier. all hand-verified against source at HEAD390f9be. none block close on #160; flagging here so you can fold any of these into a follow-up commit or spawn children as you prefer.P2 — silent fail when indexer response shape drifts (lines 90-134)
loadFromIndexeronly handles the explicitchunk === nullbranch. ifgetJsonresolves butchunk.candidatesis missing / non-iterable (older schema, partial response, intercepting proxy, malformed JSON that still parses), thefor (const c of chunk.candidates)throws synchronously inside the loop. the function hastry { ... } finally { setLoadingIdx(false) }— nocatch— so the spinner clears butsetLoadErris never set. combined withvoid loadFromIndexer()at the click handler (line 218), the rejection becomes an unhandled console error and the user sees the spinner blip with no message.suggested fix: add a top-level
catch (e) { setLoadErr(\Indexer response invalid: ${e instanceof Error ? e.message : String(e)}`); setLoadedCandidates([]); setIdxExtras(null); }. tightenchunk.candidatestoArray.isArray(chunk.candidates)` before iterating.P3 —
loadedCandidatesnot cleared at start of fresh load (lines 90-93)paired with the silent-fail above. the function clears
setLoadErr(null)upfront but leaves priorloadedCandidatesandidxExtrasin place until new pages successfully accumulate. so if load 1 succeeds with N candidates and load 2 fails per the silent-fail path, the UI still shows N + previous extras and the user thinks the second load worked.suggested fix: reset
setLoadedCandidates([])andsetIdxExtras(null)at the top of the function, alongsidesetLoadErr(null).P2 — large-candidate gas-blowup unguarded (lines 100-123, 136-170)
indexer caps the SQL DISTINCT result at
REFRESH_CANDIDATES_DISTINCT_CAP = 10_000(indexer/src/api.rs:59) plus head podium hints; frontend pages everything (guard< 50 × limit = 500 = 25,000ceiling) into one tx.refreshWarbowPodiumiterates_updateWarbowPodiumper candidate (contracts/src/TimeCurve.sol:1073-1082), so a real BP-earner population in the thousands can blow block gas in a single submit. user sees a generic revert viafriendlyRevertFromUnknown, no preflight orestimateGas, no chunking suggestion.severity scales with expected real-mainnet participation. probably P2 polish if peak BP-earner count stays in the low hundreds; closer to P1 if it's into the thousands.
suggested fix (minimum):
estimateGasbefore submit, refuse / warn if it exceeds a configured fraction of block gas. (full): chunked submit with progress UI whenloadedCandidates.lengthexceeds a soft threshold.P3 —
chainMismatchWriteMessage(chainId)called twice (lines 142-143)if (chainMismatchWriteMessage(chainId)) { setTxErr(chainMismatchWriteMessage(chainId) ?? "..."); return; }invokes the helper twice. cosmetic, but the rest of the codebase patterns the call once into a local. trivial fix:const cm = chainMismatchWriteMessage(chainId); if (cm) { setTxErr(cm); return; }.P3 — silent truncation if pagination guard ever fires (lines 100-123)
while (guard < 50)exits cleanly after 50 pages. with the current 10k indexer cap the loop always exits vianext_offset === nullfirst, but if the cap is raised in a future schema bump and the frontend is pulled forward without lifting the guard, this becomes silent truncation — user sees a candidate count without knowing more wallets were skipped.suggested fix: if
guard === 50 && chunk.next_offset !== nullafter the loop, set a non-fatal warning state.P4 — no Vitest unit/component test ships with the new section (commit
a648283)manual QA + indexer integration test landed but no Vitest covering frontend behavior — pagination accumulation across
next_offset, OR-aggregation ofdistinct_sql_cap_hitacross pages, dedup correctness across podium hints + indexer rows + connected wallet, thesaleEndedearly-return, thewbReadsnot-loaded gate. the sister flow atuseTimeCurveArenaModel.tsx:2626is similarly untested so this isn't a regression. cheap to add; high coverage gain on a permissionless write path.anti-findings (looked suspicious, are actually fine)
refreshWarbowPodiumis permissionless and the contract readsbattlePoints[c]directly. drift across awaits has no security or correctness impact. consistent with the Arena sister flow atuseTimeCurveArenaModel.tsx:2615-2641.podiumWallets.warbowPodiumFinalizedflips false on every refresh. documented perTimeCurve.sol:1075and #129 — owner re-finalizes afterendSale.saleEndeddefaults totruewhile parent reads load (TimeCurveProtocolPage.tsx:206-207). correct fail-safe.pr[0]?.status === "success" && pr[0].result === undefinedparanoia check at line 151. wagmi typing already implies result is defined when status is success, but redundant safety is harmless.open questions
podium_warbow_hint_countdiffers from the onchainwarbowLadderPodiumthe component already reads? could help operators spot stale chain-timer state.runRefreshestimateGasfirst, or rely on the wallet's own gas-estimate UX? either is defensible.happy to ship a follow-up MR covering P2 silent-fail + P3 reset-on-reload + P3 cosmetic-double-call as a single tight patch if you want it. larger scope items (gas-blowup mitigation, vitest coverage) i'd want your steer on first.
(#160 ready for close on your side regardless of what you do with these — they're follow-ups, not blockers.)
mentioned in issue #167
Once those issues are opened you can close this issue (#160)
mentioned in issue #172
mentioned in issue #173
mentioned in issue #174
mentioned in issue #175
mentioned in issue #176
@PlasticDigits — opened the follow-up tickets per your direction.
#172 — TimeCurve refreshWarbowPodium / finalizeWarbowPodium scaling (CRITICAL, design required, supersedes the original P2 framing per your bump). includes per-candidate gas math at the 10M upper bound and 5 design directions to evaluate.
#173 — chainMismatchWriteMessage double-call in runRefresh (P3 cosmetic).
#174 — pagination guard exhaustion silently truncates candidate set (P3 forward-compat — does not bite today, bites if indexer cap rises later).
#175 — missing Vitest coverage on TimeCurveProtocolWarbowRefreshSection (P4).
#176 — estimateGas + 30% buffer threading across all 5 wagmi write-path files (P2 — your separate ask). estimateGasUnits helper already exists in lib/estimateContractGas.ts but only at 6 display-only sites in the Arena hook, no buffer applied anywhere.
two of my original findings (P2 silent-fail at 88-134, P3 reset-on-reload at 90-93) were absorbed by the #170 P3 polish patch (
fff7946) — already in tree at HEAD284c15c. flagging here so the count adds up: 6 follow-up findings + estimateGas ask = 5 new tickets after dedup.closing #160 per your earlier note.
mentioned in issue #170
mentioned in merge request !377
mentioned in commit
8dfdf64871mentioned in commit
08a95bd913