verex

2026-08-05 — verex

Source docs: docs/tasks/current-plan.md (wave 2 — UMA oracle adapter, and the wave-3 prerequisites it feeds).

Confirm claude/happy-booth-746fe1 was merged

Cause: the branch was carried as an open loose end, described (by me, wrongly) as the A.4 balanceOfBatch work. Reasoning: four independent checks beat a recollection — merge-base --is-ancestor, git log origin/main..branch, branch --contains, and the worktree’s dirty state. Change: none to code; verification only. Result: fully merged into origin/main, no unique commits, worktree clean — safe to remove. Its actual contents were the /portfolio + /create stale-HTML fix, the Telegram home-visit notification and 🔮 prefix, and a gitignore entry — not balanceOfBatch, which appears only in three docs files and was never implemented anywhere. So it is an un-started design item, not lost work.

UMA adapter: deploy script, manifest recording, and the procedure

Cause: jay — “UMA는 배포해야하면 배포 스크립트 만들고 절차를 README에 추가해줘”. The adapter existed with 14 passing tests but had no way to reach a chain. Reasoning: kept it out of DeployCTF.s.sol deliberately. The adapter binds to one ConditionalTokens in its constructor, and a market’s conditionId hashes the adapter’s own address — so it must be addable to an environment that is already live without touching the backbone or invalidating existing markets. Preflight checks live in the script rather than the runbook because a wrong constructor argument is not fixable in place: it is a re-deploy plus a re-creation of every market bound to it. Change: packages/contracts/script/DeployUmaAdapter.s.sol (requires code at CTF_ADDR, requires the oracle to answer defaultLiveness(), requires UMA_OO_ADDR explicitly off-Sepolia); packages/api/scripts/save-uma-adapter.ts (folds check and save into one step — deployer identity, cross-target collision, adapter.ctf() vs the recorded CTF, operator-is-admin, and refuses silent replacement without --force); save-deployment.ts amended; runbook §2b and a README section. Result: verified against a Sepolia fork, not just compiled — the real OO answered defaultLiveness() = 7200, a full --broadcast + record cycle passed all seven checks, and both guard paths (--force replacement, new-CTF drop) fired correctly. Deploy costs ~0.003 ETH at 2 gwei against the operator’s 0.1788. 48/48 contract tests still green.

save-deployment would have silently erased the adapter record

Cause: found while wiring the manifest — save-deployment.ts did manifest[target] = entry, a whole-entry overwrite, so any umaAdapter written by the new script would vanish on the next backbone save. Reasoning: the naive fix (always preserve extra keys) is wrong in the dangerous direction. The adapter is bound to one CTF, so if the backbone was genuinely redeployed, carrying the old adapter address forward would leave a manifest pointing into a different backbone — worse than losing it. Correctness depends on whether the CTF changed. Change: preserve umaAdapter/umaOracle when the newly deployed ctf matches the recorded one; drop them with an explicit warning and a redeploy hint when it doesn’t. Result: both branches exercised on the fork. Real hazard, caught before it could bite.

Deleted forge’s historical Sepolia broadcast artifacts by accident

Cause: the fork test overwrote broadcast/DeployCTF.s.sol/11155111/run-latest.json with fork addresses — a live hazard, since save-deployment reads exactly that file. I removed the directory to clear it, but the directory also held seven historical real-chain runs. They are gitignored, so there is no copy in git and none in the other worktrees. Reasoning: logging it because the mistake is instructive, not because it costs anything: rm -rf on a path chosen to fix an unrelated problem is precisely the class of command jay’s policy reserves for himself. Change: none recoverable; recorded here instead. Result: no functional impact — deployments.json is the committed source of truth and is byte-identical to HEAD, and the historical run-*.json files have no consumer once their addresses are in the manifest. What was lost is forge’s local record of past deploy tx hashes, which the chain still holds. Going forward, point fork tests at a throwaway --broadcast path rather than cleaning up after them.

UMA becomes a creation-time choice, not a deployment-wide setting

Cause: jay — “I want the UMA should be an option in creating a market”, on the current branch rather than a new one. The adapter could be deployed but nothing could use it: the create path hardcoded prepareCondition(operator, …), the schema had no field for another resolver, and resolve always signed as account #0. Reasoning: creation is the only place the choice can live, and that’s forced by the data model, not by preference — conditionId = keccak256(oracle, questionId, 2) makes the resolver part of the market’s identity, so there is no later setting to flip, only a different market. Validation therefore sits in createMarketGroup (before anything is spent) rather than in the job. Three constraints are enforced rather than documented: UMA rejected when no adapter is deployed here, rejected for multi-outcome groups (N independent UMA questions with nothing enforcing one winner), and resolution criteria required — that text is the entire basis a voter decides on, and its absence produces “unresolvable”, which pays both sides half. Change: OracleType enum + four Market columns + ChainConfig.umaAdapterAddr (migration 20260805012808_uma_oracle_option, additive, no backfill — OPERATOR is already true of every existing row); packages/sdk/src/uma.ts (client, ancillary-data builder, id derivation); market-create.ts split into prepareViaOperator / prepareViaUma; resolveMarketFromUma + POST /markets/:slug/uma-resolve; GET /config so the UI can ask before offering; create-page oracle selector with a conditional criteria field. Result: verified end-to-end against the real Sepolia OptimisticOracleV2 on a fork — 21 checks including a live proposePrice, a real liveness window, and the verdict landing in the DB. 53/53 contract tests.

The isSettled pre-check could never have returned true

Cause: the fork run failed at the last step: isSettleable’s predecessor isSettled returned false after liveness had passed, so resolve was unreachable through the API. Reasoning: it read Request.settled, which means “someone already called settleAndGetPrice” — false for the entire window in which resolving is possible, and true only as a side effect of resolving. The right question is the request’s state: Expired (liveness passed undisputed) or Resolved (a dispute was voted on). Confirmed directly against Sepolia’s OO: getState returned 3 (Expired) while settled was still false. Change: added getState + the State enum to IOptimisticOracleV2, replaced isSettled with isSettleable, taught the test mock to model the Expired-but-not-settled window, and added five isSettleable tests covering exactly that gap. Result: the bug passed 14 green tests because the mock encoded the same misunderstanding as the contract — a mock can only confirm its author’s beliefs. It took one run against the real oracle to expose it, which is the argument for keeping packages/api/scripts/uma-e2e-fork.ts around rather than treating the unit tests as sufficient.