Implementing it
The settlement rule reads as a loop. Implemented as a loop, it is unshippable. This article is about the form that is shippable, the arithmetic decision that will silently strand funds if you get it wrong, the four rules a JavaScript test suite cannot check for you, and the suite that checks everything else.
License first, because it determines whether the rest is relevant: CC BY 4.0 on the text, MIT on the code, any chain, no permission.
Why the naive form does not ship
Rule 1 says: when a stake arrives, assign it pro-rata to every position standing on every opposing outcome. Read literally, that is a loop over the opposing book on every stake — O(m²) per market in the log length.
On-chain that is fatal. At roughly 5,000 gas per position touched, a book of a few thousand positions puts a single entry in the tens of millions of gas — beyond any per-transaction budget a user or an RPC will carry — and it grows linearly with the book, forever.
The paper is pointed about this because the mistake is attractive:
It is tempting to describe the settlement identity as one loop over a public trade log, cheap to compute on-chain. That is exactly backwards.
The constant-time form
The rule is linear in principal, so it collapses to a reward-per-share accumulator. Maintain, per outcome, the principal P_w and one scalar A_w.
On a stake of c on outcome o:
for each w ≠ o: A_w += c / P_w (O(1))
record position: (o, s, A_o at entry, vintage)
P_o += c
At resolution, position i on the realized outcome is paid:
Π_i = s_i · (1 + A_ω(T) − A_ω(τ_i))
which telescopes to exactly the sum in §4.1.
Rule 2 costs nothing here. Acceptance is min over w ≠ o of (C_w − V_w), with C_w (granted capacity, κ·P_w) and V_w (total vested-in) two more running scalars per outcome. So the entire mechanism — matching and batching included — is O(1) per outcome per entry and O(1) per claim.
State is a few words per outcome and four per position: outcome, principal, entry accumulator, vintage. Those four numbers are the complete transferable state of a position, which is what makes transfer, split, merge, and the whole positions-as-primitives idea possible.
Verified across 26,458 positions in 800 random markets at κ = 3 and κ = 9: the accumulator and the naive reference agree exactly, in rational arithmetic, including settlements where the capacity cap binds, partial fills occur, and multi-entry block vintages ration. The same equivalence in integer-cent arithmetic holds to within rounding, bounded by one cent per opposing event. One claim, two arithmetics — the rational form is the proof, the cent form is what you actually run.
The sharper statement of verifiability
Conservation is a weak property. It holds for the classic parimutuel too. The distinctive one is this:
Each position's payout is a function of two scalars — its outcome's accumulator at entry and at resolution — so no participant's payout depends on any other participant's record.
That is what makes third-party recomputation cheap and what removes pricing trust. It does not remove trust in the completeness and ordering of the trade log, in resolution, or in the venue having applied the parameters it published. §14 is explicit about all three.
Block vintages, and why they are normative
All entries in one block share a vintage, and the batching rule is specification, not an implementation choice:
- Every entry in a vintage vests to the books as they stood at the start of the vintage, so same-vintage entries never vest to each other.
- Capacity granted by a vintage's entries becomes usable only from the next vintage.
- When joint same-vintage demand
D_wagainst an opposing book exceeds its headroomH_w = C_w − V_w, every entry is filled at the same book fraction,⌊c_e · H_w / D_w⌋, and an entry's accepted amount is the minimum of its rationed caps over the books it opposes.
Rule (iii) is the one that completes the claim. Without it, acceptance under a binding κ would fill in intra-block order, and the priority race the vintage removes from vesting would reappear exactly where priority is most valuable. With it, settlement is invariant under permutation within a vintage in both vesting and acceptance (conformance cases V1–V3).
Batching stays O(1): per vintage, snapshot each A_w and P_w at the vintage start, accept and record every entry against the snapshot with one scale factor per book per vintage, then apply the summed increments and principals.
Two honesty notes. Order-free is not size-strategyproof — an entry's rationed share rises with its offered size while the refused remainder costs nothing, so under a binding κ the within-block contest becomes an oversizing game. That is the standard property of pro-rata matching everywhere it is used; the mitigations are to fee or bond the offered amount rather than the accepted one, cap per-account offers, or run κ large. And inter-block priority is a real game, treated as a limitation with its own article.
The arithmetic decision that strands funds
With fixed-point floor division the conservation identity becomes an inequality that can never break in the dangerous direction: payouts plus refused stake plus a residue equal the pool, with residue ≥ 0. The safe half is unconditional, because every floor under-pays — across 27,150 settlements at scales 10¹², 10¹⁸ and 10²⁷, and 800 more with stakes to 4×10¹⁵ units, the accepted pool was never overpaid.
But the residue bound is not one unit per winner at all scales, and this is where implementations go wrong. The analytic bound is:
residue ≤ W + Σ_i s_i · m_i / S
with W the number of winning positions, m_i the opposing vesting events after position i's entry, and S the fixed-point scale. The comfortable "one unit per winner" reading holds exactly when max s_i · m_i ≪ S.
So: choose S ≳ s_max · m_max, or store the accumulator as a rational.
Concretely:
| scale | behaviour |
|---|---|
S = 10¹⁸ | holds the one-unit reading for 6-decimal USDC up to ~10⁹-unit positions and hundreds of events, and comfortably beyond |
S = 10¹² at adversarial sizes (stakes to 4×10¹⁵) | strands tens of thousands of units per winner (measured: 68,000) |
Never insolvent in either case — only stranded. But stranded funds are a support ticket per market, forever.
One attack that does not work, worth knowing so you do not defend against it: dust-position spam cannot farm the residue. The truncation is global rather than per-position-per-event, so 300 one-unit positions against a whale extract at most 3 units.
And the residue is a single computable claim — accepted pool − Σ payouts — which must have a named owner fixed before the market opens (your fee sink, the resolver, or the last claimant, as policy). An unassigned residue is funds nobody can withdraw.
Four MUSTs a test suite cannot check for you
These are specification for any on-chain deployment, stated in §6 because the arithmetic suite cannot execute them:
(a) The settlement asset must be transfer-exact. No fee-on-transfer, no rebasing — or all accounting must use measured balance deltas.
(b) Payouts and the residue claim must be pull-based. One blocked or blacklisted recipient must not be able to delay any other claim. The per-claim O(1) form exists precisely so claims are independent; paying winners in a resolution-time loop hands one blacklisted address a lever over every claim behind it.
(c) All state updates must complete before any external transfer in an entry (P, A, C, V, the position record), and the same-transaction refund of a partial fill must revert the entry atomically if it cannot be delivered.
(d) Batched or relayed entry paths must isolate per-stake failure.
The other four pitfalls
From Appendix A.6, each caught by the suite or stated as a MUST:
- Floating point. Compute capacity or the accumulator in floats and your settlement disagrees with the published integer rule on grid points — the float form disagrees on 438 of 2,700 grid points in the pinned test. The reference computes
κ·s, headroom, and rationing in integers. - Hard-coding two outcomes. Every
n ≥ 2claim in the specification then goes untested, and the n-way coupling is where the interesting failures are. - Accepting an invalid seed. A seed leaving any outcome unbacked — including a dust leg floored to zero by integer allocation — makes the market silently un-enterable forever, because every book's capacity is zero. It must void at creation (P11).
- Admitting a third party into vintage 0. The creation floor is a theorem about atomicity: it needs the creator to be first on every outcome. An implementation that lets someone else in takes the floor to −50% in half of all branches (A.3). The conformant entrypoint assigns a same-block stake to vintage 1, and the floor holds.
Claiming conformance
The suite for the mechanism the paper specifies is version 1.1.1:
| file | covers |
|---|---|
vpm-capacity.mjs | properties P1–P7 with asymmetric-seed variants, cases P8–P11, V1–V4, P7a–P7c′, P9b, P10b, P5w, A4 |
vpm-accumulator.mjs | exact O(1) equivalence including block vintages, residue bounds, seed-validity voiding |
vpm-vectors.json | 106 fixture vectors, sha256 prefix 52b0fcdea345 |
vpm-conformance.mjs | the settler-injection harness |
Every file is served verbatim at playhunch.xyz/vpm-whitepaper/sim/<file>, including the LICENSE and the README.md documenting the settler interface.
The harness accepts any implementation exporting a settle(vector) interface. An implementation claims conformance by passing the property suite and reproducing the 106 fixture vectors exactly on acceptance and to one unit per position on payouts, and should state the suite version and vectors hash it passed.
Two scoping notes the paper insists on. Passing the suite verifies one thing: that entry-and-settlement arithmetic matches §4. The exit semantics of §4.4 (transfer, split, merge) and §12's freeze rule are specified but not exercised by the executable suite. And conformance is not an endorsement of any venue — it says nothing about resolution integrity, custody, or solvency.
Two files are not conformance: vpm-edge-tests.mjs and vpm-sim.mjs preserve the deleted v2 mechanism (the ρ-decay bounty) and its failing cases as a labelled historical record.
A build order
- Naive reference settler first, in rational arithmetic. It is slow and it is your oracle.
- Accumulator settler second. Assert exact agreement with the reference on random logs — that is conformance case P8 and it will catch most mistakes.
- Capacity and the joint vintage-0 clamp. The clamp is a fixed point (the reference iterates it); an asymmetric seed is partially refused, never allowed to violate the leverage bound.
- Block vintages with pro-rata rationing. Test permutation invariance explicitly (V1–V3).
- Integer-cent arithmetic and the residue owner. Pick
Sagainst your reals_max · m_max. - The four MUSTs, which are contract-level and need contract-level tests.
- Run
vpm-conformance.mjsagainst yoursettle(vector)and publish the version and hash you passed.
If you find a case the suite does not cover, that is the interesting one. The paper's canonical page is where to report it.