Skip to content

feat: implement v2 weighted-majority outcome resolution - #86

Merged
collinsezedike merged 2 commits into
mainfrom
feat/v2-weighted-majority-resolution
Aug 12, 2026
Merged

feat: implement v2 weighted-majority outcome resolution#86
collinsezedike merged 2 commits into
mainfrom
feat/v2-weighted-majority-resolution

Conversation

@collinsezedike

Copy link
Copy Markdown
Collaborator

Summary

Implements #68: weighted-majority outcome resolution for tholos-v2, the piece that turns revealed vote weight into a decided terminal_cause/final_outcome.

  • After every reveal, a side locks the outcome (StrictMajorityFor/StrictMajorityAgainst) the moment its weight exceeds half of the frozen eligible total W, checked via side_weight > W - side_weight rather than division, so an odd W doesn't round the wrong way. Locking doesn't move the assertion out of Reveal: other positions can keep revealing afterward to prove entitlement for settlement, matching V2_RESOLUTION.md's state diagram (OutcomeLocked is folded into the existing Reveal phase rather than adding a new one).
  • The assertion closes to Resolved once revealed_weight catches up with W, or reveal_deadline passes, whichever happens first. If neither side ever reached a majority by then, terminal_cause defaults to OptimisticTimeout and the originally asserted outcome stands, even in a lopsided-but-incomplete reveal (e.g. one side reveals 49% against with 50% of weight never showing up at all) or an exact tie at full reveal.
  • New permissionless resolve_outcome entrypoint. It's needed because reveal() itself can never be the one to close things out past reveal_deadline (a call returning Err reverts all of its state changes in Soroban, so a reveal attempt made after the deadline can't leave a partial close behind), and because a dispute that drew zero third-party registrations has nobody who could ever call reveal successfully at all (the asserter/disputer's fixed positions are already marked revealed automatically when Reveal opens). resolve_outcome requires no signature: it only applies a deterministic rule to already-committed weight and elapsed time, moving no funds.

Settlement (who actually gets paid, and how much) stays out of scope here per the issue: terminal_cause is enough to know the outcome, the payout math is #69.

Test plan

  • cargo fmt --check, cargo clippy --workspace --all-targets -- -D warnings, and cargo test pass locally
  • cargo build -p tholos --target wasm32v1-none --release and cargo build --workspace --target wasm32v1-none --release pass locally
  • mdbook build docs passes locally
  • CONTRACT.md updated if the public interface changed — not applicable, tholos-v2 isn't documented there yet (tracked separately)
  • scripts/testnet-smoke.sh run against testnet — not run; this only touches tholos-v2, which isn't deployed yet
  • What you manually verified: added 10 new tests covering early strict-majority locking (both directions, plus an odd-total boundary case), the zero-third-party stuck-registration case, resolve_outcome's three failure modes and its idempotency, and the lopsided-but-incomplete-reveal optimistic-timeout case from V2_RESOLUTION.md. Two pre-existing reveal tests needed a second never-revealing voter added to keep exercising their original single-position scenario now that a fully-revealed dispute closes automatically.

@collinsezedike
collinsezedike merged commit 20da0cb into main Aug 12, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant